cdk-nuxt 0.2.8 → 0.3.0

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.
@@ -1,4 +1,4 @@
1
- import {Duration, RemovalPolicy, Stack, Tags} from 'aws-cdk-lib';
1
+ import {Duration, RemovalPolicy, Stack} from 'aws-cdk-lib';
2
2
  import { Construct } from 'constructs';
3
3
  import {Certificate, ICertificate} from "aws-cdk-lib/aws-certificatemanager";
4
4
  import {
@@ -21,66 +21,160 @@ import {HttpMethod} from "aws-cdk-lib/aws-stepfunctions-tasks";
21
21
  import {RetentionDays} from "aws-cdk-lib/aws-logs";
22
22
  import { HttpLambdaIntegration } from '@aws-cdk/aws-apigatewayv2-integrations-alpha';
23
23
  import {HttpApi} from "@aws-cdk/aws-apigatewayv2-alpha";
24
- import {NuxtAppStaticAssets} from "./nuxt-app-static-assets";
24
+ import {getNuxtAppStaticAssetConfigs, StaticAssetConfig} from "./nuxt-app-static-assets";
25
25
  import {AppStackProps} from "./app-stack-props";
26
26
  import * as fs from "fs";
27
+ import {Rule, Schedule} from "aws-cdk-lib/aws-events";
28
+ import {LambdaFunction} from "aws-cdk-lib/aws-events-targets";
29
+ import {NuxtConfig} from "./nuxt-config";
27
30
 
31
+ /**
32
+ * Defines the props required for the {@see NuxtAppStack}.
33
+ */
28
34
  export interface NuxtAppStackProps extends AppStackProps {
35
+ /**
36
+ * The domain (without the protocol) at which the Nuxt app shall be publicly available.
37
+ * A DNS record will be automatically created in Route53 for the domain.
38
+ * This also supports subdomains.
39
+ * Examples: "example.com", "sub.example.com"
40
+ */
29
41
  readonly domain: string;
30
42
 
31
- // Used by the CDN, must be issued in us-east-1 (global)
43
+ /**
44
+ * The id of the hosted zone to create a DNS record for the specified domain.
45
+ */
46
+ readonly hostedZoneId: string;
47
+
48
+ /**
49
+ * The ARN of the certificate to use for the Nuxt app to make it accessible via HTTPS.
50
+ * The certificate must be issued for the specified domain in us-east-1 (global) regardless of the
51
+ * region used for the Nuxt app itself.
52
+ */
32
53
  readonly globalTlsCertificateArn: string;
33
54
 
34
- readonly hostedZoneId: string;
55
+ /**
56
+ * The nuxt.config.js of the Nuxt app.
57
+ */
58
+ readonly nuxtConfig: NuxtConfig;
35
59
  }
36
60
 
61
+ /**
62
+ * Creates a lambda function that renders the Nuxt app and is publicly reachable via a specified domain.
63
+ */
37
64
  export class NuxtAppStack extends Stack {
65
+
66
+ /**
67
+ * The identifier prefix of the resources created by the stack.
68
+ *
69
+ * @private
70
+ */
38
71
  private readonly resourceIdPrefix: string;
72
+
73
+ /**
74
+ * The identifier for the current deployment that is used as S3 folder name
75
+ * to store the static assets of the Nuxt app.
76
+ *
77
+ * @private
78
+ */
39
79
  private readonly deploymentRevision: string;
80
+
81
+ /**
82
+ * The certificate to use for the Nuxt app to make it accessible via HTTPS.
83
+ *
84
+ * @private
85
+ */
40
86
  private readonly tlsCertificate: ICertificate;
87
+
88
+ /**
89
+ * The identity to use for accessing the deployment assets on S3.
90
+ *
91
+ * @private
92
+ */
41
93
  private readonly cdnAccessIdentity: IOriginAccessIdentity;
94
+
95
+ /**
96
+ * The S3 bucket where the deployment assets gets stored.
97
+ */
42
98
  public staticAssetsBucket: IBucket;
43
- private readonly layer: LayerVersion;
99
+
100
+ /**
101
+ * The lambda function to render the Nuxt app on the server side.
102
+ *
103
+ * @private
104
+ */
44
105
  private readonly lambdaFunction: Function;
106
+
107
+ /**
108
+ * The API gateway to make the lambda function to render the Nuxt app publicly available.
109
+ *
110
+ * @private
111
+ */
45
112
  private apiGateway: HttpApi;
46
- private readonly httpsForwardingBehavior: BehaviorOptions;
113
+
114
+ /**
115
+ * The configs for the static assets of the Nuxt app that shall be publicly available.
116
+ *
117
+ * @private
118
+ */
119
+ private staticAssetConfigs: StaticAssetConfig[];
120
+
121
+ /**
122
+ * The cloudfront distribution to route incoming requests to the Nuxt lambda function (via the API gateway)
123
+ * or the S3 assets folder (with caching).
124
+ *
125
+ * @private
126
+ */
47
127
  private readonly cdn: Distribution;
48
- private readonly hostedZone: IHostedZone;
49
128
 
50
129
  constructor(scope: Construct, id: string, props: NuxtAppStackProps) {
51
130
  super(scope, id, props);
52
131
 
53
132
  this.resourceIdPrefix = `${props.project}-${props.service}-${props.environment}`;
54
133
  this.deploymentRevision = new Date().toISOString();
134
+ this.staticAssetConfigs = getNuxtAppStaticAssetConfigs(props.nuxtConfig);
55
135
  this.tlsCertificate = this.findTlsCertificate(props);
56
136
  this.cdnAccessIdentity = this.createCdnAccessIdentity();
57
137
  this.staticAssetsBucket = this.createStaticAssetsBucket();
58
- this.layer = this.createSsrLambdaLayer();
59
138
  this.lambdaFunction = this.createLambdaFunction();
60
139
  this.apiGateway = this.createApiGateway();
61
- this.httpsForwardingBehavior = this.createHttpsForwardingBehavior();
62
140
  this.cdn = this.createCloudFrontDistribution(props);
63
141
  this.configureDeployments();
64
- this.hostedZone = this.findHostedZone(props);
65
142
  this.createDnsRecords(props);
143
+ this.createPingRule();
66
144
  }
67
145
 
146
+ /**
147
+ * Finds the certificate to use for providing HTTPS requests to our Nuxt app.
148
+ *
149
+ * @param props
150
+ * @private
151
+ */
68
152
  private findTlsCertificate(props: NuxtAppStackProps): ICertificate {
69
153
  return Certificate.fromCertificateArn(this, `${this.resourceIdPrefix}-tls-certificate`, props.globalTlsCertificateArn);
70
154
  }
71
155
 
156
+ /**
157
+ * Creates the identity to access our S3 deployment asset files via the cloudfront distribution.
158
+ *
159
+ * @private
160
+ */
72
161
  private createCdnAccessIdentity(): IOriginAccessIdentity {
73
162
  const originAccessIdentityName = `${this.resourceIdPrefix}-cdn-s3-access`;
74
163
  return new OriginAccessIdentity(this, originAccessIdentityName);
75
164
  }
76
165
 
166
+ /**
167
+ * Creates the bucket to store the static deployment asset files of the Nuxt app.
168
+ *
169
+ * @private
170
+ */
77
171
  private createStaticAssetsBucket(): IBucket {
78
172
  const bucketName = `${this.resourceIdPrefix}-assets`;
79
173
  const bucket = new Bucket(this, bucketName, {
80
174
  accessControl: BucketAccessControl.AUTHENTICATED_READ,
81
175
  blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
82
176
  bucketName,
83
- // the bucket and all of its objects can be deleted, because all the content is managed in this project
177
+ // The bucket and all of its objects can be deleted, because all the content is managed in this project
84
178
  removalPolicy: RemovalPolicy.DESTROY,
85
179
  autoDeleteObjects: true,
86
180
  });
@@ -90,24 +184,35 @@ export class NuxtAppStack extends Stack {
90
184
  return bucket;
91
185
  }
92
186
 
187
+ /**
188
+ * Creates a lambda layer with the node_modules required to render the Nuxt app on the server side.
189
+ *
190
+ * @private
191
+ */
93
192
  private createSsrLambdaLayer(): LayerVersion {
94
193
  const layerName = `${this.resourceIdPrefix}-ssr-layer`;
95
194
  return new LayerVersion(this, layerName, {
96
195
  layerVersionName: layerName,
97
196
  code: Code.fromAsset('.nuxt/cdk-deployment/layer'),
98
197
  compatibleRuntimes: [Runtime.NODEJS_12_X],
99
- description: `Contains node_modules required for server-side of ${this.resourceIdPrefix}.`,
198
+ description: `Provides the node_modules required for SSR of ${this.resourceIdPrefix}.`,
100
199
  });
101
200
  }
102
201
 
202
+ /**
203
+ * Creates the lambda function to render the Nuxt app.
204
+ *
205
+ * @private
206
+ */
103
207
  private createLambdaFunction(): Function {
104
208
  const funcName = `${this.resourceIdPrefix}-function`;
105
209
 
106
210
  return new Function(this, funcName, {
107
211
  functionName: funcName,
212
+ description: `Renders the ${this.resourceIdPrefix} Nuxt app.`,
108
213
  runtime: Runtime.NODEJS_12_X,
109
214
  architecture: Architecture.ARM_64,
110
- layers: [this.layer],
215
+ layers: [this.createSsrLambdaLayer()],
111
216
  handler: 'index.handler',
112
217
  code: Code.fromAsset('.nuxt/cdk-deployment/src', {
113
218
  exclude: ['**.svg', '**.ico', '**.png', '**.jpg', 'chunk.*.js*', 'bundle.*.js*', 'bundle.*.js*', 'sw.js*'],
@@ -115,16 +220,21 @@ export class NuxtAppStack extends Stack {
115
220
  timeout: Duration.seconds(10),
116
221
  memorySize: 512,
117
222
  logRetention: RetentionDays.ONE_MONTH,
118
- environment: {},
119
223
  allowPublicSubnet: false
120
224
  });
121
225
  }
122
226
 
227
+ /**
228
+ * Creates the API gateway to make the Nuxt app render lambda function publicly available.
229
+ *
230
+ * @private
231
+ */
123
232
  private createApiGateway(): HttpApi {
124
233
  const lambdaIntegration = new HttpLambdaIntegration(`${this.resourceIdPrefix}-lambda-integration`, this.lambdaFunction);
125
234
  const apiName = `${this.resourceIdPrefix}-api`;
126
235
  const apiGateway = new HttpApi(this, apiName, {
127
236
  apiName,
237
+ description: `Connects the ${this.resourceIdPrefix} cloudfront distribution with the ${this.resourceIdPrefix} lambda function to make it publicly available.`,
128
238
  // The app does not allow any cross-origin access by purpose: the app should not be embeddable anywhere
129
239
  corsPreflight: undefined,
130
240
  defaultIntegration: lambdaIntegration,
@@ -138,7 +248,34 @@ export class NuxtAppStack extends Stack {
138
248
  return apiGateway;
139
249
  }
140
250
 
141
- private createHttpsForwardingBehavior(): BehaviorOptions {
251
+ /**
252
+ * Creates the cloudfront distribution that routes incoming requests to the Nuxt lambda function (via the API gateway)
253
+ * or the S3 assets folder (with caching).
254
+ *
255
+ * @param props
256
+ * @private
257
+ */
258
+ private createCloudFrontDistribution(props: NuxtAppStackProps): Distribution {
259
+ const cdnName = `${this.resourceIdPrefix}-cdn`;
260
+
261
+ return new Distribution(this, cdnName, {
262
+ domainNames: [props.domain],
263
+ comment: `${this.resourceIdPrefix}-redirect`,
264
+ minimumProtocolVersion: SecurityPolicyProtocol.TLS_V1_2_2018,
265
+ certificate: this.tlsCertificate,
266
+ defaultBehavior: this.createNuxtAppRouteBehavior(),
267
+ additionalBehaviors: this.createStaticAssetsRouteBehavior(),
268
+ priceClass: PriceClass.PRICE_CLASS_100, // Use only North America and Europe
269
+ });
270
+ }
271
+
272
+ /**
273
+ * Creates a behavior for the cloudfront distribution to route incoming requests to the Nuxt render lambda function (via API gateway).
274
+ * Additionally, this automatically redirects HTTP requests to HTTPS.
275
+ *
276
+ * @private
277
+ */
278
+ private createNuxtAppRouteBehavior(): BehaviorOptions {
142
279
  return {
143
280
  origin: new HttpOrigin(`${this.apiGateway.httpApiId}.execute-api.${this.region}.amazonaws.com`, {
144
281
  connectionAttempts: 2,
@@ -155,13 +292,14 @@ export class NuxtAppStack extends Stack {
155
292
  }
156
293
 
157
294
  /**
158
- * Eventhough we don't want to cache SSR requests, we still have to create a cache policy, in order to
295
+ * Creates a cache policy for the Nuxt app route behavior of our cloudfront distribution.
296
+ * Eventhough we don't want to cache SSR requests, we still have to create this cache policy in order to
159
297
  * forward required cookies, query params and headers. This doesn't make any sense, because if nothing
160
298
  * is cached, one would expect, that anything would/could be forwarded, but anyway...
161
299
  */
162
300
  private createSsrCachePolicy(): ICachePolicy {
163
301
 
164
- // The headers to pass to the app
302
+ // The headers to make accessible in our Nuxt app code
165
303
  const headers = [
166
304
  'User-Agent', // Required to distinguish between mobile and desktop template
167
305
  'Authorization', // For authorization
@@ -172,7 +310,7 @@ export class NuxtAppStack extends Stack {
172
310
  comment: `Passes all required request data to the ${this.resourceIdPrefix} origin.`,
173
311
  defaultTtl: Duration.seconds(0),
174
312
  minTtl: Duration.seconds(0),
175
- maxTtl: Duration.seconds(1), // the max TTL must not be 0 for a cache policy
313
+ maxTtl: Duration.seconds(1), // The max TTL must not be 0 for a cache policy
176
314
  queryStringBehavior: CacheQueryStringBehavior.all(),
177
315
  headerBehavior: CacheHeaderBehavior.allowList(...headers),
178
316
  cookieBehavior: CacheCookieBehavior.all(),
@@ -181,21 +319,13 @@ export class NuxtAppStack extends Stack {
181
319
  });
182
320
  }
183
321
 
184
- private createCloudFrontDistribution(props: NuxtAppStackProps): Distribution {
185
- const cdnName = `${this.resourceIdPrefix}-cdn`;
186
-
187
- return new Distribution(this, cdnName, {
188
- domainNames: [props.domain],
189
- comment: `${this.resourceIdPrefix}-redirect`,
190
- minimumProtocolVersion: SecurityPolicyProtocol.TLS_V1_2_2018,
191
- certificate: this.tlsCertificate,
192
- defaultBehavior: this.httpsForwardingBehavior,
193
- additionalBehaviors: this.createStaticAssetBehaviors(),
194
- priceClass: PriceClass.PRICE_CLASS_100, // Use only North America and Europe
195
- });
196
- }
197
-
198
- private createStaticAssetBehaviors(): Record<string, BehaviorOptions> {
322
+ /**
323
+ * Creates a behavior for the cloudfront distribution to route matching incoming requests for our static assets
324
+ * to the S3 bucket that holds these static assets.
325
+ *
326
+ * @private
327
+ */
328
+ private createStaticAssetsRouteBehavior(): Record<string, BehaviorOptions> {
199
329
  const staticAssetsCacheConfig: BehaviorOptions = {
200
330
  origin: new S3Origin(this.staticAssetsBucket, {
201
331
  connectionAttempts: 2,
@@ -211,7 +341,7 @@ export class NuxtAppStack extends Stack {
211
341
  };
212
342
 
213
343
  const rules: Record<string, BehaviorOptions> = {};
214
- NuxtAppStaticAssets.forEach(asset => {
344
+ this.staticAssetConfigs.forEach(asset => {
215
345
  rules[`${asset.target}${asset.pattern}`] = staticAssetsCacheConfig
216
346
  })
217
347
 
@@ -219,8 +349,10 @@ export class NuxtAppStack extends Stack {
219
349
  }
220
350
 
221
351
  /**
352
+ * Uploads the static assets of the Nuxt app as defined in {@see getNuxtAppStaticAssetConfigs} to the static assets S3 bucket.
222
353
  * In order to enable a zero-downtime deployment, we use a new subdirectory (revision) for every deployment.
223
- * The previous versions are retained to allow clients to continue to work with an older revision.
354
+ * The previous versions are retained to allow clients to continue to work with an older revision but gets cleaned up
355
+ * after a specified period of time via the lambda function in the {@see NuxtAppAssetsCleanupStack}.
224
356
  */
225
357
  private configureDeployments(): BucketDeployment[] {
226
358
  const defaultCacheConfig = [
@@ -230,7 +362,7 @@ export class NuxtAppStack extends Stack {
230
362
  ];
231
363
 
232
364
  // Returns a deployment for every configured static asset type to respect the different cache settings
233
- return NuxtAppStaticAssets.filter(asset => fs.existsSync(asset.source)).map((asset, assetIndex) => {
365
+ return this.staticAssetConfigs.filter(asset => fs.existsSync(asset.source)).map((asset, assetIndex) => {
234
366
  return new BucketDeployment(this, `${this.resourceIdPrefix}-assets-deployment-${assetIndex}`, {
235
367
  sources: [Source.asset(asset.source)],
236
368
  destinationBucket: this.staticAssetsBucket,
@@ -245,6 +377,12 @@ export class NuxtAppStack extends Stack {
245
377
  });
246
378
  }
247
379
 
380
+ /**
381
+ * Resolves the hosted zone at which the DNS records shall be created to access our Nuxt app on the internet.
382
+ *
383
+ * @param props
384
+ * @private
385
+ */
248
386
  private findHostedZone(props: NuxtAppStackProps): IHostedZone {
249
387
  const domainParts = props.domain.split('.');
250
388
 
@@ -254,21 +392,44 @@ export class NuxtAppStack extends Stack {
254
392
  });
255
393
  }
256
394
 
395
+ /**
396
+ * Creates the DNS records to access our Nuxt app on the internet via our custom domain.
397
+ *
398
+ * @param props
399
+ * @private
400
+ */
257
401
  private createDnsRecords(props: NuxtAppStackProps): void {
402
+ const hostedZone = this.findHostedZone(props);
258
403
  const dnsTarget = RecordTarget.fromAlias(new CloudFrontTarget(this.cdn));
259
404
 
260
405
  // Create a record for IPv4
261
406
  new ARecord(this, `${this.resourceIdPrefix}-ipv4-record`, {
262
407
  recordName: props.domain,
263
- zone: this.hostedZone,
408
+ zone: hostedZone,
264
409
  target: dnsTarget,
265
410
  });
266
411
 
267
412
  // Create a record for IPv6
268
413
  new AaaaRecord(this, `${this.resourceIdPrefix}-ipv6-record`, {
269
414
  recordName: props.domain,
270
- zone: this.hostedZone,
415
+ zone: hostedZone,
271
416
  target: dnsTarget,
272
417
  });
273
418
  }
419
+
420
+ /**
421
+ * Creates a scheduled rule to ping our Nuxt app lambda function every 5 minutes in order to keep it warm
422
+ * and speed up initial SSR requests.
423
+ *
424
+ * @private
425
+ */
426
+ private createPingRule(): void {
427
+ new Rule(this, `${this.resourceIdPrefix}-pinger-rule`, {
428
+ ruleName: `${this.resourceIdPrefix}-pinger`,
429
+ description: `Pings the lambda function of the ${this.resourceIdPrefix} app every 5 minutes to keep it warm.`,
430
+ enabled: true,
431
+ schedule: Schedule.rate(Duration.minutes(5)),
432
+ targets: [new LambdaFunction(this.lambdaFunction)],
433
+ });
434
+ }
274
435
  }
@@ -1,10 +1,30 @@
1
1
  import { CacheControl } from "aws-cdk-lib/aws-s3-deployment";
2
- interface StaticAssetConfig {
2
+ import { NuxtConfig } from "./nuxt-config";
3
+ export interface StaticAssetConfig {
4
+ /**
5
+ * The file pattern for the incoming requests that should be forwarded to the target path in the static assets S3 bucket
6
+ * with the appropriate cache and content settings defined in the same object.
7
+ */
3
8
  pattern: string;
4
- contentType: string;
9
+ /**
10
+ * The local directory to upload the files from.
11
+ */
5
12
  source: string;
13
+ /**
14
+ * The remote path at which to make the uploaded files from source accessible.
15
+ */
6
16
  target: string;
17
+ /**
18
+ * The content type to set for the files in the source folder when uploading them to the target.
19
+ */
20
+ contentType: string;
21
+ /**
22
+ * The cache settings to use for the uploaded source files when accessing them on the target path with the specified pattern.
23
+ */
7
24
  cacheControl?: CacheControl[];
8
25
  }
9
- export declare const NuxtAppStaticAssets: StaticAssetConfig[];
10
- export {};
26
+ /**
27
+ * Retrieves the static assets of the Nuxt app that shall be publicly available.
28
+ * These should match the files in '.nuxt/dist/client' and 'static'.
29
+ */
30
+ export declare const getNuxtAppStaticAssetConfigs: (nuxtConfig: NuxtConfig) => StaticAssetConfig[];