diff --git a/package-lock.json b/package-lock.json index ba543ed6..bfc990fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,10 @@ "resolved": "packages/lambda-at-edge-handlers", "link": true }, + "node_modules/@aligent/cdk-prerender-fargate": { + "resolved": "packages/prerender-fargate", + "link": true + }, "node_modules/@aligent/cdk-prerender-proxy": { "resolved": "packages/prerender-proxy", "link": true @@ -98,6 +102,44 @@ "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.0.1.tgz", "integrity": "sha512-DDt4SLdLOwWCjGtltH4VCST7hpOI5DzieuhGZsBpZ+AgJdSI2GCjklCXm0GCTwJG/SolkL5dtQXyUKgg9luBDg==" }, + "node_modules/@aws-cdk/aws-apigatewayv2-alpha": { + "version": "2.30.0-alpha.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-apigatewayv2-alpha/-/aws-apigatewayv2-alpha-2.30.0-alpha.0.tgz", + "integrity": "sha512-D5AB4x7Ayicbb87gb7wVJGuyFrqjelw48gvMADvNecw8md5aOSJ7jvwlSEGMZMiJ1UhjNhea7AWSNdAOI2SfBQ==", + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.30.0", + "constructs": "^10.0.0" + } + }, + "node_modules/@aws-cdk/aws-apigatewayv2-authorizers-alpha": { + "version": "2.30.0-alpha.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-apigatewayv2-authorizers-alpha/-/aws-apigatewayv2-authorizers-alpha-2.30.0-alpha.0.tgz", + "integrity": "sha512-/oIxg3boxN+a4qqRQHEZ5LgjSgMFDukMx5pbTcJ4lEoEe9FpJaJ/FY8IJ0ZR1TfpfxGhVNwHUpMprkqRh9SYSA==", + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@aws-cdk/aws-apigatewayv2-alpha": "2.30.0-alpha.0", + "aws-cdk-lib": "^2.30.0", + "constructs": "^10.0.0" + } + }, + "node_modules/@aws-cdk/aws-apigatewayv2-integrations-alpha": { + "version": "2.30.0-alpha.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-apigatewayv2-integrations-alpha/-/aws-apigatewayv2-integrations-alpha-2.30.0-alpha.0.tgz", + "integrity": "sha512-2/NAb0AFGwZQjNqpViiagJxKa8u0hy09GV7W/AKSEzLmQp1H2Jmg8oSGLudXVTF/VtsgjQUAL4MuxTRAXFtJoQ==", + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@aws-cdk/aws-apigatewayv2-alpha": "2.30.0-alpha.0", + "aws-cdk-lib": "^2.30.0", + "constructs": "^10.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.22.13", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", @@ -1723,7 +1765,8 @@ "node_modules/@types/aws-lambda": { "version": "8.10.122", "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.122.tgz", - "integrity": "sha512-vBkIh9AY22kVOCEKo5CJlyCgmSWvasC+SWUxL/x/vOwRobMpI/HG1xp/Ae3AqmSiZeLUbOhW0FCD3ZjqqUxmXw==" + "integrity": "sha512-vBkIh9AY22kVOCEKo5CJlyCgmSWvasC+SWUxL/x/vOwRobMpI/HG1xp/Ae3AqmSiZeLUbOhW0FCD3ZjqqUxmXw==", + "dev": true }, "node_modules/@types/babel__core": { "version": "7.20.2", @@ -5910,13 +5953,13 @@ "version": "2.0.0", "license": "GPL-3.0-only", "dependencies": { - "@types/aws-lambda": "^8.10.122", "aws-cdk-lib": "2.97.0", "constructs": "^10.0.0", "esbuild": "^0.17.0", "source-map-support": "^0.5.21" }, "devDependencies": { + "@types/aws-lambda": "^8.10.122", "@types/jest": "^29.5.5", "@types/node": "20.6.3", "aws-cdk": "2.97.0", @@ -5930,12 +5973,12 @@ "name": "@aligent/cdk-cloudfront-security-headers", "version": "2.0.0", "dependencies": { - "@types/aws-lambda": "^8.10.122", "aws-cdk-lib": "2.97.0", "constructs": "^10.0.0", "source-map-support": "^0.5.21" }, "devDependencies": { + "@types/aws-lambda": "^8.10.122", "@types/jest": "^29.5.5", "@types/node": "20.6.3", "aws-cdk": "2.97.0", @@ -5970,12 +6013,12 @@ "version": "0.1.0", "license": "GPL-3.0-only", "dependencies": { - "@types/aws-lambda": "^8.10.122", "aws-cdk-lib": "2.97.0", "constructs": "^10.0.0", "source-map-support": "^0.5.21" }, "devDependencies": { + "@types/aws-lambda": "^8.10.122", "@types/jest": "^29.5.5", "@types/node": "20.6.3", "aws-cdk": "2.97.0", @@ -5986,12 +6029,25 @@ } }, "packages/lambda-at-edge-handlers": { + "name": "@aligent/cdk-lambda-at-edge-handlers", "version": "0.1.0", "license": "GPL-3.0-only", "dependencies": { - "@types/aws-lambda": "^8.10.122", "axios": "^1.5.1", "source-map-support": "^0.5.21" + }, + "devDependencies": { + "@types/aws-lambda": "^8.10.122" + } + }, + "packages/prerender-fargate": { + "name": "@aligent/cdk-prerender-fargate", + "version": "0.0.1", + "license": "GPL-3.0-only", + "dependencies": { + "@aws-cdk/aws-apigatewayv2-alpha": "2.30.0-alpha.0", + "@aws-cdk/aws-apigatewayv2-authorizers-alpha": "2.30.0-alpha.0", + "@aws-cdk/aws-apigatewayv2-integrations-alpha": "2.30.0-alpha.0" } }, "packages/prerender-proxy": { @@ -5999,7 +6055,6 @@ "version": "2.0.0", "license": "GPL-3.0-only", "dependencies": { - "@types/aws-lambda": "^8.10.122", "aws-cdk-lib": "2.97.0", "axios": "^1.5.1", "constructs": "^10.0.0", @@ -6007,6 +6062,7 @@ "source-map-support": "^0.5.21" }, "devDependencies": { + "@types/aws-lambda": "^8.10.122", "@types/jest": "^29.5.5", "@types/node": "20.6.3", "aws-cdk": "2.97.0", @@ -6058,13 +6114,13 @@ "version": "2.0.0", "license": "GPL-3.0-only", "dependencies": { - "@types/aws-lambda": "^8.10.122", "aws-cdk-lib": "2.97.0", "constructs": "^10.0.0", "esbuild": "^0.17.0", "source-map-support": "^0.5.21" }, "devDependencies": { + "@types/aws-lambda": "^8.10.122", "@types/jest": "^29.5.5", "@types/node": "20.6.3", "aws-cdk": "2.97.0", diff --git a/packages/prerender-fargate/lib/prerender-fargate.ts b/packages/prerender-fargate/lib/prerender-fargate.ts index acb8113c..b6ccafbb 100644 --- a/packages/prerender-fargate/lib/prerender-fargate.ts +++ b/packages/prerender-fargate/lib/prerender-fargate.ts @@ -1,118 +1,189 @@ -import { Construct } from 'constructs'; -import * as ecs from 'aws-cdk-lib/aws-ecs'; -import * as ecsPatterns from 'aws-cdk-lib/aws-ecs-patterns'; -import * as ec2 from 'aws-cdk-lib/aws-ec2'; -import { Certificate } from 'aws-cdk-lib/aws-certificatemanager'; -import { HostedZone } from 'aws-cdk-lib/aws-route53'; -import { Bucket, BlockPublicAccess } from 'aws-cdk-lib/aws-s3' -import * as ecrAssets from 'aws-cdk-lib/aws-ecr-assets'; -import { AccessKey, User } from 'aws-cdk-lib/aws-iam'; -import { Duration, RemovalPolicy, Stack } from 'aws-cdk-lib'; -import * as path from 'path'; +import { Construct } from "constructs"; +import * as ecs from "aws-cdk-lib/aws-ecs"; +import * as ecsPatterns from "aws-cdk-lib/aws-ecs-patterns"; +import * as ec2 from "aws-cdk-lib/aws-ec2"; +import { Certificate } from "aws-cdk-lib/aws-certificatemanager"; +import { HostedZone } from "aws-cdk-lib/aws-route53"; +import { Bucket, BlockPublicAccess } from "aws-cdk-lib/aws-s3"; +import * as ecrAssets from "aws-cdk-lib/aws-ecr-assets"; +import { AccessKey, User } from "aws-cdk-lib/aws-iam"; +import { Duration, RemovalPolicy, Stack } from "aws-cdk-lib"; +import * as path from "path"; +/** + * Options for configuring the Prerender Fargate construct. + */ export interface PrerenderOptions { - prerenderName: string, - domainName: string, - vpcId?: string, - bucketName?: string, - expirationDays?: number, - tokenList: Array, - certificateArn: string, - desiredInstanceCount?: number, - maxInstanceCount?: number, - instanceCPU?: number, - instanceMemory?: number - enableRedirectCache?: string + /** + * The name of the Prerender service. + */ + prerenderName: string; + /** + * The domain name to prerender. + */ + domainName: string; + /** + * The ID of the VPC to deploy the Fargate service in. + */ + vpcId?: string; + /** + * The name of the S3 bucket to store prerendered pages in. + */ + bucketName?: string; + /** + * The number of days to keep prerendered pages in the S3 bucket before expiring them. + */ + expirationDays?: number; + /** + * A list of tokens to use for authentication with the Prerender service. + */ + tokenList: Array; + /** + * The ARN of the SSL certificate to use for HTTPS connections. + */ + certificateArn: string; + /** + * The desired number of Fargate instances to run. + */ + desiredInstanceCount?: number; + /** + * The maximum number of Fargate instances to run. + */ + maxInstanceCount?: number; + /** + * The amount of CPU to allocate to each Fargate instance. + */ + instanceCPU?: number; + /** + * The amount of memory to allocate to each Fargate instance. + */ + instanceMemory?: number; + /** + * Whether to enable caching of HTTP redirects. + */ + enableRedirectCache?: string; + /** + * Whether to enable the S3 endpoint for the VPC. + */ + enableS3Endpoint?: boolean; } export class PrerenderFargate extends Construct { - readonly bucket: Bucket; + readonly bucket: Bucket; - constructor(scope: Construct, id: string, props: PrerenderOptions) { - super(scope, id); + constructor(scope: Construct, id: string, props: PrerenderOptions) { + super(scope, id); - // Create bucket for prerender storage - this.bucket = new Bucket(this, `${props.prerenderName}-bucket`, { - bucketName: props.bucketName, - lifecycleRules: [{ - enabled: true, - expiration: Duration.days(props.expirationDays || 7) // Default to 7 day expiration - }], - removalPolicy: RemovalPolicy.DESTROY, - autoDeleteObjects: true, - blockPublicAccess: BlockPublicAccess.BLOCK_ALL, - }); + // Create bucket for prerender storage + this.bucket = new Bucket(this, `${props.prerenderName}-bucket`, { + bucketName: props.bucketName, + lifecycleRules: [ + { + enabled: true, + expiration: Duration.days(props.expirationDays || 7), // Default to 7 day expiration + }, + ], + removalPolicy: RemovalPolicy.DESTROY, + autoDeleteObjects: true, + blockPublicAccess: BlockPublicAccess.BLOCK_ALL, + }); - // Configure access to the bucket for the container - const user = new User(this, 'PrerenderAccess'); - this.bucket.grantReadWrite(user); + // Configure access to the bucket for the container + const user = new User(this, "PrerenderAccess"); + this.bucket.grantReadWrite(user); - const accessKey = new AccessKey(this, 'PrerenderAccessKey', { - user: user, - serial: 1 - }); + const accessKey = new AccessKey(this, "PrerenderAccessKey", { + user: user, + serial: 1, + }); - const vpcLookup = props.vpcId ? { vpcId: props.vpcId } : { isDefault: true }; - const vpc = ec2.Vpc.fromLookup(this, "vpc", vpcLookup); + const vpcLookup = props.vpcId + ? { vpcId: props.vpcId } + : { isDefault: true }; + const vpc = ec2.Vpc.fromLookup(this, "vpc", vpcLookup); - const cluster = new ecs.Cluster(this, `${props.prerenderName}-cluster`, { vpc: vpc }); + const cluster = new ecs.Cluster(this, `${props.prerenderName}-cluster`, { + vpc: vpc, + }); - const directory = path.join(__dirname, 'prerender'); - const asset = new ecrAssets.DockerImageAsset(this, `${props.prerenderName}-image`, { - directory, - }); + const directory = path.join(__dirname, "prerender"); + const asset = new ecrAssets.DockerImageAsset( + this, + `${props.prerenderName}-image`, + { + directory, + } + ); - // Create a load-balanced Fargate service - const fargateService = new ecsPatterns.ApplicationLoadBalancedFargateService( + // Create a load-balanced Fargate service + const fargateService = + new ecsPatterns.ApplicationLoadBalancedFargateService( + this, + `${props.prerenderName}-service`, + { + cluster, + serviceName: `${props.prerenderName}-service`, + desiredCount: props.desiredInstanceCount || 1, + cpu: props.instanceCPU || 512, // 0.5 vCPU default + memoryLimitMiB: props.instanceMemory || 1024, // 1 GB default to give Chrome enough memory + taskImageOptions: { + image: ecs.ContainerImage.fromDockerImageAsset(asset), + enableLogging: true, + containerPort: 3000, + environment: { + S3_BUCKET_NAME: this.bucket.bucketName, + AWS_ACCESS_KEY_ID: accessKey.accessKeyId, + AWS_SECRET_ACCESS_KEY: accessKey.secretAccessKey.unsafeUnwrap(), + AWS_REGION: Stack.of(this).region, + ENABLE_REDIRECT_CACHE: props.enableRedirectCache || "false", + TOKEN_LIST: props.tokenList.toString(), + }, + }, + healthCheckGracePeriod: Duration.seconds(20), + publicLoadBalancer: true, + assignPublicIp: true, + listenerPort: 443, + redirectHTTP: true, + domainName: props.domainName, + domainZone: new HostedZone(this, "hosted-zone", { + zoneName: props.domainName, + }), + certificate: Certificate.fromCertificateArn( this, - `${props.prerenderName}-service`, - { - cluster, - serviceName: `${props.prerenderName}-service`, - desiredCount: props.desiredInstanceCount || 1, - cpu: props.instanceCPU || 512, // 0.5 vCPU default - memoryLimitMiB: props.instanceMemory || 1024, // 1 GB default to give Chrome enough memory - taskImageOptions: { - image: ecs.ContainerImage.fromDockerImageAsset(asset), - enableLogging: true, - containerPort: 3000, - environment: { - S3_BUCKET_NAME: this.bucket.bucketName, - AWS_ACCESS_KEY_ID: accessKey.accessKeyId, - AWS_SECRET_ACCESS_KEY: accessKey.secretAccessKey.unsafeUnwrap(), - AWS_REGION: Stack.of(this).region, - ENABLE_REDIRECT_CACHE: props.enableRedirectCache || "false", - TOKEN_LIST: props.tokenList.toString() - } - }, - healthCheckGracePeriod: Duration.seconds(20), - publicLoadBalancer: true, - assignPublicIp: true, - listenerPort: 443, - redirectHTTP: true, - domainName: props.domainName, - domainZone: new HostedZone(this, 'hosted-zone', { zoneName: props.domainName }), - certificate: Certificate.fromCertificateArn(this, 'cert', props.certificateArn) - } - ); + "cert", + props.certificateArn + ), + } + ); - // As the prerender service will return a 401 on all unauthorised requests - // it should be considered healthy when receiving a 401 response - fargateService.targetGroup.configureHealthCheck({ - path: "/health", - interval: Duration.seconds(120), - unhealthyThresholdCount: 5, - healthyHttpCodes: '401' - }); + // As the prerender service will return a 401 on all unauthorised requests + // it should be considered healthy when receiving a 401 response + fargateService.targetGroup.configureHealthCheck({ + path: "/health", + interval: Duration.seconds(120), + unhealthyThresholdCount: 5, + healthyHttpCodes: "401", + }); - // Setup AutoScaling policy - const scaling = fargateService.service.autoScaleTaskCount({ - maxCapacity: props.maxInstanceCount || 2, - }); - scaling.scaleOnCpuUtilization(`${props.prerenderName}-scaling`, { - targetUtilizationPercent: 50, - scaleInCooldown: Duration.seconds(60), - scaleOutCooldown: Duration.seconds(60), - }); + // Setup AutoScaling policy + const scaling = fargateService.service.autoScaleTaskCount({ + maxCapacity: props.maxInstanceCount || 2, + }); + scaling.scaleOnCpuUtilization(`${props.prerenderName}-scaling`, { + targetUtilizationPercent: 50, + scaleInCooldown: Duration.seconds(60), + scaleOutCooldown: Duration.seconds(60), + }); + + /** + * Enable VPC Endpoints for S3 + * This would create S3 endpoints in all the PUBLIC subnets of the VPC + */ + if (props.enableS3Endpoint) { + vpc.addGatewayEndpoint("S3Endpoint", { + service: ec2.GatewayVpcEndpointAwsService.S3, + subnets: [{ subnetType: ec2.SubnetType.PUBLIC }], + }); } + } } diff --git a/tsconfig.json b/tsconfig.json index 4119ec45..882d25f3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,8 @@ "target": "ES2018", "module": "commonjs", "lib": ["es2018"], - "declaration": true, + "declaration": false, + "noEmit": true, "strict": true, "noImplicitAny": true, "strictNullChecks": true,