@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/src/index.ts
CHANGED
|
@@ -1,51 +1,15 @@
|
|
|
1
1
|
import { MedplumInfraConfig } from '@medplum/core';
|
|
2
|
-
import { App
|
|
2
|
+
import { App } from 'aws-cdk-lib';
|
|
3
3
|
import { readFileSync } from 'fs';
|
|
4
4
|
import { resolve } from 'path';
|
|
5
|
-
import {
|
|
6
|
-
import { FrontEnd } from './frontend';
|
|
7
|
-
import { Storage } from './storage';
|
|
8
|
-
import { CloudTrailAlarms } from './cloudtrail';
|
|
5
|
+
import { MedplumStack } from './stack';
|
|
9
6
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
constructor(scope: App, config: MedplumInfraConfig) {
|
|
18
|
-
this.primaryStack = new Stack(scope, config.stackName, {
|
|
19
|
-
env: {
|
|
20
|
-
region: config.region,
|
|
21
|
-
account: config.accountNumber,
|
|
22
|
-
},
|
|
23
|
-
});
|
|
24
|
-
Tags.of(this.primaryStack).add('medplum:environment', config.name);
|
|
25
|
-
|
|
26
|
-
this.backEnd = new BackEnd(this.primaryStack, config);
|
|
27
|
-
this.frontEnd = new FrontEnd(this.primaryStack, config, config.region);
|
|
28
|
-
this.storage = new Storage(this.primaryStack, config, config.region);
|
|
29
|
-
this.cloudTrail = new CloudTrailAlarms(this.primaryStack, config);
|
|
30
|
-
|
|
31
|
-
if (config.region !== 'us-east-1') {
|
|
32
|
-
// Some resources must be created in us-east-1
|
|
33
|
-
// For example, CloudFront distributions and ACM certificates
|
|
34
|
-
// If the primary region is not us-east-1, create these resources in us-east-1
|
|
35
|
-
const usEast1Stack = new Stack(scope, config.stackName + '-us-east-1', {
|
|
36
|
-
env: {
|
|
37
|
-
region: 'us-east-1',
|
|
38
|
-
account: config.accountNumber,
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
Tags.of(usEast1Stack).add('medplum:environment', config.name);
|
|
42
|
-
|
|
43
|
-
this.frontEnd = new FrontEnd(usEast1Stack, config, 'us-east-1');
|
|
44
|
-
this.storage = new Storage(usEast1Stack, config, 'us-east-1');
|
|
45
|
-
this.cloudTrail = new CloudTrailAlarms(usEast1Stack, config);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
7
|
+
export * from './backend';
|
|
8
|
+
export * from './cloudtrail';
|
|
9
|
+
export * from './frontend';
|
|
10
|
+
export * from './stack';
|
|
11
|
+
export * from './storage';
|
|
12
|
+
export * from './waf';
|
|
49
13
|
|
|
50
14
|
export function main(context?: Record<string, string>): void {
|
|
51
15
|
const app = new App({ context });
|
|
@@ -60,12 +24,7 @@ export function main(context?: Record<string, string>): void {
|
|
|
60
24
|
const config = JSON.parse(readFileSync(resolve(configFileName), 'utf-8')) as MedplumInfraConfig;
|
|
61
25
|
|
|
62
26
|
const stack = new MedplumStack(app, config);
|
|
63
|
-
|
|
64
27
|
console.log('Stack', stack.primaryStack.stackId);
|
|
65
|
-
console.log('BackEnd', stack.backEnd.node.id);
|
|
66
|
-
console.log('FrontEnd', stack.frontEnd.node.id);
|
|
67
|
-
console.log('Storage', stack.storage.node.id);
|
|
68
|
-
console.log('CloudTrail', stack.cloudTrail.node.id);
|
|
69
28
|
|
|
70
29
|
app.synth();
|
|
71
30
|
}
|
package/src/oai.ts
CHANGED
|
@@ -11,13 +11,15 @@ import { aws_cloudfront as cloudfront, aws_iam as iam, aws_s3 as s3 } from 'aws-
|
|
|
11
11
|
* However, if importing an S3 bucket via `s3.Bucket.fromBucketAttributes()`, that does not work.
|
|
12
12
|
*
|
|
13
13
|
* See: https://stackoverflow.com/a/60917015
|
|
14
|
+
*
|
|
14
15
|
* @param bucket The S3 bucket.
|
|
15
16
|
* @param identity The CloudFront Origin Access Identity.
|
|
17
|
+
* @returns The policy statement.
|
|
16
18
|
*/
|
|
17
19
|
export function grantBucketAccessToOriginAccessIdentity(
|
|
18
20
|
bucket: s3.IBucket,
|
|
19
21
|
identity: cloudfront.OriginAccessIdentity
|
|
20
|
-
):
|
|
22
|
+
): iam.PolicyStatement {
|
|
21
23
|
const policyStatement = new iam.PolicyStatement();
|
|
22
24
|
policyStatement.addActions('s3:GetObject*');
|
|
23
25
|
policyStatement.addActions('s3:GetBucket*');
|
|
@@ -26,4 +28,5 @@ export function grantBucketAccessToOriginAccessIdentity(
|
|
|
26
28
|
policyStatement.addResources(`${bucket.bucketArn}/*`);
|
|
27
29
|
policyStatement.addCanonicalUserPrincipal(identity.cloudFrontOriginAccessIdentityS3CanonicalUserId);
|
|
28
30
|
bucket.addToResourcePolicy(policyStatement);
|
|
31
|
+
return policyStatement;
|
|
29
32
|
}
|
package/src/stack.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { MedplumInfraConfig } from '@medplum/core';
|
|
2
|
+
import { App, Stack, Tags } from 'aws-cdk-lib';
|
|
3
|
+
import { BackEnd } from './backend';
|
|
4
|
+
import { CloudTrailAlarms } from './cloudtrail';
|
|
5
|
+
import { FrontEnd } from './frontend';
|
|
6
|
+
import { Storage } from './storage';
|
|
7
|
+
|
|
8
|
+
export class MedplumStack {
|
|
9
|
+
primaryStack: MedplumPrimaryStack;
|
|
10
|
+
globalStack?: MedplumGlobalStack;
|
|
11
|
+
|
|
12
|
+
constructor(scope: App, config: MedplumInfraConfig) {
|
|
13
|
+
this.primaryStack = new MedplumPrimaryStack(scope, config);
|
|
14
|
+
|
|
15
|
+
if (config.region !== 'us-east-1') {
|
|
16
|
+
// Some resources must be created in us-east-1
|
|
17
|
+
// For example, CloudFront distributions and ACM certificates
|
|
18
|
+
// If the primary region is not us-east-1, create these resources in us-east-1
|
|
19
|
+
this.globalStack = new MedplumGlobalStack(scope, config);
|
|
20
|
+
this.globalStack.addDependency(this.primaryStack);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class MedplumPrimaryStack extends Stack {
|
|
26
|
+
backEnd: BackEnd;
|
|
27
|
+
frontEnd: FrontEnd;
|
|
28
|
+
storage: Storage;
|
|
29
|
+
cloudTrail: CloudTrailAlarms;
|
|
30
|
+
|
|
31
|
+
constructor(scope: App, config: MedplumInfraConfig) {
|
|
32
|
+
super(scope, config.stackName, {
|
|
33
|
+
env: {
|
|
34
|
+
region: config.region,
|
|
35
|
+
account: config.accountNumber,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
Tags.of(this).add('medplum:environment', config.name);
|
|
39
|
+
|
|
40
|
+
this.backEnd = new BackEnd(this, config);
|
|
41
|
+
this.frontEnd = new FrontEnd(this, config, config.region);
|
|
42
|
+
this.storage = new Storage(this, config, config.region);
|
|
43
|
+
this.cloudTrail = new CloudTrailAlarms(this, config);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class MedplumGlobalStack extends Stack {
|
|
48
|
+
frontEnd: FrontEnd;
|
|
49
|
+
storage: Storage;
|
|
50
|
+
cloudTrail: CloudTrailAlarms;
|
|
51
|
+
|
|
52
|
+
constructor(scope: App, config: MedplumInfraConfig) {
|
|
53
|
+
super(scope, config.stackName + '-us-east-1', {
|
|
54
|
+
env: {
|
|
55
|
+
region: 'us-east-1',
|
|
56
|
+
account: config.accountNumber,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
Tags.of(this).add('medplum:environment', config.name);
|
|
60
|
+
|
|
61
|
+
this.frontEnd = new FrontEnd(this, config, 'us-east-1');
|
|
62
|
+
this.storage = new Storage(this, config, 'us-east-1');
|
|
63
|
+
this.cloudTrail = new CloudTrailAlarms(this, config);
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/storage.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
|
aws_route53 as route53,
|
|
8
9
|
aws_s3 as s3,
|
|
@@ -18,14 +19,21 @@ import { awsManagedRules } from './waf';
|
|
|
18
19
|
* Binary storage bucket and CloudFront distribution.
|
|
19
20
|
*/
|
|
20
21
|
export class Storage extends Construct {
|
|
22
|
+
storageBucket: s3.IBucket;
|
|
23
|
+
keyGroup?: cloudfront.IKeyGroup;
|
|
24
|
+
responseHeadersPolicy?: cloudfront.IResponseHeadersPolicy;
|
|
25
|
+
waf?: wafv2.CfnWebACL;
|
|
26
|
+
originAccessIdentity?: cloudfront.OriginAccessIdentity;
|
|
27
|
+
originAccessPolicyStatement?: iam.PolicyStatement;
|
|
28
|
+
distribution?: cloudfront.IDistribution;
|
|
29
|
+
dnsRecord?: route53.IRecordSet;
|
|
30
|
+
|
|
21
31
|
constructor(parent: Construct, config: MedplumInfraConfig, region: string) {
|
|
22
32
|
super(parent, 'Storage');
|
|
23
33
|
|
|
24
|
-
let storageBucket: s3.IBucket;
|
|
25
|
-
|
|
26
34
|
if (region === config.region) {
|
|
27
35
|
// S3 bucket
|
|
28
|
-
storageBucket = new s3.Bucket(this, 'StorageBucket', {
|
|
36
|
+
this.storageBucket = new s3.Bucket(this, 'StorageBucket', {
|
|
29
37
|
bucketName: config.storageBucketName,
|
|
30
38
|
publicReadAccess: false,
|
|
31
39
|
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
|
|
@@ -42,11 +50,11 @@ export class Storage extends Construct {
|
|
|
42
50
|
logsPrefix: config.clamscanLoggingPrefix,
|
|
43
51
|
},
|
|
44
52
|
});
|
|
45
|
-
sc.addSourceBucket(storageBucket);
|
|
53
|
+
sc.addSourceBucket(this.storageBucket);
|
|
46
54
|
}
|
|
47
55
|
} else {
|
|
48
|
-
// Otherwise, reference the bucket by name
|
|
49
|
-
storageBucket = s3.Bucket.fromBucketAttributes(this, 'StorageBucket', {
|
|
56
|
+
// Otherwise, reference the bucket by name and region
|
|
57
|
+
this.storageBucket = s3.Bucket.fromBucketAttributes(this, 'StorageBucket', {
|
|
50
58
|
bucketName: config.storageBucketName,
|
|
51
59
|
region: config.region,
|
|
52
60
|
});
|
|
@@ -64,12 +72,12 @@ export class Storage extends Construct {
|
|
|
64
72
|
}
|
|
65
73
|
|
|
66
74
|
// Authorized key group for presigned URLs
|
|
67
|
-
|
|
75
|
+
this.keyGroup = new cloudfront.KeyGroup(this, 'StorageKeyGroup', {
|
|
68
76
|
items: [publicKey],
|
|
69
77
|
});
|
|
70
78
|
|
|
71
79
|
// HTTP response headers policy
|
|
72
|
-
|
|
80
|
+
this.responseHeadersPolicy = new cloudfront.ResponseHeadersPolicy(this, 'ResponseHeadersPolicy', {
|
|
73
81
|
securityHeadersBehavior: {
|
|
74
82
|
contentSecurityPolicy: {
|
|
75
83
|
contentSecurityPolicy:
|
|
@@ -93,7 +101,7 @@ export class Storage extends Construct {
|
|
|
93
101
|
});
|
|
94
102
|
|
|
95
103
|
// WAF
|
|
96
|
-
|
|
104
|
+
this.waf = new wafv2.CfnWebACL(this, 'StorageWAF', {
|
|
97
105
|
defaultAction: { allow: {} },
|
|
98
106
|
scope: 'CLOUDFRONT',
|
|
99
107
|
name: `${config.stackName}-StorageWAF`,
|
|
@@ -106,20 +114,25 @@ export class Storage extends Construct {
|
|
|
106
114
|
});
|
|
107
115
|
|
|
108
116
|
// Origin access identity
|
|
109
|
-
|
|
110
|
-
grantBucketAccessToOriginAccessIdentity(
|
|
117
|
+
this.originAccessIdentity = new cloudfront.OriginAccessIdentity(this, 'OriginAccessIdentity', {});
|
|
118
|
+
this.originAccessPolicyStatement = grantBucketAccessToOriginAccessIdentity(
|
|
119
|
+
this.storageBucket,
|
|
120
|
+
this.originAccessIdentity
|
|
121
|
+
);
|
|
111
122
|
|
|
112
123
|
// CloudFront distribution
|
|
113
|
-
|
|
124
|
+
this.distribution = new cloudfront.Distribution(this, 'StorageDistribution', {
|
|
114
125
|
defaultBehavior: {
|
|
115
|
-
origin: new origins.S3Origin(storageBucket, {
|
|
116
|
-
|
|
126
|
+
origin: new origins.S3Origin(this.storageBucket, {
|
|
127
|
+
originAccessIdentity: this.originAccessIdentity,
|
|
128
|
+
}),
|
|
129
|
+
responseHeadersPolicy: this.responseHeadersPolicy,
|
|
117
130
|
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
118
|
-
trustedKeyGroups: [keyGroup],
|
|
131
|
+
trustedKeyGroups: [this.keyGroup],
|
|
119
132
|
},
|
|
120
133
|
certificate: acm.Certificate.fromCertificateArn(this, 'StorageCertificate', config.storageSslCertArn),
|
|
121
134
|
domainNames: [config.storageDomainName],
|
|
122
|
-
webAclId: waf.attrArn,
|
|
135
|
+
webAclId: this.waf.attrArn,
|
|
123
136
|
logBucket: config.storageLoggingBucket
|
|
124
137
|
? s3.Bucket.fromBucketName(this, 'LoggingBucket', config.storageLoggingBucket)
|
|
125
138
|
: undefined,
|
|
@@ -127,22 +140,18 @@ export class Storage extends Construct {
|
|
|
127
140
|
});
|
|
128
141
|
|
|
129
142
|
// DNS
|
|
130
|
-
let record = undefined;
|
|
131
143
|
if (!config.skipDns) {
|
|
132
144
|
const zone = route53.HostedZone.fromLookup(this, 'Zone', {
|
|
133
145
|
domainName: config.domainName.split('.').slice(-2).join('.'),
|
|
134
146
|
});
|
|
135
147
|
|
|
136
148
|
// Route53 alias record for the CloudFront distribution
|
|
137
|
-
|
|
149
|
+
this.dnsRecord = new route53.ARecord(this, 'StorageAliasRecord', {
|
|
138
150
|
recordName: config.storageDomainName,
|
|
139
|
-
target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(distribution)),
|
|
151
|
+
target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(this.distribution)),
|
|
140
152
|
zone,
|
|
141
153
|
});
|
|
142
154
|
}
|
|
143
|
-
|
|
144
|
-
// Debug
|
|
145
|
-
console.log('ARecord', record?.domainName);
|
|
146
155
|
}
|
|
147
156
|
}
|
|
148
157
|
}
|