Deployment

Infrastructure

Below is my AWS CDK stack to deploy the infrastructure for this site.

This stack creates an S3 bucket and Cloudfront CDN distribution. It then adds an DNS A record to the Route53 hosted zone for my domain.

import * as cdk from "aws-cdk-lib";
import { Bucket, BucketAccessControl } from "aws-cdk-lib/aws-s3";
import {
  CachePolicy,
  Distribution,
  OriginAccessIdentity,
  SecurityPolicyProtocol,
  ViewerProtocolPolicy,
  Function,
  FunctionCode,
  FunctionEventType,
} from "aws-cdk-lib/aws-cloudfront";
import { S3Origin } from "aws-cdk-lib/aws-cloudfront-origins";
import { Certificate } from "aws-cdk-lib/aws-certificatemanager";
import { ARecord, HostedZone, RecordTarget } from "aws-cdk-lib/aws-route53";
import { CloudFrontTarget } from "aws-cdk-lib/aws-route53-targets";

export class ExcerptsStack extends cdk.Stack {
  /**
   * This stack creates the following AWS resources:
   * - S3 Bucket
   * - Cloudfront distribution pointed at S3 bucket
   *  - Uses existing certificate for SSL
   *  - Defaults caching to 60 seconds
   *  - Viewer function to rewrite URLs ending with / to /index.html
   * - DNS A record pointed at Cloudfront distribution
   * @param {cdk.App} scope
   * @param {string} id
   * @param {import("./types").ExcerptsStackProps} props
   */
  constructor(scope, id, props) {
    super(scope, id, props);

    const cacheSeconds = props.cacheSeconds || 60;

    const bucket = new Bucket(this, "Bucket", {
      bucketName: props.bucketName,
      accessControl: BucketAccessControl.PRIVATE,
      removalPolicy: cdk.RemovalPolicy.RETAIN,
    });

    const originAccessIdentity = new OriginAccessIdentity(
      this,
      "OriginAccessIdentity"
    );
    bucket.grantRead(originAccessIdentity);

    const hostedZone = HostedZone.fromLookup(this, "HostedZone", {
      domainName: props.domainName,
    });

    const certificate = Certificate.fromCertificateArn(
      this,
      "Certificate",
      props.certificateARN
    );

    const rewriteFunction = new Function(this, "Function", {
      code: FunctionCode.fromInline(`function handler(event) {
        var request = event.request;
        if (request.uri.endsWith("/")) {
          request.uri += "index.html";
        }
        return request;
      }`),
    });

    const distribution = new Distribution(this, "Distribution", {
      defaultRootObject: "index.html",
      certificate: certificate,
      domainNames: [`${props.subdomainName}.${props.domainName}`],
      minimumProtocolVersion: SecurityPolicyProtocol.TLS_V1_2_2021,
      errorResponses: [
        {
          httpStatus: 404,
          responseHttpStatus: 404,
          responsePagePath: "/404.html",
          ttl: cdk.Duration.seconds(cacheSeconds),
        },
      ],
      defaultBehavior: {
        compress: true,
        viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
        origin: new S3Origin(bucket, { originAccessIdentity }),
        cachePolicy: new CachePolicy(this, "S3CachePolicy", {
          defaultTtl: cdk.Duration.seconds(cacheSeconds),
          enableAcceptEncodingGzip: true,
          enableAcceptEncodingBrotli: true,
        }),
        functionAssociations: [
          {
            function: rewriteFunction,
            eventType: FunctionEventType.VIEWER_REQUEST,
          },
        ],
      },
    });

    distribution.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY);

    new ARecord(this, "ARecord", {
      recordName: props.subdomainName,
      zone: hostedZone,
      target: RecordTarget.fromAlias(new CloudFrontTarget(distribution)),
    });
  }
}

Deployment

With the infrastructure above, deployments becomes as simple as:

aws s3 sync ./public s3://<my_bucket>

Github Action

I am using the Github Action below to run the build and deployment whenever changes to my markdown files are pushed to Github.

name: Build

on:
  push:
    paths:
      - "excerpts/**/*.md"
      - "how/**/*.md"
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/setup-node@v2
        with:
          node-version: "18"

      - uses: actions/checkout@v2

      - name: Set AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: us-east-1

      - name: Execute
        run: |
            npm install
            npm run deploy