Static website
CloudFront, S3 per PR
Day Two #
Yesterday we proved the mechanism: GitHub can assume an AWS role via OIDC, CDK can deploy on every pull request, and teardown happens automatically.
But an S3 bucket is an invisible victory.
Today we’ll make the preview tangible: a real static website you can click, review, and share—created per PR and destroyed when the PR closes.
Goal #
For every pull request:
- Upload a small static site into a private S3 bucket
- Serve it via CloudFront (HTTPS, fast, globally cached)
- Output a preview URL
- Comment that URL on the PR
- Tear it all down on PR close
This keeps the “ephemeral environment” promise while staying cheap and simple.
Prerequisites #
- You completed “Account & bootstrap” and are using a CDK TypeScript app.
- You completed “CI/CD” and your repo can deploy/destroy via GitHub OIDC (no long‑lived AWS keys).
- Your preview stack already uses a PR number (
-c pr=...) to name the stack deterministically.
We’ll extend the same preview stack entrypoint (bin/preview.ts) so the workflows stay essentially the same.
The shape of the preview site #
We want three properties:
- Private origin: the S3 bucket is not public.
- Public edge: CloudFront serves the site over HTTPS.
- Deterministic outputs: the preview URL is stable for a given PR.
This is the minimum “real app” footprint that still feels like infrastructure you can build on.
CDK - CloudFront backed site #
In your preview stack, add:
- An S3 bucket to hold static assets
- A CloudFront distribution with the S3 bucket as its origin
- A small deployment step that uploads local
site/files into the bucket - Outputs: bucket name + CloudFront domain
You can keep this as an evolution of your existing lib/stack.ts (or split it into lib/preview-site-stack.ts if you prefer).
Example (illustrative) stack body:
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as s3deploy from 'aws-cdk-lib/aws-s3-deployment';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
export class Stack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const pr = String(this.node.tryGetContext('pr') ?? 'local');
const bucket = new s3.Bucket(this, 'SiteBucket', {
autoDeleteObjects: true,
removalPolicy: cdk.RemovalPolicy.DESTROY,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
encryption: s3.BucketEncryption.S3_MANAGED,
});
const distribution = new cloudfront.Distribution(this, 'SiteDistribution', {
defaultRootObject: 'index.html',
defaultBehavior: {
origin: origins.S3BucketOrigin.withOriginAccessControl(bucket),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
},
errorResponses: [
// Nice for SPAs (optional)
{ httpStatus: 403, responseHttpStatus: 200, responsePagePath: '/index.html' },
{ httpStatus: 404, responseHttpStatus: 200, responsePagePath: '/index.html' },
],
});
new s3deploy.BucketDeployment(this, 'DeploySite', {
destinationBucket: bucket,
sources: [s3deploy.Source.asset('site')],
distribution,
distributionPaths: ['/*'],
});
new cdk.CfnOutput(this, 'PreviewUrl', {
value: `https://${distribution.domainName}/?pr=${encodeURIComponent(pr)}`,
});
}
}
A tiny site to deploy #
Create a site/ folder at the repo root with at least:
site/index.html
Keep it small. This is a proof, not a product. If you already have a frontend build, you can point the deployment at your build output folder instead.
CI - capture outputs and comment on the PR #
CDK can write stack outputs into a JSON file. That file is the bridge between “infrastructure happened” and “GitHub should tell humans where to click.”
In your PR deploy workflow, change the deploy command to include:
cdk deploy \
... \
--outputs-file cdk-outputs.json \
--require-approval never
Then add a step to post a PR comment using the PreviewUrl output:
- name: Comment preview URL
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const outputs = JSON.parse(fs.readFileSync('cdk-outputs.json', 'utf8'));
const stackName = `Stack-PR${process.env.PR_NUMBER}`;
const url = outputs?.[stackName]?.PreviewUrl;
if (!url) throw new Error(`Missing PreviewUrl for ${stackName}`);
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `Preview: ${url}`,
});
Tip: if you’d rather avoid a new comment on every push, you can:
- Search for an existing “Preview:” comment and update it, or
- Use PR checks/statuses instead of comments
We’ll keep it simple first.
Teardown notes #
S3 buckets destroy cleanly when autoDeleteObjects is on.
CloudFront sometimes takes longer to delete than other services. If you see teardown delays, that’s normal: CloudFront invalidations and distribution disable/delete can be asynchronous. For a tutorial, accept the latency; for a production workflow, you may want a more deliberate teardown strategy.
Notes and hardening #
- Least privilege: the GitHub Actions role can start with broad access, but tighten it once the pattern works (S3 + CloudFront + CloudFormation + IAM read + STS).
- Origin protection: keep the bucket private; use CloudFront’s origin access control (not a public bucket policy).
- Naming: keep stack IDs deterministic (
Stack-PR123) socdk destroynever has to guess. - Outputs as interface: treat CDK stack outputs as your CI contract (URLs, ARNs, bucket names).
- Governance: protect workflow files and the OIDC trust policy with CODEOWNERS + branch protection.
What you should see #
- A PR opens → workflow runs → CloudFront deploys → a PR comment appears with a URL.
- The URL loads a static page over HTTPS.
- PR closes → teardown workflow runs → stack is destroyed, bucket emptied and deleted, distribution removed.
Well‑Architected Framework #
- Security:
- private S3 origin, public access only through CloudFront
- no long‑lived credentials in CI thanks to OIDC
- easy to add WAF later.
- Operational Excellence:
- reproducible previews on every PR
- outputs and PR comments create a clean feedback loop for reviewers.
- Reliability:
- CDK/CloudFormation give idempotent deploys and safe rollbacks
- deterministic naming reduces operator error.
- CDK/CloudFormation give idempotent deploys and safe rollbacks
- Cost Optimization:
- resources exist only while the PR is alive
- CloudFront + S3 keep costs low for static content.
- resources exist only while the PR is alive
- Performance Efficiency
- CloudFront caches globally by default
- static assets are fast and simple.
- Sustainability:
- ephemeral environments minimize idle infrastructure and prevent forgotten resources.