@mbc-cqrs-serverless/cli
Version:
a CLI to get started with MBC CQRS serverless framework
1,174 lines (1,096 loc) • 37.7 kB
text/typescript
import * as cdk from 'aws-cdk-lib'
import * as apigwv2 from 'aws-cdk-lib/aws-apigatewayv2'
import * as apigatewayv2_authorizers from 'aws-cdk-lib/aws-apigatewayv2-authorizers'
import * as apigwv2_integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations'
import { Construct } from 'constructs'
import { randomBytes } from 'crypto'
import { IgnoreMode } from 'aws-cdk-lib'
import { Repository } from 'aws-cdk-lib/aws-ecr'
import { DockerImageAsset, Platform } from 'aws-cdk-lib/aws-ecr-assets'
import { ContainerImage } from 'aws-cdk-lib/aws-ecs'
import { ApplicationLoadBalancedFargateService } from 'aws-cdk-lib/aws-ecs-patterns'
import { DockerImageName, ECRDeployment } from 'cdk-ecr-deployment'
import * as path from 'path'
import {
ACM_APPSYNC_CERTIFICATE_ARN,
ACM_HTTP_CERTIFICATE_ARN,
HOSTED_ZONE_ID,
HOSTED_ZONE_NAME,
} from '../config'
import { Config } from '../config/type'
import { buildApp } from './build-app'
import { DistributedMap } from './distributed-map'
export interface InfraStackProps extends cdk.StackProps {
config: Config
}
export class InfraStack extends cdk.Stack {
public readonly userPoolId: cdk.CfnOutput
public readonly userPoolClientId: cdk.CfnOutput
public readonly graphqlApiUrl: cdk.CfnOutput
public readonly graphqlApiKey: cdk.CfnOutput
public readonly httpApiUrl: cdk.CfnOutput
public readonly stateMachineArn: cdk.CfnOutput
public readonly httpDistributionDomain: cdk.CfnOutput
public readonly sfnTaskStateMachineArn: cdk.CfnOutput
constructor(scope: Construct, id: string, props: InfraStackProps) {
super(scope, id, props)
const name = props.config.appName
const env = props.config.env
const prefix = `${env}-${name}-`
const originVerifyToken = prefix + randomBytes(32).toString('hex')
cdk.Tags.of(scope).add('name', props.config.appName)
cdk.Tags.of(scope).add('env', props.config.env)
// Cognito
let userPool: cdk.aws_cognito.IUserPool
if (props.config.userPoolId) {
userPool = cdk.aws_cognito.UserPool.fromUserPoolId(
this,
'main-user-pool',
props.config.userPoolId,
)
} else {
// create new cognito
userPool = new cdk.aws_cognito.UserPool(this, prefix + 'user-pool', {
userPoolName: prefix + 'user-pool',
selfSignUpEnabled: false,
signInAliases: {
username: true,
preferredUsername: true,
},
passwordPolicy: {
minLength: 6,
requireDigits: false,
requireLowercase: false,
requireSymbols: false,
requireUppercase: false,
},
mfa: cdk.aws_cognito.Mfa.OFF,
accountRecovery: cdk.aws_cognito.AccountRecovery.NONE,
customAttributes: {
tenant: new cdk.aws_cognito.StringAttribute({
mutable: true,
maxLen: 50,
}),
company_code: new cdk.aws_cognito.StringAttribute({
mutable: true,
maxLen: 50,
}),
member_id: new cdk.aws_cognito.StringAttribute({
mutable: true,
maxLen: 2024,
}),
roles: new cdk.aws_cognito.StringAttribute({ mutable: true }),
},
email: cdk.aws_cognito.UserPoolEmail.withCognito(),
deletionProtection: true,
})
}
this.userPoolId = new cdk.CfnOutput(this, 'UserPoolId', {
value: userPool.userPoolId,
})
// SNS
const mainSns = new cdk.aws_sns.Topic(this, 'main-sns', {
topicName: prefix + 'main-sns',
})
const alarmSns = new cdk.aws_sns.Topic(this, 'alarm-sns', {
topicName: prefix + 'alarm-sns',
})
// SQS
const taskDlSqs = new cdk.aws_sqs.Queue(this, 'task-dead-letter-sqs', {
queueName: prefix + 'task-dead-letter-queue',
})
const taskSqs = new cdk.aws_sqs.Queue(this, 'task-sqs', {
queueName: prefix + 'task-action-queue',
deadLetterQueue: {
queue: taskDlSqs,
maxReceiveCount: 5,
},
})
const importActionSqs = new cdk.aws_sqs.Queue(this, 'import-action-sqs', {
queueName: prefix + 'import-action-queue',
})
const subTaskStatusSqs = new cdk.aws_sqs.Queue(
this,
'sub-task-status-sqs',
{
queueName: prefix + 'sub-task-status-queue',
},
)
alarmSns.addSubscription(
new cdk.aws_sns_subscriptions.SqsSubscription(taskDlSqs, {
rawMessageDelivery: true,
}),
)
mainSns.addSubscription(
new cdk.aws_sns_subscriptions.SqsSubscription(taskSqs, {
rawMessageDelivery: true,
filterPolicy: {
action: cdk.aws_sns.SubscriptionFilter.stringFilter({
allowlist: ['task-execute'],
}),
},
}),
)
const notifySqs = new cdk.aws_sqs.Queue(this, 'notify-sqs', {
queueName: prefix + 'notification-queue',
})
mainSns.addSubscription(
new cdk.aws_sns_subscriptions.SqsSubscription(notifySqs, {
rawMessageDelivery: true,
filterPolicy: {
action: cdk.aws_sns.SubscriptionFilter.stringFilter({
allowlist: ['command-status', 'task-status'],
}),
},
}),
)
mainSns.addSubscription(
new cdk.aws_sns_subscriptions.SqsSubscription(subTaskStatusSqs, {
rawMessageDelivery: true,
filterPolicy: {
action: cdk.aws_sns.SubscriptionFilter.stringFilter({
allowlist: ['sub-task-status'],
}),
},
}),
)
mainSns.addSubscription(
new cdk.aws_sns_subscriptions.SqsSubscription(importActionSqs, {
rawMessageDelivery: true,
filterPolicy: {
action: cdk.aws_sns.SubscriptionFilter.stringFilter({
allowlist: ['import-execute'],
}),
},
}),
)
// host zone
const hostedZone = cdk.aws_route53.HostedZone.fromHostedZoneAttributes(
this,
'HostedZone',
{
hostedZoneId: HOSTED_ZONE_ID,
zoneName: HOSTED_ZONE_NAME,
},
)
// AppSync
const appSyncCertificate =
cdk.aws_certificatemanager.Certificate.fromCertificateArn(
this,
'appsync-certificate',
ACM_APPSYNC_CERTIFICATE_ARN,
)
const appSyncApi = new cdk.aws_appsync.GraphqlApi(this, 'realtime', {
name: prefix + 'realtime',
definition: cdk.aws_appsync.Definition.fromFile('asset/schema.graphql'), // Define the schema
authorizationConfig: {
defaultAuthorization: {
authorizationType: cdk.aws_appsync.AuthorizationType.API_KEY, // Defining authorization type
apiKeyConfig: {
expires: cdk.Expiration.after(cdk.Duration.days(365)), // Set expiration for API Key
},
},
additionalAuthorizationModes: [
{
authorizationType: cdk.aws_appsync.AuthorizationType.IAM,
},
{
authorizationType: cdk.aws_appsync.AuthorizationType.USER_POOL,
userPoolConfig: { userPool },
},
],
},
xrayEnabled: true, // Enable X-Ray for monitoring
domainName: {
certificate: appSyncCertificate,
domainName: props.config.domain.appsync,
},
})
const noneDS = appSyncApi.addNoneDataSource('NoneDataSource')
noneDS.createResolver('sendMessageResolver', {
typeName: 'Mutation',
fieldName: 'sendMessage',
requestMappingTemplate: cdk.aws_appsync.MappingTemplate.fromString(
'{"version": "2018-05-29","payload": $util.toJson($context.arguments.message)}',
),
responseMappingTemplate: cdk.aws_appsync.MappingTemplate.fromString(
'$util.toJson($context.result)',
),
})
// route to AppSync
new cdk.aws_route53.CnameRecord(this, `AppSyncCnameRecord`, {
zone: hostedZone,
recordName: props.config.domain.appsync,
domainName: appSyncApi.appSyncDomainName,
})
this.graphqlApiUrl = new cdk.CfnOutput(this, 'GraphQLAPIURL', {
value: appSyncApi.graphqlUrl,
})
this.graphqlApiKey = new cdk.CfnOutput(this, 'GraphQLAPIKey', {
value: appSyncApi.apiKey || '',
})
// S3
const ddbBucket = new cdk.aws_s3.Bucket(this, 'ddb-attributes', {
bucketName: prefix + 'ddb-attributes', // Globally unique bucket name
versioned: false,
publicReadAccess: false, // Block public read access
blockPublicAccess: cdk.aws_s3.BlockPublicAccess.BLOCK_ALL, // Block all public access
removalPolicy: cdk.RemovalPolicy.DESTROY, // Define removal policy (use with caution in production)
cors: [
{
allowedHeaders: ['*'],
allowedMethods: [
cdk.aws_s3.HttpMethods.GET,
cdk.aws_s3.HttpMethods.PUT,
cdk.aws_s3.HttpMethods.POST,
],
allowedOrigins: ['*'],
maxAge: 3000,
},
],
})
const publicBucket = new cdk.aws_s3.Bucket(this, 'public-bucket', {
bucketName: prefix + 'public', // Globally unique bucket name
versioned: false,
publicReadAccess: false, // Block public read access
blockPublicAccess: cdk.aws_s3.BlockPublicAccess.BLOCK_ALL, // Block all public access
removalPolicy: cdk.RemovalPolicy.DESTROY, // Define removal policy (use with caution in production)
cors: [
{
allowedHeaders: ['*'],
allowedMethods: [
cdk.aws_s3.HttpMethods.GET,
cdk.aws_s3.HttpMethods.PUT,
cdk.aws_s3.HttpMethods.POST,
],
allowedOrigins: ['*'],
maxAge: 3000,
},
],
})
// cloudfront
const publicBucketOAI = new cdk.aws_cloudfront.OriginAccessIdentity(
this,
'public-bucket-OAI',
)
publicBucket.addToResourcePolicy(
new cdk.aws_iam.PolicyStatement({
actions: ['s3:GetObject'],
effect: cdk.aws_iam.Effect.ALLOW,
resources: [publicBucket.arnForObjects('*')],
principals: [
new cdk.aws_iam.CanonicalUserPrincipal(
publicBucketOAI.cloudFrontOriginAccessIdentityS3CanonicalUserId,
),
],
}),
)
const publicBucketDistribution = new cdk.aws_cloudfront.Distribution(
this,
'public-bucket-distribution',
{
defaultBehavior: {
allowedMethods: cdk.aws_cloudfront.AllowedMethods.ALLOW_GET_HEAD,
cachedMethods: cdk.aws_cloudfront.CachedMethods.CACHE_GET_HEAD,
cachePolicy: cdk.aws_cloudfront.CachePolicy.CACHING_OPTIMIZED,
viewerProtocolPolicy:
cdk.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
origin: new cdk.aws_cloudfront_origins.S3Origin(publicBucket, {
originAccessIdentity: publicBucketOAI,
}),
},
priceClass: cdk.aws_cloudfront.PriceClass.PRICE_CLASS_200,
geoRestriction: cdk.aws_cloudfront.GeoRestriction.allowlist('JP', 'VN'),
},
)
// VPC
const vpc = cdk.aws_ec2.Vpc.fromLookup(this, 'main-vpc', {
vpcId: props.config.vpc.id,
})
const subnets = cdk.aws_ec2.SubnetFilter.byIds(props.config.vpc.subnetIds)
const securityGroups = props.config.vpc.securityGroupIds.map((id, idx) =>
cdk.aws_ec2.SecurityGroup.fromSecurityGroupId(
this,
'main-security-group-' + idx,
id,
),
)
// Lambda Layer
const { layerPath, appPath } = buildApp(env)
console.log('dist path:', layerPath, appPath)
const lambdaLayer = new cdk.aws_lambda.LayerVersion(this, 'main-layer', {
layerVersionName: prefix + 'main-layer',
code: cdk.aws_lambda.AssetCode.fromAsset(layerPath),
compatibleRuntimes: [cdk.aws_lambda.Runtime.NODEJS_20_X],
compatibleArchitectures: [cdk.aws_lambda.Architecture.ARM_64],
})
const commandSfnArn = cdk.Arn.format({
partition: 'aws',
region: this.region,
account: this.account,
service: 'states',
resource: 'stateMachine',
resourceName: prefix + 'command-handler',
arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME,
})
const taskStateMachineName = prefix + 'sfn-task-handler'
const taskSfnArn = cdk.Arn.format({
partition: 'aws',
region: this.region,
account: this.account,
service: 'states',
resource: 'stateMachine',
resourceName: taskStateMachineName,
arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME,
})
const importCsvStateMachineName = prefix + 'import-csv-handler'
const importCsvSfnArn = cdk.Arn.format({
partition: 'aws',
region: this.region,
account: this.account,
service: 'states',
resource: 'stateMachine',
resourceName: importCsvStateMachineName,
arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME,
})
// Lambda api ( arm64 )
const execEnv = {
NODE_OPTIONS: '--enable-source-maps',
TZ: 'Asia/Tokyo',
NODE_ENV: env,
APP_NAME: name,
LOG_LEVEL: props.config.logLevel?.level || 'info',
EVENT_SOURCE_DISABLED: 'false',
ATTRIBUTE_LIMIT_SIZE: '389120',
S3_BUCKET_NAME: ddbBucket.bucketName,
SFN_COMMAND_ARN: commandSfnArn,
SFN_TASK_ARN: taskSfnArn,
SFN_IMPORT_CSV_ARN: importCsvSfnArn,
SNS_TOPIC_ARN: mainSns.topicArn,
SNS_ALARM_TOPIC_ARN: alarmSns.topicArn,
COGNITO_USER_POOL_ID: userPool.userPoolId,
APPSYNC_ENDPOINT: appSyncApi.graphqlUrl,
SES_FROM_EMAIL: props.config.fromEmailAddress,
DATABASE_URL: `postgresql://${props.config.rds.accountSsmKey}@${props.config.rds.endpoint}/${props.config.rds.dbName}?schema=public`,
S3_PUBLIC_BUCKET_NAME: publicBucket.bucketName,
FRONT_BASE_URL: props.config.frontBaseUrl,
}
const lambdaApi = new cdk.aws_lambda.Function(this, 'lambda-api', {
vpc,
vpcSubnets: {
subnetFilters: [subnets],
},
securityGroups,
architecture: cdk.aws_lambda.Architecture.ARM_64,
functionName: prefix + 'lambda-api',
layers: [lambdaLayer],
code: cdk.aws_lambda.Code.fromAsset(appPath),
handler: 'main.handler',
runtime: cdk.aws_lambda.Runtime.NODEJS_20_X,
timeout: cdk.Duration.seconds(30),
memorySize: 512,
tracing: cdk.aws_lambda.Tracing.ACTIVE,
loggingFormat: cdk.aws_lambda.LoggingFormat.JSON,
applicationLogLevelV2: props.config.logLevel?.lambdaApplication,
systemLogLevelV2: props.config.logLevel?.lambdaSystem,
environment: execEnv,
})
// API GW
const httpApi = new apigwv2.HttpApi(this, 'main-api', {
description: 'HTTP API for Lambda integration',
apiName: prefix + 'api',
corsPreflight: {
allowOrigins: ['*'],
allowCredentials: false,
allowHeaders: ['*'],
allowMethods: [apigwv2.CorsHttpMethod.ANY],
maxAge: cdk.Duration.hours(1),
},
})
const lambdaIntegration = new apigwv2_integrations.HttpLambdaIntegration(
'main-api-lambda',
lambdaApi,
)
// event routes
httpApi.addRoutes({
path: '/event/{proxy+}',
integration: lambdaIntegration,
authorizer: new apigatewayv2_authorizers.HttpIamAuthorizer(),
})
// api protected routes
const userPoolClient = new cdk.aws_cognito.UserPoolClient(
this,
'apigw-client',
{
userPool,
authFlows: {
userPassword: true,
userSrp: true,
},
},
)
this.userPoolClientId = new cdk.CfnOutput(this, 'UserPoolClientId', {
value: userPoolClient.userPoolClientId,
})
const authorizer = new apigatewayv2_authorizers.HttpUserPoolAuthorizer(
'CognitoAuthorizer',
userPool,
{
userPoolClients: [userPoolClient],
},
)
let apiIntegration: apigwv2.HttpRouteIntegration
let taskRole: cdk.aws_iam.Role | undefined
if (!props.config.ecs) {
apiIntegration = lambdaIntegration
} else {
// ecs api
const resp = new Repository(this, 'main-ecr-repo', {
repositoryName: `${prefix}api`,
removalPolicy: cdk.RemovalPolicy.RETAIN,
})
const image = new DockerImageAsset(this, 'main-image', {
directory: path.resolve(__dirname, '../..'),
platform: Platform.LINUX_AMD64,
ignoreMode: IgnoreMode.DOCKER,
})
const imageTag = process.env.CODEBUILD_RESOLVED_SOURCE_VERSION
? process.env.CODEBUILD_RESOLVED_SOURCE_VERSION.substring(0, 4)
: 'latest'
new ECRDeployment(this, `${prefix}deploy`, {
src: new DockerImageName(image.imageUri),
dest: new DockerImageName(`${resp.repositoryUri}:${imageTag}`),
})
taskRole = new cdk.aws_iam.Role(this, 'ecs-role', {
assumedBy: new cdk.aws_iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
})
const ecsService = new ApplicationLoadBalancedFargateService(
this,
'main-service',
{
vpc,
taskSubnets: {
subnetFilters: [subnets],
},
securityGroups,
circuitBreaker: {
rollback: props.config.ecs.autoRollback,
},
publicLoadBalancer: false,
memoryLimitMiB: props.config.ecs.memory,
cpu: props.config.ecs.cpu,
desiredCount: props.config.ecs.minInstances,
taskImageOptions: {
image: ContainerImage.fromDockerImageAsset(image),
environment: {
...execEnv,
APP_PORT: '80',
EVENT_SOURCE_DISABLED: 'true',
PRISMA_EXPLICIT_CONNECT: 'false',
},
secrets: {
DATABASE_USER_PASS: cdk.aws_ecs.Secret.fromSsmParameter(
cdk.aws_ssm.StringParameter.fromSecureStringParameterAttributes(
this,
'dbUserPass',
{
parameterName: props.config.rds.accountSsmKey,
},
),
),
},
taskRole,
},
},
)
if (props.config.ecs.cpuThreshold) {
const scalableTarget = ecsService.service.autoScaleTaskCount({
minCapacity: props.config.ecs.minInstances,
maxCapacity: props.config.ecs.maxInstances,
})
scalableTarget.scaleOnCpuUtilization('CpuScaling', {
targetUtilizationPercent: props.config.ecs.cpuThreshold,
})
}
const vpcLink = new apigwv2.VpcLink(this, 'ecs-vpc-link', {
vpc,
})
const vpcLinkIntegration = new apigwv2_integrations.HttpAlbIntegration(
'ecs-vpc-link-integration',
ecsService.loadBalancer.listeners[0],
{
vpcLink,
parameterMapping: new apigwv2.ParameterMapping()
.appendHeader(
'x-source-ip',
apigwv2.MappingValue.contextVariable('identity.sourceIp'),
)
.appendHeader(
'x-request-id',
apigwv2.MappingValue.contextVariable('extendedRequestId'),
),
},
)
apiIntegration = vpcLinkIntegration
}
// health check api (public)
httpApi.addRoutes({
path: '/',
methods: [apigwv2.HttpMethod.GET],
integration: apiIntegration,
})
// protected api
httpApi.addRoutes({
path: '/{proxy+}',
methods: [
apigwv2.HttpMethod.HEAD,
apigwv2.HttpMethod.GET,
apigwv2.HttpMethod.POST,
apigwv2.HttpMethod.DELETE,
apigwv2.HttpMethod.PUT,
apigwv2.HttpMethod.PATCH,
],
integration: apiIntegration,
authorizer,
})
// Output the URL of the HTTP API
this.httpApiUrl = new cdk.CfnOutput(this, 'HttpApiUrl', {
value: httpApi.url!,
})
// cloudfront to HTTP API
const httpDistributionCertificate =
cdk.aws_certificatemanager.Certificate.fromCertificateArn(
this,
'http-distribution-certificate',
ACM_HTTP_CERTIFICATE_ARN,
)
const httpDistribution = new cdk.aws_cloudfront.Distribution(
this,
'http-distribution',
{
defaultBehavior: {
origin: new cdk.aws_cloudfront_origins.HttpOrigin(
`${httpApi.apiId}.execute-api.${this.region}.amazonaws.com`,
{
customHeaders: {
'X-Origin-Verify': originVerifyToken,
},
},
),
originRequestPolicy:
cdk.aws_cloudfront.OriginRequestPolicy
.ALL_VIEWER_EXCEPT_HOST_HEADER,
responseHeadersPolicy:
cdk.aws_cloudfront.ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS,
allowedMethods: cdk.aws_cloudfront.AllowedMethods.ALLOW_ALL,
cachePolicy: cdk.aws_cloudfront.CachePolicy.CACHING_DISABLED,
viewerProtocolPolicy:
cdk.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
},
priceClass: cdk.aws_cloudfront.PriceClass.PRICE_CLASS_200,
geoRestriction: cdk.aws_cloudfront.GeoRestriction.allowlist('JP', 'VN'),
domainNames: [props.config.domain.http],
certificate: httpDistributionCertificate,
webAclId: props.config.wafArn,
enableIpv6: false,
},
)
new cdk.aws_route53.CnameRecord(this, 'http-distribution-a-record', {
zone: hostedZone,
recordName: props.config.domain.http,
domainName: httpDistribution.distributionDomainName,
})
this.httpDistributionDomain = new cdk.CfnOutput(
this,
'http-distribution-domain',
{
value: httpDistribution.distributionDomainName,
},
)
// api gateway logging
// Setup the access log for APIGWv2
const httpApiAccessLogs = new cdk.aws_logs.LogGroup(
this,
'http-api-AccessLogs',
)
const httpApiDefaultStage = httpApi.defaultStage?.node
.defaultChild as cdk.aws_apigatewayv2.CfnStage
httpApiDefaultStage.accessLogSettings = {
destinationArn: httpApiAccessLogs.logGroupArn,
format: JSON.stringify({
requestId: '$context.requestId',
ip: '$context.identity.sourceIp',
userAgent: '$context.identity.userAgent',
sourceIp: '$context.identity.sourceIp',
requestTime: '$context.requestTime',
requestTimeEpoch: '$context.requestTimeEpoch',
httpMethod: '$context.httpMethod',
routeKey: '$context.routeKey',
path: '$context.path',
status: '$context.status',
protocol: '$context.protocol',
responseLength: '$context.responseLength',
domainName: '$context.domainName',
responseLatency: '$context.responseLatency',
integrationLatency: '$context.integrationLatency',
username: '$context.authorizer.claims.sub',
}),
}
httpApiDefaultStage.defaultRouteSettings = {
detailedMetricsEnabled: true,
}
// StepFunction
// Define the lambda invoke task with common configurations
const lambdaInvoke = (
stateName: string,
nextState: cdk.aws_stepfunctions.IChainable | null,
integrationPattern: cdk.aws_stepfunctions.IntegrationPattern,
) => {
const payloadObject: {
[key: string]: any
} = {
'input.$': '$',
'context.$': '$$',
}
if (
integrationPattern ===
cdk.aws_stepfunctions.IntegrationPattern.WAIT_FOR_TASK_TOKEN
) {
payloadObject['taskToken'] = cdk.aws_stepfunctions.JsonPath.taskToken // '$$.Task.Token'
}
const lambdaTask = new cdk.aws_stepfunctions_tasks.LambdaInvoke(
this,
stateName,
{
lambdaFunction: lambdaApi,
payload: cdk.aws_stepfunctions.TaskInput.fromObject(payloadObject),
retryOnServiceExceptions: true,
stateName,
outputPath: '$.Payload[0][0]',
integrationPattern,
},
)
if (nextState) {
return lambdaTask.next(nextState)
}
return lambdaTask
}
// Define states
const fail = new cdk.aws_stepfunctions.Fail(this, 'fail', {
stateName: 'fail',
causePath: '$.cause',
errorPath: '$.error',
})
const success = new cdk.aws_stepfunctions.Succeed(this, 'success', {
stateName: 'success',
})
const finish = lambdaInvoke(
'finish',
success,
cdk.aws_stepfunctions.IntegrationPattern.REQUEST_RESPONSE,
)
const syncData = lambdaInvoke(
'sync_data',
null,
cdk.aws_stepfunctions.IntegrationPattern.REQUEST_RESPONSE,
)
// Define Map state
const syncDataAll = new cdk.aws_stepfunctions.Map(this, 'sync_data_all', {
stateName: 'sync_data_all',
maxConcurrency: 0,
itemsPath: cdk.aws_stepfunctions.JsonPath.stringAt('$'),
})
.itemProcessor(syncData)
.next(finish)
const transformData = lambdaInvoke(
'transform_data',
syncDataAll,
cdk.aws_stepfunctions.IntegrationPattern.REQUEST_RESPONSE,
)
const historyCopy = lambdaInvoke(
'history_copy',
transformData,
cdk.aws_stepfunctions.IntegrationPattern.REQUEST_RESPONSE,
)
const setTtlCommand = lambdaInvoke(
'set_ttl_command',
historyCopy,
cdk.aws_stepfunctions.IntegrationPattern.REQUEST_RESPONSE,
)
const waitPrevCommand = lambdaInvoke(
'wait_prev_command',
setTtlCommand,
cdk.aws_stepfunctions.IntegrationPattern.WAIT_FOR_TASK_TOKEN,
)
// Define Choice state
const checkVersionResult = new cdk.aws_stepfunctions.Choice(
this,
'check_version_result',
{
stateName: 'check_version_result',
},
)
.when(
cdk.aws_stepfunctions.Condition.numberEquals('$.result', 0),
setTtlCommand,
)
.when(
cdk.aws_stepfunctions.Condition.numberEquals('$.result', 1),
waitPrevCommand,
)
.when(cdk.aws_stepfunctions.Condition.numberEquals('$.result', -1), fail)
.otherwise(waitPrevCommand)
const sfnDefinition = lambdaInvoke(
'check_version',
checkVersionResult,
cdk.aws_stepfunctions.IntegrationPattern.REQUEST_RESPONSE,
)
const commandSfnLogGroup = new cdk.aws_logs.LogGroup(
this,
'command-handler-sfn-log',
{
logGroupName: `/aws/vendedlogs/states/${prefix}-command-handler-state-machine-Logs`, // Specify a log group name
removalPolicy: cdk.RemovalPolicy.DESTROY, // Policy for log group removal
retention: cdk.aws_logs.RetentionDays.SIX_MONTHS,
},
)
// Define the state machine
const stateMachine = new cdk.aws_stepfunctions.StateMachine(
this,
'command-handler-state-machine',
{
stateMachineName: prefix + 'command-handler',
comment: 'A state machine that run the command stream handler',
definitionBody:
cdk.aws_stepfunctions.DefinitionBody.fromChainable(sfnDefinition),
tracingEnabled: true,
logs: {
destination: commandSfnLogGroup,
level: cdk.aws_stepfunctions.LogLevel.ALL, // Log level (ALL, ERROR, or FATAL)
},
},
)
// Output the State Machine's ARN
this.stateMachineArn = new cdk.CfnOutput(this, 'StateMachineArn', {
value: stateMachine.stateMachineArn,
})
//
const invokeLambdaSfnTask = lambdaInvoke(
'iterator',
null,
cdk.aws_stepfunctions.IntegrationPattern.REQUEST_RESPONSE,
)
const sfnTaskMapState = new cdk.aws_stepfunctions.Map(
this,
'TaskMapState',
{
stateName: 'map_state',
maxConcurrency: 2,
inputPath: '$',
itemsPath: cdk.aws_stepfunctions.JsonPath.stringAt('$'),
},
).itemProcessor(invokeLambdaSfnTask)
const taskSfnLogGroup = new cdk.aws_logs.LogGroup(
this,
'task-handler-sfn-log',
{
logGroupName: `/aws/vendedlogs/states/${prefix}task-handler-state-machine-Logs`, // Specify a log group name
removalPolicy: cdk.RemovalPolicy.DESTROY, // Policy for log group removal
retention: cdk.aws_logs.RetentionDays.SIX_MONTHS,
},
)
const taskStateMachine = new cdk.aws_stepfunctions.StateMachine(
this,
'task-handler-state-machine',
{
stateMachineName: taskStateMachineName,
comment: 'A state machine for task handler',
definition: sfnTaskMapState,
timeout: cdk.Duration.minutes(15), // Define overall state machine timeout if needed
tracingEnabled: true,
logs: {
destination: taskSfnLogGroup,
level: cdk.aws_stepfunctions.LogLevel.ALL, // Log level (ALL, ERROR, or FATAL)
},
},
)
this.sfnTaskStateMachineArn = new cdk.CfnOutput(
this,
'sfnTaskStateMachineArn',
{
value: taskStateMachine.stateMachineArn,
},
)
// Csv import
const csvRowsHandlerState = lambdaInvoke(
'csv_rows_handler',
null,
cdk.aws_stepfunctions.IntegrationPattern.REQUEST_RESPONSE,
)
const sfnImportCsvDefinition = new DistributedMap(this, 'import-csv', {
maxConcurrency: 50,
})
.setLabel('import-csv')
.setItemReader({
Resource: 'arn:aws:states:::s3:getObject',
ReaderConfig: {
InputType: 'CSV',
CSVHeaderLocation: 'FIRST_ROW',
},
Parameters: {
'Bucket.$': '$.bucket',
'Key.$': '$.key',
},
})
.setItemBatcher({
MaxInputBytesPerBatch: 10,
BatchInput: {
'Attributes.$': '$',
},
})
.itemProcessor(csvRowsHandlerState, {
executionType: cdk.aws_stepfunctions.ProcessorType.EXPRESS,
})
const sfnImportCsvLogGroup = new cdk.aws_logs.LogGroup(
this,
'import-csv-handler-sfn-log',
{
logGroupName: `/aws/vendedlogs/states/${prefix}-import-csv-handler-state-machine-logs`, // Specify a log group name
removalPolicy: cdk.RemovalPolicy.DESTROY, // Policy for log group removal
retention: cdk.aws_logs.RetentionDays.SIX_MONTHS,
},
)
const importCsvStateMachine = new cdk.aws_stepfunctions.StateMachine(
this,
'import-csv-state-machine',
{
stateMachineName: importCsvStateMachineName,
comment: 'A state machine for import-csv module handler',
definitionBody: cdk.aws_stepfunctions.DefinitionBody.fromChainable(
sfnImportCsvDefinition,
),
tracingEnabled: true,
logs: {
destination: sfnImportCsvLogGroup,
level: cdk.aws_stepfunctions.LogLevel.ALL, // Log level (ALL, ERROR, or FATAL)
},
},
)
// add event sources to lambda event
lambdaApi.addEventSource(
new cdk.aws_lambda_event_sources.SqsEventSource(taskSqs, {
batchSize: 1,
}),
)
lambdaApi.addEventSource(
new cdk.aws_lambda_event_sources.SqsEventSource(notifySqs, {
batchSize: 1,
}),
)
lambdaApi.addEventSource(
new cdk.aws_lambda_event_sources.SqsEventSource(subTaskStatusSqs, {
batchSize: 1,
}),
)
lambdaApi.addEventSource(
new cdk.aws_lambda_event_sources.SqsEventSource(importActionSqs, {
batchSize: 1,
}),
)
// dynamodb event source
const tableNames = ['tasks', 'sample-command', 'import_tmp']
for (const tableName of tableNames) {
const tableDesc = new cdk.custom_resources.AwsCustomResource(
this,
tableName + '-decs',
{
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 tableCdk = cdk.aws_dynamodb.Table.fromTableAttributes(
this,
tableName + '-table',
{
tableArn: tableDesc.getResponseField('Table.TableArn'),
tableStreamArn: tableDesc.getResponseField('Table.LatestStreamArn'),
},
)
lambdaApi.addEventSource(
new cdk.aws_lambda_event_sources.DynamoEventSource(tableCdk, {
startingPosition: cdk.aws_lambda.StartingPosition.TRIM_HORIZON,
batchSize: 1,
filters: [
cdk.aws_lambda.FilterCriteria.filter({
eventName: cdk.aws_lambda.FilterRule.isEqual('INSERT'),
}),
],
}),
)
}
// add lambda role
userPool.grant(
lambdaApi,
'cognito-idp:AdminGetUser',
'cognito-idp:AdminAddUserToGroup',
'cognito-idp:AdminCreateUser',
'cognito-idp:AdminDeleteUser',
'cognito-idp:AdminDisableUser',
'cognito-idp:AdminEnableUser',
'cognito-idp:AdminSetUserPassword',
'cognito-idp:AdminResetUserPassword',
'cognito-idp:AdminUpdateUserAttributes',
)
ddbBucket.grantReadWrite(lambdaApi)
ddbBucket.grantRead(importCsvStateMachine)
importCsvStateMachine.role?.attachInlinePolicy(
new cdk.aws_iam.Policy(this, 'csv-import-map-policy', {
statements: [
new cdk.aws_iam.PolicyStatement({
actions: ['states:StartExecution'],
resources: [importCsvSfnArn],
}),
],
}),
)
publicBucket.grantReadWrite(lambdaApi)
mainSns.grantPublish(lambdaApi)
alarmSns.grantPublish(lambdaApi)
taskSqs.grantSendMessages(lambdaApi)
notifySqs.grantSendMessages(lambdaApi)
appSyncApi.grantMutation(lambdaApi)
// Define an IAM policy for full DynamoDB access
const dynamoDbTablePrefixArn = cdk.Arn.format({
partition: 'aws',
region: this.region,
account: this.account,
service: 'dynamodb',
resource: 'table',
resourceName: prefix + '*',
})
const dynamodbPolicy = new cdk.aws_iam.PolicyStatement({
actions: [
'dynamodb:PutItem',
'dynamodb:UpdateItem',
'dynamodb:GetItem',
'dynamodb:Query',
],
resources: [dynamoDbTablePrefixArn], // Access to all resources
})
// Attach the policy to the Lambda function's execution role
lambdaApi.role?.attachInlinePolicy(
new cdk.aws_iam.Policy(this, 'lambda-api-ddb-policy', {
statements: [dynamodbPolicy],
}),
)
const sfnPolicy = new cdk.aws_iam.PolicyStatement({
actions: [
'states:StartExecution',
'states:GetExecutionHistory',
'states:DescribeExecution',
],
resources: [commandSfnArn],
})
const taskSfnPolicy = new cdk.aws_iam.PolicyStatement({
actions: ['states:*'],
resources: [taskSfnArn], // Access to all resources
})
const importCsvSfnPolicy = new cdk.aws_iam.PolicyStatement({
actions: ['states:*'],
resources: [importCsvSfnArn], // Access to all resources
})
// Attach the policy to the Lambda function's execution role
lambdaApi.role?.attachInlinePolicy(
new cdk.aws_iam.Policy(this, 'lambda-event-sfn-policy', {
statements: [sfnPolicy],
}),
)
const sesPolicy = new cdk.aws_iam.PolicyStatement({
actions: ['ses:SendEmail', 'ses:SendTemplatedEmail'],
resources: ['*'],
})
lambdaApi.role?.attachInlinePolicy(
new cdk.aws_iam.Policy(this, 'lambda-task-sfn-step-function-policy', {
statements: [taskSfnPolicy.copy()],
}),
)
lambdaApi.role?.attachInlinePolicy(
new cdk.aws_iam.Policy(
this,
'lambda-import-csv-sfn-step-function-policy',
{
statements: [importCsvSfnPolicy.copy()],
},
),
)
// Attach the policy to the Lambda function's execution role
lambdaApi.role?.attachInlinePolicy(
new cdk.aws_iam.Policy(this, 'lambda-ses-policy', {
statements: [sesPolicy],
}),
)
const ssmPolicy = new cdk.aws_iam.PolicyStatement({
actions: ['ssm:GetParameter', 'kms:Decrypt'],
resources: ['*'],
})
// allow lambdaApi role to access ssm
lambdaApi.role?.attachInlinePolicy(
new cdk.aws_iam.Policy(this, 'lambda-api-ssm-policy', {
statements: [ssmPolicy],
}),
)
if (!!taskRole) {
ddbBucket.grantReadWrite(taskRole)
publicBucket.grantReadWrite(taskRole)
mainSns.grantPublish(taskRole)
alarmSns.grantPublish(taskRole)
taskSqs.grantSendMessages(taskRole)
notifySqs.grantSendMessages(taskRole)
appSyncApi.grantMutation(taskRole)
taskRole.addToPrincipalPolicy(
new cdk.aws_iam.PolicyStatement({
actions: [
'ssmmessages:CreateControlChannel',
'ssmmessages:CreateDataChannel',
'ssmmessages:OpenControlChannel',
'ssmmessages:OpenDataChannel',
],
resources: ['*'],
}),
)
taskRole.attachInlinePolicy(
new cdk.aws_iam.Policy(this, 'ecs-api-ddb-policy', {
statements: [dynamodbPolicy],
}),
)
taskRole.attachInlinePolicy(
new cdk.aws_iam.Policy(this, 'ecs-event-sfn-policy', {
statements: [sfnPolicy],
}),
)
taskRole.attachInlinePolicy(
new cdk.aws_iam.Policy(this, 'ecs-ses-policy', {
statements: [sesPolicy],
}),
)
taskRole.attachInlinePolicy(
new cdk.aws_iam.Policy(this, 'ecs-api-ssm-policy', {
statements: [ssmPolicy],
}),
)
}
}
}