CDKインフラストラクチャ
このドキュメントでは、MBC CQRS ServerlessフレームワークがAWS CDKを使用してプロビジョニングするAWSインフラストラクチャについて説明します。このアーキテクチャを理解することで、デプロイのカスタマイズや問題のトラブルシューティングに役立ちます。
システムアーキテクチャ概要
以下の図は、フレームワークによって作成される完全なAWSインフラストラクチャを示しています:
作成されるAWSリソース
認証(Cognito)
Amazon Cognitoはユーザー認証と認可 を提供します:
CDK実装:
import * as cognito from 'aws-cdk-lib/aws-cognito';
// Create User Pool
const userPool = new cognito.UserPool(this, 'UserPool', {
userPoolName: `${env}-${appName}-user-pool`,
selfSignUpEnabled: false,
signInAliases: {
username: true,
preferredUsername: true,
},
passwordPolicy: {
minLength: 6,
requireLowercase: false,
requireUppercase: false,
requireDigits: false,
requireSymbols: false,
},
mfa: cognito.Mfa.OFF,
accountRecovery: cognito.AccountRecovery.NONE,
deletionProtection: true,
customAttributes: {
tenant: new cognito.StringAttribute({ maxLen: 50, mutable: true }),
company_code: new cognito.StringAttribute({ maxLen: 50, mutable: true }),
member_id: new cognito.StringAttribute({ maxLen: 2024, mutable: true }),
roles: new cognito.StringAttribute({ mutable: true }),
},
});
// Create User Pool Client
const userPoolClient = userPool.addClient('UserPoolClient', {
authFlows: {
userPassword: true,
userSrp: true,
},
});
API Gateway
HTTP APIはCognito認可付きのRESTfulエンドポイントを提供します:
CDK実装:
import * as apigatewayv2 from 'aws-cdk-lib/aws-apigatewayv2';
import * as apigatewayv2Integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations';
import * as apigatewayv2Authorizers from 'aws-cdk-lib/aws-apigatewayv2-authorizers';
// Create HTTP API
const httpApi = new apigatewayv2.HttpApi(this, 'HttpApi', {
apiName: `${env}-${appName}-api`,
corsPreflight: {
allowOrigins: ['*'],
allowMethods: [apigatewayv2.CorsHttpMethod.ANY],
allowHeaders: ['*'],
maxAge: cdk.Duration.hours(1),
},
});
// Lambda integration
const lambdaIntegration = new apigatewayv2Integrations.HttpLambdaIntegration(
'LambdaIntegration',
lambdaFunction
);
// Cognito authorizer
const cognitoAuthorizer = new apigatewayv2Authorizers.HttpUserPoolAuthorizer(
'CognitoAuthorizer',
userPool,
{ userPoolClients: [userPoolClient] }
);
// Public health check route
httpApi.addRoutes({
path: '/',
methods: [apigatewayv2.HttpMethod.GET],
integration: lambdaIntegration,
});
// Protected API routes
httpApi.addRoutes({
path: '/{proxy+}',
methods: [
apigatewayv2.HttpMethod.GET,
apigatewayv2.HttpMethod.POST,
apigatewayv2.HttpMethod.PUT,
apigatewayv2.HttpMethod.DELETE,
apigatewayv2.HttpMethod.PATCH,
],
integration: lambdaIntegration,
authorizer: cognitoAuthorizer,
});
Lambda関数
メインコンピューティングレイヤーはNestJSアプリケーションを実行します:
CDK実装:
import * as lambda from 'aws-cdk-lib/aws-lambda';
// Create Lambda Layer for dependencies
const lambdaLayer = new lambda.LayerVersion(this, 'MainLayer', {
layerVersionName: `${env}-${appName}-main-layer`,
code: lambda.Code.fromAsset('dist_layer'),
compatibleRuntimes: [lambda.Runtime.NODEJS_20_X],
compatibleArchitectures: [lambda.Architecture.ARM_64],
});
// Create Lambda Function
const lambdaFunction = new lambda.Function(this, 'LambdaApi', {
functionName: `${env}-${appName}-lambda-api`,
runtime: lambda.Runtime.NODEJS_20_X,
architecture: lambda.Architecture.ARM_64,
handler: 'main.handler',
code: lambda.Code.fromAsset('dist'),
memorySize: 512,
timeout: cdk.Duration.seconds(30),
layers: [lambdaLayer],
tracing: lambda.Tracing.ACTIVE,
loggingFormat: lambda.LoggingFormat.JSON,
vpc: vpc,
vpcSubnets: { subnets: privateSubnets },
securityGroups: securityGroups,
environment: {
NODE_OPTIONS: '--enable-source-maps',
TZ: 'Asia/Tokyo',
NODE_ENV: env,
APP_NAME: appName,
LOG_LEVEL: 'info',
S3_BUCKET_NAME: ddbBucket.bucketName,
SNS_TOPIC_ARN: mainSnsTopic.topicArn,
COGNITO_USER_POOL_ID: userPool.userPoolId,
DATABASE_URL: databaseUrl,
},
});
SNSとSQS(イベント駆動メッセージング)
異なるイベントタイプに対するフィルタリング付きメッセージルーティング:
CDK実装:
import * as sns from 'aws-cdk-lib/aws-sns';
import * as sqs from 'aws-cdk-lib/aws-sqs';
import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions';
// Create SNS Topics
const mainSnsTopic = new sns.Topic(this, 'MainSnsTopic', {
topicName: `${env}-${appName}-main-sns`,
});
const alarmSnsTopic = new sns.Topic(this, 'AlarmSnsTopic', {
topicName: `${env}-${appName}-alarm-sns`,
});
// Create Dead Letter Queue
const taskDlq = new sqs.Queue(this, 'TaskDLQ', {
queueName: `${env}-${appName}-task-dead-letter-queue`,
});
// Create Task Queue with DLQ
const taskQueue = new sqs.Queue(this, 'TaskQueue', {
queueName: `${env}-${appName}-task-action-queue`,
deadLetterQueue: {
queue: taskDlq,
maxReceiveCount: 5,
},
});
// Create Notification Queue
const notificationQueue = new sqs.Queue(this, 'NotificationQueue', {
queueName: `${env}-${appName}-notification-queue`,
});
// Create Import Queue
const importQueue = new sqs.Queue(this, 'ImportQueue', {
queueName: `${env}-${appName}-import-action-queue`,
});
// Subscribe queues with message filtering
mainSnsTopic.addSubscription(
new subscriptions.SqsSubscription(taskQueue, {
filterPolicy: {
action: sns.SubscriptionFilter.stringFilter({
allowlist: ['task-execute'],
}),
},
})
);
mainSnsTopic.addSubscription(
new subscriptions.SqsSubscription(notificationQueue, {
filterPolicy: {
action: sns.SubscriptionFilter.stringFilter({
allowlist: ['command-status', 'task-status'],
}),
},
rawMessageDelivery: true,
})
);
mainSnsTopic.addSubscription(
new subscriptions.SqsSubscription(importQueue, {
filterPolicy: {
action: sns.SubscriptionFilter.stringFilter({
allowlist: ['import-execute'],
}),
},
})
);
S3バケット
アプリケーションデータと静的アセットのストレージ:
CDK実装:
import * as s3 from 'aws-cdk-lib/aws-s3';
// DynamoDB attributes bucket
const ddbBucket = new s3.Bucket(this, 'DdbAttributesBucket', {
bucketName: `${env}-${appName}-ddb-attributes`,
versioned: false,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
removalPolicy: cdk.RemovalPolicy.DESTROY,
cors: [
{
allowedMethods: [
s3.HttpMethods.GET,
s3.HttpMethods.PUT,
s3.HttpMethods.POST,
],
allowedOrigins: ['*'],
allowedHeaders: ['*'],
maxAge: 3000,
},
],
});
// Public assets bucket
const publicBucket = new s3.Bucket(this, 'PublicBucket', {
bucketName: `${env}-${appName}-public`,
versioned: false,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
// Grant Lambda access
ddbBucket.grantReadWrite(lambdaFunction);
publicBucket.grantReadWrite(lambdaFunction);
CloudFrontディストリビューション
地理的制限付きのAPIおよび静的アセット用CDN:
CDK実装:
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
// Origin Access Identity for S3
const oai = new cloudfront.OriginAccessIdentity(this, 'OAI');
publicBucket.grantRead(oai);
// Static assets distribution
const staticDistribution = new cloudfront.Distribution(this, 'StaticDistribution', {
defaultBehavior: {
origin: new origins.S3Origin(publicBucket, {
originAccessIdentity: oai,
}),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD,
},
priceClass: cloudfront.PriceClass.PRICE_CLASS_200,
geoRestriction: cloudfront.GeoRestriction.allowlist('JP', 'VN'),
});
// API distribution
const apiDistribution = new cloudfront.Distribution(this, 'ApiDistribution', {
defaultBehavior: {
origin: new origins.HttpOrigin(httpApiDomain, {
customHeaders: {
'X-Origin-Verify': originVerifyToken,
},
}),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
responseHeadersPolicy: cloudfront.ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS,
allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
},
priceClass: cloudfront.PriceClass.PRICE_CLASS_200,
geoRestriction: cloudfront.GeoRestriction.allowlist('JP', 'VN'),
});
AppSync(GraphQL API)
サブス クリプション付きリアルタイムGraphQL API:
CDK実装:
import * as appsync from 'aws-cdk-lib/aws-appsync';
// Create AppSync API
const graphqlApi = new appsync.GraphqlApi(this, 'GraphqlApi', {
name: `${env}-${appName}-realtime`,
definition: appsync.Definition.fromFile('asset/schema.graphql'),
authorizationConfig: {
defaultAuthorization: {
authorizationType: appsync.AuthorizationType.API_KEY,
apiKeyConfig: {
expires: cdk.Expiration.after(cdk.Duration.days(365)),
},
},
additionalAuthorizationModes: [
{ authorizationType: appsync.AuthorizationType.IAM },
{
authorizationType: appsync.AuthorizationType.USER_POOL,
userPoolConfig: { userPool },
},
],
},
xrayEnabled: true,
});
// None data source for local resolvers
const noneDataSource = graphqlApi.addNoneDataSource('NoneDataSource');
// Mutation resolver
noneDataSource.createResolver('SendMessageResolver', {
typeName: 'Mutation',
fieldName: 'sendMessage',
requestMappingTemplate: appsync.MappingTemplate.fromString(`
{
"version": "2017-02-28",
"payload": $util.toJson($context.arguments.message)
}
`),
responseMappingTemplate: appsync.MappingTemplate.fromString(
'$util.toJson($context.result)'
),
});
Step Functionsステートマシン
長時間実行プロセスのワークフローオーケストレーション:
CDK実装:
import * as sfn from 'aws-cdk-lib/aws-stepfunctions';
import * as tasks from 'aws-cdk-lib/aws-stepfunctions-tasks';
// Helper function to create Lambda invoke tasks
const createLambdaTask = (
stateName: string,
integrationPattern: sfn.IntegrationPattern = sfn.IntegrationPattern.REQUEST_RESPONSE
) => {
const payload: Record<string, any> = {
'source': 'step-function',
'context.$': '$$',
'input.$': '$',
};
if (integrationPattern === sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN) {
payload['taskToken'] = sfn.JsonPath.taskToken;
}
return new tasks.LambdaInvoke(this, stateName, {
lambdaFunction,
payload: sfn.TaskInput.fromObject(payload),
stateName,
outputPath: '$.Payload[0][0]',
integrationPattern,
retryOnServiceExceptions: true,
});
};
// Define states
const fail = new sfn.Fail(this, 'fail', {
stateName: 'fail',
causePath: '$.cause',
errorPath: '$.error',
});
const success = new sfn.Succeed(this, 'success', {
stateName: 'success',
});
const finish = createLambdaTask('finish').next(success);
// Map state for parallel data sync
const syncData = createLambdaTask('sync_data');
const syncDataAll = new sfn.Map(this, 'sync_data_all', {
stateName: 'sync_data_all',
maxConcurrency: 0, // Unlimited
itemsPath: sfn.JsonPath.stringAt('$'),
})
.itemProcessor(syncData)
.next(finish);
const transformData = createLambdaTask('transform_data').next(syncDataAll);
const historyCopy = createLambdaTask('history_copy').next(transformData);
const setTtlCommand = createLambdaTask('set_ttl_command').next(historyCopy);
// Callback pattern for async waiting
const waitPrevCommand = createLambdaTask(
'wait_prev_command',
sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN
).next(setTtlCommand);
// Version check choice
const checkVersionResult = new sfn.Choice(this, 'check_version_result')
.when(sfn.Condition.numberEquals('$.result', 0), setTtlCommand)
.when(sfn.Condition.numberEquals('$.result', 1), waitPrevCommand)
.when(sfn.Condition.numberEquals('$.result', -1), fail)
.otherwise(waitPrevCommand);
const checkVersion = createLambdaTask('check_version').next(checkVersionResult);
// Create log group
const commandSfnLogGroup = new logs.LogGroup(this, 'CommandSfnLogGroup', {
logGroupName: `/aws/vendedlogs/states/${prefix}-command-handler-logs`,
removalPolicy: cdk.RemovalPolicy.DESTROY,
retention: logs.RetentionDays.SIX_MONTHS,
});
// Create state machine
const commandStateMachine = new sfn.StateMachine(this, 'CommandStateMachine', {
stateMachineName: `${prefix}command-handler`,
definitionBody: sfn.DefinitionBody.fromChainable(checkVersion),
tracingEnabled: true,
logs: {
destination: commandSfnLogGroup,
level: sfn.LogLevel.ALL,
},
});
DynamoDBイベントソース
DynamoDB StreamsをLambdaトリガーとして設定:
CDK実装:
import * as lambdaEventSources from 'aws-cdk-lib/aws-lambda-event-sources';
// Tables to monitor for changes
const tableNames = ['tasks', 'sample-command', 'import_tmp'];
for (const tableName of tableNames) {
// Lookup existing table
const tableDesc = new cdk.custom_resources.AwsCustomResource(
this,
`${tableName}-desc`,
{
onCreate: {
service: 'DynamoDB',
action: 'describeTable',
parameters: { TableName: `${prefix}${tableName}` },
physicalResourceId: cdk.custom_resources.PhysicalResourceId.fromResponse(
'Table.TableArn'
),
},
policy: cdk.custom_resources.AwsCustomResourcePolicy.fromSdkCalls({
resources: cdk.custom_resources.AwsCustomResourcePolicy.ANY_RESOURCE,
}),
}
);
const table = dynamodb.Table.fromTableAttributes(this, `${tableName}-table`, {
tableArn: tableDesc.getResponseField('Table.TableArn'),
tableStreamArn: tableDesc.getResponseField('Table.LatestStreamArn'),
});
// Add event source with INSERT filter
lambdaFunction.addEventSource(
new lambdaEventSources.DynamoEventSource(table, {
startingPosition: lambda.StartingPosition.TRIM_HORIZON,
batchSize: 1,
filters: [
lambda.FilterCriteria.filter({
eventName: lambda.FilterRule.isEqual('INSERT'),
}),
],
})
);
}