@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.
- package/dist/cjs/index.cjs +1 -1
- package/dist/cjs/index.cjs.map +4 -4
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/index.mjs.map +4 -4
- package/dist/types/backend.d.ts +28 -0
- package/dist/types/frontend.d.ts +9 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/oai.d.ts +4 -2
- package/dist/types/stack.d.ts +24 -0
- package/dist/types/storage.d.ts +9 -0
- package/package.json +7 -7
- package/src/backend.ts +107 -85
- package/src/cloudtrail.ts +0 -5
- package/src/frontend.ts +29 -20
- package/src/index.ts +8 -49
- package/src/oai.ts +4 -1
- package/src/stack.ts +65 -0
- package/src/storage.ts +31 -22
package/dist/types/storage.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
23
|
+
"@aws-sdk/types": "3.413.0",
|
|
24
24
|
"@medplum/core": "*",
|
|
25
|
-
"aws-cdk-lib": "2.
|
|
26
|
-
"cdk": "2.
|
|
27
|
-
"cdk-nag": "2.27.
|
|
28
|
-
"cdk-serverless-clamscan": "2.5.
|
|
29
|
-
"constructs": "10.2.
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
166
|
-
vpc: vpc,
|
|
190
|
+
this.ecsCluster = new ecs.Cluster(this, 'Cluster', {
|
|
191
|
+
vpc: this.vpc,
|
|
167
192
|
});
|
|
168
193
|
|
|
169
194
|
// Task Policies
|
|
170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
286
|
+
this.logGroup = new logs.LogGroup(this, 'LogGroup', {
|
|
262
287
|
logGroupName: '/ecs/medplum/' + name,
|
|
263
288
|
removalPolicy: RemovalPolicy.DESTROY,
|
|
264
289
|
});
|
|
265
290
|
|
|
266
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
304
|
-
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
119
|
-
grantBucketAccessToOriginAccessIdentity(
|
|
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
|
-
|
|
133
|
+
this.distribution = new cloudfront.Distribution(this, 'AppDistribution', {
|
|
123
134
|
defaultRootObject: 'index.html',
|
|
124
135
|
defaultBehavior: {
|
|
125
|
-
origin: new origins.S3Origin(appBucket, {
|
|
126
|
-
|
|
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
|
-
|
|
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
|
}
|