cdk-nuxt 0.1.1

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 ADDED
@@ -0,0 +1,14 @@
1
+ # Welcome to your CDK TypeScript project!
2
+
3
+ This is a blank project for TypeScript development with CDK.
4
+
5
+ The `cdk.json` file tells the CDK Toolkit how to execute your app.
6
+
7
+ ## Useful commands
8
+
9
+ * `npm run build` compile typescript to js
10
+ * `npm run watch` watch for changes and compile
11
+ * `npm run test` perform the jest unit tests
12
+ * `cdk deploy` deploy this stack to your default AWS account/region
13
+ * `cdk diff` compare deployed stack with current state
14
+ * `cdk synth` emits the synthesized CloudFormation template
@@ -0,0 +1,282 @@
1
+ import {Duration, RemovalPolicy, Stack, StackProps, Tags, aws_lambda} from 'aws-cdk-lib';
2
+ import { Construct } from 'constructs';
3
+ import {Certificate, ICertificate} from "aws-cdk-lib/aws-certificatemanager";
4
+ import {
5
+ AllowedMethods,
6
+ BehaviorOptions, CacheCookieBehavior,
7
+ CachedMethods, CacheHeaderBehavior,
8
+ CachePolicy, CacheQueryStringBehavior,
9
+ Distribution, ICachePolicy,
10
+ IOriginAccessIdentity, OriginAccessIdentity, OriginProtocolPolicy, PriceClass,
11
+ SecurityPolicyProtocol,
12
+ ViewerProtocolPolicy
13
+ } from "aws-cdk-lib/aws-cloudfront";
14
+ import {Architecture, Code, LayerVersion, Runtime} from "aws-cdk-lib/aws-lambda";
15
+ import {BlockPublicAccess, Bucket, BucketAccessControl, IBucket} from "aws-cdk-lib/aws-s3";
16
+ import {ARecord, AaaaRecord, HostedZone, IHostedZone, RecordTarget} from "aws-cdk-lib/aws-route53";
17
+ import {BucketDeployment, CacheControl, Source, StorageClass} from "aws-cdk-lib/aws-s3-deployment";
18
+ import {HttpOrigin, S3Origin} from "aws-cdk-lib/aws-cloudfront-origins";
19
+ import {CloudFrontTarget} from "aws-cdk-lib/aws-route53-targets";
20
+ import {HttpMethod} from "aws-cdk-lib/aws-stepfunctions-tasks";
21
+ import {RetentionDays} from "aws-cdk-lib/aws-logs";
22
+ import { HttpLambdaIntegration } from '@aws-cdk/aws-apigatewayv2-integrations-alpha';
23
+ import {HttpApi} from "@aws-cdk/aws-apigatewayv2-alpha";
24
+ import {NuxtAppStaticAssets} from "./nuxt-app-static-assets";
25
+
26
+ export interface AppStackProps extends StackProps {
27
+ readonly project: string;
28
+ readonly service: string;
29
+ readonly environment: string;
30
+ }
31
+
32
+ export interface NuxtAppStackProps extends AppStackProps {
33
+ readonly baseDomain: string;
34
+ readonly subDomain?: string;
35
+
36
+ // Used by the CDN, must be issued in us-east-1 (global)
37
+ readonly globalTlsCertificateArn: string;
38
+
39
+ readonly hostedZoneId: string;
40
+ }
41
+
42
+ export class NuxtAppStack extends Stack {
43
+ private readonly resourceIdPrefix: string;
44
+ private readonly deploymentRevision: string;
45
+ private readonly tlsCertificate: ICertificate;
46
+ private readonly cdnAccessIdentity: IOriginAccessIdentity;
47
+ public staticAssetsBucket: IBucket;
48
+ private readonly layer: LayerVersion;
49
+ private readonly lambdaFunction: aws_lambda.Function;
50
+ private apiGateway: HttpApi;
51
+ private readonly httpsForwardingBehavior: BehaviorOptions;
52
+ private readonly cdn: Distribution;
53
+ private readonly hostedZone: IHostedZone;
54
+
55
+ constructor(scope: Construct, id: string, props: NuxtAppStackProps) {
56
+ super(scope, id, props);
57
+
58
+ Tags.of(scope).add('project', props.project);
59
+ Tags.of(scope).add('domain', props.subDomain ? `${props.subDomain}.${props.baseDomain}` : props.baseDomain);
60
+ Tags.of(scope).add('service', props.service);
61
+ Tags.of(scope).add('environment', props.environment);
62
+
63
+ this.resourceIdPrefix = `${props.project}-${props.service}-${props.environment}`;
64
+ this.deploymentRevision = new Date().toISOString();
65
+ this.tlsCertificate = this.findTlsCertificate(props);
66
+ this.cdnAccessIdentity = this.createCdnAccessIdentity();
67
+ this.staticAssetsBucket = this.createStaticAssetsBucket();
68
+ this.layer = this.createSsrLambdaLayer();
69
+ this.lambdaFunction = this.createLambdaFunction();
70
+ this.apiGateway = this.createApiGateway();
71
+ this.httpsForwardingBehavior = this.createHttpsForwardingBehavior();
72
+ this.cdn = this.createCloudFrontDistribution(props);
73
+ this.configureDeployments();
74
+ this.hostedZone = this.findHostedZone(props);
75
+ this.createDnsRecords(props);
76
+ }
77
+
78
+ private findTlsCertificate(props: NuxtAppStackProps): ICertificate {
79
+ return Certificate.fromCertificateArn(this, `${this.resourceIdPrefix}-tls-certificate`, props.globalTlsCertificateArn);
80
+ }
81
+
82
+ private createCdnAccessIdentity(): IOriginAccessIdentity {
83
+ const originAccessIdentityName = `${this.resourceIdPrefix}-cdn-s3-access`;
84
+ return new OriginAccessIdentity(this, originAccessIdentityName);
85
+ }
86
+
87
+ private createStaticAssetsBucket(): IBucket {
88
+ const bucketName = `${this.resourceIdPrefix}-assets`;
89
+ const bucket = new Bucket(this, bucketName, {
90
+ accessControl: BucketAccessControl.AUTHENTICATED_READ,
91
+ blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
92
+ bucketName,
93
+ // the bucket and all of its objects can be deleted, because all the content is managed in this project
94
+ removalPolicy: RemovalPolicy.DESTROY,
95
+ autoDeleteObjects: true,
96
+ });
97
+
98
+ bucket.grantReadWrite(this.cdnAccessIdentity);
99
+
100
+ return bucket;
101
+ }
102
+
103
+ private createSsrLambdaLayer(): LayerVersion {
104
+ const layerName = `${this.resourceIdPrefix}-ssr-layer`;
105
+ return new LayerVersion(this, layerName, {
106
+ layerVersionName: layerName,
107
+ code: Code.fromAsset('./server/layer'),
108
+ compatibleRuntimes: [Runtime.NODEJS_12_X],
109
+ description: `Contains node_modules required for server-side of ${this.resourceIdPrefix}.`,
110
+ });
111
+ }
112
+
113
+ private createLambdaFunction(): aws_lambda.Function {
114
+ const funcName = `${this.resourceIdPrefix}-function`;
115
+
116
+ return new aws_lambda.Function(this, funcName, {
117
+ functionName: funcName,
118
+ runtime: Runtime.NODEJS_12_X,
119
+ architecture: Architecture.ARM_64,
120
+ layers: [this.layer],
121
+ handler: 'lambda-handler.render',
122
+ code: Code.fromAsset('./deployment', {
123
+ exclude: ['**.svg', '**.ico', '**.png', '**.jpg', 'chunk.*.js*', 'bundle.*.js*', 'bundle.*.js*', 'sw.js*'],
124
+ }),
125
+ timeout: Duration.seconds(10),
126
+ memorySize: 512,
127
+ logRetention: RetentionDays.ONE_MONTH,
128
+ environment: {},
129
+ allowPublicSubnet: false
130
+ });
131
+ }
132
+
133
+ private createApiGateway(): HttpApi {
134
+ const lambdaIntegration = new HttpLambdaIntegration(`${this.resourceIdPrefix}-lambda-integration`, this.lambdaFunction);
135
+ const apiName = `${this.resourceIdPrefix}-api`;
136
+ const apiGateway = new HttpApi(this, apiName, {
137
+ apiName,
138
+ // The app does not allow any cross-origin access by purpose: the app should not be embeddable anywhere
139
+ corsPreflight: undefined,
140
+ defaultIntegration: lambdaIntegration,
141
+ });
142
+
143
+ apiGateway.addRoutes({
144
+ integration: lambdaIntegration,
145
+ path: '/{proxy+}',
146
+ methods: [HttpMethod.GET, HttpMethod.HEAD],
147
+ });
148
+ return apiGateway;
149
+ }
150
+
151
+ private createHttpsForwardingBehavior(): BehaviorOptions {
152
+ return {
153
+ origin: new HttpOrigin(`${this.apiGateway.httpApiId}.execute-api.${this.region}.amazonaws.com`, {
154
+ connectionAttempts: 2,
155
+ connectionTimeout: Duration.seconds(2),
156
+ readTimeout: Duration.seconds(10),
157
+ protocolPolicy: OriginProtocolPolicy.HTTPS_ONLY,
158
+ }),
159
+ allowedMethods: AllowedMethods.ALLOW_GET_HEAD,
160
+ compress: true,
161
+ viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
162
+ originRequestPolicy: undefined,
163
+ cachePolicy: this.createSsrCachePolicy(),
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Eventhough we don't want to cache SSR requests, we still have to create a cache policy, in order to
169
+ * forward required cookies, query params and headers. This doesn't make any sense, because if nothing
170
+ * is cached, one would expect, that anything would/could be forwarded, but anyway...
171
+ */
172
+ private createSsrCachePolicy(): ICachePolicy {
173
+
174
+ // The headers to pass to the app
175
+ const headers = [
176
+ 'User-Agent', // Required to distinguish between mobile and desktop template
177
+ 'Authorization', // For authorization
178
+ ];
179
+
180
+ return new CachePolicy(this, `${this.resourceIdPrefix}-cache-policy`, {
181
+ cachePolicyName: `${this.resourceIdPrefix}-cdn-cache-policy`,
182
+ comment: `Passes all required request data to the ${this.resourceIdPrefix} origin.`,
183
+ defaultTtl: Duration.seconds(0),
184
+ minTtl: Duration.seconds(0),
185
+ maxTtl: Duration.seconds(1), // the max TTL must not be 0 for a cache policy
186
+ queryStringBehavior: CacheQueryStringBehavior.all(),
187
+ headerBehavior: CacheHeaderBehavior.allowList(...headers),
188
+ cookieBehavior: CacheCookieBehavior.all(),
189
+ enableAcceptEncodingBrotli: true,
190
+ enableAcceptEncodingGzip: true,
191
+ });
192
+ }
193
+
194
+ private createCloudFrontDistribution(props: NuxtAppStackProps): Distribution {
195
+ const cdnName = `${this.resourceIdPrefix}-cdn`;
196
+
197
+ return new Distribution(this, cdnName, {
198
+ domainNames: props.subDomain ? [`${props.subDomain}.${props.baseDomain}`] : [props.baseDomain, `*.${props.baseDomain}`],
199
+ comment: `${this.resourceIdPrefix}-redirect`,
200
+ minimumProtocolVersion: SecurityPolicyProtocol.TLS_V1_2_2018,
201
+ certificate: this.tlsCertificate,
202
+ defaultBehavior: this.httpsForwardingBehavior,
203
+ additionalBehaviors: this.createStaticAssetBehaviors(),
204
+ priceClass: PriceClass.PRICE_CLASS_100, // Use only North America and Europe
205
+ });
206
+ }
207
+
208
+ private createStaticAssetBehaviors(): Record<string, BehaviorOptions> {
209
+ const staticAssetsCacheConfig: BehaviorOptions = {
210
+ origin: new S3Origin(this.staticAssetsBucket, {
211
+ connectionAttempts: 2,
212
+ connectionTimeout: Duration.seconds(3),
213
+ originAccessIdentity: this.cdnAccessIdentity,
214
+ originPath: this.deploymentRevision,
215
+ }),
216
+ compress: true,
217
+ allowedMethods: AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
218
+ cachedMethods: CachedMethods.CACHE_GET_HEAD_OPTIONS,
219
+ cachePolicy: CachePolicy.CACHING_OPTIMIZED,
220
+ viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
221
+ };
222
+
223
+ const rules: Record<string, BehaviorOptions> = {};
224
+ NuxtAppStaticAssets.forEach(asset => {
225
+ rules[`${asset.target}${asset.pattern}`] = staticAssetsCacheConfig
226
+ })
227
+
228
+ return rules
229
+ }
230
+
231
+ /**
232
+ * In order to enable a zero-downtime deployment, we use a new subdirectory (revision) for every deployment.
233
+ * The previous versions are retained to allow clients to continue to work with an older revision.
234
+ */
235
+ private configureDeployments(): BucketDeployment[] {
236
+ const defaultCacheConfig = [
237
+ CacheControl.setPublic(),
238
+ CacheControl.maxAge(Duration.days(365)),
239
+ CacheControl.fromString('immutable'),
240
+ ];
241
+
242
+ // Returns a deployment for every configured static asset type to respect the different cache settings
243
+ return NuxtAppStaticAssets.map((asset, assetIndex) => {
244
+ return new BucketDeployment(this, `${this.resourceIdPrefix}-assets-deployment-${assetIndex}`, {
245
+ sources: [Source.asset(asset.source)],
246
+ destinationBucket: this.staticAssetsBucket,
247
+ destinationKeyPrefix: this.deploymentRevision + asset.target,
248
+ prune: false,
249
+ storageClass: StorageClass.STANDARD,
250
+ exclude: ['*'],
251
+ include: [asset.pattern],
252
+ cacheControl: asset.cacheControl ?? defaultCacheConfig,
253
+ contentType: asset.contentType,
254
+ })
255
+ });
256
+ }
257
+
258
+ private findHostedZone(props: NuxtAppStackProps): IHostedZone {
259
+ return HostedZone.fromHostedZoneAttributes(this, `${this.resourceIdPrefix}-hosted-zone`, {
260
+ hostedZoneId: props.hostedZoneId,
261
+ zoneName: props.baseDomain,
262
+ });
263
+ }
264
+
265
+ private createDnsRecords(props: NuxtAppStackProps): void {
266
+ const dnsTarget = RecordTarget.fromAlias(new CloudFrontTarget(this.cdn));
267
+
268
+ // Create a record for IPv4
269
+ new ARecord(this, `${this.resourceIdPrefix}-ipv4-record`, {
270
+ recordName: props.subDomain ? `${props.subDomain}.${props.baseDomain}` : props.baseDomain,
271
+ zone: this.hostedZone,
272
+ target: dnsTarget,
273
+ });
274
+
275
+ // Create a record for IPv6
276
+ new AaaaRecord(this, `${this.resourceIdPrefix}-ipv6-record`, {
277
+ recordName: props.subDomain ? `${props.subDomain}.${props.baseDomain}` : props.baseDomain,
278
+ zone: this.hostedZone,
279
+ target: dnsTarget,
280
+ });
281
+ }
282
+ }
@@ -0,0 +1,107 @@
1
+ import {CacheControl} from "aws-cdk-lib/aws-s3-deployment";
2
+ import {Duration} from "aws-cdk-lib";
3
+
4
+ interface StaticAssetConfig {
5
+ pattern: string, // The pattern to use for accessing the files
6
+ contentType: string, // The type of the files to upload
7
+ source: string, // The local directory to upload the files from
8
+ target: string, // The remote path at which to make the uploaded files accessible
9
+ cacheControl?: CacheControl[] // The custom cache settings
10
+ }
11
+
12
+ const buildAssetsSourcePath = './.nuxt/dist/client';
13
+ const buildAssetsTargetPath = '/assets/'; // Must match 'build.publicPath' in nuxt.config.js
14
+
15
+ const customAssetsSourcePath = './src/static';
16
+ const customAssetsTargetPath = '/';
17
+
18
+ // Defines the paths with their cache settings that shall be public available in our app
19
+ // These should match the files in 'src/.nuxt/dist/client' and 'static'
20
+ export const NuxtAppStaticAssets: StaticAssetConfig[] = [
21
+
22
+ // Build Assets
23
+ {
24
+ pattern: '*.js',
25
+ target: buildAssetsTargetPath,
26
+ source: buildAssetsSourcePath,
27
+ contentType: 'application/javascript; charset=UTF-8',
28
+ },
29
+ {
30
+ pattern: '*.js.map',
31
+ target: buildAssetsTargetPath,
32
+ source: buildAssetsSourcePath,
33
+ contentType: 'application/json; charset=UTF-8',
34
+ },
35
+ {
36
+ pattern: 'sw.js',
37
+ target: buildAssetsTargetPath,
38
+ source: buildAssetsSourcePath,
39
+ contentType: 'application/javascript; charset=UTF-8',
40
+ cacheControl: [CacheControl.setPublic(), CacheControl.maxAge(Duration.days(1))],
41
+ },
42
+ {
43
+ pattern: 'sw.js.map',
44
+ target: buildAssetsTargetPath,
45
+ source: buildAssetsSourcePath,
46
+ contentType: 'application/json; charset=UTF-8',
47
+ cacheControl: [CacheControl.setPublic(), CacheControl.maxAge(Duration.days(1))],
48
+ },
49
+ {
50
+ pattern: '*.svg',
51
+ target: buildAssetsTargetPath,
52
+ source: buildAssetsSourcePath,
53
+ contentType: 'image/svg+xml',
54
+ },
55
+ {
56
+ pattern: '*.eot',
57
+ target: buildAssetsTargetPath,
58
+ source: buildAssetsSourcePath,
59
+ contentType: 'application/vnd.ms-fontobject',
60
+ },
61
+ {
62
+ pattern: '*.ttf',
63
+ target: buildAssetsTargetPath,
64
+ source: buildAssetsSourcePath,
65
+ contentType: 'application/font-sfnt',
66
+ },
67
+ {
68
+ pattern: '*.woff',
69
+ target: buildAssetsTargetPath,
70
+ source: buildAssetsSourcePath,
71
+ contentType: 'font/woff',
72
+ },
73
+ {
74
+ pattern: '*.woff2',
75
+ target: buildAssetsTargetPath,
76
+ source: buildAssetsSourcePath,
77
+ contentType: 'font/woff2',
78
+ },
79
+
80
+ // Custom Static Assets
81
+ {
82
+ pattern: '*.png',
83
+ source: customAssetsSourcePath,
84
+ target: customAssetsTargetPath,
85
+ contentType: 'image/png',
86
+ },
87
+ {
88
+ pattern: '*.jpg',
89
+ source: customAssetsSourcePath,
90
+ target: customAssetsTargetPath,
91
+ contentType: 'image/jpg',
92
+ },
93
+ {
94
+ pattern: 'robots.txt',
95
+ source: customAssetsSourcePath,
96
+ target: customAssetsTargetPath,
97
+ contentType: 'text/plain; charset=UTF-8',
98
+ cacheControl: [CacheControl.setPublic(), CacheControl.maxAge(Duration.days(1))],
99
+ },
100
+ {
101
+ pattern: 'manifest.json',
102
+ source: customAssetsSourcePath,
103
+ target: customAssetsTargetPath,
104
+ contentType: 'application/json; charset=UTF-8',
105
+ cacheControl: [CacheControl.setPublic(), CacheControl.maxAge(Duration.days(2))],
106
+ },
107
+ ];
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "cdk-nuxt",
3
+ "version": "0.1.1",
4
+ "files": [
5
+ "lib"
6
+ ],
7
+ "main": "./lib/nuxt-app-stack.js",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "watch": "tsc -w",
11
+ "cdk": "cdk"
12
+ },
13
+ "devDependencies": {
14
+ "@types/node": "10.17.27",
15
+ "aws-cdk": "2.10.0",
16
+ "ts-node": "^9.0.0",
17
+ "typescript": "~3.9.7"
18
+ },
19
+ "dependencies": {
20
+ "@aws-cdk/aws-apigatewayv2-alpha": "^2.10.0-alpha.0",
21
+ "@aws-cdk/aws-apigatewayv2-integrations-alpha": "^2.10.0-alpha.0",
22
+ "aws-cdk-lib": "2.10.0",
23
+ "constructs": "^10.0.0",
24
+ "source-map-support": "^0.5.16"
25
+ }
26
+ }