diff --git a/infra/README.md b/infra/README.md index 2bd06de3..5b32b617 100644 --- a/infra/README.md +++ b/infra/README.md @@ -28,6 +28,11 @@ for them to build on, and exports `JarlSiteBucketName`, `JarlDistributionId` and `/ssr/*` behaviour pointing at its own origin while every other path keeps hitting S3; `JarlDomainStack` hands over the hosted zone and the certificate the distribution is created with. +> **Mid-cutover: the `/ssr/*` behaviour is deliberately absent.** CloudFront refuses to update a VPC +> origin that a distribution is still associated with, so detaching the behaviour and repointing the +> origin cannot happen in one deploy. Until the next deploy restores it, `/ssr/*` falls through to the +> distribution's static 404, and `JarlSsr` deploys *after* `JarlStaticSite`, inverting the order below. + `JarlStaticSite` therefore deploys last, whichever way the wiring runs: its template references `JarlSsr`'s origin and `JarlDomain`'s certificate, so CloudFormation needs both in place before it can be created or updated. `cdk deploy --all` works this out from the templates; deploying stacks one at a diff --git a/infra/bin/jarl-infra.ts b/infra/bin/jarl-infra.ts index 83f71d6a..5336bf10 100644 --- a/infra/bin/jarl-infra.ts +++ b/infra/bin/jarl-infra.ts @@ -28,5 +28,7 @@ const staticSite = new JarlStaticSiteStack(app, "JarlStaticSite", { certificate: domain.certificate, }); -// Instantiated last but deployed before JarlStaticSite, whose template references this origin. -new JarlSsrStack(app, "JarlSsr", { ...stackPropsIn(primaryRegion), distribution: staticSite.distribution }); +const ssr = new JarlSsrStack(app, "JarlSsr", stackPropsIn(primaryRegion)); + +// The distribution has to let go of the VPC origin before CloudFront will let this stack update it. +ssr.addDependency(staticSite); diff --git a/infra/lib/jarl-stacks.ts b/infra/lib/jarl-stacks.ts index a9aa3eea..c70f78b5 100644 --- a/infra/lib/jarl-stacks.ts +++ b/infra/lib/jarl-stacks.ts @@ -9,14 +9,13 @@ import { FunctionEventType, HttpVersion, OriginProtocolPolicy, - OriginRequestPolicy, PriceClass, ResponseHeadersPolicy, ViewerProtocolPolicy, VpcOrigin as VpcOriginResource, VpcOriginEndpoint, } from "aws-cdk-lib/aws-cloudfront"; -import { S3BucketOrigin, VpcOrigin } from "aws-cdk-lib/aws-cloudfront-origins"; +import { S3BucketOrigin } from "aws-cdk-lib/aws-cloudfront-origins"; import { AmazonLinuxCpuType, BlockDeviceVolume, @@ -261,14 +260,9 @@ systemctl daemon-reload systemctl enable ${ssrServiceName} `; -export interface JarlSsrStackProps extends StackProps { - /** {@link JarlStaticSiteStack.distribution} — this stack adds its behaviour to it rather than creating a front. */ - readonly distribution: Distribution; -} - -/** EC2 instance running the docs SSR server, attached to the distribution as a second origin. */ +/** EC2 instance running the docs SSR server, and the CloudFront VPC origin that reaches it. */ export class JarlSsrStack extends Stack { - constructor(scope: Construct, id: string, props: JarlSsrStackProps) { + constructor(scope: Construct, id: string, props: StackProps) { super(scope, id, props); // No NAT gateway: one would cost more per month than the instance it serves, so outbound @@ -333,6 +327,8 @@ export class JarlSsrStack extends Stack { // The server terminates nothing, so CloudFront has to speak plain HTTP to it; the default // policy would follow the viewer onto port 443, where nothing listens. + // No distribution behaviour points here yet: CloudFront refuses to update a VPC origin while a + // distribution is associated with it, so attaching this one is a deploy of its own. const vpcOrigin = new VpcOriginResource(this, "SsrVpcOrigin", { endpoint: VpcOriginEndpoint.ec2Instance(instance), httpPort: ssrPort, @@ -382,23 +378,6 @@ export class JarlSsrStack extends Stack { Port.tcp(ssrPort), ); - // JarlStaticSiteStack's errorResponses are distribution-wide: a 403/404 from the server still - // serves the static build's 404.html, not anything from this origin. - props.distribution.addBehavior( - ssrPathPattern, - VpcOrigin.withVpcOrigin(vpcOrigin, { - // CloudFront makes three connection attempts, so the default 10 seconds is 30 before a 504. - connectionTimeout: Duration.seconds(2), - }), - { - viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, - allowedMethods: AllowedMethods.ALLOW_GET_HEAD_OPTIONS, - cachePolicy: CachePolicy.CACHING_DISABLED, - originRequestPolicy: OriginRequestPolicy.ALL_VIEWER, - compress: true, - }, - ); - // The deploy role's baseline permissions (assume-cdk-roles, DescribeStacks, site-bucket S3, // CloudFront invalidation) were granted out of band alongside the role itself; this adds // only what rolling the SSR server needs, as its own policy on the same role.