@medplum/cdk 2.1.1 → 2.1.3

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.
@@ -1,8 +1,17 @@
1
1
  import { MedplumInfraConfig } from '@medplum/core';
2
+ import { aws_cloudfront as cloudfront, aws_iam as iam, aws_route53 as route53, aws_s3 as s3, aws_wafv2 as wafv2 } from 'aws-cdk-lib';
2
3
  import { Construct } from 'constructs';
3
4
  /**
4
5
  * Binary storage bucket and CloudFront distribution.
5
6
  */
6
7
  export declare class Storage extends Construct {
8
+ storageBucket: s3.IBucket;
9
+ keyGroup?: cloudfront.IKeyGroup;
10
+ responseHeadersPolicy?: cloudfront.IResponseHeadersPolicy;
11
+ waf?: wafv2.CfnWebACL;
12
+ originAccessIdentity?: cloudfront.OriginAccessIdentity;
13
+ originAccessPolicyStatement?: iam.PolicyStatement;
14
+ distribution?: cloudfront.IDistribution;
15
+ dnsRecord?: route53.IRecordSet;
7
16
  constructor(parent: Construct, config: MedplumInfraConfig, region: string);
8
17
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@medplum/cdk",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
4
4
  "description": "Medplum CDK Infra as Code",
5
5
  "author": "Medplum <hello@medplum.com>",
6
6
  "license": "Apache-2.0",
@@ -20,13 +20,13 @@
20
20
  "test": "jest --runInBand"
21
21
  },
22
22
  "dependencies": {
23
- "@aws-sdk/types": "3.391.0",
23
+ "@aws-sdk/types": "3.413.0",
24
24
  "@medplum/core": "*",
25
- "aws-cdk-lib": "2.92.0",
26
- "cdk": "2.92.0",
27
- "cdk-nag": "2.27.107",
28
- "cdk-serverless-clamscan": "2.5.60",
29
- "constructs": "10.2.69"
25
+ "aws-cdk-lib": "2.96.2",
26
+ "cdk": "2.96.2",
27
+ "cdk-nag": "2.27.133",
28
+ "cdk-serverless-clamscan": "2.5.87",
29
+ "constructs": "10.2.70"
30
30
  },
31
31
  "bin": {
32
32
  "medplum-cdk-init": "./dist/cjs/init.cjs"
package/src/backend.ts CHANGED
@@ -27,17 +27,43 @@ import { awsManagedRules } from './waf';
27
27
  * RDS config: https://docs.aws.amazon.com/cdk/api/latest/docs/aws-rds-readme.html
28
28
  */
29
29
  export class BackEnd extends Construct {
30
+ vpc: ec2.IVpc;
31
+ botLambdaRole: iam.IRole;
32
+ rdsSecretsArn?: string;
33
+ rdsCluster?: rds.DatabaseCluster;
34
+ redisSubnetGroup: elasticache.CfnSubnetGroup;
35
+ redisSecurityGroup: ec2.SecurityGroup;
36
+ redisPassword: secretsmanager.ISecret;
37
+ redisCluster: elasticache.CfnReplicationGroup;
38
+ redisSecrets: secretsmanager.ISecret;
39
+ ecsCluster: ecs.Cluster;
40
+ taskRolePolicies: iam.PolicyDocument;
41
+ taskRole: iam.Role;
42
+ taskDefinition: ecs.FargateTaskDefinition;
43
+ logGroup: logs.ILogGroup;
44
+ logDriver: ecs.AwsLogDriver;
45
+ serviceContainer: ecs.ContainerDefinition;
46
+ fargateSecurityGroup: ec2.SecurityGroup;
47
+ fargateService: ecs.FargateService;
48
+ targetGroup: elbv2.ApplicationTargetGroup;
49
+ loadBalancer: elbv2.ApplicationLoadBalancer;
50
+ waf: wafv2.CfnWebACL;
51
+ wafAssociation: wafv2.CfnWebACLAssociation;
52
+ dnsRecord?: route53.ARecord;
53
+ regionParameter: ssm.StringParameter;
54
+ databaseSecretsParameter: ssm.StringParameter;
55
+ redisSecretsParameter: ssm.StringParameter;
56
+ botLambdaRoleParameter: ssm.StringParameter;
57
+
30
58
  constructor(scope: Construct, config: MedplumInfraConfig) {
31
59
  super(scope, 'BackEnd');
32
60
 
33
61
  const name = config.name;
34
62
 
35
63
  // VPC
36
- let vpc: ec2.IVpc;
37
-
38
64
  if (config.vpcId) {
39
65
  // Lookup VPC by ARN
40
- vpc = ec2.Vpc.fromLookup(this, 'VPC', { vpcId: config.vpcId });
66
+ this.vpc = ec2.Vpc.fromLookup(this, 'VPC', { vpcId: config.vpcId });
41
67
  } else {
42
68
  // VPC Flow Logs
43
69
  const vpcFlowLogs = new logs.LogGroup(this, 'VpcFlowLogs', {
@@ -46,7 +72,7 @@ export class BackEnd extends Construct {
46
72
  });
47
73
 
48
74
  // Create VPC
49
- vpc = new ec2.Vpc(this, 'VPC', {
75
+ this.vpc = new ec2.Vpc(this, 'VPC', {
50
76
  maxAzs: config.maxAzs,
51
77
  flowLogs: {
52
78
  cloudwatch: {
@@ -58,14 +84,13 @@ export class BackEnd extends Construct {
58
84
  }
59
85
 
60
86
  // Bot Lambda Role
61
- const botLambdaRole = new iam.Role(this, 'BotLambdaRole', {
87
+ this.botLambdaRole = new iam.Role(this, 'BotLambdaRole', {
62
88
  assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
63
89
  });
64
90
 
65
91
  // RDS
66
- let rdsCluster = undefined;
67
- let rdsSecretsArn = config.rdsSecretsArn;
68
- if (!rdsSecretsArn) {
92
+ this.rdsSecretsArn = config.rdsSecretsArn;
93
+ if (!this.rdsSecretsArn) {
69
94
  // See: https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_rds-readme.html#migrating-from-instanceprops
70
95
  const instanceProps: rds.ProvisionedClusterInstanceProps = {
71
96
  instanceType: config.rdsInstanceType ? new ec2.InstanceType(config.rdsInstanceType) : undefined,
@@ -85,14 +110,14 @@ export class BackEnd extends Construct {
85
110
  }
86
111
  }
87
112
 
88
- rdsCluster = new rds.DatabaseCluster(this, 'DatabaseCluster', {
113
+ this.rdsCluster = new rds.DatabaseCluster(this, 'DatabaseCluster', {
89
114
  engine: rds.DatabaseClusterEngine.auroraPostgres({
90
115
  version: rds.AuroraPostgresEngineVersion.VER_12_9,
91
116
  }),
92
117
  credentials: rds.Credentials.fromGeneratedSecret('clusteradmin'),
93
118
  defaultDatabaseName: 'medplum',
94
119
  storageEncrypted: true,
95
- vpc: vpc,
120
+ vpc: this.vpc,
96
121
  vpcSubnets: {
97
122
  subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
98
123
  },
@@ -107,23 +132,23 @@ export class BackEnd extends Construct {
107
132
  instanceUpdateBehaviour: rds.InstanceUpdateBehaviour.ROLLING,
108
133
  });
109
134
 
110
- rdsSecretsArn = (rdsCluster.secret as secretsmanager.ISecret).secretArn;
135
+ this.rdsSecretsArn = (this.rdsCluster.secret as secretsmanager.ISecret).secretArn;
111
136
  }
112
137
 
113
138
  // Redis
114
139
  // Important: For HIPAA compliance, you must specify TransitEncryptionEnabled as true, an AuthToken, and a CacheSubnetGroup.
115
- const redisSubnetGroup = new elasticache.CfnSubnetGroup(this, 'RedisSubnetGroup', {
140
+ this.redisSubnetGroup = new elasticache.CfnSubnetGroup(this, 'RedisSubnetGroup', {
116
141
  description: 'Redis Subnet Group',
117
- subnetIds: vpc.privateSubnets.map((subnet) => subnet.subnetId),
142
+ subnetIds: this.vpc.privateSubnets.map((subnet) => subnet.subnetId),
118
143
  });
119
144
 
120
- const redisSecurityGroup = new ec2.SecurityGroup(this, 'RedisSecurityGroup', {
121
- vpc,
145
+ this.redisSecurityGroup = new ec2.SecurityGroup(this, 'RedisSecurityGroup', {
146
+ vpc: this.vpc,
122
147
  description: 'Redis Security Group',
123
148
  allowAllOutbound: false,
124
149
  });
125
150
 
126
- const redisPassword = new secretsmanager.Secret(this, 'RedisPassword', {
151
+ this.redisPassword = new secretsmanager.Secret(this, 'RedisPassword', {
127
152
  generateSecretString: {
128
153
  secretStringTemplate: '{}',
129
154
  generateStringKey: 'password',
@@ -131,43 +156,43 @@ export class BackEnd extends Construct {
131
156
  },
132
157
  });
133
158
 
134
- const redisCluster = new elasticache.CfnReplicationGroup(this, 'RedisCluster', {
159
+ this.redisCluster = new elasticache.CfnReplicationGroup(this, 'RedisCluster', {
135
160
  engine: 'Redis',
136
161
  engineVersion: '6.x',
137
162
  cacheNodeType: config.cacheNodeType ?? 'cache.t2.medium',
138
163
  replicationGroupDescription: 'RedisReplicationGroup',
139
- authToken: redisPassword.secretValueFromJson('password').toString(),
164
+ authToken: this.redisPassword.secretValueFromJson('password').toString(),
140
165
  transitEncryptionEnabled: true,
141
166
  atRestEncryptionEnabled: true,
142
167
  multiAzEnabled: true,
143
- cacheSubnetGroupName: redisSubnetGroup.ref,
168
+ cacheSubnetGroupName: this.redisSubnetGroup.ref,
144
169
  numNodeGroups: 1,
145
170
  replicasPerNodeGroup: 1,
146
- securityGroupIds: [redisSecurityGroup.securityGroupId],
171
+ securityGroupIds: [this.redisSecurityGroup.securityGroupId],
147
172
  });
148
- redisCluster.node.addDependency(redisPassword);
173
+ this.redisCluster.node.addDependency(this.redisPassword);
149
174
 
150
- const redisSecrets = new secretsmanager.Secret(this, 'RedisSecrets', {
175
+ this.redisSecrets = new secretsmanager.Secret(this, 'RedisSecrets', {
151
176
  generateSecretString: {
152
177
  secretStringTemplate: JSON.stringify({
153
- host: redisCluster.attrPrimaryEndPointAddress,
154
- port: redisCluster.attrPrimaryEndPointPort,
155
- password: redisPassword.secretValueFromJson('password').toString(),
178
+ host: this.redisCluster.attrPrimaryEndPointAddress,
179
+ port: this.redisCluster.attrPrimaryEndPointPort,
180
+ password: this.redisPassword.secretValueFromJson('password').toString(),
156
181
  tls: {},
157
182
  }),
158
183
  generateStringKey: 'unused',
159
184
  },
160
185
  });
161
- redisSecrets.node.addDependency(redisPassword);
162
- redisSecrets.node.addDependency(redisCluster);
186
+ this.redisSecrets.node.addDependency(this.redisPassword);
187
+ this.redisSecrets.node.addDependency(this.redisCluster);
163
188
 
164
189
  // ECS Cluster
165
- const cluster = new ecs.Cluster(this, 'Cluster', {
166
- vpc: vpc,
190
+ this.ecsCluster = new ecs.Cluster(this, 'Cluster', {
191
+ vpc: this.vpc,
167
192
  });
168
193
 
169
194
  // Task Policies
170
- const taskRolePolicies = new iam.PolicyDocument({
195
+ this.taskRolePolicies = new iam.PolicyDocument({
171
196
  statements: [
172
197
  // CloudWatch Logs: Create streams and put events
173
198
  new iam.PolicyStatement({
@@ -219,7 +244,7 @@ export class BackEnd extends Construct {
219
244
  new iam.PolicyStatement({
220
245
  effect: iam.Effect.ALLOW,
221
246
  actions: ['iam:ListRoles', 'iam:GetRole', 'iam:PassRole'],
222
- resources: [botLambdaRole.roleArn],
247
+ resources: [this.botLambdaRole.roleArn],
223
248
  }),
224
249
 
225
250
  // Lambda: Create, read, update, delete, and invoke functions
@@ -242,85 +267,85 @@ export class BackEnd extends Construct {
242
267
  });
243
268
 
244
269
  // Task Role
245
- const taskRole = new iam.Role(this, 'TaskExecutionRole', {
270
+ this.taskRole = new iam.Role(this, 'TaskExecutionRole', {
246
271
  assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
247
272
  description: 'Medplum Server Task Execution Role',
248
273
  inlinePolicies: {
249
- TaskExecutionPolicies: taskRolePolicies,
274
+ TaskExecutionPolicies: this.taskRolePolicies,
250
275
  },
251
276
  });
252
277
 
253
278
  // Task Definitions
254
- const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDefinition', {
279
+ this.taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDefinition', {
255
280
  memoryLimitMiB: config.serverMemory,
256
281
  cpu: config.serverCpu,
257
- taskRole: taskRole,
282
+ taskRole: this.taskRole,
258
283
  });
259
284
 
260
285
  // Log Groups
261
- const logGroup = new logs.LogGroup(this, 'LogGroup', {
286
+ this.logGroup = new logs.LogGroup(this, 'LogGroup', {
262
287
  logGroupName: '/ecs/medplum/' + name,
263
288
  removalPolicy: RemovalPolicy.DESTROY,
264
289
  });
265
290
 
266
- const logDriver = new ecs.AwsLogDriver({
267
- logGroup: logGroup,
291
+ this.logDriver = new ecs.AwsLogDriver({
292
+ logGroup: this.logGroup,
268
293
  streamPrefix: 'Medplum',
269
294
  });
270
295
 
271
296
  // Task Containers
272
- const serviceContainer = taskDefinition.addContainer('MedplumTaskDefinition', {
297
+ this.serviceContainer = this.taskDefinition.addContainer('MedplumTaskDefinition', {
273
298
  image: this.getContainerImage(config, config.serverImage),
274
299
  command: [config.region === 'us-east-1' ? `aws:/medplum/${name}/` : `aws:${config.region}:/medplum/${name}/`],
275
- logging: logDriver,
300
+ logging: this.logDriver,
276
301
  });
277
302
 
278
- serviceContainer.addPortMappings({
303
+ this.serviceContainer.addPortMappings({
279
304
  containerPort: config.apiPort,
280
305
  hostPort: config.apiPort,
281
306
  });
282
307
 
283
308
  if (config.additionalContainers) {
284
309
  for (const container of config.additionalContainers) {
285
- taskDefinition.addContainer('AdditionalContainer-' + container.name, {
310
+ this.taskDefinition.addContainer('AdditionalContainer-' + container.name, {
286
311
  containerName: container.name,
287
312
  image: this.getContainerImage(config, container.image),
288
313
  command: container.command,
289
314
  environment: container.environment,
290
- logging: logDriver,
315
+ logging: this.logDriver,
291
316
  });
292
317
  }
293
318
  }
294
319
 
295
320
  // Security Groups
296
- const fargateSecurityGroup = new ec2.SecurityGroup(this, 'ServiceSecurityGroup', {
321
+ this.fargateSecurityGroup = new ec2.SecurityGroup(this, 'ServiceSecurityGroup', {
297
322
  allowAllOutbound: true,
298
323
  securityGroupName: 'MedplumSecurityGroup',
299
- vpc: vpc,
324
+ vpc: this.vpc,
300
325
  });
301
326
 
302
327
  // Fargate Services
303
- const fargateService = new ecs.FargateService(this, 'FargateService', {
304
- cluster: cluster,
305
- taskDefinition: taskDefinition,
328
+ this.fargateService = new ecs.FargateService(this, 'FargateService', {
329
+ cluster: this.ecsCluster,
330
+ taskDefinition: this.taskDefinition,
306
331
  assignPublicIp: false,
307
332
  vpcSubnets: {
308
333
  subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
309
334
  },
310
335
  desiredCount: config.desiredServerCount,
311
- securityGroups: [fargateSecurityGroup],
336
+ securityGroups: [this.fargateSecurityGroup],
312
337
  healthCheckGracePeriod: Duration.minutes(5),
313
338
  });
314
339
 
315
340
  // Add dependencies - make sure Fargate service is created after RDS and Redis
316
- if (rdsCluster) {
317
- fargateService.node.addDependency(rdsCluster);
341
+ if (this.rdsCluster) {
342
+ this.fargateService.node.addDependency(this.rdsCluster);
318
343
  }
319
- fargateService.node.addDependency(redisCluster);
344
+ this.fargateService.node.addDependency(this.redisCluster);
320
345
 
321
346
  // Load Balancer Target Group
322
- const targetGroup = new elbv2.ApplicationTargetGroup(this, 'TargetGroup', {
323
- vpc: vpc,
347
+ this.targetGroup = new elbv2.ApplicationTargetGroup(this, 'TargetGroup', {
348
+ vpc: this.vpc,
324
349
  port: config.apiPort,
325
350
  protocol: elbv2.ApplicationProtocol.HTTP,
326
351
  healthCheck: {
@@ -330,19 +355,19 @@ export class BackEnd extends Construct {
330
355
  healthyThresholdCount: 2,
331
356
  unhealthyThresholdCount: 5,
332
357
  },
333
- targets: [fargateService],
358
+ targets: [this.fargateService],
334
359
  });
335
360
 
336
361
  // Load Balancer
337
- const loadBalancer = new elbv2.ApplicationLoadBalancer(this, 'LoadBalancer', {
338
- vpc: vpc,
362
+ this.loadBalancer = new elbv2.ApplicationLoadBalancer(this, 'LoadBalancer', {
363
+ vpc: this.vpc,
339
364
  internetFacing: config.apiInternetFacing !== false, // default true
340
365
  http2Enabled: true,
341
366
  });
342
367
 
343
368
  if (config.loadBalancerLoggingBucket) {
344
369
  // Load Balancer logging
345
- loadBalancer.logAccessLogs(
370
+ this.loadBalancer.logAccessLogs(
346
371
  s3.Bucket.fromBucketName(this, 'LoggingBucket', config.loadBalancerLoggingBucket),
347
372
  config.loadBalancerLoggingPrefix
348
373
  );
@@ -350,7 +375,7 @@ export class BackEnd extends Construct {
350
375
 
351
376
  // HTTPS Listener
352
377
  // Forward to the target group
353
- loadBalancer.addListener('HttpsListener', {
378
+ this.loadBalancer.addListener('HttpsListener', {
354
379
  port: 443,
355
380
  certificates: [
356
381
  {
@@ -358,11 +383,11 @@ export class BackEnd extends Construct {
358
383
  },
359
384
  ],
360
385
  sslPolicy: elbv2.SslPolicy.FORWARD_SECRECY_TLS12_RES_GCM,
361
- defaultAction: elbv2.ListenerAction.forward([targetGroup]),
386
+ defaultAction: elbv2.ListenerAction.forward([this.targetGroup]),
362
387
  });
363
388
 
364
389
  // WAF
365
- const waf = new wafv2.CfnWebACL(this, 'BackEndWAF', {
390
+ this.waf = new wafv2.CfnWebACL(this, 'BackEndWAF', {
366
391
  defaultAction: { allow: {} },
367
392
  scope: 'REGIONAL',
368
393
  name: `${config.stackName}-BackEndWAF`,
@@ -375,21 +400,20 @@ export class BackEnd extends Construct {
375
400
  });
376
401
 
377
402
  // Create an association between the load balancer and the WAF
378
- const wafAssociation = new wafv2.CfnWebACLAssociation(this, 'LoadBalancerAssociation', {
379
- resourceArn: loadBalancer.loadBalancerArn,
380
- webAclArn: waf.attrArn,
403
+ this.wafAssociation = new wafv2.CfnWebACLAssociation(this, 'LoadBalancerAssociation', {
404
+ resourceArn: this.loadBalancer.loadBalancerArn,
405
+ webAclArn: this.waf.attrArn,
381
406
  });
382
407
 
383
408
  // Grant RDS access to the fargate group
384
- if (rdsCluster) {
385
- rdsCluster.connections.allowDefaultPortFrom(fargateSecurityGroup);
409
+ if (this.rdsCluster) {
410
+ this.rdsCluster.connections.allowDefaultPortFrom(this.fargateSecurityGroup);
386
411
  }
387
412
 
388
413
  // Grant Redis access to the fargate group
389
- redisSecurityGroup.addIngressRule(fargateSecurityGroup, ec2.Port.tcp(6379));
414
+ this.redisSecurityGroup.addIngressRule(this.fargateSecurityGroup, ec2.Port.tcp(6379));
390
415
 
391
416
  // DNS
392
- let record = undefined;
393
417
  if (!config.skipDns) {
394
418
  // Route 53
395
419
  const zone = route53.HostedZone.fromLookup(this, 'Zone', {
@@ -397,43 +421,41 @@ export class BackEnd extends Construct {
397
421
  });
398
422
 
399
423
  // Route53 alias record for the load balancer
400
- record = new route53.ARecord(this, 'LoadBalancerAliasRecord', {
424
+ this.dnsRecord = new route53.ARecord(this, 'LoadBalancerAliasRecord', {
401
425
  recordName: config.apiDomainName,
402
- target: route53.RecordTarget.fromAlias(new targets.LoadBalancerTarget(loadBalancer)),
426
+ target: route53.RecordTarget.fromAlias(new targets.LoadBalancerTarget(this.loadBalancer)),
403
427
  zone: zone,
404
428
  });
405
429
  }
406
430
 
407
431
  // SSM Parameters
408
- const databaseSecrets = new ssm.StringParameter(this, 'DatabaseSecretsParameter', {
432
+ this.regionParameter = new ssm.StringParameter(this, 'RegionParameter', {
433
+ tier: ssm.ParameterTier.STANDARD,
434
+ parameterName: `/medplum/${name}/awsRegion`,
435
+ description: 'AWS region',
436
+ stringValue: config.region,
437
+ });
438
+
439
+ this.databaseSecretsParameter = new ssm.StringParameter(this, 'DatabaseSecretsParameter', {
409
440
  tier: ssm.ParameterTier.STANDARD,
410
441
  parameterName: `/medplum/${name}/DatabaseSecrets`,
411
442
  description: 'Database secrets ARN',
412
- stringValue: rdsSecretsArn,
443
+ stringValue: this.rdsSecretsArn,
413
444
  });
414
445
 
415
- const redisSecretsParameter = new ssm.StringParameter(this, 'RedisSecretsParameter', {
446
+ this.redisSecretsParameter = new ssm.StringParameter(this, 'RedisSecretsParameter', {
416
447
  tier: ssm.ParameterTier.STANDARD,
417
448
  parameterName: `/medplum/${name}/RedisSecrets`,
418
449
  description: 'Redis secrets ARN',
419
- stringValue: redisSecrets.secretArn,
450
+ stringValue: this.redisSecrets.secretArn,
420
451
  });
421
452
 
422
- const botLambdaRoleParameter = new ssm.StringParameter(this, 'BotLambdaRoleParameter', {
453
+ this.botLambdaRoleParameter = new ssm.StringParameter(this, 'BotLambdaRoleParameter', {
423
454
  tier: ssm.ParameterTier.STANDARD,
424
455
  parameterName: `/medplum/${name}/botLambdaRoleArn`,
425
456
  description: 'Bot lambda execution role ARN',
426
- stringValue: botLambdaRole.roleArn,
457
+ stringValue: this.botLambdaRole.roleArn,
427
458
  });
428
-
429
- // Debug
430
- console.log('ARecord', record?.domainName);
431
- console.log('DatabaseSecretsParameter', databaseSecrets.parameterArn);
432
- console.log('RedisSecretsParameter', redisSecretsParameter.parameterArn);
433
- console.log('RedisCluster', redisCluster.attrPrimaryEndPointAddress);
434
- console.log('BotLambdaRole', botLambdaRoleParameter.stringValue);
435
- console.log('WAF', waf.attrArn);
436
- console.log('WAF Association', wafAssociation.node.id);
437
459
  }
438
460
 
439
461
  /**
package/src/cloudtrail.ts CHANGED
@@ -103,11 +103,6 @@ export class CloudTrailAlarms extends Construct {
103
103
  for (const [name, filterPattern] of alarmDefinitions) {
104
104
  this.createMetricAlarm(name, filterPattern);
105
105
  }
106
-
107
- // Debug
108
- console.log('LogGroup', this.logGroup?.node.id);
109
- console.log('CloudTrail', this.cloudTrail?.node.id);
110
- console.log('AlarmTopic', this.alarmTopic?.node.id);
111
106
  }
112
107
 
113
108
  createMetricAlarm(name: string, filterPattern: string): void {
package/src/frontend.ts CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  aws_certificatemanager as acm,
4
4
  aws_cloudfront as cloudfront,
5
5
  Duration,
6
+ aws_iam as iam,
6
7
  aws_cloudfront_origins as origins,
7
8
  RemovalPolicy,
8
9
  aws_route53 as route53,
@@ -21,14 +22,21 @@ import { awsManagedRules } from './waf';
21
22
  * Route53 alias record, and ACM certificate.
22
23
  */
23
24
  export class FrontEnd extends Construct {
25
+ appBucket: s3.IBucket;
26
+ responseHeadersPolicy?: cloudfront.IResponseHeadersPolicy;
27
+ waf?: wafv2.CfnWebACL;
28
+ apiOriginCachePolicy?: cloudfront.ICachePolicy;
29
+ originAccessIdentity?: cloudfront.OriginAccessIdentity;
30
+ originAccessPolicyStatement?: iam.PolicyStatement;
31
+ distribution?: cloudfront.IDistribution;
32
+ dnsRecord?: route53.IRecordSet;
33
+
24
34
  constructor(parent: Construct, config: MedplumInfraConfig, region: string) {
25
35
  super(parent, 'FrontEnd');
26
36
 
27
- let appBucket: s3.IBucket;
28
-
29
37
  if (region === config.region) {
30
38
  // S3 bucket
31
- appBucket = new s3.Bucket(this, 'AppBucket', {
39
+ this.appBucket = new s3.Bucket(this, 'AppBucket', {
32
40
  bucketName: config.appDomainName,
33
41
  publicReadAccess: false,
34
42
  blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
@@ -39,7 +47,7 @@ export class FrontEnd extends Construct {
39
47
  });
40
48
  } else {
41
49
  // Otherwise, reference the bucket by name and region
42
- appBucket = s3.Bucket.fromBucketAttributes(this, 'AppBucket', {
50
+ this.appBucket = s3.Bucket.fromBucketAttributes(this, 'AppBucket', {
43
51
  bucketName: config.appDomainName,
44
52
  region: config.region,
45
53
  });
@@ -47,7 +55,7 @@ export class FrontEnd extends Construct {
47
55
 
48
56
  if (region === 'us-east-1') {
49
57
  // HTTP response headers policy
50
- const responseHeadersPolicy = new cloudfront.ResponseHeadersPolicy(this, 'ResponseHeadersPolicy', {
58
+ this.responseHeadersPolicy = new cloudfront.ResponseHeadersPolicy(this, 'ResponseHeadersPolicy', {
51
59
  securityHeadersBehavior: {
52
60
  contentSecurityPolicy: {
53
61
  contentSecurityPolicy: [
@@ -85,7 +93,7 @@ export class FrontEnd extends Construct {
85
93
  });
86
94
 
87
95
  // WAF
88
- const waf = new wafv2.CfnWebACL(this, 'FrontEndWAF', {
96
+ this.waf = new wafv2.CfnWebACL(this, 'FrontEndWAF', {
89
97
  defaultAction: { allow: {} },
90
98
  scope: 'CLOUDFRONT',
91
99
  name: `${config.stackName}-FrontEndWAF`,
@@ -98,7 +106,7 @@ export class FrontEnd extends Construct {
98
106
  });
99
107
 
100
108
  // API Origin Cache Policy
101
- const apiOriginCachePolicy = new cloudfront.CachePolicy(this, 'ApiOriginCachePolicy', {
109
+ this.apiOriginCachePolicy = new cloudfront.CachePolicy(this, 'ApiOriginCachePolicy', {
102
110
  cachePolicyName: `${config.stackName}-ApiOriginCachePolicy`,
103
111
  cookieBehavior: cloudfront.CacheCookieBehavior.all(),
104
112
  headerBehavior: cloudfront.CacheHeaderBehavior.allowList(
@@ -115,15 +123,20 @@ export class FrontEnd extends Construct {
115
123
  });
116
124
 
117
125
  // Origin access identity
118
- const originAccessIdentity = new cloudfront.OriginAccessIdentity(this, 'OriginAccessIdentity', {});
119
- grantBucketAccessToOriginAccessIdentity(appBucket, originAccessIdentity);
126
+ this.originAccessIdentity = new cloudfront.OriginAccessIdentity(this, 'OriginAccessIdentity', {});
127
+ this.originAccessPolicyStatement = grantBucketAccessToOriginAccessIdentity(
128
+ this.appBucket,
129
+ this.originAccessIdentity
130
+ );
120
131
 
121
132
  // CloudFront distribution
122
- const distribution = new cloudfront.Distribution(this, 'AppDistribution', {
133
+ this.distribution = new cloudfront.Distribution(this, 'AppDistribution', {
123
134
  defaultRootObject: 'index.html',
124
135
  defaultBehavior: {
125
- origin: new origins.S3Origin(appBucket, { originAccessIdentity }),
126
- responseHeadersPolicy,
136
+ origin: new origins.S3Origin(this.appBucket, {
137
+ originAccessIdentity: this.originAccessIdentity,
138
+ }),
139
+ responseHeadersPolicy: this.responseHeadersPolicy,
127
140
  viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
128
141
  },
129
142
  additionalBehaviors: config.appApiProxy
@@ -131,7 +144,7 @@ export class FrontEnd extends Construct {
131
144
  '/api/*': {
132
145
  origin: new origins.HttpOrigin(config.apiDomainName),
133
146
  allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
134
- cachePolicy: apiOriginCachePolicy,
147
+ cachePolicy: this.apiOriginCachePolicy,
135
148
  viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
136
149
  },
137
150
  }
@@ -150,7 +163,7 @@ export class FrontEnd extends Construct {
150
163
  responsePagePath: '/index.html',
151
164
  },
152
165
  ],
153
- webAclId: waf.attrArn,
166
+ webAclId: this.waf.attrArn,
154
167
  logBucket: config.appLoggingBucket
155
168
  ? s3.Bucket.fromBucketName(this, 'LoggingBucket', config.appLoggingBucket)
156
169
  : undefined,
@@ -158,22 +171,18 @@ export class FrontEnd extends Construct {
158
171
  });
159
172
 
160
173
  // DNS
161
- let record = undefined;
162
174
  if (!config.skipDns) {
163
175
  const zone = route53.HostedZone.fromLookup(this, 'Zone', {
164
176
  domainName: config.domainName.split('.').slice(-2).join('.'),
165
177
  });
166
178
 
167
179
  // Route53 alias record for the CloudFront distribution
168
- record = new route53.ARecord(this, 'AppAliasRecord', {
180
+ this.dnsRecord = new route53.ARecord(this, 'AppAliasRecord', {
169
181
  recordName: config.appDomainName,
170
- target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(distribution)),
182
+ target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(this.distribution)),
171
183
  zone,
172
184
  });
173
185
  }
174
-
175
- // Debug
176
- console.log('ARecord', record?.domainName);
177
186
  }
178
187
  }
179
188
  }