diff --git a/.changeset/chilly-islands-knock.md b/.changeset/chilly-islands-knock.md new file mode 100644 index 00000000..5d9288c6 --- /dev/null +++ b/.changeset/chilly-islands-knock.md @@ -0,0 +1,5 @@ +--- +"@codedazur/cdk-docker-cluster": minor +--- + +Support for a custom domain name was added. diff --git a/.changeset/lazy-drinks-bow.md b/.changeset/lazy-drinks-bow.md new file mode 100644 index 00000000..d2686696 --- /dev/null +++ b/.changeset/lazy-drinks-bow.md @@ -0,0 +1,5 @@ +--- +"@codedazur/cdk-static-site": major +--- + +The props were reorganized into source and distribution. diff --git a/.changeset/shy-roses-collect.md b/.changeset/shy-roses-collect.md new file mode 100644 index 00000000..f12db552 --- /dev/null +++ b/.changeset/shy-roses-collect.md @@ -0,0 +1,5 @@ +--- +"@codedazur/cdk-site-distribution": minor +--- + +Experimental release. diff --git a/package-lock.json b/package-lock.json index dfcbbaee..f91597e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3521,6 +3521,10 @@ "resolved": "packages/cdk-rpc-api", "link": true }, + "node_modules/@codedazur/cdk-site-distribution": { + "resolved": "packages/cdk-site-distribution", + "link": true + }, "node_modules/@codedazur/cdk-static-site": { "resolved": "packages/cdk-static-site", "link": true @@ -27346,10 +27350,10 @@ }, "packages/cdk-docker-cluster": { "name": "@codedazur/cdk-docker-cluster", - "version": "0.2.0", + "version": "0.4.0", "license": "MIT", "dependencies": { - "@codedazur/cdk-cache-invalidator": "*" + "@codedazur/cdk-site-distribution": "^0.0.0" }, "devDependencies": { "@codedazur/eslint-config": "*", @@ -27787,10 +27791,10 @@ }, "packages/cdk-next-app": { "name": "@codedazur/cdk-next-app", - "version": "0.2.0", + "version": "0.2.2", "license": "MIT", "dependencies": { - "@codedazur/cdk-docker-cluster": "*" + "@codedazur/cdk-docker-cluster": "^0.4.0" }, "devDependencies": { "@codedazur/eslint-config": "*", @@ -28213,12 +28217,425 @@ "constructs": ">=10" } }, + "packages/cdk-site-distribution": { + "name": "@codedazur/cdk-site-distribution", + "version": "0.0.0", + "license": "MIT", + "dependencies": { + "@codedazur/cdk-cache-invalidator": "^1.2.0" + }, + "devDependencies": { + "@codedazur/eslint-config": "*", + "@codedazur/tsconfig": "*", + "@types/node": "^20.14.2", + "aws-cdk-lib": "^2.145.0", + "constructs": "^10.3.0", + "esbuild": "^0.20.2", + "eslint": "^8.30.0", + "tsconfig": "*", + "typescript": "^5.4.5" + }, + "peerDependencies": { + "aws-cdk-lib": ">=2", + "constructs": ">=10" + } + }, + "packages/cdk-site-distribution/node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "packages/cdk-site-distribution/node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "packages/cdk-site-distribution/node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "packages/cdk-site-distribution/node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "packages/cdk-site-distribution/node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "packages/cdk-site-distribution/node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "packages/cdk-site-distribution/node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "packages/cdk-site-distribution/node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/cdk-site-distribution/node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/cdk-site-distribution/node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/cdk-site-distribution/node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/cdk-site-distribution/node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/cdk-site-distribution/node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/cdk-site-distribution/node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/cdk-site-distribution/node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/cdk-site-distribution/node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/cdk-site-distribution/node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "packages/cdk-site-distribution/node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "packages/cdk-site-distribution/node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "packages/cdk-site-distribution/node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "packages/cdk-site-distribution/node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "packages/cdk-site-distribution/node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "packages/cdk-site-distribution/node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, "packages/cdk-static-site": { "name": "@codedazur/cdk-static-site", - "version": "1.1.0", + "version": "1.1.1", "license": "MIT", "dependencies": { - "@codedazur/cdk-cache-invalidator": "*" + "@codedazur/cdk-site-distribution": "^0.0.0" }, "devDependencies": { "@codedazur/eslint-config": "*", @@ -28237,7 +28654,7 @@ }, "packages/eslint-config": { "name": "@codedazur/eslint-config", - "version": "0.1.0", + "version": "0.1.1", "license": "MIT", "devDependencies": { "eslint": "^8.46.0", @@ -28303,7 +28720,7 @@ }, "packages/react-essentials": { "name": "@codedazur/react-essentials", - "version": "1.3.0", + "version": "1.4.1", "license": "MIT", "dependencies": { "@codedazur/essentials": "*", diff --git a/packages/cdk-docker-cluster/README.md b/packages/cdk-docker-cluster/README.md index c855dd00..e4c892d8 100644 --- a/packages/cdk-docker-cluster/README.md +++ b/packages/cdk-docker-cluster/README.md @@ -2,15 +2,19 @@ This construct creates a load balanced Fargate service, for which it builds a Docker image from a local directory. A CloudFront distribution is connected to the load balancer. +## Examples + ```ts new DockerCluster({ - path: "../../", // path to Docker build context - file: "apps/myApp/DockerFile", // path to Dockerfile - arguments: { - MY_BUILD_ARGUMENT: "foo", - }, - secrets: { - myBuildSecret: DockerBuildSecret.fromSrc("./foo"), + source: { + path: "../../", // path to Docker build context + file: "apps/myApp/DockerFile", // path to Dockerfile + arguments: { + MY_BUILD_ARGUMENT: "foo", + }, + secrets: { + myBuildSecret: DockerBuildSecret.fromSrc("./foo"), + }, }, port: 3000, cpu: 1024, // 1vCPU @@ -21,3 +25,25 @@ new DockerCluster({ }, }); ``` + +### From Tarball + +```ts +new DockerCluster({ + source: ContainerImage.fromTarball("./path/to/image.tar"), + // ... +}); +``` + +### From Image Asset + +```ts +new DockerCluster({ + source: ContainerImage.fromImageAsset( + new DockerImageAsset(this, "ImageAsset", { + // ... + }), + ), + // ... +}); +``` diff --git a/packages/cdk-docker-cluster/package.json b/packages/cdk-docker-cluster/package.json index 1cd60e97..768ffdaa 100644 --- a/packages/cdk-docker-cluster/package.json +++ b/packages/cdk-docker-cluster/package.json @@ -23,7 +23,7 @@ "test": "vitest run --passWithNoTests" }, "dependencies": { - "@codedazur/cdk-cache-invalidator": "*" + "@codedazur/cdk-site-distribution": "^0.0.0" }, "peerDependencies": { "aws-cdk-lib": ">=2", diff --git a/packages/cdk-docker-cluster/src/constructs/DockerCluster.ts b/packages/cdk-docker-cluster/src/constructs/DockerCluster.ts index 8b041526..c1ac542c 100644 --- a/packages/cdk-docker-cluster/src/constructs/DockerCluster.ts +++ b/packages/cdk-docker-cluster/src/constructs/DockerCluster.ts @@ -1,5 +1,9 @@ -import { CacheInvalidator } from "@codedazur/cdk-cache-invalidator"; -import { Distribution, OriginProtocolPolicy } from "aws-cdk-lib/aws-cloudfront"; +import { + SiteDistribution, + SiteDistributionProps, +} from "@codedazur/cdk-site-distribution"; +import { Certificate } from "aws-cdk-lib/aws-certificatemanager"; +import { OriginProtocolPolicy } from "aws-cdk-lib/aws-cloudfront"; import { LoadBalancerV2Origin } from "aws-cdk-lib/aws-cloudfront-origins"; import { DockerImageAsset, Platform } from "aws-cdk-lib/aws-ecr-assets"; import { Cluster, ContainerImage } from "aws-cdk-lib/aws-ecs"; @@ -10,10 +14,7 @@ import { import { Construct } from "constructs"; export interface DockerClusterProps { - path: string; - file?: string; - arguments?: Record; - secrets?: Record; + source: ContainerImage | DockerBuildProps; port?: number; tasks?: | number @@ -23,6 +24,14 @@ export interface DockerClusterProps { }; cpu?: ApplicationLoadBalancedFargateServiceProps["cpu"]; memory?: ApplicationLoadBalancedFargateServiceProps["memoryLimitMiB"]; + distribution?: Omit; +} + +interface DockerBuildProps { + path: string; + file?: string; + arguments?: Record; + secrets?: Record; } /** @@ -30,10 +39,31 @@ export interface DockerClusterProps { * built from a Dockerfile in a directory and pushed to ECR. */ export class DockerCluster extends Construct { - constructor(scope: Construct, id: string, props: DockerClusterProps) { + public readonly domain?: string; + public readonly image: ContainerImage; + public readonly service: ApplicationLoadBalancedFargateService; + public readonly distribution: SiteDistribution; + + constructor( + scope: Construct, + id: string, + protected readonly props: DockerClusterProps, + ) { super(scope, id); - const image = new DockerImageAsset(this, "Image", { + this.image = + this.props.source instanceof ContainerImage + ? this.props.source + : this.createImage(); + + this.service = this.createService(); + this.distribution = this.createDistribution(); + } + + protected createImage() { + const props = this.props.source as DockerBuildProps; + + const asset = new DockerImageAsset(this, "Image", { directory: props.path, file: props.file, exclude: ["**/cdk.out"], @@ -42,44 +72,51 @@ export class DockerCluster extends Construct { platform: Platform.LINUX_AMD64, }); + return ContainerImage.fromDockerImageAsset(asset); + } + + protected createService() { const desiredTasks = - typeof props.tasks === "number" ? props.tasks : props.tasks?.minimum; + typeof this.props.tasks === "number" + ? this.props.tasks + : this.props.tasks?.minimum; - const fargate = new ApplicationLoadBalancedFargateService(this, "Service", { + const service = new ApplicationLoadBalancedFargateService(this, "Service", { cluster: new Cluster(this, "Cluster"), - cpu: props.cpu, - memoryLimitMiB: props.memory, + cpu: this.props.cpu, + memoryLimitMiB: this.props.memory, desiredCount: desiredTasks, taskImageOptions: { // image: ContainerImage.fromEcrRepository(image.repository, image.imageTag), - image: ContainerImage.fromDockerImageAsset(image), - containerPort: props.port, + image: this.image, + containerPort: this.props.port, }, circuitBreaker: { enable: true, rollback: true, }, - // @todo certificate: ... - // @todo publicLoadBalancer: false, + publicLoadBalancer: false, + certificate: new Certificate(this, "Certificate", { + domainName: "example.com", + }), }); - if (typeof props.tasks === "object") { - fargate.service.autoScaleTaskCount({ - minCapacity: props.tasks.minimum, - maxCapacity: props.tasks.maximum, + if (typeof this.props.tasks === "object") { + this.service.service.autoScaleTaskCount({ + minCapacity: this.props.tasks.minimum, + maxCapacity: this.props.tasks.maximum, }); } - const distribution = new Distribution(this, "Distribution", { - defaultBehavior: { - origin: new LoadBalancerV2Origin(fargate.loadBalancer, { - protocolPolicy: OriginProtocolPolicy.HTTP_ONLY, // @todo OriginProtocolPolicy.HTTPS_ONLY - }), - }, - }); + return service; + } - new CacheInvalidator(this, "CacheInvalidator", { - distribution, + protected createDistribution() { + return new SiteDistribution(this, "Distribution", { + ...this.props.distribution, + origin: new LoadBalancerV2Origin(this.service.loadBalancer, { + protocolPolicy: OriginProtocolPolicy.HTTP_ONLY, + }), }); } } diff --git a/packages/cdk-next-app/README.md b/packages/cdk-next-app/README.md index 29d3da34..2300170b 100644 --- a/packages/cdk-next-app/README.md +++ b/packages/cdk-next-app/README.md @@ -32,7 +32,9 @@ For a non-monorepo build, simply provide the path to the directory that contains ```ts new NextApp(this, "NextApp", { - path: "../next", + source: { + path: "../next", + }, }); ``` @@ -42,8 +44,10 @@ For monorepo builds, set the build context to the root of your monorepo, and pro ```ts new NextApp(this, "NextApp", { - path: "../../", - file: "apps/next/Dockerfile", + source: { + path: "../../", + file: "apps/next/Dockerfile", + }, }); ``` @@ -57,12 +61,14 @@ These arguments and secrets need to be handled appropriately by your Dockerfile import { DockerBuildSecret } from "aws-cdk-lib"; new NextApp(this, "NextApp", { - // ... - arguments: { - MY_BUILD_ARGUMENT: process.env.MY_BUILD_ARGUMENT, - }, - secrets: { - myBuildSecret: new DockerBuildSecret.fromEnvironment("MY_BUILD_SECRET"), + source: { + // ... + arguments: { + MY_BUILD_ARGUMENT: process.env.MY_BUILD_ARGUMENT, + }, + secrets: { + myBuildSecret: new DockerBuildSecret.fromEnvironment("MY_BUILD_SECRET"), + }, }, }); ``` diff --git a/packages/cdk-next-app/src/constructs/NextApp.ts b/packages/cdk-next-app/src/constructs/NextApp.ts index 252059a4..d0b24ba9 100644 --- a/packages/cdk-next-app/src/constructs/NextApp.ts +++ b/packages/cdk-next-app/src/constructs/NextApp.ts @@ -6,9 +6,12 @@ import { DockerClusterProps, } from "@codedazur/cdk-docker-cluster"; import { DockerBuildSecret } from "aws-cdk-lib"; +import { ContainerImage } from "aws-cdk-lib/aws-ecs"; import { Construct } from "constructs"; -export interface NextAppProps extends DockerClusterProps {} +export interface NextAppProps extends Omit { + source: Exclude; +} /** * A docker cluster preconfigured for running a Next.js application with support @@ -18,13 +21,16 @@ export class NextApp extends DockerCluster { constructor( scope: Construct, id: string, - { port = 3000, secrets, ...props }: NextAppProps, + { port = 3000, source, ...props }: NextAppProps, ) { super(scope, id, { port, - secrets: { - npmrc: DockerBuildSecret.fromSrc(path.join(os.homedir(), "/.npmrc")), - ...secrets, + source: { + ...source, + secrets: { + npmrc: DockerBuildSecret.fromSrc(path.join(os.homedir(), "/.npmrc")), + ...source.secrets, + }, }, ...props, }); diff --git a/packages/cdk-site-distribution/.npmignore b/packages/cdk-site-distribution/.npmignore new file mode 100644 index 00000000..37eddcc3 --- /dev/null +++ b/packages/cdk-site-distribution/.npmignore @@ -0,0 +1,5 @@ +# Source Code +src + +# Turbo Artifacts +.turbo diff --git a/packages/cdk-site-distribution/LICENSE.md b/packages/cdk-site-distribution/LICENSE.md new file mode 100644 index 00000000..4cd5ff85 --- /dev/null +++ b/packages/cdk-site-distribution/LICENSE.md @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright (c) 2023 code d'azur Interactive B.V. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/cdk-site-distribution/README.md b/packages/cdk-site-distribution/README.md new file mode 100644 index 00000000..6c0713a4 --- /dev/null +++ b/packages/cdk-site-distribution/README.md @@ -0,0 +1 @@ +# @codedazur/cdk-site-distribution diff --git a/packages/cdk-site-distribution/package.json b/packages/cdk-site-distribution/package.json new file mode 100644 index 00000000..591cd412 --- /dev/null +++ b/packages/cdk-site-distribution/package.json @@ -0,0 +1,43 @@ +{ + "name": "@codedazur/cdk-site-distribution", + "version": "0.0.0", + "main": ".dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "require": "./dist/index.js", + "import": "./dist/index.mjs" + } + }, + "publishConfig": { + "access": "public" + }, + "license": "MIT", + "scripts": { + "develop": "tsup src/index.ts --format esm,cjs --dts --watch", + "build": "tsup src/index.ts --format esm,cjs --dts", + "audit": "npm audit --omit dev", + "lint": "TIMING=1 eslint \"**/*.ts*\"", + "types": "tsc --noEmit", + "test": "vitest run --passWithNoTests" + }, + "dependencies": { + "@codedazur/cdk-cache-invalidator": "^1.2.0" + }, + "peerDependencies": { + "aws-cdk-lib": ">=2", + "constructs": ">=10" + }, + "devDependencies": { + "@codedazur/eslint-config": "*", + "@codedazur/tsconfig": "*", + "@types/node": "^20.14.2", + "aws-cdk-lib": "^2.145.0", + "constructs": "^10.3.0", + "esbuild": "^0.20.2", + "eslint": "^8.30.0", + "tsconfig": "*", + "typescript": "^5.4.5" + } +} diff --git a/packages/cdk-site-distribution/src/constructs/SiteDistribution.ts b/packages/cdk-site-distribution/src/constructs/SiteDistribution.ts new file mode 100644 index 00000000..4f361aa2 --- /dev/null +++ b/packages/cdk-site-distribution/src/constructs/SiteDistribution.ts @@ -0,0 +1,347 @@ +import { CacheInvalidator } from "@codedazur/cdk-cache-invalidator"; +import { CfnOutput } from "aws-cdk-lib"; +import { + DnsValidatedCertificate, + ICertificate, +} from "aws-cdk-lib/aws-certificatemanager"; +import { + Function as CloudFrontFunction, + Distribution, + FunctionCode, + FunctionEventType, + IOrigin, + PriceClass, + ViewerProtocolPolicy, +} from "aws-cdk-lib/aws-cloudfront"; +import { + ARecord, + HostedZone, + IHostedZone, + RecordTarget, +} from "aws-cdk-lib/aws-route53"; +import { CloudFrontTarget } from "aws-cdk-lib/aws-route53-targets"; +import { Secret } from "aws-cdk-lib/aws-secretsmanager"; +import { Construct } from "constructs"; + +export interface SiteDistributionProps { + origin: IOrigin; + priceClass?: PriceClass; + functions?: { + viewerRequest?: FunctionCode[]; + viewerResponse?: FunctionCode[]; + }; + authentication?: { + username: string; + password?: string | Secret; + }; + domain?: { + name: string; + subdomain?: string; + zone?: IHostedZone; + }; + invalidateCache?: boolean | string[]; +} + +export class SiteDistribution extends Construct { + public readonly domain?: string; + public readonly zone?: IHostedZone; + public readonly certificate?: ICertificate; + public readonly passwordSecret?: Secret; + public readonly functions: { + viewerRequest?: CloudFrontFunction; + viewerResponse?: CloudFrontFunction; + }; + public readonly distribution: Distribution; + public readonly alias?: ARecord; + public readonly cacheInvalidator?: CacheInvalidator; + + constructor( + scope: Construct, + id: string, + protected readonly props: SiteDistributionProps, + ) { + super(scope, id); + + this.domain = this.determineDomain(); + this.zone = this.findHostedZone(); + this.certificate = this.createCertificate(); + + if (this.props.authentication && !this.props.authentication.password) { + this.passwordSecret = this.createPasswordSecret(); + } + + this.functions = this.createFunctions(); + this.distribution = this.createDistribution(); + this.alias = this.createAlias(); + + if (!(props.invalidateCache ?? true)) { + this.cacheInvalidator = this.createCacheInvalidator(); + } + } + + protected determineDomain() { + const domain = this.props.domain + ? [this.props.domain.subdomain, this.props.domain.name] + .filter(Boolean) + .join(".") + : undefined; + + if (domain) { + new CfnOutput(this, "URL", { value: "https://" + domain }); + } + + return domain; + } + + protected findHostedZone() { + return this.props.domain + ? this.props.domain?.zone ?? + HostedZone.fromLookup(this, "HostedZone", { + domainName: this.props.domain.name, + }) + : undefined; + } + + protected createCertificate() { + const certificate = + this.domain && this.zone + ? new DnsValidatedCertificate(this, "Certificate", { + domainName: this.domain, + hostedZone: this.zone, + region: "us-east-1", + }) + : undefined; + + if (certificate) { + new CfnOutput(this, "CertificateArn", { + value: certificate.certificateArn, + }); + } + + return certificate; + } + + protected createPasswordSecret() { + return new Secret(this, "PasswordSecret", { + generateSecretString: { excludePunctuation: true }, + }); + } + + protected createFunctions() { + return { + viewerRequest: this.createViewerRequestFunction(), + viewerResponse: this.createViewerResponseFunction(), + }; + } + + protected createViewerRequestFunction() { + const handlers = [ + this.getAuthenticateCode(), + this.getAppendSlashCode(), + ...(this.props.functions?.viewerRequest ?? []), + ].filter((handler): handler is FunctionCode => !!handler); + + if (handlers.length === 0) { + return undefined; + } + + return new CloudFrontFunction(this, "ViewerRequestFunction", { + code: this.getHandlerChainCode(handlers, "request"), + }); + } + + protected createViewerResponseFunction() { + const handlers = [ + this.getSecurityHeadersCode(), + ...(this.props.functions?.viewerResponse ?? []), + ].filter((handler): handler is FunctionCode => !!handler); + + if (handlers.length === 0) { + return undefined; + } + + return new CloudFrontFunction(this, "ViewerResponseFunction", { + code: this.getHandlerChainCode(handlers, "response"), + }); + } + + protected getHandlerChainCode( + handlers: FunctionCode[], + completion: "request" | "response", + ) { + return FunctionCode.fromInline(/* js */ ` + function handler(event) { + return chain(event, [ + ${handlers.map((code) => code.render().replace(/;\s*$/, "")).join(",")} + ]) + } + + function chain(event, handlers) { + var current = handlers.shift() ?? complete; + return current(event, (event) => chain(event, handlers)) + } + + function complete(event) { + return event.${completion}; + } + `); + } + + protected getAuthenticateCode() { + const token = this.getAuthenticationToken(); + + if (!token) { + return; + } + + return FunctionCode.fromInline(/* js */ ` + function authenticate(event, next) { + var header = event.request.headers.authorization; + var expected = "Basic ${token}"; + + if (!header || header.value !== expected) { + return { + statusCode: 401, + statusDescription: "Unauthorized", + headers: { + "www-authenticate": { + value: "Basic", + }, + }, + }; + } + + return next(event); + } + `); + } + + protected getAuthenticationToken() { + const password = this.getPassword(); + + return this.props.authentication + ? Buffer.from( + [this.props.authentication?.username, password].join(":"), + ).toString("base64") + : undefined; + } + + protected getPassword() { + if (!this.props.authentication?.password) { + return this.passwordSecret!.secretValue.toString(); + } + + if (this.props.authentication.password instanceof Secret) { + return this.props.authentication.password.secretValue.toString(); + } + + return this.props.authentication.password; + } + + protected getAppendSlashCode() { + return FunctionCode.fromInline(/* js */ ` + function appendSlash(event, next) { + if ( + !event.request.uri.endsWith("/") && + !event.request.uri.includes(".") + ) { + event.request.uri += "/"; + } + + return next(event); + } + `); + } + + /** + * @todo Make these headers configurable. + * @todo Research CSP and define a good default. + */ + protected getSecurityHeadersCode() { + return FunctionCode.fromInline(/* js */ ` + function securityHeaders(event, next) { + event.response.headers["strict-transport-security"] = { + value: "max-age=63072000; includeSubDomains; preload", + }; + + // event.response.headers["content-security-policy"] = { + // value: + // "default-src 'self'; img-src *; media-src *; frame-src *; font-src * + // }; + + event.response.headers["x-content-type-options"] = { + value: "nosniff", + }; + + event.response.headers["x-frame-options"] = { + value: "SAMEORIGIN", + }; + + return next(event); + } + `); + } + + protected createDistribution() { + const distribution = new Distribution(this, "Distribution", { + priceClass: this.props.priceClass, + certificate: this.certificate, + domainNames: this.domain ? [this.domain] : undefined, + defaultBehavior: { + origin: this.props.origin, + viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, + functionAssociations: [ + ...(this.functions.viewerRequest + ? [ + { + function: this.functions.viewerRequest, + eventType: FunctionEventType.VIEWER_REQUEST, + }, + ] + : []), + ...(this.functions.viewerResponse + ? [ + { + function: this.functions.viewerResponse, + eventType: FunctionEventType.VIEWER_RESPONSE, + }, + ] + : []), + ], + }, + }); + + new CfnOutput(this, "DistributionId", { + value: distribution.distributionId, + }); + + new CfnOutput(this, "DistributionDomainName", { + value: distribution.distributionDomainName, + }); + + return distribution; + } + + protected createAlias() { + return this.domain && this.zone + ? new ARecord(this, "DomainAlias", { + recordName: this.domain, + target: RecordTarget.fromAlias( + new CloudFrontTarget(this.distribution), + ), + zone: this.zone, + }) + : undefined; + } + + protected createCacheInvalidator() { + const paths = Array.isArray(this.props.invalidateCache) + ? this.props.invalidateCache + : undefined; + + return new CacheInvalidator(this, "CacheInvalidator", { + distribution: this.distribution, + paths, + }); + } +} diff --git a/packages/cdk-site-distribution/src/index.ts b/packages/cdk-site-distribution/src/index.ts new file mode 100644 index 00000000..2bda82e3 --- /dev/null +++ b/packages/cdk-site-distribution/src/index.ts @@ -0,0 +1 @@ +export * from "./constructs/SiteDistribution"; diff --git a/packages/cdk-site-distribution/tsconfig.json b/packages/cdk-site-distribution/tsconfig.json new file mode 100644 index 00000000..3bed3f50 --- /dev/null +++ b/packages/cdk-site-distribution/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@codedazur/tsconfig/cdk.json", + "include": ["."], + "exclude": ["dist", "build", "node_modules"] +} diff --git a/packages/cdk-static-site/README.md b/packages/cdk-static-site/README.md index d3d7af48..6cddbebc 100644 --- a/packages/cdk-static-site/README.md +++ b/packages/cdk-static-site/README.md @@ -1 +1,116 @@ -# @codedazur/cdk-static-site +# StaticSite + +## Examples + +The minimum needed to get a StaticSite up and running is to provide the path to the directory that you want to deploy as your website. + +```ts +new StaticSite({ + source: { + directory: "./path/to/build/output", + }, +}); +``` + +### With a Custom Domain Name + +If you provide a domain name and an optional subdomain, Route53 and Certificate Manager will be used to create the necessary resources. + +```ts +new StaticSite(this, "StaticSite", { + // ... + distribution: { + domain: { + name: "example.com", + subdomain: "foo", + }, + }, +}); +``` + +In the example above, a hosted zone will _NOT_ be created automatically, but will instead be looked up in the AWS account using the domain name. In other words, given the example above, a hosted zone for the domain "example.com" is expected to be present in the account. + +Alternatively, you can explicitly provide a reference to the hosted zone. + +```ts +new StaticSite(this, "StaticSite", { + // ... + distribution: { + domain: { + // ... + zone: new HostedZone(/* ... */), + // zone: HostedZone.fromLookup(/* ... */), + // zone: HostedZone.fromHostedZoneId(/* ... */), + }, + }, +}); +``` + +### With Basic Authentication + +If you do not provide a password, a Secrets Manager secret will be created and its secret value used to verify the Authorization header using a CloudFront function. Keep in mind that editing the secret's value will not update the CloudFront function. + +```ts +new StaticSite(this, "StaticSite", { + // ... + distribution: { + authentication: { + username: "username", + }, + }, +}); +``` + +You can also provide your own secret. Once again, editing this secret will not automatically update the CloudFront function. + +```ts +new StaticSite(this, "StaticSite", { + // ... + distribution: { + authentication: { + username: "username", + password: new Secret(this, "PasswordSecret", { + generateSecretString: { excludePunctuation: true }, + }), + }, + }, +}); +``` + +Or, you could provide the password as a plain string, typically read from an environment variable. + +> WARNING: This method does NOT meet production security standards, even if the password is not hardcoded into the codebase, because it will result in the password string being readable in the CloudFormation template, which is uploaded to AWS. Use this approach with caution and only in cases where a leak is not considered problematic. + +```ts +new StaticSite(this, "StaticSite", { + // ... + distribution: { + authentication: { + username: "username", + password: process.env.PASSWORD, + }, + }, +}); +``` + +### With Explicit Cache Invalidation + +By default, all routes will be flushed from the cache upon deployment. This is performed asynchronously using a step function, so that the deployment procedure does not hold until the invalidation is completed. + +You can disable this behavior if you prefer an alternative means of invalidating the cache. + +```ts +new StaticSite(this, "StaticSite", { + // ... + invalidateCache: false, +}); +``` + +Or, you can provide the specific routes that you want to flush. + +```ts +new StaticSite(this, "StaticSite", { + // ... + invalidateCache: ["/foo/*", "/bar/baz"], +}); +``` diff --git a/packages/cdk-static-site/package.json b/packages/cdk-static-site/package.json index 1603450c..861b8a88 100644 --- a/packages/cdk-static-site/package.json +++ b/packages/cdk-static-site/package.json @@ -23,7 +23,7 @@ "test": "vitest run --passWithNoTests" }, "dependencies": { - "@codedazur/cdk-cache-invalidator": "*" + "@codedazur/cdk-site-distribution": "^0.0.0" }, "peerDependencies": { "aws-cdk-lib": ">=2", diff --git a/packages/cdk-static-site/src/constructs/StaticSite.ts b/packages/cdk-static-site/src/constructs/StaticSite.ts index 7350a8e5..5d1696af 100644 --- a/packages/cdk-static-site/src/constructs/StaticSite.ts +++ b/packages/cdk-static-site/src/constructs/StaticSite.ts @@ -1,62 +1,31 @@ -import { CacheInvalidator } from "@codedazur/cdk-cache-invalidator"; -import { CfnOutput, RemovalPolicy } from "aws-cdk-lib"; -import { - DnsValidatedCertificate, - ICertificate, -} from "aws-cdk-lib/aws-certificatemanager"; import { - Function as CloudFrontFunction, - Distribution, - FunctionCode, - FunctionEventType, - OriginProtocolPolicy, - PriceClass, - ViewerProtocolPolicy, -} from "aws-cdk-lib/aws-cloudfront"; + SiteDistribution, + SiteDistributionProps, +} from "@codedazur/cdk-site-distribution"; +import { CfnOutput, RemovalPolicy } from "aws-cdk-lib"; +import { OriginProtocolPolicy } from "aws-cdk-lib/aws-cloudfront"; import { HttpOrigin } from "aws-cdk-lib/aws-cloudfront-origins"; import { AnyPrincipal, Effect, PolicyStatement } from "aws-cdk-lib/aws-iam"; -import { - ARecord, - HostedZone, - IHostedZone, - RecordTarget, -} from "aws-cdk-lib/aws-route53"; -import { CloudFrontTarget } from "aws-cdk-lib/aws-route53-targets"; import { BlockPublicAccess, Bucket } from "aws-cdk-lib/aws-s3"; import { BucketDeployment, Source } from "aws-cdk-lib/aws-s3-deployment"; import { Secret } from "aws-cdk-lib/aws-secretsmanager"; import { Construct } from "constructs"; export interface StaticSiteProps { - path: string; - authentication?: { - username: string; - password: string; + source: { + directory: string; }; bucket?: { accelerate?: boolean; }; - domain?: { - name: string; - subdomain?: string; - zone?: IHostedZone; - }; website?: { indexDocument?: string; errorDocument?: string; }; - distribution?: { - priceClass?: PriceClass; - functions?: { - viewerRequest?: FunctionCode[]; - viewerResponse?: FunctionCode[]; - }; - }; + distribution?: Omit; deployment?: { memoryLimit?: number; prefix?: string; - cacheInvalidations?: string[]; - awaitCacheInvalidations?: boolean; }; } @@ -73,19 +42,10 @@ export interface StaticSiteProps { * @see https://repost.aws/knowledge-center/cloudfront-serve-static-website */ export class StaticSite extends Construct { - public readonly domain?: string; public readonly refererSecret: Secret; public readonly bucket: Bucket; - public readonly zone?: IHostedZone; - public readonly certificate?: ICertificate; - public readonly functions: { - viewerRequest?: CloudFrontFunction; - viewerResponse?: CloudFrontFunction; - }; - public readonly distribution: Distribution; - public readonly alias?: ARecord; public readonly deployment: BucketDeployment; - public readonly cacheInvalidator?: CacheInvalidator; + public readonly siteDistribution: SiteDistribution; constructor( scope: Construct, @@ -94,33 +54,10 @@ export class StaticSite extends Construct { ) { super(scope, id); - this.domain = this.determineDomain(); this.refererSecret = this.createRefererSecret(); this.bucket = this.createBucket(); - this.zone = this.findHostedZone(); - this.certificate = this.createCertificate(); - this.functions = this.createFunctions(); - this.distribution = this.createDistribution(); - this.alias = this.createAlias(); + this.siteDistribution = this.createSiteDistribution(); this.deployment = this.createDeployment(); - - if (!props.deployment?.awaitCacheInvalidations) { - this.cacheInvalidator = this.createCacheInvalidator(); - } - } - - protected determineDomain() { - const domain = this.props.domain - ? [this.props.domain.subdomain, this.props.domain.name] - .filter(Boolean) - .join(".") - : undefined; - - if (domain) { - new CfnOutput(this, "URL", { value: "https://" + domain }); - } - - return domain; } protected createRefererSecret() { @@ -164,252 +101,24 @@ export class StaticSite extends Construct { return bucket; } - protected findHostedZone() { - return this.props.domain - ? this.props.domain?.zone ?? - HostedZone.fromLookup(this, "HostedZone", { - domainName: this.props.domain.name, - }) - : undefined; - } - - protected createCertificate() { - const certificate = - this.domain && this.zone - ? new DnsValidatedCertificate(this, "Certificate", { - domainName: this.domain, - hostedZone: this.zone, - region: "us-east-1", - }) - : undefined; - - if (certificate) { - new CfnOutput(this, "CertificateArn", { - value: certificate.certificateArn, - }); - } - - return certificate; - } - - protected createFunctions() { - return { - viewerRequest: this.createViewerRequestFunction(), - viewerResponse: this.createViewerResponseFunction(), - }; - } - - protected createViewerRequestFunction() { - const handlers = [ - this.getAuthenticateCode(), - this.getAppendSlashCode(), - ...(this.props.distribution?.functions?.viewerRequest ?? []), - ].filter((handler): handler is FunctionCode => !!handler); - - if (handlers.length === 0) { - return undefined; - } - - return new CloudFrontFunction(this, "ViewerRequestFunction", { - code: this.getHandlerChainCode(handlers, "request"), - }); - } - - protected createViewerResponseFunction() { - const handlers = [ - this.getSecurityHeadersCode(), - ...(this.props.distribution?.functions?.viewerResponse ?? []), - ].filter((handler): handler is FunctionCode => !!handler); - - if (handlers.length === 0) { - return undefined; - } - - return new CloudFrontFunction(this, "ViewerResponseFunction", { - code: this.getHandlerChainCode(handlers, "response"), - }); - } - - protected getHandlerChainCode( - handlers: FunctionCode[], - completion: "request" | "response", - ) { - return FunctionCode.fromInline(/* js */ ` - function handler(event) { - return chain(event, [ - ${handlers.map((code) => code.render().replace(/;\s*$/, "")).join(",")} - ]) - } - - function chain(event, handlers) { - var current = handlers.shift() ?? complete; - return current(event, (event) => chain(event, handlers)) - } - - function complete(event) { - return event.${completion}; - } - `); - } - - protected getAuthenticateCode() { - const token = this.getAuthenticationToken(); - - if (!token) { - return; - } - - return FunctionCode.fromInline(/* js */ ` - function authenticate(event, next) { - var header = event.request.headers.authorization; - var expected = "Basic ${token}"; - - if (!header || header.value !== expected) { - return { - statusCode: 401, - statusDescription: "Unauthorized", - headers: { - "www-authenticate": { - value: "Basic", - }, - }, - }; - } - - return next(event); - } - `); - } - - protected getAuthenticationToken() { - return this.props.authentication - ? Buffer.from( - [ - this.props.authentication?.username, - this.props.authentication?.password, - ].join(":"), - ).toString("base64") - : undefined; - } - - protected getAppendSlashCode() { - return FunctionCode.fromInline(/* js */ ` - function appendSlash(event, next) { - if ( - !event.request.uri.endsWith("/") && - !event.request.uri.includes(".") - ) { - event.request.uri += "/"; - } - - return next(event); - } - `); - } - - /** - * @todo Make these headers configurable. - * @todo Research CSP and define a good default. - */ - protected getSecurityHeadersCode() { - return FunctionCode.fromInline(/* js */ ` - function securityHeaders(event, next) { - event.response.headers["strict-transport-security"] = { - value: "max-age=63072000; includeSubDomains; preload", - }; - - // event.response.headers["content-security-policy"] = { - // value: - // "default-src 'self'; img-src *; media-src *; frame-src *; font-src * - // }; - - event.response.headers["x-content-type-options"] = { - value: "nosniff", - }; - - event.response.headers["x-frame-options"] = { - value: "SAMEORIGIN", - }; - - return next(event); - } - `); - } - - protected createDistribution() { - const distribution = new Distribution(this, "Distribution", { - priceClass: this.props.distribution?.priceClass, - certificate: this.certificate, - domainNames: this.domain ? [this.domain] : undefined, - defaultBehavior: { - origin: new HttpOrigin(this.bucket.bucketWebsiteDomainName, { - protocolPolicy: OriginProtocolPolicy.HTTP_ONLY, - customHeaders: { - Referer: this.refererSecret.secretValue.toString(), - }, - }), - viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, - functionAssociations: [ - ...(this.functions.viewerRequest - ? [ - { - function: this.functions.viewerRequest, - eventType: FunctionEventType.VIEWER_REQUEST, - }, - ] - : []), - ...(this.functions.viewerResponse - ? [ - { - function: this.functions.viewerResponse, - eventType: FunctionEventType.VIEWER_RESPONSE, - }, - ] - : []), - ], - }, - }); - - new CfnOutput(this, "DistributionId", { - value: distribution.distributionId, - }); - - new CfnOutput(this, "DistributionDomainName", { - value: distribution.distributionDomainName, + protected createSiteDistribution() { + return new SiteDistribution(this, "SiteDistribution", { + ...this.props.distribution, + origin: new HttpOrigin(this.bucket.bucketWebsiteDomainName, { + protocolPolicy: OriginProtocolPolicy.HTTP_ONLY, + customHeaders: { + Referer: this.refererSecret.secretValue.toString(), + }, + }), }); - - return distribution; - } - - protected createAlias() { - return this.domain && this.zone - ? new ARecord(this, "DomainAlias", { - recordName: this.domain, - target: RecordTarget.fromAlias( - new CloudFrontTarget(this.distribution), - ), - zone: this.zone, - }) - : undefined; } protected createDeployment() { - return new BucketDeployment(this, "Deployment", { - sources: [Source.asset(this.props.path)], + return new BucketDeployment(this, "BucketDeployment", { + sources: [Source.asset(this.props.source.directory)], destinationBucket: this.bucket, destinationKeyPrefix: this.props.deployment?.prefix, memoryLimit: this.props.deployment?.memoryLimit, - distribution: this.props.deployment?.awaitCacheInvalidations - ? this.distribution - : undefined, - distributionPaths: this.props.deployment?.cacheInvalidations, - }); - } - - protected createCacheInvalidator() { - return new CacheInvalidator(this, "CacheInvalidator", { - distribution: this.distribution, - paths: this.props.deployment?.cacheInvalidations ?? ["/*"], }); } }