@mbc-cqrs-serverless/cli 0.1.22-beta.0 → 0.1.24-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,948 @@
1
+ import * as cdk from 'aws-cdk-lib'
2
+ import * as apigwv2 from 'aws-cdk-lib/aws-apigatewayv2'
3
+ import * as apigatewayv2_authorizers from 'aws-cdk-lib/aws-apigatewayv2-authorizers'
4
+ import * as apigwv2_integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations'
5
+ import { Construct } from 'constructs'
6
+ import { randomBytes } from 'crypto'
7
+
8
+ import { IgnoreMode } from 'aws-cdk-lib'
9
+ import { Repository } from 'aws-cdk-lib/aws-ecr'
10
+ import { DockerImageAsset, Platform } from 'aws-cdk-lib/aws-ecr-assets'
11
+ import { ContainerImage } from 'aws-cdk-lib/aws-ecs'
12
+ import { ApplicationLoadBalancedFargateService } from 'aws-cdk-lib/aws-ecs-patterns'
13
+ import { DockerImageName, ECRDeployment } from 'cdk-ecr-deployment'
14
+ import * as path from 'path'
15
+ import {
16
+ ACM_APPSYNC_CERTIFICATE_ARN,
17
+ ACM_HTTP_CERTIFICATE_ARN,
18
+ HOSTED_ZONE_ID,
19
+ HOSTED_ZONE_NAME,
20
+ } from '../config'
21
+ import { Config } from '../config/type'
22
+ import { buildApp } from './build-app'
23
+
24
+ export interface InfraStackProps extends cdk.StackProps {
25
+ config: Config
26
+ }
27
+
28
+ export class InfraStack extends cdk.Stack {
29
+ public readonly userPoolId: cdk.CfnOutput
30
+ public readonly userPoolClientId: cdk.CfnOutput
31
+ public readonly graphqlApiUrl: cdk.CfnOutput
32
+ public readonly graphqlApiKey: cdk.CfnOutput
33
+ public readonly httpApiUrl: cdk.CfnOutput
34
+ public readonly stateMachineArn: cdk.CfnOutput
35
+ public readonly httpDistributionDomain: cdk.CfnOutput
36
+
37
+ constructor(scope: Construct, id: string, props: InfraStackProps) {
38
+ super(scope, id, props)
39
+
40
+ const name = props.config.appName
41
+ const env = props.config.env
42
+ const prefix = `${env}-${name}-`
43
+ const originVerifyToken = prefix + randomBytes(32).toString('hex')
44
+
45
+ cdk.Tags.of(scope).add('name', props.config.appName)
46
+ cdk.Tags.of(scope).add('env', props.config.env)
47
+
48
+ // Cognito
49
+ let userPool: cdk.aws_cognito.IUserPool
50
+ if (props.config.userPoolId) {
51
+ userPool = cdk.aws_cognito.UserPool.fromUserPoolId(
52
+ this,
53
+ 'main-user-pool',
54
+ props.config.userPoolId,
55
+ )
56
+ } else {
57
+ // create new cognito
58
+ userPool = new cdk.aws_cognito.UserPool(this, prefix + 'user-pool', {
59
+ userPoolName: prefix + 'user-pool',
60
+ selfSignUpEnabled: false,
61
+ signInAliases: {
62
+ username: true,
63
+ preferredUsername: true,
64
+ },
65
+ passwordPolicy: {
66
+ minLength: 6,
67
+ requireDigits: false,
68
+ requireLowercase: false,
69
+ requireSymbols: false,
70
+ requireUppercase: false,
71
+ },
72
+ mfa: cdk.aws_cognito.Mfa.OFF,
73
+ accountRecovery: cdk.aws_cognito.AccountRecovery.NONE,
74
+ customAttributes: {
75
+ cci_code: new cdk.aws_cognito.StringAttribute({
76
+ mutable: true,
77
+ maxLen: 20,
78
+ }),
79
+ company_code: new cdk.aws_cognito.StringAttribute({
80
+ mutable: true,
81
+ maxLen: 50,
82
+ }),
83
+ member_id: new cdk.aws_cognito.StringAttribute({
84
+ mutable: true,
85
+ maxLen: 2024,
86
+ }),
87
+ user_type: new cdk.aws_cognito.StringAttribute({
88
+ mutable: true,
89
+ maxLen: 20,
90
+ }),
91
+ },
92
+ email: cdk.aws_cognito.UserPoolEmail.withCognito(),
93
+ deletionProtection: true,
94
+ })
95
+ }
96
+ this.userPoolId = new cdk.CfnOutput(this, 'UserPoolId', {
97
+ value: userPool.userPoolId,
98
+ })
99
+
100
+ // SNS
101
+ const mainSns = new cdk.aws_sns.Topic(this, 'main-sns', {
102
+ topicName: prefix + 'main-sns',
103
+ })
104
+
105
+ const alarmSns = new cdk.aws_sns.Topic(this, 'alarm-sns', {
106
+ topicName: prefix + 'alarm-sns',
107
+ })
108
+ // SQS
109
+ const taskDlSqs = new cdk.aws_sqs.Queue(this, 'task-dead-letter-sqs', {
110
+ queueName: prefix + 'task-dead-letter-queue',
111
+ })
112
+ const taskSqs = new cdk.aws_sqs.Queue(this, 'task-sqs', {
113
+ queueName: prefix + 'task-action-queue',
114
+ deadLetterQueue: {
115
+ queue: taskDlSqs,
116
+ maxReceiveCount: 5,
117
+ },
118
+ })
119
+
120
+ alarmSns.addSubscription(
121
+ new cdk.aws_sns_subscriptions.SqsSubscription(taskDlSqs, {
122
+ rawMessageDelivery: true,
123
+ }),
124
+ )
125
+
126
+ mainSns.addSubscription(
127
+ new cdk.aws_sns_subscriptions.SqsSubscription(taskSqs, {
128
+ rawMessageDelivery: true,
129
+ filterPolicy: {
130
+ action: cdk.aws_sns.SubscriptionFilter.stringFilter({
131
+ allowlist: ['task-execute'],
132
+ }),
133
+ },
134
+ }),
135
+ )
136
+ const notifySqs = new cdk.aws_sqs.Queue(this, 'notify-sqs', {
137
+ queueName: prefix + 'notification-queue',
138
+ })
139
+ mainSns.addSubscription(
140
+ new cdk.aws_sns_subscriptions.SqsSubscription(notifySqs, {
141
+ rawMessageDelivery: true,
142
+ filterPolicy: {
143
+ action: cdk.aws_sns.SubscriptionFilter.stringFilter({
144
+ allowlist: ['command-status', 'task-status'],
145
+ }),
146
+ },
147
+ }),
148
+ )
149
+ // host zone
150
+ const hostedZone = cdk.aws_route53.HostedZone.fromHostedZoneAttributes(
151
+ this,
152
+ 'HostedZone',
153
+ {
154
+ hostedZoneId: HOSTED_ZONE_ID,
155
+ zoneName: HOSTED_ZONE_NAME,
156
+ },
157
+ )
158
+
159
+ // AppSync
160
+ const appSyncCertificate =
161
+ cdk.aws_certificatemanager.Certificate.fromCertificateArn(
162
+ this,
163
+ 'appsync-certificate',
164
+ ACM_APPSYNC_CERTIFICATE_ARN,
165
+ )
166
+
167
+ const appSyncApi = new cdk.aws_appsync.GraphqlApi(this, 'realtime', {
168
+ name: prefix + 'realtime',
169
+ definition: cdk.aws_appsync.Definition.fromFile('asset/schema.graphql'), // Define the schema
170
+ authorizationConfig: {
171
+ defaultAuthorization: {
172
+ authorizationType: cdk.aws_appsync.AuthorizationType.API_KEY, // Defining authorization type
173
+ apiKeyConfig: {
174
+ expires: cdk.Expiration.after(cdk.Duration.days(365)), // Set expiration for API Key
175
+ },
176
+ },
177
+ additionalAuthorizationModes: [
178
+ {
179
+ authorizationType: cdk.aws_appsync.AuthorizationType.IAM,
180
+ },
181
+ {
182
+ authorizationType: cdk.aws_appsync.AuthorizationType.USER_POOL,
183
+ userPoolConfig: { userPool },
184
+ },
185
+ ],
186
+ },
187
+ xrayEnabled: true, // Enable X-Ray for monitoring
188
+ domainName: {
189
+ certificate: appSyncCertificate,
190
+ domainName: props.config.domain.appsync,
191
+ },
192
+ })
193
+
194
+ const noneDS = appSyncApi.addNoneDataSource('NoneDataSource')
195
+ noneDS.createResolver('sendMessageResolver', {
196
+ typeName: 'Mutation',
197
+ fieldName: 'sendMessage',
198
+ requestMappingTemplate: cdk.aws_appsync.MappingTemplate.fromString(
199
+ '{"version": "2018-05-29","payload": $util.toJson($context.arguments.message)}',
200
+ ),
201
+ responseMappingTemplate: cdk.aws_appsync.MappingTemplate.fromString(
202
+ '$util.toJson($context.result)',
203
+ ),
204
+ })
205
+
206
+ // route to AppSync
207
+ new cdk.aws_route53.CnameRecord(this, `AppSyncCnameRecord`, {
208
+ zone: hostedZone,
209
+ recordName: props.config.domain.appsync,
210
+ domainName: appSyncApi.appSyncDomainName,
211
+ })
212
+
213
+ this.graphqlApiUrl = new cdk.CfnOutput(this, 'GraphQLAPIURL', {
214
+ value: appSyncApi.graphqlUrl,
215
+ })
216
+ this.graphqlApiKey = new cdk.CfnOutput(this, 'GraphQLAPIKey', {
217
+ value: appSyncApi.apiKey || '',
218
+ })
219
+ // S3
220
+ const ddbBucket = new cdk.aws_s3.Bucket(this, 'ddb-attributes', {
221
+ bucketName: prefix + 'ddb-attributes', // Globally unique bucket name
222
+ versioned: false,
223
+ publicReadAccess: false, // Block public read access
224
+ blockPublicAccess: cdk.aws_s3.BlockPublicAccess.BLOCK_ALL, // Block all public access
225
+ removalPolicy: cdk.RemovalPolicy.DESTROY, // Define removal policy (use with caution in production)
226
+ cors: [
227
+ {
228
+ allowedHeaders: ['*'],
229
+ allowedMethods: [
230
+ cdk.aws_s3.HttpMethods.GET,
231
+ cdk.aws_s3.HttpMethods.PUT,
232
+ cdk.aws_s3.HttpMethods.POST,
233
+ ],
234
+ allowedOrigins: ['*'],
235
+ maxAge: 3000,
236
+ },
237
+ ],
238
+ })
239
+
240
+ const publicBucket = new cdk.aws_s3.Bucket(this, 'public-bucket', {
241
+ bucketName: prefix + 'public', // Globally unique bucket name
242
+ versioned: false,
243
+ publicReadAccess: false, // Block public read access
244
+ blockPublicAccess: cdk.aws_s3.BlockPublicAccess.BLOCK_ALL, // Block all public access
245
+ removalPolicy: cdk.RemovalPolicy.DESTROY, // Define removal policy (use with caution in production)
246
+ cors: [
247
+ {
248
+ allowedHeaders: ['*'],
249
+ allowedMethods: [
250
+ cdk.aws_s3.HttpMethods.GET,
251
+ cdk.aws_s3.HttpMethods.PUT,
252
+ cdk.aws_s3.HttpMethods.POST,
253
+ ],
254
+ allowedOrigins: ['*'],
255
+ maxAge: 3000,
256
+ },
257
+ ],
258
+ })
259
+ // cloudfront
260
+ const publicBucketOAI = new cdk.aws_cloudfront.OriginAccessIdentity(
261
+ this,
262
+ 'public-bucket-OAI',
263
+ )
264
+ publicBucket.addToResourcePolicy(
265
+ new cdk.aws_iam.PolicyStatement({
266
+ actions: ['s3:GetObject'],
267
+ effect: cdk.aws_iam.Effect.ALLOW,
268
+ resources: [publicBucket.arnForObjects('*')],
269
+ principals: [
270
+ new cdk.aws_iam.CanonicalUserPrincipal(
271
+ publicBucketOAI.cloudFrontOriginAccessIdentityS3CanonicalUserId,
272
+ ),
273
+ ],
274
+ }),
275
+ )
276
+ const publicBucketDistribution = new cdk.aws_cloudfront.Distribution(
277
+ this,
278
+ 'public-bucket-distribution',
279
+ {
280
+ defaultBehavior: {
281
+ allowedMethods: cdk.aws_cloudfront.AllowedMethods.ALLOW_GET_HEAD,
282
+ cachedMethods: cdk.aws_cloudfront.CachedMethods.CACHE_GET_HEAD,
283
+ cachePolicy: cdk.aws_cloudfront.CachePolicy.CACHING_OPTIMIZED,
284
+ viewerProtocolPolicy:
285
+ cdk.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
286
+ origin: new cdk.aws_cloudfront_origins.S3Origin(publicBucket, {
287
+ originAccessIdentity: publicBucketOAI,
288
+ }),
289
+ },
290
+ priceClass: cdk.aws_cloudfront.PriceClass.PRICE_CLASS_200,
291
+ geoRestriction: cdk.aws_cloudfront.GeoRestriction.allowlist('JP', 'VN'),
292
+ },
293
+ )
294
+
295
+ // VPC
296
+ const vpc = cdk.aws_ec2.Vpc.fromLookup(this, 'main-vpc', {
297
+ vpcId: props.config.vpc.id,
298
+ })
299
+
300
+ const subnets = cdk.aws_ec2.SubnetFilter.byIds(props.config.vpc.subnetIds)
301
+ const securityGroups = props.config.vpc.securityGroupIds.map((id, idx) =>
302
+ cdk.aws_ec2.SecurityGroup.fromSecurityGroupId(
303
+ this,
304
+ 'main-security-group-' + idx,
305
+ id,
306
+ ),
307
+ )
308
+ // Lambda Layer
309
+ const { layerPath, appPath } = buildApp(env)
310
+ console.log('dist path:', layerPath, appPath)
311
+ const lambdaLayer = new cdk.aws_lambda.LayerVersion(this, 'main-layer', {
312
+ layerVersionName: prefix + 'main-layer',
313
+ code: cdk.aws_lambda.AssetCode.fromAsset(layerPath),
314
+ compatibleRuntimes: [cdk.aws_lambda.Runtime.NODEJS_18_X],
315
+ compatibleArchitectures: [cdk.aws_lambda.Architecture.ARM_64],
316
+ })
317
+
318
+ const commandSfnArn = cdk.Arn.format({
319
+ partition: 'aws',
320
+ region: this.region,
321
+ account: this.account,
322
+ service: 'states',
323
+ resource: 'stateMachine',
324
+ resourceName: prefix + 'command-handler',
325
+ arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME,
326
+ })
327
+
328
+ // Lambda api ( arm64 )
329
+ const execEnv = {
330
+ NODE_OPTIONS: '--enable-source-maps',
331
+ TZ: 'Asia/Tokyo',
332
+ NODE_ENV: env,
333
+ APP_NAME: name,
334
+ LOG_LEVEL: props.config.logLevel?.level || 'info',
335
+ EVENT_SOURCE_DISABLED: 'false',
336
+ ATTRIBUTE_LIMIT_SIZE: '389120',
337
+ S3_BUCKET_NAME: ddbBucket.bucketName,
338
+ SFN_COMMAND_ARN: commandSfnArn,
339
+ SNS_TOPIC_ARN: mainSns.topicArn,
340
+ COGNITO_USER_POOL_ID: userPool.userPoolId,
341
+ APPSYNC_ENDPOINT: appSyncApi.graphqlUrl,
342
+ SES_FROM_EMAIL: props.config.fromEmailAddress,
343
+ DATABASE_URL: `postgresql://${props.config.rds.accountSsmKey}@${props.config.rds.endpoint}/${props.config.rds.dbName}?schema=public`,
344
+ S3_PUBLIC_BUCKET_NAME: publicBucket.bucketName,
345
+ FRONT_BASE_URL: props.config.frontBaseUrl,
346
+ }
347
+ const lambdaApi = new cdk.aws_lambda.Function(this, 'lambda-api', {
348
+ vpc,
349
+ vpcSubnets: {
350
+ subnetFilters: [subnets],
351
+ },
352
+ securityGroups,
353
+ architecture: cdk.aws_lambda.Architecture.ARM_64,
354
+ functionName: prefix + 'lambda-api',
355
+ layers: [lambdaLayer],
356
+ code: cdk.aws_lambda.Code.fromAsset(appPath),
357
+ handler: 'main.handler',
358
+ runtime: cdk.aws_lambda.Runtime.NODEJS_LATEST,
359
+ timeout: cdk.Duration.seconds(30),
360
+ memorySize: 512,
361
+ tracing: cdk.aws_lambda.Tracing.ACTIVE,
362
+ loggingFormat: cdk.aws_lambda.LoggingFormat.JSON,
363
+ applicationLogLevelV2: props.config.logLevel?.lambdaApplication,
364
+ systemLogLevelV2: props.config.logLevel?.lambdaSystem,
365
+ environment: execEnv,
366
+ })
367
+
368
+ // API GW
369
+ const httpApi = new apigwv2.HttpApi(this, 'main-api', {
370
+ description: 'HTTP API for Lambda integration',
371
+ apiName: prefix + 'api',
372
+ corsPreflight: {
373
+ allowOrigins: ['*'],
374
+ allowCredentials: false,
375
+ allowHeaders: ['*'],
376
+ allowMethods: [apigwv2.CorsHttpMethod.ANY],
377
+ maxAge: cdk.Duration.hours(1),
378
+ },
379
+ })
380
+ const lambdaIntegration = new apigwv2_integrations.HttpLambdaIntegration(
381
+ 'main-api-lambda',
382
+ lambdaApi,
383
+ )
384
+ // event routes
385
+ httpApi.addRoutes({
386
+ path: '/event/{proxy+}',
387
+ integration: lambdaIntegration,
388
+ authorizer: new apigatewayv2_authorizers.HttpIamAuthorizer(),
389
+ })
390
+
391
+ // api protected routes
392
+ const userPoolClient = new cdk.aws_cognito.UserPoolClient(
393
+ this,
394
+ 'apigw-client',
395
+ {
396
+ userPool,
397
+ authFlows: {
398
+ userPassword: true,
399
+ userSrp: true,
400
+ },
401
+ },
402
+ )
403
+
404
+ this.userPoolClientId = new cdk.CfnOutput(this, 'UserPoolClientId', {
405
+ value: userPoolClient.userPoolClientId,
406
+ })
407
+
408
+ const authorizer = new apigatewayv2_authorizers.HttpUserPoolAuthorizer(
409
+ 'CognitoAuthorizer',
410
+ userPool,
411
+ {
412
+ userPoolClients: [userPoolClient],
413
+ },
414
+ )
415
+
416
+ let apiIntegration: apigwv2.HttpRouteIntegration
417
+ let taskRole: cdk.aws_iam.Role | undefined
418
+ if (!props.config.ecs) {
419
+ apiIntegration = lambdaIntegration
420
+ } else {
421
+ // ecs api
422
+ const resp = new Repository(this, 'main-ecr-repo', {
423
+ repositoryName: `${prefix}api`,
424
+ removalPolicy: cdk.RemovalPolicy.RETAIN,
425
+ })
426
+
427
+ const image = new DockerImageAsset(this, 'main-image', {
428
+ directory: path.resolve(__dirname, '../..'),
429
+ platform: Platform.LINUX_AMD64,
430
+ ignoreMode: IgnoreMode.DOCKER,
431
+ })
432
+
433
+ const imageTag = process.env.CODEBUILD_RESOLVED_SOURCE_VERSION
434
+ ? process.env.CODEBUILD_RESOLVED_SOURCE_VERSION.substring(0, 4)
435
+ : 'latest'
436
+
437
+ new ECRDeployment(this, `${prefix}deploy`, {
438
+ src: new DockerImageName(image.imageUri),
439
+ dest: new DockerImageName(`${resp.repositoryUri}:${imageTag}`),
440
+ })
441
+
442
+ taskRole = new cdk.aws_iam.Role(this, 'ecs-role', {
443
+ assumedBy: new cdk.aws_iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
444
+ })
445
+
446
+ const ecsService = new ApplicationLoadBalancedFargateService(
447
+ this,
448
+ 'main-service',
449
+ {
450
+ vpc,
451
+ taskSubnets: {
452
+ subnetFilters: [subnets],
453
+ },
454
+ securityGroups,
455
+ circuitBreaker: {
456
+ rollback: props.config.ecs.autoRollback,
457
+ },
458
+ publicLoadBalancer: false,
459
+ memoryLimitMiB: props.config.ecs.memory,
460
+ cpu: props.config.ecs.cpu,
461
+ desiredCount: props.config.ecs.minInstances,
462
+ taskImageOptions: {
463
+ image: ContainerImage.fromDockerImageAsset(image),
464
+ environment: {
465
+ ...execEnv,
466
+ APP_PORT: '80',
467
+ EVENT_SOURCE_DISABLED: 'true',
468
+ PRISMA_EXPLICIT_CONNECT: 'false',
469
+ },
470
+ secrets: {
471
+ DATABASE_USER_PASS: cdk.aws_ecs.Secret.fromSsmParameter(
472
+ cdk.aws_ssm.StringParameter.fromSecureStringParameterAttributes(
473
+ this,
474
+ 'dbUserPass',
475
+ {
476
+ parameterName: props.config.rds.accountSsmKey,
477
+ },
478
+ ),
479
+ ),
480
+ },
481
+ taskRole,
482
+ },
483
+ },
484
+ )
485
+
486
+ if (props.config.ecs.cpuThreshold) {
487
+ const scalableTarget = ecsService.service.autoScaleTaskCount({
488
+ minCapacity: props.config.ecs.minInstances,
489
+ maxCapacity: props.config.ecs.maxInstances,
490
+ })
491
+
492
+ scalableTarget.scaleOnCpuUtilization('CpuScaling', {
493
+ targetUtilizationPercent: props.config.ecs.cpuThreshold,
494
+ })
495
+ }
496
+
497
+ const vpcLink = new apigwv2.VpcLink(this, 'ecs-vpc-link', {
498
+ vpc,
499
+ })
500
+ const vpcLinkIntegration = new apigwv2_integrations.HttpAlbIntegration(
501
+ 'ecs-vpc-link-integration',
502
+ ecsService.loadBalancer.listeners[0],
503
+ {
504
+ vpcLink,
505
+ parameterMapping: new apigwv2.ParameterMapping()
506
+ .appendHeader(
507
+ 'x-source-ip',
508
+ apigwv2.MappingValue.contextVariable('identity.sourceIp'),
509
+ )
510
+ .appendHeader(
511
+ 'x-request-id',
512
+ apigwv2.MappingValue.contextVariable('extendedRequestId'),
513
+ ),
514
+ },
515
+ )
516
+ apiIntegration = vpcLinkIntegration
517
+ }
518
+ // health check api (public)
519
+ httpApi.addRoutes({
520
+ path: '/',
521
+ methods: [apigwv2.HttpMethod.GET],
522
+ integration: apiIntegration,
523
+ })
524
+ // protected api
525
+ httpApi.addRoutes({
526
+ path: '/{proxy+}',
527
+ methods: [
528
+ apigwv2.HttpMethod.HEAD,
529
+ apigwv2.HttpMethod.GET,
530
+ apigwv2.HttpMethod.POST,
531
+ apigwv2.HttpMethod.DELETE,
532
+ apigwv2.HttpMethod.PUT,
533
+ apigwv2.HttpMethod.PATCH,
534
+ ],
535
+ integration: apiIntegration,
536
+ authorizer,
537
+ })
538
+ // Output the URL of the HTTP API
539
+ this.httpApiUrl = new cdk.CfnOutput(this, 'HttpApiUrl', {
540
+ value: httpApi.url!,
541
+ })
542
+
543
+ // cloudfront to HTTP API
544
+ const httpDistributionCertificate =
545
+ cdk.aws_certificatemanager.Certificate.fromCertificateArn(
546
+ this,
547
+ 'http-distribution-certificate',
548
+ ACM_HTTP_CERTIFICATE_ARN,
549
+ )
550
+ const httpDistribution = new cdk.aws_cloudfront.Distribution(
551
+ this,
552
+ 'http-distribution',
553
+ {
554
+ defaultBehavior: {
555
+ origin: new cdk.aws_cloudfront_origins.HttpOrigin(
556
+ `${httpApi.apiId}.execute-api.${this.region}.amazonaws.com`,
557
+ {
558
+ customHeaders: {
559
+ 'X-Origin-Verify': originVerifyToken,
560
+ },
561
+ },
562
+ ),
563
+ originRequestPolicy:
564
+ cdk.aws_cloudfront.OriginRequestPolicy
565
+ .ALL_VIEWER_EXCEPT_HOST_HEADER,
566
+ responseHeadersPolicy:
567
+ cdk.aws_cloudfront.ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS,
568
+ allowedMethods: cdk.aws_cloudfront.AllowedMethods.ALLOW_ALL,
569
+ cachePolicy: cdk.aws_cloudfront.CachePolicy.CACHING_DISABLED,
570
+ viewerProtocolPolicy:
571
+ cdk.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
572
+ },
573
+ priceClass: cdk.aws_cloudfront.PriceClass.PRICE_CLASS_200,
574
+ geoRestriction: cdk.aws_cloudfront.GeoRestriction.allowlist('JP', 'VN'),
575
+ domainNames: [props.config.domain.http],
576
+ certificate: httpDistributionCertificate,
577
+ webAclId: props.config.wafArn,
578
+ enableIpv6: false,
579
+ },
580
+ )
581
+
582
+ new cdk.aws_route53.CnameRecord(this, 'http-distribution-a-record', {
583
+ zone: hostedZone,
584
+ recordName: props.config.domain.http,
585
+ domainName: httpDistribution.distributionDomainName,
586
+ })
587
+
588
+ this.httpDistributionDomain = new cdk.CfnOutput(
589
+ this,
590
+ 'http-distribution-domain',
591
+ {
592
+ value: httpDistribution.distributionDomainName,
593
+ },
594
+ )
595
+
596
+ // api gateway logging
597
+ // Setup the access log for APIGWv2
598
+ const httpApiAccessLogs = new cdk.aws_logs.LogGroup(
599
+ this,
600
+ 'http-api-AccessLogs',
601
+ )
602
+ const httpApiDefaultStage = httpApi.defaultStage?.node
603
+ .defaultChild as cdk.aws_apigatewayv2.CfnStage
604
+ httpApiDefaultStage.accessLogSettings = {
605
+ destinationArn: httpApiAccessLogs.logGroupArn,
606
+ format: JSON.stringify({
607
+ requestId: '$context.requestId',
608
+ ip: '$context.identity.sourceIp',
609
+ userAgent: '$context.identity.userAgent',
610
+ sourceIp: '$context.identity.sourceIp',
611
+ requestTime: '$context.requestTime',
612
+ requestTimeEpoch: '$context.requestTimeEpoch',
613
+ httpMethod: '$context.httpMethod',
614
+ routeKey: '$context.routeKey',
615
+ path: '$context.path',
616
+ status: '$context.status',
617
+ protocol: '$context.protocol',
618
+ responseLength: '$context.responseLength',
619
+ domainName: '$context.domainName',
620
+ responseLatency: '$context.responseLatency',
621
+ integrationLatency: '$context.integrationLatency',
622
+ username: '$context.authorizer.claims.sub',
623
+ }),
624
+ }
625
+ httpApiDefaultStage.defaultRouteSettings = {
626
+ detailedMetricsEnabled: true,
627
+ }
628
+
629
+ // StepFunction
630
+ // Define the lambda invoke task with common configurations
631
+ const lambdaInvoke = (
632
+ stateName: string,
633
+ nextState: cdk.aws_stepfunctions.IChainable | null,
634
+ integrationPattern: cdk.aws_stepfunctions.IntegrationPattern,
635
+ ) => {
636
+ const payloadObject: {
637
+ [key: string]: any
638
+ } = {
639
+ 'input.$': '$',
640
+ 'context.$': '$$',
641
+ }
642
+ if (
643
+ integrationPattern ===
644
+ cdk.aws_stepfunctions.IntegrationPattern.WAIT_FOR_TASK_TOKEN
645
+ ) {
646
+ payloadObject['taskToken'] = cdk.aws_stepfunctions.JsonPath.taskToken // '$$.Task.Token'
647
+ }
648
+ const lambdaTask = new cdk.aws_stepfunctions_tasks.LambdaInvoke(
649
+ this,
650
+ stateName,
651
+ {
652
+ lambdaFunction: lambdaApi,
653
+ payload: cdk.aws_stepfunctions.TaskInput.fromObject(payloadObject),
654
+ retryOnServiceExceptions: true,
655
+ stateName,
656
+ outputPath: '$.Payload[0][0]',
657
+ integrationPattern,
658
+ },
659
+ )
660
+ if (nextState) {
661
+ return lambdaTask.next(nextState)
662
+ }
663
+ return lambdaTask
664
+ }
665
+
666
+ // Define states
667
+ const fail = new cdk.aws_stepfunctions.Fail(this, 'fail', {
668
+ stateName: 'fail',
669
+ causePath: '$.cause',
670
+ errorPath: '$.error',
671
+ })
672
+ const success = new cdk.aws_stepfunctions.Succeed(this, 'success', {
673
+ stateName: 'success',
674
+ })
675
+ const finish = lambdaInvoke(
676
+ 'finish',
677
+ success,
678
+ cdk.aws_stepfunctions.IntegrationPattern.REQUEST_RESPONSE,
679
+ )
680
+ const syncData = lambdaInvoke(
681
+ 'sync_data',
682
+ null,
683
+ cdk.aws_stepfunctions.IntegrationPattern.REQUEST_RESPONSE,
684
+ )
685
+ // Define Map state
686
+ const syncDataAll = new cdk.aws_stepfunctions.Map(this, 'sync_data_all', {
687
+ stateName: 'sync_data_all',
688
+ maxConcurrency: 0,
689
+ itemsPath: cdk.aws_stepfunctions.JsonPath.stringAt('$'),
690
+ })
691
+ .itemProcessor(syncData)
692
+ .next(finish)
693
+ const transformData = lambdaInvoke(
694
+ 'transform_data',
695
+ syncDataAll,
696
+ cdk.aws_stepfunctions.IntegrationPattern.REQUEST_RESPONSE,
697
+ )
698
+ const historyCopy = lambdaInvoke(
699
+ 'history_copy',
700
+ transformData,
701
+ cdk.aws_stepfunctions.IntegrationPattern.REQUEST_RESPONSE,
702
+ )
703
+ const waitPrevCommand = lambdaInvoke(
704
+ 'wait_prev_command',
705
+ historyCopy,
706
+ cdk.aws_stepfunctions.IntegrationPattern.WAIT_FOR_TASK_TOKEN,
707
+ )
708
+
709
+ // Define Choice state
710
+ const checkVersionResult = new cdk.aws_stepfunctions.Choice(
711
+ this,
712
+ 'check_version_result',
713
+ {
714
+ stateName: 'check_version_result',
715
+ },
716
+ )
717
+ .when(
718
+ cdk.aws_stepfunctions.Condition.numberEquals('$.result', 0),
719
+ historyCopy,
720
+ )
721
+ .when(
722
+ cdk.aws_stepfunctions.Condition.numberEquals('$.result', 1),
723
+ waitPrevCommand,
724
+ )
725
+ .when(cdk.aws_stepfunctions.Condition.numberEquals('$.result', -1), fail)
726
+ .otherwise(waitPrevCommand)
727
+
728
+ const sfnDefinition = lambdaInvoke(
729
+ 'check_version',
730
+ checkVersionResult,
731
+ cdk.aws_stepfunctions.IntegrationPattern.REQUEST_RESPONSE,
732
+ )
733
+
734
+ const sfnLogGroup = new cdk.aws_logs.LogGroup(
735
+ this,
736
+ 'command-handler-sfn-log',
737
+ {
738
+ logGroupName: `/aws/vendedlogs/states/${prefix}-command-handler-state-machine-Logs`, // Specify a log group name
739
+ removalPolicy: cdk.RemovalPolicy.DESTROY, // Policy for log group removal
740
+ retention: cdk.aws_logs.RetentionDays.SIX_MONTHS,
741
+ },
742
+ )
743
+
744
+ // Define the state machine
745
+ const stateMachine = new cdk.aws_stepfunctions.StateMachine(
746
+ this,
747
+ 'command-handler-state-machine',
748
+ {
749
+ stateMachineName: prefix + 'command-handler',
750
+ comment: 'A state machine that run the command stream handler',
751
+ definitionBody:
752
+ cdk.aws_stepfunctions.DefinitionBody.fromChainable(sfnDefinition),
753
+ tracingEnabled: true,
754
+ logs: {
755
+ destination: sfnLogGroup,
756
+ level: cdk.aws_stepfunctions.LogLevel.ALL, // Log level (ALL, ERROR, or FATAL)
757
+ },
758
+ },
759
+ )
760
+
761
+ // Output the State Machine's ARN
762
+ this.stateMachineArn = new cdk.CfnOutput(this, 'StateMachineArn', {
763
+ value: stateMachine.stateMachineArn,
764
+ })
765
+
766
+ // add event sources to lambda event
767
+ lambdaApi.addEventSource(
768
+ new cdk.aws_lambda_event_sources.SqsEventSource(taskSqs, {
769
+ batchSize: 1,
770
+ }),
771
+ )
772
+ lambdaApi.addEventSource(
773
+ new cdk.aws_lambda_event_sources.SqsEventSource(notifySqs, {
774
+ batchSize: 1,
775
+ }),
776
+ )
777
+ // dynamodb event source
778
+ const tableNames = ['tasks', 'master-command']
779
+ for (const tableName of tableNames) {
780
+ const tableDesc = new cdk.custom_resources.AwsCustomResource(
781
+ this,
782
+ tableName + '-decs',
783
+ {
784
+ onCreate: {
785
+ service: 'DynamoDB',
786
+ action: 'describeTable',
787
+ parameters: {
788
+ TableName: prefix + tableName,
789
+ },
790
+ physicalResourceId:
791
+ cdk.custom_resources.PhysicalResourceId.fromResponse(
792
+ 'Table.TableArn',
793
+ ),
794
+ },
795
+ policy: cdk.custom_resources.AwsCustomResourcePolicy.fromSdkCalls({
796
+ resources:
797
+ cdk.custom_resources.AwsCustomResourcePolicy.ANY_RESOURCE,
798
+ }),
799
+ },
800
+ )
801
+ const tableCdk = cdk.aws_dynamodb.Table.fromTableAttributes(
802
+ this,
803
+ tableName + '-table',
804
+ {
805
+ tableArn: tableDesc.getResponseField('Table.TableArn'),
806
+ tableStreamArn: tableDesc.getResponseField('Table.LatestStreamArn'),
807
+ },
808
+ )
809
+ lambdaApi.addEventSource(
810
+ new cdk.aws_lambda_event_sources.DynamoEventSource(tableCdk, {
811
+ startingPosition: cdk.aws_lambda.StartingPosition.TRIM_HORIZON,
812
+ batchSize: 1,
813
+ filters: [
814
+ cdk.aws_lambda.FilterCriteria.filter({
815
+ eventName: cdk.aws_lambda.FilterRule.isEqual('INSERT'),
816
+ }),
817
+ ],
818
+ }),
819
+ )
820
+ }
821
+
822
+ // add lambda role
823
+ userPool.grant(
824
+ lambdaApi,
825
+ 'cognito-idp:AdminGetUser',
826
+ 'cognito-idp:AdminAddUserToGroup',
827
+ 'cognito-idp:AdminCreateUser',
828
+ 'cognito-idp:AdminDeleteUser',
829
+ 'cognito-idp:AdminDisableUser',
830
+ 'cognito-idp:AdminEnableUser',
831
+ 'cognito-idp:AdminSetUserPassword',
832
+ 'cognito-idp:AdminResetUserPassword',
833
+ 'cognito-idp:AdminUpdateUserAttributes',
834
+ )
835
+ ddbBucket.grantReadWrite(lambdaApi)
836
+ publicBucket.grantReadWrite(lambdaApi)
837
+ mainSns.grantPublish(lambdaApi)
838
+ taskSqs.grantSendMessages(lambdaApi)
839
+ notifySqs.grantSendMessages(lambdaApi)
840
+ appSyncApi.grantMutation(lambdaApi)
841
+
842
+ // Define an IAM policy for full DynamoDB access
843
+ const dynamoDbTablePrefixArn = cdk.Arn.format({
844
+ partition: 'aws',
845
+ region: this.region,
846
+ account: this.account,
847
+ service: 'dynamodb',
848
+ resource: 'table',
849
+ resourceName: prefix + '*',
850
+ })
851
+ const dynamodbPolicy = new cdk.aws_iam.PolicyStatement({
852
+ actions: [
853
+ 'dynamodb:PutItem',
854
+ 'dynamodb:UpdateItem',
855
+ 'dynamodb:GetItem',
856
+ 'dynamodb:Query',
857
+ ],
858
+ resources: [dynamoDbTablePrefixArn], // Access to all resources
859
+ })
860
+
861
+ // Attach the policy to the Lambda function's execution role
862
+ lambdaApi.role?.attachInlinePolicy(
863
+ new cdk.aws_iam.Policy(this, 'lambda-api-ddb-policy', {
864
+ statements: [dynamodbPolicy],
865
+ }),
866
+ )
867
+
868
+ const sfnPolicy = new cdk.aws_iam.PolicyStatement({
869
+ actions: [
870
+ 'states:StartExecution',
871
+ 'states:GetExecutionHistory',
872
+ 'states:DescribeExecution',
873
+ ],
874
+ resources: [commandSfnArn],
875
+ })
876
+
877
+ // Attach the policy to the Lambda function's execution role
878
+ lambdaApi.role?.attachInlinePolicy(
879
+ new cdk.aws_iam.Policy(this, 'lambda-event-sfn-policy', {
880
+ statements: [sfnPolicy],
881
+ }),
882
+ )
883
+
884
+ const sesPolicy = new cdk.aws_iam.PolicyStatement({
885
+ actions: ['ses:SendEmail'],
886
+ resources: ['*'],
887
+ })
888
+
889
+ // Attach the policy to the Lambda function's execution role
890
+ lambdaApi.role?.attachInlinePolicy(
891
+ new cdk.aws_iam.Policy(this, 'lambda-ses-policy', {
892
+ statements: [sesPolicy],
893
+ }),
894
+ )
895
+
896
+ const ssmPolicy = new cdk.aws_iam.PolicyStatement({
897
+ actions: ['ssm:GetParameter', 'kms:Decrypt'],
898
+ resources: ['*'],
899
+ })
900
+
901
+ // allow lambdaApi role to access ssm
902
+ lambdaApi.role?.attachInlinePolicy(
903
+ new cdk.aws_iam.Policy(this, 'lambda-api-ssm-policy', {
904
+ statements: [ssmPolicy],
905
+ }),
906
+ )
907
+
908
+ if (!!taskRole) {
909
+ ddbBucket.grantReadWrite(taskRole)
910
+ publicBucket.grantReadWrite(taskRole)
911
+ mainSns.grantPublish(taskRole)
912
+ taskSqs.grantSendMessages(taskRole)
913
+ notifySqs.grantSendMessages(taskRole)
914
+ appSyncApi.grantMutation(taskRole)
915
+ taskRole.addToPrincipalPolicy(
916
+ new cdk.aws_iam.PolicyStatement({
917
+ actions: [
918
+ 'ssmmessages:CreateControlChannel',
919
+ 'ssmmessages:CreateDataChannel',
920
+ 'ssmmessages:OpenControlChannel',
921
+ 'ssmmessages:OpenDataChannel',
922
+ ],
923
+ resources: ['*'],
924
+ }),
925
+ )
926
+ taskRole.attachInlinePolicy(
927
+ new cdk.aws_iam.Policy(this, 'ecs-api-ddb-policy', {
928
+ statements: [dynamodbPolicy],
929
+ }),
930
+ )
931
+ taskRole.attachInlinePolicy(
932
+ new cdk.aws_iam.Policy(this, 'ecs-event-sfn-policy', {
933
+ statements: [sfnPolicy],
934
+ }),
935
+ )
936
+ taskRole.attachInlinePolicy(
937
+ new cdk.aws_iam.Policy(this, 'ecs-ses-policy', {
938
+ statements: [sesPolicy],
939
+ }),
940
+ )
941
+ taskRole.attachInlinePolicy(
942
+ new cdk.aws_iam.Policy(this, 'ecs-api-ssm-policy', {
943
+ statements: [ssmPolicy],
944
+ }),
945
+ )
946
+ }
947
+ }
948
+ }