quidproquo-deploy-awscdk 0.0.34 → 0.0.35
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/package.json +4 -5
- package/src/DeploymentSettings.ts +5 -0
- package/src/DeploymentType.ts +5 -0
- package/src/QPQApp.ts +5 -0
- package/src/QPQPrototypeSingleServiceStack.ts +366 -0
- package/src/index.ts +6 -0
- package/src/utils.ts +9 -0
package/package.json
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quidproquo-deploy-awscdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.35",
|
|
4
4
|
"description": "",
|
|
5
|
-
"main": "./
|
|
6
|
-
"types": "./lib/index.d.js",
|
|
5
|
+
"main": "./src/index.ts",
|
|
7
6
|
"files": [
|
|
8
7
|
"lib/**/*",
|
|
9
|
-
"src
|
|
8
|
+
"src/**/*"
|
|
10
9
|
],
|
|
11
10
|
"scripts": {
|
|
12
11
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
13
12
|
"clean": "npx rimraf lib",
|
|
14
|
-
"build": "
|
|
13
|
+
"build": "",
|
|
15
14
|
"watch": "npm run clean && tsc -w"
|
|
16
15
|
},
|
|
17
16
|
"repository": {
|
package/src/QPQApp.ts
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { DeploymentSettings } from './DeploymentSettings';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
Stack,
|
|
6
|
+
aws_lambda_nodejs,
|
|
7
|
+
aws_lambda,
|
|
8
|
+
aws_apigateway,
|
|
9
|
+
aws_s3,
|
|
10
|
+
aws_s3_deployment,
|
|
11
|
+
aws_events,
|
|
12
|
+
aws_events_targets,
|
|
13
|
+
aws_secretsmanager,
|
|
14
|
+
aws_route53,
|
|
15
|
+
aws_certificatemanager,
|
|
16
|
+
aws_route53_targets,
|
|
17
|
+
aws_cloudfront,
|
|
18
|
+
aws_ssm,
|
|
19
|
+
} from 'aws-cdk-lib';
|
|
20
|
+
import * as cdk from 'aws-cdk-lib';
|
|
21
|
+
|
|
22
|
+
import { Construct } from 'constructs';
|
|
23
|
+
|
|
24
|
+
import { QPQAWSLambdaConfig } from 'quidproquo-actionprocessor-awslambda';
|
|
25
|
+
|
|
26
|
+
import { qpqCoreUtils } from 'quidproquo-core';
|
|
27
|
+
import { qpqWebServerUtils } from 'quidproquo-webserver';
|
|
28
|
+
|
|
29
|
+
export interface QPQPrototypeStackProps extends DeploymentSettings {
|
|
30
|
+
account: string;
|
|
31
|
+
region: string;
|
|
32
|
+
|
|
33
|
+
apiBuildPath: string;
|
|
34
|
+
webBuildPath: string;
|
|
35
|
+
|
|
36
|
+
lambdaAPIGatewayEventPath?: string;
|
|
37
|
+
lambdaEventBridgeEventPath?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const createApiDomainName = (stack: cdk.Stack, id: string, stackProps: QPQPrototypeStackProps) => {
|
|
41
|
+
const apexDomain = qpqWebServerUtils.getFeatureDomainName(stackProps.qpqConfig);
|
|
42
|
+
|
|
43
|
+
const apiDomainName = `api.${apexDomain}`;
|
|
44
|
+
|
|
45
|
+
const apexHostedZone = aws_route53.HostedZone.fromLookup(stack, `${id}-apex-hostedZone`, {
|
|
46
|
+
domainName: apexDomain,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const certificate = new aws_certificatemanager.Certificate(stack, `${id}-api-certificate`, {
|
|
50
|
+
domainName: apiDomainName,
|
|
51
|
+
certificateName: `api-cert-${qpqCoreUtils.getAppName(stackProps.qpqConfig)}`,
|
|
52
|
+
validation: aws_certificatemanager.CertificateValidation.fromDns(apexHostedZone),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const domainName = new aws_apigateway.DomainName(stack, `${id}-api-custom-domain`, {
|
|
56
|
+
domainName: apiDomainName,
|
|
57
|
+
certificate,
|
|
58
|
+
securityPolicy: aws_apigateway.SecurityPolicy.TLS_1_2,
|
|
59
|
+
endpointType: aws_apigateway.EndpointType.REGIONAL,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
new aws_route53.ARecord(stack, `${id}-a-record`, {
|
|
63
|
+
zone: apexHostedZone,
|
|
64
|
+
recordName: apiDomainName,
|
|
65
|
+
target: aws_route53.RecordTarget.fromAlias(
|
|
66
|
+
new aws_route53_targets.ApiGatewayDomain(domainName),
|
|
67
|
+
),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return domainName;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const createWebDistribution = (
|
|
74
|
+
stack: cdk.Stack,
|
|
75
|
+
id: string,
|
|
76
|
+
stackProps: QPQPrototypeStackProps,
|
|
77
|
+
) => {
|
|
78
|
+
const environment = qpqCoreUtils.getAppFeature(stackProps.qpqConfig);
|
|
79
|
+
const apexDomain = qpqWebServerUtils.getFeatureDomainName(stackProps.qpqConfig);
|
|
80
|
+
const serviceName = qpqCoreUtils.getAppName(stackProps.qpqConfig);
|
|
81
|
+
|
|
82
|
+
// create an s3 bucket
|
|
83
|
+
const staticWebFilesBucket = new aws_s3.Bucket(stack, `${id}-web`, {
|
|
84
|
+
bucketName: `${serviceName}-${environment}-web`,
|
|
85
|
+
// Disable public access to this bucket, CloudFront will do that
|
|
86
|
+
publicReadAccess: false,
|
|
87
|
+
blockPublicAccess: aws_s3.BlockPublicAccess.BLOCK_ALL,
|
|
88
|
+
|
|
89
|
+
// Default website file
|
|
90
|
+
websiteIndexDocument: 'index.html',
|
|
91
|
+
|
|
92
|
+
// Allow bucket to auto delete upon cdk:Destroy
|
|
93
|
+
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
|
94
|
+
autoDeleteObjects: true,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Create OriginAccessIdentity for the bucket.
|
|
98
|
+
const websiteOAI = new aws_cloudfront.OriginAccessIdentity(stack, `${id}-website-OAI`);
|
|
99
|
+
staticWebFilesBucket.grantRead(websiteOAI);
|
|
100
|
+
|
|
101
|
+
// Grab the hosted zone we want to add
|
|
102
|
+
const serviceHostedZone = aws_route53.HostedZone.fromLookup(stack, 'hosted-zone', {
|
|
103
|
+
domainName: apexDomain,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Create a certificate for the distribution - Seems to a bug where Route 53 records not cleaned up
|
|
107
|
+
// after removing the DNS Validated certificate see: https://github.com/aws/aws-cdk/issues/3333
|
|
108
|
+
// `switch over to using the Certificate with the new built-in (CloudFormation-based) DNS validation`
|
|
109
|
+
const validationCertificate =
|
|
110
|
+
aws_certificatemanager.CertificateValidation.fromDns(serviceHostedZone);
|
|
111
|
+
const certificate = new aws_certificatemanager.DnsValidatedCertificate(
|
|
112
|
+
stack,
|
|
113
|
+
`${id}-viewer-cert`,
|
|
114
|
+
{
|
|
115
|
+
hostedZone: serviceHostedZone,
|
|
116
|
+
domainName: apexDomain,
|
|
117
|
+
region: 'us-east-1', // AWS certificates can only exist in the us-east-1 region
|
|
118
|
+
validation: validationCertificate,
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
const viewerCertificate = aws_cloudfront.ViewerCertificate.fromAcmCertificate(certificate, {
|
|
122
|
+
aliases: [apexDomain],
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
new aws_s3_deployment.BucketDeployment(stack, `${id}-DeployWebsite`, {
|
|
126
|
+
sources: [aws_s3_deployment.Source.asset(stackProps.webBuildPath)],
|
|
127
|
+
destinationBucket: staticWebFilesBucket,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Create a cloud front distribution
|
|
131
|
+
// TODO: use aws_cloudfront.Distribution
|
|
132
|
+
const distribution = new aws_cloudfront.CloudFrontWebDistribution(stack, 'cf-distribution', {
|
|
133
|
+
originConfigs: [
|
|
134
|
+
{
|
|
135
|
+
s3OriginSource: {
|
|
136
|
+
s3BucketSource: staticWebFilesBucket,
|
|
137
|
+
originAccessIdentity: websiteOAI,
|
|
138
|
+
},
|
|
139
|
+
behaviors: [
|
|
140
|
+
...[
|
|
141
|
+
'/remoteEntry.js',
|
|
142
|
+
'/index.js', // Add this if we need it
|
|
143
|
+
'/index.html', // Add this if we need it
|
|
144
|
+
].map((pp) => ({
|
|
145
|
+
pathPattern: pp,
|
|
146
|
+
maxTtl: cdk.Duration.seconds(0),
|
|
147
|
+
minTtl: cdk.Duration.seconds(0),
|
|
148
|
+
defaultTtl: cdk.Duration.seconds(0),
|
|
149
|
+
})),
|
|
150
|
+
{
|
|
151
|
+
isDefaultBehavior: true,
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
viewerCertificate: viewerCertificate,
|
|
157
|
+
errorConfigurations: [
|
|
158
|
+
{
|
|
159
|
+
errorCode: 404,
|
|
160
|
+
responseCode: 200,
|
|
161
|
+
responsePagePath: '/',
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Create a cdn link
|
|
167
|
+
new aws_route53.ARecord(stack, `${id}-web-alias`, {
|
|
168
|
+
zone: serviceHostedZone,
|
|
169
|
+
recordName: apexDomain,
|
|
170
|
+
target: aws_route53.RecordTarget.fromAlias(
|
|
171
|
+
new aws_route53_targets.CloudFrontTarget(distribution),
|
|
172
|
+
),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return distribution;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export class QPQPrototypeSingleServiceStack extends Stack {
|
|
179
|
+
constructor(scope: Construct, id: string, props: QPQPrototypeStackProps) {
|
|
180
|
+
super(scope, id, {
|
|
181
|
+
env: {
|
|
182
|
+
account: props.account,
|
|
183
|
+
region: props.region,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const settings = {
|
|
188
|
+
environment: qpqCoreUtils.getAppFeature(props.qpqConfig),
|
|
189
|
+
service: qpqCoreUtils.getAppName(props.qpqConfig),
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const domainName = createApiDomainName(this, id, props);
|
|
193
|
+
|
|
194
|
+
createWebDistribution(this, `${id}-web-dist`, props);
|
|
195
|
+
|
|
196
|
+
const resourceName = (name: string) => {
|
|
197
|
+
return `${settings.service}-${settings.environment}-${name}`;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const ownedSecrets = qpqCoreUtils.getOwnedSecrets(props.qpqConfig).map((secret) => {
|
|
201
|
+
const realSecretName = `${secret.key}-${settings.service}-${settings.environment}`;
|
|
202
|
+
return {
|
|
203
|
+
secretName: secret.key,
|
|
204
|
+
realName: realSecretName,
|
|
205
|
+
secret: new aws_secretsmanager.Secret(this, `${id}-secret-${secret.key}`, {
|
|
206
|
+
secretName: realSecretName,
|
|
207
|
+
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
|
208
|
+
description: `${settings.environment}-${settings.service}`,
|
|
209
|
+
}),
|
|
210
|
+
};
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const ownedParameters = qpqCoreUtils.getOwnedParameters(props.qpqConfig).map((parameter) => {
|
|
214
|
+
const realParameterName = `${parameter.key}-${settings.service}-${settings.environment}`;
|
|
215
|
+
return {
|
|
216
|
+
parameterName: parameter.key,
|
|
217
|
+
realName: realParameterName,
|
|
218
|
+
parameter: new aws_ssm.StringParameter(this, `${id}-parameter-${parameter.key}`, {
|
|
219
|
+
parameterName: realParameterName,
|
|
220
|
+
description: `${settings.environment}-${settings.service}`,
|
|
221
|
+
stringValue: parameter.value,
|
|
222
|
+
|
|
223
|
+
// No additional costs ~ 4k max size
|
|
224
|
+
tier: aws_ssm.ParameterTier.STANDARD,
|
|
225
|
+
}),
|
|
226
|
+
};
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const buckets = qpqCoreUtils.getStorageDriveNames(props.qpqConfig).map((driveName) => {
|
|
230
|
+
const bucketName = resourceName(driveName);
|
|
231
|
+
|
|
232
|
+
const bucket = new aws_s3.Bucket(this, bucketName, {
|
|
233
|
+
bucketName: resourceName(driveName),
|
|
234
|
+
|
|
235
|
+
// Disable public access to this bucket, CloudFront will do that
|
|
236
|
+
publicReadAccess: false,
|
|
237
|
+
blockPublicAccess: aws_s3.BlockPublicAccess.BLOCK_ALL,
|
|
238
|
+
|
|
239
|
+
// Allow bucket to auto delete upon cdk:Destroy
|
|
240
|
+
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
|
241
|
+
autoDeleteObjects: true,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
driveName,
|
|
246
|
+
bucketName: bucketName,
|
|
247
|
+
bucket: bucket,
|
|
248
|
+
};
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// This should be all resource names
|
|
252
|
+
const runtimeConfigLambdaConfig: QPQAWSLambdaConfig = {
|
|
253
|
+
qpqConfig: props.qpqConfig,
|
|
254
|
+
secretNameMap: ownedSecrets.reduce(
|
|
255
|
+
(acc, os) => ({
|
|
256
|
+
...acc,
|
|
257
|
+
[os.secretName]: os.realName,
|
|
258
|
+
}),
|
|
259
|
+
{},
|
|
260
|
+
),
|
|
261
|
+
parameterNameMap: ownedParameters.reduce(
|
|
262
|
+
(acc, os) => ({
|
|
263
|
+
...acc,
|
|
264
|
+
[os.parameterName]: os.realName,
|
|
265
|
+
}),
|
|
266
|
+
{},
|
|
267
|
+
),
|
|
268
|
+
resourceNameMap: buckets.reduce(
|
|
269
|
+
(acc, b) => ({
|
|
270
|
+
...acc,
|
|
271
|
+
[b.driveName]: b.bucketName,
|
|
272
|
+
}),
|
|
273
|
+
{},
|
|
274
|
+
),
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const lambdaHandler = new aws_lambda.Function(
|
|
278
|
+
this,
|
|
279
|
+
`${settings.environment}-${settings.service}-lambda-rest`,
|
|
280
|
+
{
|
|
281
|
+
functionName: `lambda-rest-${settings.service}-${settings.environment}`,
|
|
282
|
+
timeout: cdk.Duration.seconds(25),
|
|
283
|
+
|
|
284
|
+
runtime: aws_lambda.Runtime.NODEJS_16_X,
|
|
285
|
+
memorySize: 1024,
|
|
286
|
+
|
|
287
|
+
code: aws_lambda.Code.fromAsset(props.apiBuildPath),
|
|
288
|
+
handler: 'lambdaAPIGatewayEvent.executeAPIGatewayEvent',
|
|
289
|
+
|
|
290
|
+
environment: {
|
|
291
|
+
TABLES: JSON.stringify([]),
|
|
292
|
+
lambdaRuntimeConfig: JSON.stringify(runtimeConfigLambdaConfig),
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
// Grant access to the lambda
|
|
298
|
+
buckets.forEach((b) => b.bucket.grantReadWrite(lambdaHandler));
|
|
299
|
+
ownedSecrets.forEach((os) => os.secret.grantRead(lambdaHandler));
|
|
300
|
+
ownedParameters.forEach((os) => os.parameter.grantRead(lambdaHandler));
|
|
301
|
+
|
|
302
|
+
// Create a rest api
|
|
303
|
+
const api = new aws_apigateway.LambdaRestApi(
|
|
304
|
+
this,
|
|
305
|
+
`${settings.environment}-${settings.service}-http-api`,
|
|
306
|
+
{
|
|
307
|
+
restApiName: `${settings.environment}-${settings.service}-http-api`,
|
|
308
|
+
handler: lambdaHandler,
|
|
309
|
+
deployOptions: {
|
|
310
|
+
loggingLevel: aws_apigateway.MethodLoggingLevel.INFO,
|
|
311
|
+
dataTraceEnabled: true,
|
|
312
|
+
},
|
|
313
|
+
proxy: true,
|
|
314
|
+
},
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
// Map all requests to this service to /serviceName/*
|
|
318
|
+
new aws_apigateway.BasePathMapping(this, `${id}-rest-bpm`, {
|
|
319
|
+
domainName: domainName,
|
|
320
|
+
restApi: api,
|
|
321
|
+
|
|
322
|
+
// the properties below are optional
|
|
323
|
+
// basePath: settings.service,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
qpqCoreUtils.getScheduleEvents(props.qpqConfig).forEach((se, index) => {
|
|
327
|
+
const schedulerFunction = new aws_lambda.Function(
|
|
328
|
+
this,
|
|
329
|
+
`${settings.environment}-${settings.service}-SE-${index}`,
|
|
330
|
+
{
|
|
331
|
+
functionName: `SE-${index}-${se.runtime}-${settings.environment}-${settings.service}`,
|
|
332
|
+
timeout: cdk.Duration.minutes(15),
|
|
333
|
+
|
|
334
|
+
runtime: aws_lambda.Runtime.NODEJS_16_X,
|
|
335
|
+
memorySize: 1024,
|
|
336
|
+
|
|
337
|
+
code: aws_lambda.Code.fromAsset(props.apiBuildPath),
|
|
338
|
+
handler: 'lambdaEventBridgeEvent.executeEventBridgeEvent',
|
|
339
|
+
|
|
340
|
+
environment: {
|
|
341
|
+
TABLES: JSON.stringify([]),
|
|
342
|
+
lambdaRuntimeConfig: JSON.stringify({
|
|
343
|
+
...runtimeConfigLambdaConfig,
|
|
344
|
+
lambdaRuntimeConfig: {
|
|
345
|
+
src: se.src,
|
|
346
|
+
runtime: se.runtime,
|
|
347
|
+
},
|
|
348
|
+
}),
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
buckets.forEach((b) => b.bucket.grantReadWrite(schedulerFunction));
|
|
354
|
+
ownedSecrets.forEach((os) => os.secret.grantRead(schedulerFunction));
|
|
355
|
+
ownedParameters.forEach((os) => os.parameter.grantRead(schedulerFunction));
|
|
356
|
+
|
|
357
|
+
// EventBridge rule which runs every five minutes
|
|
358
|
+
const cronRule = new aws_events.Rule(this, `${id}-se-cronrule-${index}`, {
|
|
359
|
+
schedule: aws_events.Schedule.expression(`cron(${se.cronExpression})`),
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Set the target as lambda function
|
|
363
|
+
cronRule.addTarget(new aws_events_targets.LambdaFunction(schedulerFunction));
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
package/src/index.ts
ADDED
package/src/utils.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { qpqCoreUtils } from 'quidproquo-core';
|
|
2
|
+
import { DeploymentSettings } from './DeploymentSettings';
|
|
3
|
+
|
|
4
|
+
export const getStackName = (deploymentSettings: DeploymentSettings) => {
|
|
5
|
+
const appName = qpqCoreUtils.getAppName(deploymentSettings.qpqConfig);
|
|
6
|
+
const environment = qpqCoreUtils.getAppFeature(deploymentSettings.qpqConfig);
|
|
7
|
+
|
|
8
|
+
return `${appName}-${environment}`;
|
|
9
|
+
};
|