@medplum/cdk 2.1.1 → 2.1.2

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/src/index.ts CHANGED
@@ -1,51 +1,15 @@
1
1
  import { MedplumInfraConfig } from '@medplum/core';
2
- import { App, Stack, Tags } from 'aws-cdk-lib';
2
+ import { App } from 'aws-cdk-lib';
3
3
  import { readFileSync } from 'fs';
4
4
  import { resolve } from 'path';
5
- import { BackEnd } from './backend';
6
- import { FrontEnd } from './frontend';
7
- import { Storage } from './storage';
8
- import { CloudTrailAlarms } from './cloudtrail';
5
+ import { MedplumStack } from './stack';
9
6
 
10
- class MedplumStack {
11
- primaryStack: Stack;
12
- backEnd: BackEnd;
13
- frontEnd: FrontEnd;
14
- storage: Storage;
15
- cloudTrail: CloudTrailAlarms;
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
- ): void {
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
- const keyGroup = new cloudfront.KeyGroup(this, 'StorageKeyGroup', {
75
+ this.keyGroup = new cloudfront.KeyGroup(this, 'StorageKeyGroup', {
68
76
  items: [publicKey],
69
77
  });
70
78
 
71
79
  // HTTP response headers policy
72
- const responseHeadersPolicy = new cloudfront.ResponseHeadersPolicy(this, 'ResponseHeadersPolicy', {
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
- const waf = new wafv2.CfnWebACL(this, 'StorageWAF', {
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
- const originAccessIdentity = new cloudfront.OriginAccessIdentity(this, 'OriginAccessIdentity', {});
110
- grantBucketAccessToOriginAccessIdentity(storageBucket, originAccessIdentity);
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
- const distribution = new cloudfront.Distribution(this, 'StorageDistribution', {
124
+ this.distribution = new cloudfront.Distribution(this, 'StorageDistribution', {
114
125
  defaultBehavior: {
115
- origin: new origins.S3Origin(storageBucket, { originAccessIdentity }),
116
- responseHeadersPolicy,
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
- record = new route53.ARecord(this, 'StorageAliasRecord', {
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
  }