@medplum/cdk 2.1.17 → 2.1.18
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.map +2 -2
- package/dist/esm/index.mjs.map +2 -2
- package/package.json +7 -1
- package/babel.config.json +0 -3
- package/cdk.json +0 -3
- package/esbuild.mjs +0 -47
- package/jest.config.json +0 -11
- package/src/backend.ts +0 -496
- package/src/cloudtrail.ts +0 -134
- package/src/config.test.ts +0 -487
- package/src/config.ts +0 -202
- package/src/frontend.ts +0 -188
- package/src/index.test.ts +0 -539
- package/src/index.ts +0 -42
- package/src/oai.ts +0 -32
- package/src/stack.ts +0 -65
- package/src/storage.ts +0 -157
- package/src/waf.ts +0 -122
- package/tsconfig.build.json +0 -9
- package/tsconfig.json +0 -8
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@medplum/cdk",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.18",
|
|
4
4
|
"description": "Medplum CDK Infra as Code",
|
|
5
5
|
"homepage": "https://www.medplum.com/",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/medplum/medplum/issues"
|
|
8
|
+
},
|
|
6
9
|
"repository": {
|
|
7
10
|
"type": "git",
|
|
8
11
|
"url": "git+https://github.com/medplum/medplum.git",
|
|
@@ -10,6 +13,9 @@
|
|
|
10
13
|
},
|
|
11
14
|
"license": "Apache-2.0",
|
|
12
15
|
"author": "Medplum <hello@medplum.com>",
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
13
19
|
"scripts": {
|
|
14
20
|
"build": "npm run clean && tsc --project tsconfig.build.json && node esbuild.mjs",
|
|
15
21
|
"cdk": "cdk",
|
package/babel.config.json
DELETED
package/cdk.json
DELETED
package/esbuild.mjs
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/* global console */
|
|
2
|
-
/* eslint no-console: "off" */
|
|
3
|
-
|
|
4
|
-
import esbuild from 'esbuild';
|
|
5
|
-
import { writeFileSync } from 'fs';
|
|
6
|
-
|
|
7
|
-
const options = {
|
|
8
|
-
entryPoints: ['./src/index.ts'],
|
|
9
|
-
bundle: true,
|
|
10
|
-
platform: 'node',
|
|
11
|
-
loader: { '.ts': 'ts' },
|
|
12
|
-
resolveExtensions: ['.ts'],
|
|
13
|
-
target: 'es2021',
|
|
14
|
-
tsconfig: 'tsconfig.json',
|
|
15
|
-
minify: true,
|
|
16
|
-
sourcemap: true,
|
|
17
|
-
external: [
|
|
18
|
-
'@aws-sdk/client-acm',
|
|
19
|
-
'@aws-sdk/client-ssm',
|
|
20
|
-
'@aws-sdk/client-sts',
|
|
21
|
-
'aws-cdk-lib',
|
|
22
|
-
'aws-cdk-lib/aws-ecr',
|
|
23
|
-
'aws-cdk-lib/aws-rds',
|
|
24
|
-
'cdk',
|
|
25
|
-
'cdk-nag',
|
|
26
|
-
'cdk-serverless-clamscan',
|
|
27
|
-
'constructs',
|
|
28
|
-
],
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
esbuild
|
|
32
|
-
.build({
|
|
33
|
-
...options,
|
|
34
|
-
format: 'cjs',
|
|
35
|
-
outfile: './dist/cjs/index.cjs',
|
|
36
|
-
})
|
|
37
|
-
.then(() => writeFileSync('./dist/cjs/package.json', '{"type": "commonjs"}'))
|
|
38
|
-
.catch(console.error);
|
|
39
|
-
|
|
40
|
-
esbuild
|
|
41
|
-
.build({
|
|
42
|
-
...options,
|
|
43
|
-
format: 'esm',
|
|
44
|
-
outfile: './dist/esm/index.mjs',
|
|
45
|
-
})
|
|
46
|
-
.then(() => writeFileSync('./dist/esm/package.json', '{"type": "module"}'))
|
|
47
|
-
.catch(console.error);
|
package/jest.config.json
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"testEnvironment": "node",
|
|
3
|
-
"transform": {
|
|
4
|
-
"^.+\\.(js|jsx|ts|tsx)$": "babel-jest"
|
|
5
|
-
},
|
|
6
|
-
"moduleFileExtensions": ["ts", "js", "json", "node"],
|
|
7
|
-
"testMatch": ["**/src/**/*.test.ts"],
|
|
8
|
-
"coverageDirectory": "coverage",
|
|
9
|
-
"coverageReporters": ["json", "text"],
|
|
10
|
-
"collectCoverageFrom": ["**/src/**/*", "!**/src/__mocks__/**/*.ts"]
|
|
11
|
-
}
|
package/src/backend.ts
DELETED
|
@@ -1,496 +0,0 @@
|
|
|
1
|
-
import { MedplumInfraConfig } from '@medplum/core';
|
|
2
|
-
import {
|
|
3
|
-
Duration,
|
|
4
|
-
RemovalPolicy,
|
|
5
|
-
aws_ec2 as ec2,
|
|
6
|
-
aws_ecs as ecs,
|
|
7
|
-
aws_elasticache as elasticache,
|
|
8
|
-
aws_elasticloadbalancingv2 as elbv2,
|
|
9
|
-
aws_iam as iam,
|
|
10
|
-
aws_logs as logs,
|
|
11
|
-
aws_rds as rds,
|
|
12
|
-
aws_route53 as route53,
|
|
13
|
-
aws_s3 as s3,
|
|
14
|
-
aws_secretsmanager as secretsmanager,
|
|
15
|
-
aws_ssm as ssm,
|
|
16
|
-
aws_route53_targets as targets,
|
|
17
|
-
aws_wafv2 as wafv2,
|
|
18
|
-
} from 'aws-cdk-lib';
|
|
19
|
-
import { Repository } from 'aws-cdk-lib/aws-ecr';
|
|
20
|
-
import { ClusterInstance } from 'aws-cdk-lib/aws-rds';
|
|
21
|
-
import { Construct } from 'constructs';
|
|
22
|
-
import { awsManagedRules } from './waf';
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Based on: https://github.com/aws-samples/http-api-aws-fargate-cdk/blob/master/cdk/singleAccount/lib/fargate-vpclink-stack.ts
|
|
26
|
-
*
|
|
27
|
-
* RDS config: https://docs.aws.amazon.com/cdk/api/latest/docs/aws-rds-readme.html
|
|
28
|
-
*/
|
|
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
|
-
|
|
58
|
-
constructor(scope: Construct, config: MedplumInfraConfig) {
|
|
59
|
-
super(scope, 'BackEnd');
|
|
60
|
-
|
|
61
|
-
const name = config.name;
|
|
62
|
-
|
|
63
|
-
// VPC
|
|
64
|
-
if (config.vpcId) {
|
|
65
|
-
// Lookup VPC by ARN
|
|
66
|
-
this.vpc = ec2.Vpc.fromLookup(this, 'VPC', { vpcId: config.vpcId });
|
|
67
|
-
} else {
|
|
68
|
-
// VPC Flow Logs
|
|
69
|
-
const vpcFlowLogs = new logs.LogGroup(this, 'VpcFlowLogs', {
|
|
70
|
-
logGroupName: '/medplum/flowlogs/' + name,
|
|
71
|
-
removalPolicy: RemovalPolicy.DESTROY,
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// Create VPC
|
|
75
|
-
this.vpc = new ec2.Vpc(this, 'VPC', {
|
|
76
|
-
maxAzs: config.maxAzs,
|
|
77
|
-
flowLogs: {
|
|
78
|
-
cloudwatch: {
|
|
79
|
-
destination: ec2.FlowLogDestination.toCloudWatchLogs(vpcFlowLogs),
|
|
80
|
-
trafficType: ec2.FlowLogTrafficType.ALL,
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Bot Lambda Role
|
|
87
|
-
this.botLambdaRole = new iam.Role(this, 'BotLambdaRole', {
|
|
88
|
-
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
// RDS
|
|
92
|
-
this.rdsSecretsArn = config.rdsSecretsArn;
|
|
93
|
-
if (!this.rdsSecretsArn) {
|
|
94
|
-
// See: https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_rds-readme.html#migrating-from-instanceprops
|
|
95
|
-
const instanceProps: rds.ProvisionedClusterInstanceProps = {
|
|
96
|
-
instanceType: config.rdsInstanceType ? new ec2.InstanceType(config.rdsInstanceType) : undefined,
|
|
97
|
-
enablePerformanceInsights: true,
|
|
98
|
-
isFromLegacyInstanceProps: true,
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
let readers = undefined;
|
|
102
|
-
if (config.rdsInstances > 1) {
|
|
103
|
-
readers = [];
|
|
104
|
-
for (let i = 0; i < config.rdsInstances - 1; i++) {
|
|
105
|
-
readers.push(
|
|
106
|
-
ClusterInstance.provisioned('Instance' + (i + 2), {
|
|
107
|
-
...instanceProps,
|
|
108
|
-
})
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
this.rdsCluster = new rds.DatabaseCluster(this, 'DatabaseCluster', {
|
|
114
|
-
engine: rds.DatabaseClusterEngine.auroraPostgres({
|
|
115
|
-
version: config.rdsInstanceVersion
|
|
116
|
-
? rds.AuroraPostgresEngineVersion.of(
|
|
117
|
-
config.rdsInstanceVersion,
|
|
118
|
-
config.rdsInstanceVersion.slice(0, config.rdsInstanceVersion.indexOf('.')),
|
|
119
|
-
{ s3Import: true, s3Export: true }
|
|
120
|
-
)
|
|
121
|
-
: rds.AuroraPostgresEngineVersion.VER_12_9,
|
|
122
|
-
}),
|
|
123
|
-
credentials: rds.Credentials.fromGeneratedSecret('clusteradmin'),
|
|
124
|
-
defaultDatabaseName: 'medplum',
|
|
125
|
-
storageEncrypted: true,
|
|
126
|
-
vpc: this.vpc,
|
|
127
|
-
vpcSubnets: {
|
|
128
|
-
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
|
|
129
|
-
},
|
|
130
|
-
writer: ClusterInstance.provisioned('Instance1', {
|
|
131
|
-
...instanceProps,
|
|
132
|
-
}),
|
|
133
|
-
readers,
|
|
134
|
-
backup: {
|
|
135
|
-
retention: Duration.days(7),
|
|
136
|
-
},
|
|
137
|
-
cloudwatchLogsExports: ['postgresql'],
|
|
138
|
-
instanceUpdateBehaviour: rds.InstanceUpdateBehaviour.ROLLING,
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
this.rdsSecretsArn = (this.rdsCluster.secret as secretsmanager.ISecret).secretArn;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Redis
|
|
145
|
-
// Important: For HIPAA compliance, you must specify TransitEncryptionEnabled as true, an AuthToken, and a CacheSubnetGroup.
|
|
146
|
-
this.redisSubnetGroup = new elasticache.CfnSubnetGroup(this, 'RedisSubnetGroup', {
|
|
147
|
-
description: 'Redis Subnet Group',
|
|
148
|
-
subnetIds: this.vpc.privateSubnets.map((subnet) => subnet.subnetId),
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
this.redisSecurityGroup = new ec2.SecurityGroup(this, 'RedisSecurityGroup', {
|
|
152
|
-
vpc: this.vpc,
|
|
153
|
-
description: 'Redis Security Group',
|
|
154
|
-
allowAllOutbound: false,
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
this.redisPassword = new secretsmanager.Secret(this, 'RedisPassword', {
|
|
158
|
-
generateSecretString: {
|
|
159
|
-
secretStringTemplate: '{}',
|
|
160
|
-
generateStringKey: 'password',
|
|
161
|
-
excludeCharacters: '@%*()_+=`~{}|[]\\:";\'?,./',
|
|
162
|
-
},
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
this.redisCluster = new elasticache.CfnReplicationGroup(this, 'RedisCluster', {
|
|
166
|
-
engine: 'Redis',
|
|
167
|
-
engineVersion: '6.x',
|
|
168
|
-
cacheNodeType: config.cacheNodeType ?? 'cache.t2.medium',
|
|
169
|
-
replicationGroupDescription: 'RedisReplicationGroup',
|
|
170
|
-
authToken: this.redisPassword.secretValueFromJson('password').toString(),
|
|
171
|
-
transitEncryptionEnabled: true,
|
|
172
|
-
atRestEncryptionEnabled: true,
|
|
173
|
-
multiAzEnabled: true,
|
|
174
|
-
cacheSubnetGroupName: this.redisSubnetGroup.ref,
|
|
175
|
-
numNodeGroups: 1,
|
|
176
|
-
replicasPerNodeGroup: 1,
|
|
177
|
-
securityGroupIds: [this.redisSecurityGroup.securityGroupId],
|
|
178
|
-
});
|
|
179
|
-
this.redisCluster.node.addDependency(this.redisPassword);
|
|
180
|
-
|
|
181
|
-
this.redisSecrets = new secretsmanager.Secret(this, 'RedisSecrets', {
|
|
182
|
-
generateSecretString: {
|
|
183
|
-
secretStringTemplate: JSON.stringify({
|
|
184
|
-
host: this.redisCluster.attrPrimaryEndPointAddress,
|
|
185
|
-
port: this.redisCluster.attrPrimaryEndPointPort,
|
|
186
|
-
password: this.redisPassword.secretValueFromJson('password').toString(),
|
|
187
|
-
tls: {},
|
|
188
|
-
}),
|
|
189
|
-
generateStringKey: 'unused',
|
|
190
|
-
},
|
|
191
|
-
});
|
|
192
|
-
this.redisSecrets.node.addDependency(this.redisPassword);
|
|
193
|
-
this.redisSecrets.node.addDependency(this.redisCluster);
|
|
194
|
-
|
|
195
|
-
// ECS Cluster
|
|
196
|
-
this.ecsCluster = new ecs.Cluster(this, 'Cluster', {
|
|
197
|
-
vpc: this.vpc,
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
// Task Policies
|
|
201
|
-
this.taskRolePolicies = new iam.PolicyDocument({
|
|
202
|
-
statements: [
|
|
203
|
-
// CloudWatch Logs: Create streams and put events
|
|
204
|
-
new iam.PolicyStatement({
|
|
205
|
-
effect: iam.Effect.ALLOW,
|
|
206
|
-
actions: ['logs:CreateLogStream', 'logs:PutLogEvents'],
|
|
207
|
-
resources: ['arn:aws:logs:*'],
|
|
208
|
-
}),
|
|
209
|
-
|
|
210
|
-
// Secrets Manager: Read only access to secrets
|
|
211
|
-
// https://docs.aws.amazon.com/mediaconnect/latest/ug/iam-policy-examples-asm-secrets.html
|
|
212
|
-
new iam.PolicyStatement({
|
|
213
|
-
effect: iam.Effect.ALLOW,
|
|
214
|
-
actions: [
|
|
215
|
-
'secretsmanager:GetResourcePolicy',
|
|
216
|
-
'secretsmanager:GetSecretValue',
|
|
217
|
-
'secretsmanager:DescribeSecret',
|
|
218
|
-
'secretsmanager:ListSecrets',
|
|
219
|
-
'secretsmanager:ListSecretVersionIds',
|
|
220
|
-
],
|
|
221
|
-
resources: ['arn:aws:secretsmanager:*'],
|
|
222
|
-
}),
|
|
223
|
-
|
|
224
|
-
// Parameter Store: Read only access
|
|
225
|
-
// https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-access.html
|
|
226
|
-
new iam.PolicyStatement({
|
|
227
|
-
effect: iam.Effect.ALLOW,
|
|
228
|
-
actions: ['ssm:GetParametersByPath', 'ssm:GetParameters', 'ssm:GetParameter', 'ssm:DescribeParameters'],
|
|
229
|
-
resources: ['arn:aws:ssm:*'],
|
|
230
|
-
}),
|
|
231
|
-
|
|
232
|
-
// SES: Send emails
|
|
233
|
-
// https://docs.aws.amazon.com/ses/latest/dg/sending-authorization-policy-examples.html
|
|
234
|
-
new iam.PolicyStatement({
|
|
235
|
-
effect: iam.Effect.ALLOW,
|
|
236
|
-
actions: ['ses:SendEmail', 'ses:SendRawEmail'],
|
|
237
|
-
resources: ['arn:aws:ses:*'],
|
|
238
|
-
}),
|
|
239
|
-
|
|
240
|
-
// S3: Read and write access to buckets
|
|
241
|
-
// https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazons3.html
|
|
242
|
-
new iam.PolicyStatement({
|
|
243
|
-
effect: iam.Effect.ALLOW,
|
|
244
|
-
actions: ['s3:ListBucket', 's3:GetObject', 's3:PutObject', 's3:DeleteObject'],
|
|
245
|
-
resources: ['arn:aws:s3:::*'],
|
|
246
|
-
}),
|
|
247
|
-
|
|
248
|
-
// IAM: Pass role to innvoke lambda functions
|
|
249
|
-
// https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_passrole.html
|
|
250
|
-
new iam.PolicyStatement({
|
|
251
|
-
effect: iam.Effect.ALLOW,
|
|
252
|
-
actions: ['iam:ListRoles', 'iam:GetRole', 'iam:PassRole'],
|
|
253
|
-
resources: [this.botLambdaRole.roleArn],
|
|
254
|
-
}),
|
|
255
|
-
|
|
256
|
-
// Lambda: Create, read, update, delete, and invoke functions
|
|
257
|
-
// https://docs.aws.amazon.com/lambda/latest/dg/access-control-identity-based.html
|
|
258
|
-
new iam.PolicyStatement({
|
|
259
|
-
effect: iam.Effect.ALLOW,
|
|
260
|
-
actions: [
|
|
261
|
-
'lambda:CreateFunction',
|
|
262
|
-
'lambda:GetFunction',
|
|
263
|
-
'lambda:GetFunctionConfiguration',
|
|
264
|
-
'lambda:UpdateFunctionCode',
|
|
265
|
-
'lambda:UpdateFunctionConfiguration',
|
|
266
|
-
'lambda:ListLayerVersions',
|
|
267
|
-
'lambda:GetLayerVersion',
|
|
268
|
-
'lambda:InvokeFunction',
|
|
269
|
-
],
|
|
270
|
-
resources: ['arn:aws:lambda:*'],
|
|
271
|
-
}),
|
|
272
|
-
],
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
// Task Role
|
|
276
|
-
this.taskRole = new iam.Role(this, 'TaskExecutionRole', {
|
|
277
|
-
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
|
|
278
|
-
description: 'Medplum Server Task Execution Role',
|
|
279
|
-
inlinePolicies: {
|
|
280
|
-
TaskExecutionPolicies: this.taskRolePolicies,
|
|
281
|
-
},
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
// Task Definitions
|
|
285
|
-
this.taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDefinition', {
|
|
286
|
-
memoryLimitMiB: config.serverMemory,
|
|
287
|
-
cpu: config.serverCpu,
|
|
288
|
-
taskRole: this.taskRole,
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
// Log Groups
|
|
292
|
-
this.logGroup = new logs.LogGroup(this, 'LogGroup', {
|
|
293
|
-
logGroupName: '/ecs/medplum/' + name,
|
|
294
|
-
removalPolicy: RemovalPolicy.DESTROY,
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
this.logDriver = new ecs.AwsLogDriver({
|
|
298
|
-
logGroup: this.logGroup,
|
|
299
|
-
streamPrefix: 'Medplum',
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
// Task Containers
|
|
303
|
-
this.serviceContainer = this.taskDefinition.addContainer('MedplumTaskDefinition', {
|
|
304
|
-
image: this.getContainerImage(config, config.serverImage),
|
|
305
|
-
command: [config.region === 'us-east-1' ? `aws:/medplum/${name}/` : `aws:${config.region}:/medplum/${name}/`],
|
|
306
|
-
logging: this.logDriver,
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
this.serviceContainer.addPortMappings({
|
|
310
|
-
containerPort: config.apiPort,
|
|
311
|
-
hostPort: config.apiPort,
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
if (config.additionalContainers) {
|
|
315
|
-
for (const container of config.additionalContainers) {
|
|
316
|
-
this.taskDefinition.addContainer('AdditionalContainer-' + container.name, {
|
|
317
|
-
containerName: container.name,
|
|
318
|
-
image: this.getContainerImage(config, container.image),
|
|
319
|
-
command: container.command,
|
|
320
|
-
environment: container.environment,
|
|
321
|
-
logging: this.logDriver,
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Security Groups
|
|
327
|
-
this.fargateSecurityGroup = new ec2.SecurityGroup(this, 'ServiceSecurityGroup', {
|
|
328
|
-
allowAllOutbound: true,
|
|
329
|
-
securityGroupName: 'MedplumSecurityGroup',
|
|
330
|
-
vpc: this.vpc,
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
// Fargate Services
|
|
334
|
-
this.fargateService = new ecs.FargateService(this, 'FargateService', {
|
|
335
|
-
cluster: this.ecsCluster,
|
|
336
|
-
taskDefinition: this.taskDefinition,
|
|
337
|
-
assignPublicIp: false,
|
|
338
|
-
vpcSubnets: {
|
|
339
|
-
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
|
|
340
|
-
},
|
|
341
|
-
desiredCount: config.desiredServerCount,
|
|
342
|
-
securityGroups: [this.fargateSecurityGroup],
|
|
343
|
-
healthCheckGracePeriod: Duration.minutes(5),
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
// Add dependencies - make sure Fargate service is created after RDS and Redis
|
|
347
|
-
if (this.rdsCluster) {
|
|
348
|
-
this.fargateService.node.addDependency(this.rdsCluster);
|
|
349
|
-
}
|
|
350
|
-
this.fargateService.node.addDependency(this.redisCluster);
|
|
351
|
-
|
|
352
|
-
// Load Balancer Target Group
|
|
353
|
-
this.targetGroup = new elbv2.ApplicationTargetGroup(this, 'TargetGroup', {
|
|
354
|
-
vpc: this.vpc,
|
|
355
|
-
port: config.apiPort,
|
|
356
|
-
protocol: elbv2.ApplicationProtocol.HTTP,
|
|
357
|
-
healthCheck: {
|
|
358
|
-
path: '/healthcheck',
|
|
359
|
-
interval: Duration.seconds(30),
|
|
360
|
-
timeout: Duration.seconds(3),
|
|
361
|
-
healthyThresholdCount: 2,
|
|
362
|
-
unhealthyThresholdCount: 5,
|
|
363
|
-
},
|
|
364
|
-
targets: [this.fargateService],
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
// Load Balancer
|
|
368
|
-
this.loadBalancer = new elbv2.ApplicationLoadBalancer(this, 'LoadBalancer', {
|
|
369
|
-
vpc: this.vpc,
|
|
370
|
-
internetFacing: config.apiInternetFacing !== false, // default true
|
|
371
|
-
http2Enabled: true,
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
if (config.loadBalancerLoggingBucket) {
|
|
375
|
-
// Load Balancer logging
|
|
376
|
-
this.loadBalancer.logAccessLogs(
|
|
377
|
-
s3.Bucket.fromBucketName(this, 'LoggingBucket', config.loadBalancerLoggingBucket),
|
|
378
|
-
config.loadBalancerLoggingPrefix
|
|
379
|
-
);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// HTTPS Listener
|
|
383
|
-
// Forward to the target group
|
|
384
|
-
this.loadBalancer.addListener('HttpsListener', {
|
|
385
|
-
port: 443,
|
|
386
|
-
certificates: [
|
|
387
|
-
{
|
|
388
|
-
certificateArn: config.apiSslCertArn,
|
|
389
|
-
},
|
|
390
|
-
],
|
|
391
|
-
sslPolicy: elbv2.SslPolicy.FORWARD_SECRECY_TLS12_RES_GCM,
|
|
392
|
-
defaultAction: elbv2.ListenerAction.forward([this.targetGroup]),
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
// WAF
|
|
396
|
-
this.waf = new wafv2.CfnWebACL(this, 'BackEndWAF', {
|
|
397
|
-
defaultAction: { allow: {} },
|
|
398
|
-
scope: 'REGIONAL',
|
|
399
|
-
name: `${config.stackName}-BackEndWAF`,
|
|
400
|
-
rules: awsManagedRules,
|
|
401
|
-
visibilityConfig: {
|
|
402
|
-
cloudWatchMetricsEnabled: true,
|
|
403
|
-
metricName: `${config.stackName}-BackEndWAF-Metric`,
|
|
404
|
-
sampledRequestsEnabled: false,
|
|
405
|
-
},
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
// Create an association between the load balancer and the WAF
|
|
409
|
-
this.wafAssociation = new wafv2.CfnWebACLAssociation(this, 'LoadBalancerAssociation', {
|
|
410
|
-
resourceArn: this.loadBalancer.loadBalancerArn,
|
|
411
|
-
webAclArn: this.waf.attrArn,
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
// Grant RDS access to the fargate group
|
|
415
|
-
if (this.rdsCluster) {
|
|
416
|
-
this.rdsCluster.connections.allowDefaultPortFrom(this.fargateSecurityGroup);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// Grant Redis access to the fargate group
|
|
420
|
-
this.redisSecurityGroup.addIngressRule(this.fargateSecurityGroup, ec2.Port.tcp(6379));
|
|
421
|
-
|
|
422
|
-
// DNS
|
|
423
|
-
if (!config.skipDns) {
|
|
424
|
-
// Route 53
|
|
425
|
-
const zone = route53.HostedZone.fromLookup(this, 'Zone', {
|
|
426
|
-
domainName: config.domainName.split('.').slice(-2).join('.'),
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
// Route53 alias record for the load balancer
|
|
430
|
-
this.dnsRecord = new route53.ARecord(this, 'LoadBalancerAliasRecord', {
|
|
431
|
-
recordName: config.apiDomainName,
|
|
432
|
-
target: route53.RecordTarget.fromAlias(new targets.LoadBalancerTarget(this.loadBalancer)),
|
|
433
|
-
zone: zone,
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// SSM Parameters
|
|
438
|
-
this.regionParameter = new ssm.StringParameter(this, 'RegionParameter', {
|
|
439
|
-
tier: ssm.ParameterTier.STANDARD,
|
|
440
|
-
parameterName: `/medplum/${name}/awsRegion`,
|
|
441
|
-
description: 'AWS region',
|
|
442
|
-
stringValue: config.region,
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
this.databaseSecretsParameter = new ssm.StringParameter(this, 'DatabaseSecretsParameter', {
|
|
446
|
-
tier: ssm.ParameterTier.STANDARD,
|
|
447
|
-
parameterName: `/medplum/${name}/DatabaseSecrets`,
|
|
448
|
-
description: 'Database secrets ARN',
|
|
449
|
-
stringValue: this.rdsSecretsArn,
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
this.redisSecretsParameter = new ssm.StringParameter(this, 'RedisSecretsParameter', {
|
|
453
|
-
tier: ssm.ParameterTier.STANDARD,
|
|
454
|
-
parameterName: `/medplum/${name}/RedisSecrets`,
|
|
455
|
-
description: 'Redis secrets ARN',
|
|
456
|
-
stringValue: this.redisSecrets.secretArn,
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
this.botLambdaRoleParameter = new ssm.StringParameter(this, 'BotLambdaRoleParameter', {
|
|
460
|
-
tier: ssm.ParameterTier.STANDARD,
|
|
461
|
-
parameterName: `/medplum/${name}/botLambdaRoleArn`,
|
|
462
|
-
description: 'Bot lambda execution role ARN',
|
|
463
|
-
stringValue: this.botLambdaRole.roleArn,
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
/**
|
|
468
|
-
* Returns a container image for the given image name.
|
|
469
|
-
* If the image name is an ECR image, then the image will be pulled from ECR.
|
|
470
|
-
* Otherwise, the image name is assumed to be a Docker Hub image.
|
|
471
|
-
* @param config - The config settings (account number and region).
|
|
472
|
-
* @param imageName - The image name.
|
|
473
|
-
* @returns The container image.
|
|
474
|
-
*/
|
|
475
|
-
private getContainerImage(config: MedplumInfraConfig, imageName: string): ecs.ContainerImage {
|
|
476
|
-
// Pull out the image name and tag from the image URI if it's an ECR image
|
|
477
|
-
const ecrImageUriRegex = new RegExp(
|
|
478
|
-
`^${config.accountNumber}\\.dkr\\.ecr\\.${config.region}\\.amazonaws\\.com/(.*)[:@](.*)$`
|
|
479
|
-
);
|
|
480
|
-
const nameTagMatches = ecrImageUriRegex.exec(imageName);
|
|
481
|
-
const serverImageName = nameTagMatches?.[1];
|
|
482
|
-
const serverImageTag = nameTagMatches?.[2];
|
|
483
|
-
if (serverImageName && serverImageTag) {
|
|
484
|
-
// Creating an ecr repository image will automatically grant fine-grained permissions to ecs to access the image
|
|
485
|
-
const ecrRepo = Repository.fromRepositoryArn(
|
|
486
|
-
this,
|
|
487
|
-
'ServerImageRepo',
|
|
488
|
-
`arn:aws:ecr:${config.region}:${config.accountNumber}:repository/${serverImageName}`
|
|
489
|
-
);
|
|
490
|
-
return ecs.ContainerImage.fromEcrRepository(ecrRepo, serverImageTag);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// Otherwise, use the standard container image
|
|
494
|
-
return ecs.ContainerImage.fromRegistry(imageName);
|
|
495
|
-
}
|
|
496
|
-
}
|