@medplum/cdk 2.0.7
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/README.md +83 -0
- package/babel.config.json +3 -0
- package/cdk.json +3 -0
- package/dist/cjs/index.cjs +767 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/init.cjs +443 -0
- package/dist/cjs/init.cjs.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/jest.config.json +14 -0
- package/package.json +33 -0
- package/rollup.config.mjs +56 -0
- package/src/__mocks__/@aws-sdk/client-acm.ts +45 -0
- package/src/__mocks__/@aws-sdk/client-ssm.ts +13 -0
- package/src/__mocks__/@aws-sdk/client-sts.ts +18 -0
- package/src/backend.ts +416 -0
- package/src/config.ts +31 -0
- package/src/frontend.ts +168 -0
- package/src/index.test.ts +232 -0
- package/src/index.ts +68 -0
- package/src/init.test.ts +378 -0
- package/src/init.ts +505 -0
- package/src/storage.ts +134 -0
- package/src/waf.ts +122 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var awsCdkLib = require('aws-cdk-lib');
|
|
4
|
+
var fs = require('fs');
|
|
5
|
+
var path = require('path');
|
|
6
|
+
var awsEcr = require('aws-cdk-lib/aws-ecr');
|
|
7
|
+
var constructs = require('constructs');
|
|
8
|
+
var cdkServerlessClamscan = require('cdk-serverless-clamscan');
|
|
9
|
+
|
|
10
|
+
// Based on https://gist.github.com/statik/f1ac9d6227d98d30c7a7cec0c83f4e64
|
|
11
|
+
const awsManagedRules = [
|
|
12
|
+
// Common Rule Set aligns with major portions of OWASP Core Rule Set
|
|
13
|
+
// https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-baseline.html
|
|
14
|
+
{
|
|
15
|
+
name: 'AWS-AWSManagedRulesCommonRuleSet',
|
|
16
|
+
priority: 10,
|
|
17
|
+
statement: {
|
|
18
|
+
managedRuleGroupStatement: {
|
|
19
|
+
vendorName: 'AWS',
|
|
20
|
+
name: 'AWSManagedRulesCommonRuleSet',
|
|
21
|
+
// Excluding generic RFI body rule for sns notifications
|
|
22
|
+
// https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html
|
|
23
|
+
excludedRules: [
|
|
24
|
+
{ name: 'NoUserAgent_HEADER' },
|
|
25
|
+
{ name: 'UserAgent_BadBots_HEADER' },
|
|
26
|
+
{ name: 'SizeRestrictions_QUERYSTRING' },
|
|
27
|
+
{ name: 'SizeRestrictions_Cookie_HEADER' },
|
|
28
|
+
{ name: 'SizeRestrictions_BODY' },
|
|
29
|
+
{ name: 'SizeRestrictions_URIPATH' },
|
|
30
|
+
{ name: 'EC2MetaDataSSRF_BODY' },
|
|
31
|
+
{ name: 'EC2MetaDataSSRF_COOKIE' },
|
|
32
|
+
{ name: 'EC2MetaDataSSRF_URIPATH' },
|
|
33
|
+
{ name: 'EC2MetaDataSSRF_QUERYARGUMENTS' },
|
|
34
|
+
{ name: 'GenericLFI_QUERYARGUMENTS' },
|
|
35
|
+
{ name: 'GenericLFI_URIPATH' },
|
|
36
|
+
{ name: 'GenericLFI_BODY' },
|
|
37
|
+
{ name: 'RestrictedExtensions_URIPATH' },
|
|
38
|
+
{ name: 'RestrictedExtensions_QUERYARGUMENTS' },
|
|
39
|
+
{ name: 'GenericRFI_QUERYARGUMENTS' },
|
|
40
|
+
{ name: 'GenericRFI_BODY' },
|
|
41
|
+
{ name: 'GenericRFI_URIPATH' },
|
|
42
|
+
{ name: 'CrossSiteScripting_COOKIE' },
|
|
43
|
+
{ name: 'CrossSiteScripting_QUERYARGUMENTS' },
|
|
44
|
+
{ name: 'CrossSiteScripting_BODY' },
|
|
45
|
+
{ name: 'CrossSiteScripting_URIPATH' },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
overrideAction: {
|
|
50
|
+
count: {},
|
|
51
|
+
},
|
|
52
|
+
visibilityConfig: {
|
|
53
|
+
sampledRequestsEnabled: true,
|
|
54
|
+
cloudWatchMetricsEnabled: true,
|
|
55
|
+
metricName: 'AWS-AWSManagedRulesCommonRuleSet',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
// AWS IP Reputation list includes known malicious actors/bots and is regularly updated
|
|
59
|
+
// https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-ip-rep.html
|
|
60
|
+
{
|
|
61
|
+
name: 'AWS-AWSManagedRulesAmazonIpReputationList',
|
|
62
|
+
priority: 20,
|
|
63
|
+
statement: {
|
|
64
|
+
managedRuleGroupStatement: {
|
|
65
|
+
vendorName: 'AWS',
|
|
66
|
+
name: 'AWSManagedRulesAmazonIpReputationList',
|
|
67
|
+
excludedRules: [{ name: 'AWSManagedIPReputationList' }, { name: 'AWSManagedReconnaissanceList' }],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
overrideAction: {
|
|
71
|
+
count: {},
|
|
72
|
+
},
|
|
73
|
+
visibilityConfig: {
|
|
74
|
+
sampledRequestsEnabled: true,
|
|
75
|
+
cloudWatchMetricsEnabled: true,
|
|
76
|
+
metricName: 'AWSManagedRulesAmazonIpReputationList',
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
// Blocks common SQL Injection
|
|
80
|
+
// https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-use-case.html#aws-managed-rule-groups-use-case-sql-db
|
|
81
|
+
{
|
|
82
|
+
name: 'AWSManagedRulesSQLiRuleSet',
|
|
83
|
+
priority: 30,
|
|
84
|
+
visibilityConfig: {
|
|
85
|
+
sampledRequestsEnabled: true,
|
|
86
|
+
cloudWatchMetricsEnabled: true,
|
|
87
|
+
metricName: 'AWSManagedRulesSQLiRuleSet',
|
|
88
|
+
},
|
|
89
|
+
overrideAction: {
|
|
90
|
+
count: {},
|
|
91
|
+
},
|
|
92
|
+
statement: {
|
|
93
|
+
managedRuleGroupStatement: {
|
|
94
|
+
vendorName: 'AWS',
|
|
95
|
+
name: 'AWSManagedRulesSQLiRuleSet',
|
|
96
|
+
excludedRules: [
|
|
97
|
+
{ name: 'SQLi_QUERYARGUMENTS' },
|
|
98
|
+
{ name: 'SQLiExtendedPatterns_QUERYARGUMENTS' },
|
|
99
|
+
{ name: 'SQLi_BODY' },
|
|
100
|
+
{ name: 'SQLiExtendedPatterns_BODY' },
|
|
101
|
+
{ name: 'SQLi_COOKIE' },
|
|
102
|
+
{ name: 'SQLi_URIPATH' },
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
// Blocks attacks targeting LFI(Local File Injection) for linux systems
|
|
108
|
+
// https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-use-case.html#aws-managed-rule-groups-use-case-linux-os
|
|
109
|
+
{
|
|
110
|
+
name: 'AWSManagedRuleLinux',
|
|
111
|
+
priority: 40,
|
|
112
|
+
visibilityConfig: {
|
|
113
|
+
sampledRequestsEnabled: true,
|
|
114
|
+
cloudWatchMetricsEnabled: true,
|
|
115
|
+
metricName: 'AWSManagedRuleLinux',
|
|
116
|
+
},
|
|
117
|
+
overrideAction: {
|
|
118
|
+
count: {},
|
|
119
|
+
},
|
|
120
|
+
statement: {
|
|
121
|
+
managedRuleGroupStatement: {
|
|
122
|
+
vendorName: 'AWS',
|
|
123
|
+
name: 'AWSManagedRulesLinuxRuleSet',
|
|
124
|
+
excludedRules: [{ name: 'LFI_URIPATH' }, { name: 'LFI_QUERYSTRING' }, { name: 'LFI_COOKIE' }],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Based on: https://github.com/aws-samples/http-api-aws-fargate-cdk/blob/master/cdk/singleAccount/lib/fargate-vpclink-stack.ts
|
|
132
|
+
*
|
|
133
|
+
* RDS config: https://docs.aws.amazon.com/cdk/api/latest/docs/aws-rds-readme.html
|
|
134
|
+
*/
|
|
135
|
+
class BackEnd extends constructs.Construct {
|
|
136
|
+
constructor(scope, config) {
|
|
137
|
+
super(scope, 'BackEnd');
|
|
138
|
+
const name = config.name;
|
|
139
|
+
// VPC
|
|
140
|
+
let vpc;
|
|
141
|
+
if (config.vpcId) {
|
|
142
|
+
// Lookup VPC by ARN
|
|
143
|
+
vpc = awsCdkLib.aws_ec2.Vpc.fromLookup(this, 'VPC', { vpcId: config.vpcId });
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
// VPC Flow Logs
|
|
147
|
+
const vpcFlowLogs = new awsCdkLib.aws_logs.LogGroup(this, 'VpcFlowLogs', {
|
|
148
|
+
logGroupName: '/medplum/flowlogs/' + name,
|
|
149
|
+
removalPolicy: awsCdkLib.RemovalPolicy.DESTROY,
|
|
150
|
+
});
|
|
151
|
+
// Create VPC
|
|
152
|
+
vpc = new awsCdkLib.aws_ec2.Vpc(this, 'VPC', {
|
|
153
|
+
maxAzs: config.maxAzs,
|
|
154
|
+
flowLogs: {
|
|
155
|
+
cloudwatch: {
|
|
156
|
+
destination: awsCdkLib.aws_ec2.FlowLogDestination.toCloudWatchLogs(vpcFlowLogs),
|
|
157
|
+
trafficType: awsCdkLib.aws_ec2.FlowLogTrafficType.ALL,
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
// Bot Lambda Role
|
|
163
|
+
const botLambdaRole = new awsCdkLib.aws_iam.Role(this, 'BotLambdaRole', {
|
|
164
|
+
assumedBy: new awsCdkLib.aws_iam.ServicePrincipal('lambda.amazonaws.com'),
|
|
165
|
+
});
|
|
166
|
+
// RDS
|
|
167
|
+
let rdsCluster = undefined;
|
|
168
|
+
let rdsSecretsArn = config.rdsSecretsArn;
|
|
169
|
+
if (!rdsSecretsArn) {
|
|
170
|
+
rdsCluster = new awsCdkLib.aws_rds.DatabaseCluster(this, 'DatabaseCluster', {
|
|
171
|
+
engine: awsCdkLib.aws_rds.DatabaseClusterEngine.auroraPostgres({
|
|
172
|
+
version: awsCdkLib.aws_rds.AuroraPostgresEngineVersion.VER_12_9,
|
|
173
|
+
}),
|
|
174
|
+
credentials: awsCdkLib.aws_rds.Credentials.fromGeneratedSecret('clusteradmin'),
|
|
175
|
+
defaultDatabaseName: 'medplum',
|
|
176
|
+
storageEncrypted: true,
|
|
177
|
+
instances: config.rdsInstances,
|
|
178
|
+
instanceProps: {
|
|
179
|
+
vpc: vpc,
|
|
180
|
+
vpcSubnets: {
|
|
181
|
+
subnetType: awsCdkLib.aws_ec2.SubnetType.PRIVATE_WITH_EGRESS,
|
|
182
|
+
},
|
|
183
|
+
instanceType: config.rdsInstanceType ? new awsCdkLib.aws_ec2.InstanceType(config.rdsInstanceType) : undefined,
|
|
184
|
+
enablePerformanceInsights: true,
|
|
185
|
+
},
|
|
186
|
+
backup: {
|
|
187
|
+
retention: awsCdkLib.Duration.days(7),
|
|
188
|
+
},
|
|
189
|
+
cloudwatchLogsExports: ['postgresql'],
|
|
190
|
+
instanceUpdateBehaviour: awsCdkLib.aws_rds.InstanceUpdateBehaviour.ROLLING,
|
|
191
|
+
});
|
|
192
|
+
rdsSecretsArn = rdsCluster.secret.secretArn;
|
|
193
|
+
}
|
|
194
|
+
// Redis
|
|
195
|
+
// Important: For HIPAA compliance, you must specify TransitEncryptionEnabled as true, an AuthToken, and a CacheSubnetGroup.
|
|
196
|
+
const redisSubnetGroup = new awsCdkLib.aws_elasticache.CfnSubnetGroup(this, 'RedisSubnetGroup', {
|
|
197
|
+
description: 'Redis Subnet Group',
|
|
198
|
+
subnetIds: vpc.privateSubnets.map((subnet) => subnet.subnetId),
|
|
199
|
+
});
|
|
200
|
+
const redisSecurityGroup = new awsCdkLib.aws_ec2.SecurityGroup(this, 'RedisSecurityGroup', {
|
|
201
|
+
vpc,
|
|
202
|
+
description: 'Redis Security Group',
|
|
203
|
+
allowAllOutbound: false,
|
|
204
|
+
});
|
|
205
|
+
const redisPassword = new awsCdkLib.aws_secretsmanager.Secret(this, 'RedisPassword', {
|
|
206
|
+
generateSecretString: {
|
|
207
|
+
secretStringTemplate: '{}',
|
|
208
|
+
generateStringKey: 'password',
|
|
209
|
+
excludeCharacters: '@%*()_+=`~{}|[]\\:";\'?,./',
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
const redisCluster = new awsCdkLib.aws_elasticache.CfnReplicationGroup(this, 'RedisCluster', {
|
|
213
|
+
engine: 'Redis',
|
|
214
|
+
engineVersion: '6.x',
|
|
215
|
+
cacheNodeType: 'cache.t2.medium',
|
|
216
|
+
replicationGroupDescription: 'RedisReplicationGroup',
|
|
217
|
+
authToken: redisPassword.secretValueFromJson('password').toString(),
|
|
218
|
+
transitEncryptionEnabled: true,
|
|
219
|
+
atRestEncryptionEnabled: true,
|
|
220
|
+
multiAzEnabled: true,
|
|
221
|
+
cacheSubnetGroupName: redisSubnetGroup.ref,
|
|
222
|
+
numNodeGroups: 1,
|
|
223
|
+
replicasPerNodeGroup: 1,
|
|
224
|
+
securityGroupIds: [redisSecurityGroup.securityGroupId],
|
|
225
|
+
});
|
|
226
|
+
redisCluster.node.addDependency(redisPassword);
|
|
227
|
+
const redisSecrets = new awsCdkLib.aws_secretsmanager.Secret(this, 'RedisSecrets', {
|
|
228
|
+
generateSecretString: {
|
|
229
|
+
secretStringTemplate: JSON.stringify({
|
|
230
|
+
host: redisCluster.attrPrimaryEndPointAddress,
|
|
231
|
+
port: redisCluster.attrPrimaryEndPointPort,
|
|
232
|
+
password: redisPassword.secretValueFromJson('password').toString(),
|
|
233
|
+
tls: {},
|
|
234
|
+
}),
|
|
235
|
+
generateStringKey: 'unused',
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
redisSecrets.node.addDependency(redisPassword);
|
|
239
|
+
redisSecrets.node.addDependency(redisCluster);
|
|
240
|
+
// ECS Cluster
|
|
241
|
+
const cluster = new awsCdkLib.aws_ecs.Cluster(this, 'Cluster', {
|
|
242
|
+
vpc: vpc,
|
|
243
|
+
});
|
|
244
|
+
// Task Policies
|
|
245
|
+
const taskRolePolicies = new awsCdkLib.aws_iam.PolicyDocument({
|
|
246
|
+
statements: [
|
|
247
|
+
// CloudWatch Logs: Create streams and put events
|
|
248
|
+
new awsCdkLib.aws_iam.PolicyStatement({
|
|
249
|
+
effect: awsCdkLib.aws_iam.Effect.ALLOW,
|
|
250
|
+
actions: ['logs:CreateLogStream', 'logs:PutLogEvents'],
|
|
251
|
+
resources: ['arn:aws:logs:*'],
|
|
252
|
+
}),
|
|
253
|
+
// Secrets Manager: Read only access to secrets
|
|
254
|
+
// https://docs.aws.amazon.com/mediaconnect/latest/ug/iam-policy-examples-asm-secrets.html
|
|
255
|
+
new awsCdkLib.aws_iam.PolicyStatement({
|
|
256
|
+
effect: awsCdkLib.aws_iam.Effect.ALLOW,
|
|
257
|
+
actions: [
|
|
258
|
+
'secretsmanager:GetResourcePolicy',
|
|
259
|
+
'secretsmanager:GetSecretValue',
|
|
260
|
+
'secretsmanager:DescribeSecret',
|
|
261
|
+
'secretsmanager:ListSecrets',
|
|
262
|
+
'secretsmanager:ListSecretVersionIds',
|
|
263
|
+
],
|
|
264
|
+
resources: ['arn:aws:secretsmanager:*'],
|
|
265
|
+
}),
|
|
266
|
+
// Parameter Store: Read only access
|
|
267
|
+
// https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-access.html
|
|
268
|
+
new awsCdkLib.aws_iam.PolicyStatement({
|
|
269
|
+
effect: awsCdkLib.aws_iam.Effect.ALLOW,
|
|
270
|
+
actions: ['ssm:GetParametersByPath', 'ssm:GetParameters', 'ssm:GetParameter', 'ssm:DescribeParameters'],
|
|
271
|
+
resources: ['arn:aws:ssm:*'],
|
|
272
|
+
}),
|
|
273
|
+
// SES: Send emails
|
|
274
|
+
// https://docs.aws.amazon.com/ses/latest/dg/sending-authorization-policy-examples.html
|
|
275
|
+
new awsCdkLib.aws_iam.PolicyStatement({
|
|
276
|
+
effect: awsCdkLib.aws_iam.Effect.ALLOW,
|
|
277
|
+
actions: ['ses:SendEmail', 'ses:SendRawEmail'],
|
|
278
|
+
resources: ['arn:aws:ses:*'],
|
|
279
|
+
}),
|
|
280
|
+
// S3: Read and write access to buckets
|
|
281
|
+
// https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazons3.html
|
|
282
|
+
new awsCdkLib.aws_iam.PolicyStatement({
|
|
283
|
+
effect: awsCdkLib.aws_iam.Effect.ALLOW,
|
|
284
|
+
actions: ['s3:ListBucket', 's3:GetObject', 's3:PutObject', 's3:DeleteObject'],
|
|
285
|
+
resources: ['arn:aws:s3:::*'],
|
|
286
|
+
}),
|
|
287
|
+
// IAM: Pass role to innvoke lambda functions
|
|
288
|
+
// https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_passrole.html
|
|
289
|
+
new awsCdkLib.aws_iam.PolicyStatement({
|
|
290
|
+
effect: awsCdkLib.aws_iam.Effect.ALLOW,
|
|
291
|
+
actions: ['iam:ListRoles', 'iam:GetRole', 'iam:PassRole'],
|
|
292
|
+
resources: [botLambdaRole.roleArn],
|
|
293
|
+
}),
|
|
294
|
+
// Lambda: Create, read, update, delete, and invoke functions
|
|
295
|
+
// https://docs.aws.amazon.com/lambda/latest/dg/access-control-identity-based.html
|
|
296
|
+
new awsCdkLib.aws_iam.PolicyStatement({
|
|
297
|
+
effect: awsCdkLib.aws_iam.Effect.ALLOW,
|
|
298
|
+
actions: [
|
|
299
|
+
'lambda:CreateFunction',
|
|
300
|
+
'lambda:GetFunction',
|
|
301
|
+
'lambda:GetFunctionConfiguration',
|
|
302
|
+
'lambda:UpdateFunctionCode',
|
|
303
|
+
'lambda:UpdateFunctionConfiguration',
|
|
304
|
+
'lambda:ListLayerVersions',
|
|
305
|
+
'lambda:GetLayerVersion',
|
|
306
|
+
'lambda:InvokeFunction',
|
|
307
|
+
],
|
|
308
|
+
resources: ['arn:aws:lambda:*'],
|
|
309
|
+
}),
|
|
310
|
+
],
|
|
311
|
+
});
|
|
312
|
+
// Task Role
|
|
313
|
+
const taskRole = new awsCdkLib.aws_iam.Role(this, 'TaskExecutionRole', {
|
|
314
|
+
assumedBy: new awsCdkLib.aws_iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
|
|
315
|
+
description: 'Medplum Server Task Execution Role',
|
|
316
|
+
inlinePolicies: {
|
|
317
|
+
TaskExecutionPolicies: taskRolePolicies,
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
// Task Definitions
|
|
321
|
+
const taskDefinition = new awsCdkLib.aws_ecs.FargateTaskDefinition(this, 'TaskDefinition', {
|
|
322
|
+
memoryLimitMiB: config.serverMemory,
|
|
323
|
+
cpu: config.serverCpu,
|
|
324
|
+
taskRole: taskRole,
|
|
325
|
+
});
|
|
326
|
+
// Log Groups
|
|
327
|
+
const logGroup = new awsCdkLib.aws_logs.LogGroup(this, 'LogGroup', {
|
|
328
|
+
logGroupName: '/ecs/medplum/' + name,
|
|
329
|
+
removalPolicy: awsCdkLib.RemovalPolicy.DESTROY,
|
|
330
|
+
});
|
|
331
|
+
const logDriver = new awsCdkLib.aws_ecs.AwsLogDriver({
|
|
332
|
+
logGroup: logGroup,
|
|
333
|
+
streamPrefix: 'Medplum',
|
|
334
|
+
});
|
|
335
|
+
// Task Containers
|
|
336
|
+
let serverImage = undefined;
|
|
337
|
+
// Pull out the image name and tag from the image URI if it's an ECR image
|
|
338
|
+
const ecrImageUriRegex = new RegExp(`^${config.accountNumber}\\.dkr\\.ecr\\.${config.region}\\.amazonaws\\.com/(.*)[:@](.*)$`);
|
|
339
|
+
const nameTagMatches = config.serverImage.match(ecrImageUriRegex);
|
|
340
|
+
const serverImageName = nameTagMatches?.[1];
|
|
341
|
+
const serverImageTag = nameTagMatches?.[2];
|
|
342
|
+
if (serverImageName && serverImageTag) {
|
|
343
|
+
// Creating an ecr repository image will automatically grant fine-grained permissions to ecs to access the image
|
|
344
|
+
const ecrRepo = awsEcr.Repository.fromRepositoryArn(this, 'ServerImageRepo', `arn:aws:ecr:${config.region}:${config.accountNumber}:repository/${serverImageName}`);
|
|
345
|
+
serverImage = awsCdkLib.aws_ecs.ContainerImage.fromEcrRepository(ecrRepo, serverImageTag);
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
// Otherwise, use the standard container image
|
|
349
|
+
serverImage = awsCdkLib.aws_ecs.ContainerImage.fromRegistry(config.serverImage);
|
|
350
|
+
}
|
|
351
|
+
const serviceContainer = taskDefinition.addContainer('MedplumTaskDefinition', {
|
|
352
|
+
image: serverImage,
|
|
353
|
+
command: [config.region === 'us-east-1' ? `aws:/medplum/${name}/` : `aws:${config.region}:/medplum/${name}/`],
|
|
354
|
+
logging: logDriver,
|
|
355
|
+
});
|
|
356
|
+
serviceContainer.addPortMappings({
|
|
357
|
+
containerPort: config.apiPort,
|
|
358
|
+
hostPort: config.apiPort,
|
|
359
|
+
});
|
|
360
|
+
// Security Groups
|
|
361
|
+
const fargateSecurityGroup = new awsCdkLib.aws_ec2.SecurityGroup(this, 'ServiceSecurityGroup', {
|
|
362
|
+
allowAllOutbound: true,
|
|
363
|
+
securityGroupName: 'MedplumSecurityGroup',
|
|
364
|
+
vpc: vpc,
|
|
365
|
+
});
|
|
366
|
+
// Fargate Services
|
|
367
|
+
const fargateService = new awsCdkLib.aws_ecs.FargateService(this, 'FargateService', {
|
|
368
|
+
cluster: cluster,
|
|
369
|
+
taskDefinition: taskDefinition,
|
|
370
|
+
assignPublicIp: false,
|
|
371
|
+
vpcSubnets: {
|
|
372
|
+
subnetType: awsCdkLib.aws_ec2.SubnetType.PRIVATE_WITH_EGRESS,
|
|
373
|
+
},
|
|
374
|
+
desiredCount: config.desiredServerCount,
|
|
375
|
+
securityGroups: [fargateSecurityGroup],
|
|
376
|
+
});
|
|
377
|
+
// Load Balancer Target Group
|
|
378
|
+
const targetGroup = new awsCdkLib.aws_elasticloadbalancingv2.ApplicationTargetGroup(this, 'TargetGroup', {
|
|
379
|
+
vpc: vpc,
|
|
380
|
+
port: config.apiPort,
|
|
381
|
+
protocol: awsCdkLib.aws_elasticloadbalancingv2.ApplicationProtocol.HTTP,
|
|
382
|
+
healthCheck: {
|
|
383
|
+
path: '/healthcheck',
|
|
384
|
+
interval: awsCdkLib.Duration.seconds(30),
|
|
385
|
+
timeout: awsCdkLib.Duration.seconds(3),
|
|
386
|
+
healthyThresholdCount: 2,
|
|
387
|
+
unhealthyThresholdCount: 5,
|
|
388
|
+
},
|
|
389
|
+
targets: [fargateService],
|
|
390
|
+
});
|
|
391
|
+
// Load Balancer
|
|
392
|
+
const loadBalancer = new awsCdkLib.aws_elasticloadbalancingv2.ApplicationLoadBalancer(this, 'LoadBalancer', {
|
|
393
|
+
vpc: vpc,
|
|
394
|
+
internetFacing: true,
|
|
395
|
+
http2Enabled: true,
|
|
396
|
+
});
|
|
397
|
+
if (config.loadBalancerLoggingEnabled) {
|
|
398
|
+
// Load Balancer logging
|
|
399
|
+
loadBalancer.logAccessLogs(awsCdkLib.aws_s3.Bucket.fromBucketName(this, 'LoggingBucket', config.loadBalancerLoggingBucket), config.loadBalancerLoggingPrefix);
|
|
400
|
+
}
|
|
401
|
+
// HTTPS Listener
|
|
402
|
+
// Forward to the target group
|
|
403
|
+
loadBalancer.addListener('HttpsListener', {
|
|
404
|
+
port: 443,
|
|
405
|
+
certificates: [
|
|
406
|
+
{
|
|
407
|
+
certificateArn: config.apiSslCertArn,
|
|
408
|
+
},
|
|
409
|
+
],
|
|
410
|
+
sslPolicy: awsCdkLib.aws_elasticloadbalancingv2.SslPolicy.FORWARD_SECRECY_TLS12_RES_GCM,
|
|
411
|
+
defaultAction: awsCdkLib.aws_elasticloadbalancingv2.ListenerAction.forward([targetGroup]),
|
|
412
|
+
});
|
|
413
|
+
// WAF
|
|
414
|
+
const waf = new awsCdkLib.aws_wafv2.CfnWebACL(this, 'BackEndWAF', {
|
|
415
|
+
defaultAction: { allow: {} },
|
|
416
|
+
scope: 'REGIONAL',
|
|
417
|
+
name: `${config.stackName}-BackEndWAF`,
|
|
418
|
+
rules: awsManagedRules,
|
|
419
|
+
visibilityConfig: {
|
|
420
|
+
cloudWatchMetricsEnabled: true,
|
|
421
|
+
metricName: `${config.stackName}-BackEndWAF-Metric`,
|
|
422
|
+
sampledRequestsEnabled: false,
|
|
423
|
+
},
|
|
424
|
+
});
|
|
425
|
+
// Create an association between the load balancer and the WAF
|
|
426
|
+
const wafAssociation = new awsCdkLib.aws_wafv2.CfnWebACLAssociation(this, 'LoadBalancerAssociation', {
|
|
427
|
+
resourceArn: loadBalancer.loadBalancerArn,
|
|
428
|
+
webAclArn: waf.attrArn,
|
|
429
|
+
});
|
|
430
|
+
// Grant RDS access to the fargate group
|
|
431
|
+
if (rdsCluster) {
|
|
432
|
+
rdsCluster.connections.allowDefaultPortFrom(fargateSecurityGroup);
|
|
433
|
+
}
|
|
434
|
+
// Grant Redis access to the fargate group
|
|
435
|
+
redisSecurityGroup.addIngressRule(fargateSecurityGroup, awsCdkLib.aws_ec2.Port.tcp(6379));
|
|
436
|
+
// Route 53
|
|
437
|
+
const zone = awsCdkLib.aws_route53.HostedZone.fromLookup(this, 'Zone', {
|
|
438
|
+
domainName: config.domainName.split('.').slice(-2).join('.'),
|
|
439
|
+
});
|
|
440
|
+
// Route53 alias record for the load balancer
|
|
441
|
+
const record = new awsCdkLib.aws_route53.ARecord(this, 'LoadBalancerAliasRecord', {
|
|
442
|
+
recordName: config.apiDomainName,
|
|
443
|
+
target: awsCdkLib.aws_route53.RecordTarget.fromAlias(new awsCdkLib.aws_route53_targets.LoadBalancerTarget(loadBalancer)),
|
|
444
|
+
zone: zone,
|
|
445
|
+
});
|
|
446
|
+
// SSM Parameters
|
|
447
|
+
const databaseSecrets = new awsCdkLib.aws_ssm.StringParameter(this, 'DatabaseSecretsParameter', {
|
|
448
|
+
tier: awsCdkLib.aws_ssm.ParameterTier.STANDARD,
|
|
449
|
+
parameterName: `/medplum/${name}/DatabaseSecrets`,
|
|
450
|
+
description: 'Database secrets ARN',
|
|
451
|
+
stringValue: rdsSecretsArn,
|
|
452
|
+
});
|
|
453
|
+
const redisSecretsParameter = new awsCdkLib.aws_ssm.StringParameter(this, 'RedisSecretsParameter', {
|
|
454
|
+
tier: awsCdkLib.aws_ssm.ParameterTier.STANDARD,
|
|
455
|
+
parameterName: `/medplum/${name}/RedisSecrets`,
|
|
456
|
+
description: 'Redis secrets ARN',
|
|
457
|
+
stringValue: redisSecrets.secretArn,
|
|
458
|
+
});
|
|
459
|
+
const botLambdaRoleParameter = new awsCdkLib.aws_ssm.StringParameter(this, 'BotLambdaRoleParameter', {
|
|
460
|
+
tier: awsCdkLib.aws_ssm.ParameterTier.STANDARD,
|
|
461
|
+
parameterName: `/medplum/${name}/botLambdaRoleArn`,
|
|
462
|
+
description: 'Bot lambda execution role ARN',
|
|
463
|
+
stringValue: botLambdaRole.roleArn,
|
|
464
|
+
});
|
|
465
|
+
// Debug
|
|
466
|
+
console.log('ARecord', record.domainName);
|
|
467
|
+
console.log('DatabaseSecretsParameter', databaseSecrets.parameterArn);
|
|
468
|
+
console.log('RedisSecretsParameter', redisSecretsParameter.parameterArn);
|
|
469
|
+
console.log('RedisCluster', redisCluster.attrPrimaryEndPointAddress);
|
|
470
|
+
console.log('BotLambdaRole', botLambdaRoleParameter.stringValue);
|
|
471
|
+
console.log('WAF', waf.attrArn);
|
|
472
|
+
console.log('WAF Association', wafAssociation.node.id);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Static app infrastructure, which deploys app content to an S3 bucket.
|
|
478
|
+
*
|
|
479
|
+
* The app redirects from HTTP to HTTPS, using a CloudFront distribution,
|
|
480
|
+
* Route53 alias record, and ACM certificate.
|
|
481
|
+
*/
|
|
482
|
+
class FrontEnd extends constructs.Construct {
|
|
483
|
+
constructor(parent, config, region) {
|
|
484
|
+
super(parent, 'FrontEnd');
|
|
485
|
+
let appBucket;
|
|
486
|
+
if (region === config.region) {
|
|
487
|
+
// S3 bucket
|
|
488
|
+
appBucket = new awsCdkLib.aws_s3.Bucket(this, 'AppBucket', {
|
|
489
|
+
bucketName: config.appDomainName,
|
|
490
|
+
publicReadAccess: false,
|
|
491
|
+
blockPublicAccess: awsCdkLib.aws_s3.BlockPublicAccess.BLOCK_ALL,
|
|
492
|
+
removalPolicy: awsCdkLib.RemovalPolicy.DESTROY,
|
|
493
|
+
encryption: awsCdkLib.aws_s3.BucketEncryption.S3_MANAGED,
|
|
494
|
+
enforceSSL: true,
|
|
495
|
+
versioned: false,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
// Otherwise, reference the bucket by name and region
|
|
500
|
+
appBucket = awsCdkLib.aws_s3.Bucket.fromBucketAttributes(this, 'AppBucket', {
|
|
501
|
+
bucketName: config.appDomainName,
|
|
502
|
+
region: config.region,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
if (region === 'us-east-1') {
|
|
506
|
+
// HTTP response headers policy
|
|
507
|
+
const responseHeadersPolicy = new awsCdkLib.aws_cloudfront.ResponseHeadersPolicy(this, 'ResponseHeadersPolicy', {
|
|
508
|
+
securityHeadersBehavior: {
|
|
509
|
+
contentSecurityPolicy: {
|
|
510
|
+
contentSecurityPolicy: [
|
|
511
|
+
`default-src 'none'`,
|
|
512
|
+
`base-uri 'self'`,
|
|
513
|
+
`child-src 'self'`,
|
|
514
|
+
`connect-src 'self' ${config.apiDomainName} *.google.com`,
|
|
515
|
+
`font-src 'self' fonts.gstatic.com`,
|
|
516
|
+
`form-action 'self' *.gstatic.com *.google.com`,
|
|
517
|
+
`frame-ancestors 'none'`,
|
|
518
|
+
`frame-src 'self' *.medplum.com *.gstatic.com *.google.com`,
|
|
519
|
+
`img-src 'self' data: ${config.storageDomainName} *.gstatic.com *.google.com *.googleapis.com`,
|
|
520
|
+
`manifest-src 'self'`,
|
|
521
|
+
`media-src 'self' ${config.storageDomainName}`,
|
|
522
|
+
`script-src 'self' *.medplum.com *.gstatic.com *.google.com`,
|
|
523
|
+
`style-src 'self' 'unsafe-inline' *.medplum.com *.gstatic.com *.google.com`,
|
|
524
|
+
`worker-src 'self' blob: *.gstatic.com *.google.com`,
|
|
525
|
+
`upgrade-insecure-requests`,
|
|
526
|
+
].join('; '),
|
|
527
|
+
override: true,
|
|
528
|
+
},
|
|
529
|
+
contentTypeOptions: { override: true },
|
|
530
|
+
frameOptions: { frameOption: awsCdkLib.aws_cloudfront.HeadersFrameOption.DENY, override: true },
|
|
531
|
+
strictTransportSecurity: {
|
|
532
|
+
accessControlMaxAge: awsCdkLib.Duration.seconds(63072000),
|
|
533
|
+
includeSubdomains: true,
|
|
534
|
+
override: true,
|
|
535
|
+
},
|
|
536
|
+
xssProtection: {
|
|
537
|
+
protection: true,
|
|
538
|
+
modeBlock: true,
|
|
539
|
+
override: true,
|
|
540
|
+
},
|
|
541
|
+
},
|
|
542
|
+
});
|
|
543
|
+
// WAF
|
|
544
|
+
const waf = new awsCdkLib.aws_wafv2.CfnWebACL(this, 'FrontEndWAF', {
|
|
545
|
+
defaultAction: { allow: {} },
|
|
546
|
+
scope: 'CLOUDFRONT',
|
|
547
|
+
name: `${config.stackName}-FrontEndWAF`,
|
|
548
|
+
rules: awsManagedRules,
|
|
549
|
+
visibilityConfig: {
|
|
550
|
+
cloudWatchMetricsEnabled: true,
|
|
551
|
+
metricName: `${config.stackName}-FrontEndWAF-Metric`,
|
|
552
|
+
sampledRequestsEnabled: false,
|
|
553
|
+
},
|
|
554
|
+
});
|
|
555
|
+
// API Origin Cache Policy
|
|
556
|
+
const apiOriginCachePolicy = new awsCdkLib.aws_cloudfront.CachePolicy(this, 'ApiOriginCachePolicy', {
|
|
557
|
+
cachePolicyName: `${config.stackName}-ApiOriginCachePolicy`,
|
|
558
|
+
cookieBehavior: awsCdkLib.aws_cloudfront.CacheCookieBehavior.all(),
|
|
559
|
+
headerBehavior: awsCdkLib.aws_cloudfront.CacheHeaderBehavior.allowList('Authorization', 'Content-Encoding', 'Content-Type', 'If-None-Match', 'Origin', 'Referer', 'User-Agent', 'X-Medplum'),
|
|
560
|
+
queryStringBehavior: awsCdkLib.aws_cloudfront.CacheQueryStringBehavior.all(),
|
|
561
|
+
});
|
|
562
|
+
// Origin access identity
|
|
563
|
+
const originAccessIdentity = new awsCdkLib.aws_cloudfront.OriginAccessIdentity(this, 'OriginAccessIdentity', {});
|
|
564
|
+
appBucket.grantRead(originAccessIdentity);
|
|
565
|
+
// CloudFront distribution
|
|
566
|
+
const distribution = new awsCdkLib.aws_cloudfront.Distribution(this, 'AppDistribution', {
|
|
567
|
+
defaultRootObject: 'index.html',
|
|
568
|
+
defaultBehavior: {
|
|
569
|
+
origin: new awsCdkLib.aws_cloudfront_origins.S3Origin(appBucket, { originAccessIdentity }),
|
|
570
|
+
responseHeadersPolicy,
|
|
571
|
+
viewerProtocolPolicy: awsCdkLib.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
572
|
+
},
|
|
573
|
+
additionalBehaviors: {
|
|
574
|
+
'/api/*': {
|
|
575
|
+
origin: new awsCdkLib.aws_cloudfront_origins.HttpOrigin(config.apiDomainName),
|
|
576
|
+
allowedMethods: awsCdkLib.aws_cloudfront.AllowedMethods.ALLOW_ALL,
|
|
577
|
+
cachePolicy: apiOriginCachePolicy,
|
|
578
|
+
viewerProtocolPolicy: awsCdkLib.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
certificate: awsCdkLib.aws_certificatemanager.Certificate.fromCertificateArn(this, 'AppCertificate', config.appSslCertArn),
|
|
582
|
+
domainNames: [config.appDomainName],
|
|
583
|
+
errorResponses: [
|
|
584
|
+
{
|
|
585
|
+
httpStatus: 403,
|
|
586
|
+
responseHttpStatus: 200,
|
|
587
|
+
responsePagePath: '/index.html',
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
httpStatus: 404,
|
|
591
|
+
responseHttpStatus: 200,
|
|
592
|
+
responsePagePath: '/index.html',
|
|
593
|
+
},
|
|
594
|
+
],
|
|
595
|
+
webAclId: waf.attrArn,
|
|
596
|
+
});
|
|
597
|
+
const zone = awsCdkLib.aws_route53.HostedZone.fromLookup(this, 'Zone', {
|
|
598
|
+
domainName: config.domainName.split('.').slice(-2).join('.'),
|
|
599
|
+
});
|
|
600
|
+
// Route53 alias record for the CloudFront distribution
|
|
601
|
+
const record = new awsCdkLib.aws_route53.ARecord(this, 'AppAliasRecord', {
|
|
602
|
+
recordName: config.appDomainName,
|
|
603
|
+
target: awsCdkLib.aws_route53.RecordTarget.fromAlias(new awsCdkLib.aws_route53_targets.CloudFrontTarget(distribution)),
|
|
604
|
+
zone,
|
|
605
|
+
});
|
|
606
|
+
// Debug
|
|
607
|
+
console.log('ARecord', record.domainName);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Binary storage bucket and CloudFront distribution.
|
|
614
|
+
*/
|
|
615
|
+
class Storage extends constructs.Construct {
|
|
616
|
+
constructor(parent, config, region) {
|
|
617
|
+
super(parent, 'Storage');
|
|
618
|
+
let storageBucket;
|
|
619
|
+
if (region === config.region) {
|
|
620
|
+
// S3 bucket
|
|
621
|
+
storageBucket = new awsCdkLib.aws_s3.Bucket(this, 'StorageBucket', {
|
|
622
|
+
bucketName: config.storageBucketName,
|
|
623
|
+
publicReadAccess: false,
|
|
624
|
+
blockPublicAccess: awsCdkLib.aws_s3.BlockPublicAccess.BLOCK_ALL,
|
|
625
|
+
encryption: awsCdkLib.aws_s3.BucketEncryption.S3_MANAGED,
|
|
626
|
+
enforceSSL: true,
|
|
627
|
+
versioned: false,
|
|
628
|
+
});
|
|
629
|
+
if (config.clamscanEnabled) {
|
|
630
|
+
// ClamAV serverless scan
|
|
631
|
+
const sc = new cdkServerlessClamscan.ServerlessClamscan(this, 'ServerlessClamscan', {
|
|
632
|
+
defsBucketAccessLogsConfig: {
|
|
633
|
+
logsBucket: awsCdkLib.aws_s3.Bucket.fromBucketName(this, 'LoggingBucket', config.clamscanLoggingBucket),
|
|
634
|
+
logsPrefix: config.clamscanLoggingPrefix,
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
sc.addSourceBucket(storageBucket);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
// Otherwise, reference the bucket by name
|
|
642
|
+
storageBucket = awsCdkLib.aws_s3.Bucket.fromBucketAttributes(this, 'StorageBucket', {
|
|
643
|
+
bucketName: config.storageBucketName,
|
|
644
|
+
region: config.region,
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
if (region === 'us-east-1') {
|
|
648
|
+
// Public key in PEM format
|
|
649
|
+
const publicKey = new awsCdkLib.aws_cloudfront.PublicKey(this, 'StoragePublicKey', {
|
|
650
|
+
encodedKey: config.storagePublicKey,
|
|
651
|
+
});
|
|
652
|
+
// Authorized key group for presigned URLs
|
|
653
|
+
const keyGroup = new awsCdkLib.aws_cloudfront.KeyGroup(this, 'StorageKeyGroup', {
|
|
654
|
+
items: [publicKey],
|
|
655
|
+
});
|
|
656
|
+
// HTTP response headers policy
|
|
657
|
+
const responseHeadersPolicy = new awsCdkLib.aws_cloudfront.ResponseHeadersPolicy(this, 'ResponseHeadersPolicy', {
|
|
658
|
+
securityHeadersBehavior: {
|
|
659
|
+
contentSecurityPolicy: {
|
|
660
|
+
contentSecurityPolicy: "default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors *.medplum.com;",
|
|
661
|
+
override: true,
|
|
662
|
+
},
|
|
663
|
+
contentTypeOptions: { override: true },
|
|
664
|
+
frameOptions: { frameOption: awsCdkLib.aws_cloudfront.HeadersFrameOption.DENY, override: true },
|
|
665
|
+
referrerPolicy: { referrerPolicy: awsCdkLib.aws_cloudfront.HeadersReferrerPolicy.NO_REFERRER, override: true },
|
|
666
|
+
strictTransportSecurity: {
|
|
667
|
+
accessControlMaxAge: awsCdkLib.Duration.seconds(63072000),
|
|
668
|
+
includeSubdomains: true,
|
|
669
|
+
override: true,
|
|
670
|
+
},
|
|
671
|
+
xssProtection: {
|
|
672
|
+
protection: true,
|
|
673
|
+
modeBlock: true,
|
|
674
|
+
override: true,
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
});
|
|
678
|
+
// WAF
|
|
679
|
+
const waf = new awsCdkLib.aws_wafv2.CfnWebACL(this, 'StorageWAF', {
|
|
680
|
+
defaultAction: { allow: {} },
|
|
681
|
+
scope: 'CLOUDFRONT',
|
|
682
|
+
name: `${config.stackName}-StorageWAF`,
|
|
683
|
+
rules: awsManagedRules,
|
|
684
|
+
visibilityConfig: {
|
|
685
|
+
cloudWatchMetricsEnabled: true,
|
|
686
|
+
metricName: `${config.stackName}-StorageWAF-Metric`,
|
|
687
|
+
sampledRequestsEnabled: false,
|
|
688
|
+
},
|
|
689
|
+
});
|
|
690
|
+
// Origin access identity
|
|
691
|
+
const originAccessIdentity = new awsCdkLib.aws_cloudfront.OriginAccessIdentity(this, 'OriginAccessIdentity', {});
|
|
692
|
+
storageBucket.grantRead(originAccessIdentity);
|
|
693
|
+
// CloudFront distribution
|
|
694
|
+
const distribution = new awsCdkLib.aws_cloudfront.Distribution(this, 'StorageDistribution', {
|
|
695
|
+
defaultBehavior: {
|
|
696
|
+
origin: new awsCdkLib.aws_cloudfront_origins.S3Origin(storageBucket, { originAccessIdentity }),
|
|
697
|
+
responseHeadersPolicy,
|
|
698
|
+
viewerProtocolPolicy: awsCdkLib.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
699
|
+
trustedKeyGroups: [keyGroup],
|
|
700
|
+
},
|
|
701
|
+
certificate: awsCdkLib.aws_certificatemanager.Certificate.fromCertificateArn(this, 'StorageCertificate', config.storageSslCertArn),
|
|
702
|
+
domainNames: [config.storageDomainName],
|
|
703
|
+
webAclId: waf.attrArn,
|
|
704
|
+
});
|
|
705
|
+
const zone = awsCdkLib.aws_route53.HostedZone.fromLookup(this, 'Zone', {
|
|
706
|
+
domainName: config.domainName.split('.').slice(-2).join('.'),
|
|
707
|
+
});
|
|
708
|
+
// Route53 alias record for the CloudFront distribution
|
|
709
|
+
const record = new awsCdkLib.aws_route53.ARecord(this, 'StorageAliasRecord', {
|
|
710
|
+
recordName: config.storageDomainName,
|
|
711
|
+
target: awsCdkLib.aws_route53.RecordTarget.fromAlias(new awsCdkLib.aws_route53_targets.CloudFrontTarget(distribution)),
|
|
712
|
+
zone,
|
|
713
|
+
});
|
|
714
|
+
// Debug
|
|
715
|
+
console.log('ARecord', record.domainName);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
class MedplumStack {
|
|
721
|
+
constructor(scope, config) {
|
|
722
|
+
this.primaryStack = new awsCdkLib.Stack(scope, config.stackName, {
|
|
723
|
+
env: {
|
|
724
|
+
region: config.region,
|
|
725
|
+
account: config.accountNumber,
|
|
726
|
+
},
|
|
727
|
+
});
|
|
728
|
+
this.backEnd = new BackEnd(this.primaryStack, config);
|
|
729
|
+
this.frontEnd = new FrontEnd(this.primaryStack, config, config.region);
|
|
730
|
+
this.storage = new Storage(this.primaryStack, config, config.region);
|
|
731
|
+
if (config.region !== 'us-east-1') {
|
|
732
|
+
// Some resources must be created in us-east-1
|
|
733
|
+
// For example, CloudFront distributions and ACM certificates
|
|
734
|
+
// If the primary region is not us-east-1, create these resources in us-east-1
|
|
735
|
+
const usEast1Stack = new awsCdkLib.Stack(scope, config.stackName + '-us-east-1', {
|
|
736
|
+
env: {
|
|
737
|
+
region: 'us-east-1',
|
|
738
|
+
account: config.accountNumber,
|
|
739
|
+
},
|
|
740
|
+
});
|
|
741
|
+
this.frontEnd = new FrontEnd(usEast1Stack, config, 'us-east-1');
|
|
742
|
+
this.storage = new Storage(usEast1Stack, config, 'us-east-1');
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
function main(context) {
|
|
747
|
+
const app = new awsCdkLib.App({ context });
|
|
748
|
+
const configFileName = app.node.tryGetContext('config');
|
|
749
|
+
if (!configFileName) {
|
|
750
|
+
console.log('Missing "config" context variable');
|
|
751
|
+
console.log('Usage: cdk deploy -c config=my-config.json');
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
const config = JSON.parse(fs.readFileSync(path.resolve(configFileName), 'utf-8'));
|
|
755
|
+
const stack = new MedplumStack(app, config);
|
|
756
|
+
console.log('Stack', stack.primaryStack.stackId);
|
|
757
|
+
console.log('BackEnd', stack.backEnd.node.id);
|
|
758
|
+
console.log('FrontEnd', stack.frontEnd.node.id);
|
|
759
|
+
console.log('Storage', stack.storage.node.id);
|
|
760
|
+
app.synth();
|
|
761
|
+
}
|
|
762
|
+
if (require.main === module) {
|
|
763
|
+
main();
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
exports.main = main;
|
|
767
|
+
//# sourceMappingURL=index.cjs.map
|