Skip to content

Deploying a static site on AWS

tutorial

Let's deploy a HydePHP site on AWS CloudFront using AWS CDK. This setup delivers sub-second load times at minimal cost.

Here is the folder structure we'll start with:

.
├── cdk/          # AWS CDK infrastructure (S3, CloudFront, Route 53)
├── hyde/         # HydePHP static site source & Markdown content
└── compose.yml   # Docker Compose for local PHP development

Prerequisites

  1. You have a domain registered or managed in Route 53
  2. You have the CDK CLI installed
  3. You have an AWS profile configured in your terminal

HydePHP

Using HydePHP is a personal preference, so we'll keep this section concise. The same workflow will work with any other static site generator.

It is a Laravel-based framework for generating static site artifacts—great for a portfolio, blog, or documentation site.

Install HydePHP:

composer create-project hyde/hyde

Running php hyde build (in the /hyde folder) compiles the site into _site/. This is what will be synchronized to the S3 bucket that CloudFront distributes.


CDK

We will define our infrastructure using AWS CDK (Infrastructure as Code).

Create the cdk/ directory, navigate into it, and initialize the project:

cdk init app --language typescript .

Infrastructure Code

Hosted Zone

import * as route53 from "aws-cdk-lib/aws-route53";
 
const zone = route53.HostedZone.fromLookup(this, "HostedZone", {
  domainName: "example.com",
});

We're looking up an already set up Hosted Zone (created after purchasing or configuring your domain).

Certificate

import * as certificatemanager from "aws-cdk-lib/aws-certificatemanager";

const certificate = new certificatemanager.Certificate(this, "Certificate", {
  domainName: "example.com",
  subjectAlternativeNames: ["www.example.com"],
  validation: certificatemanager.CertificateValidation.fromDns(zone),
});

We'll need this for HTTPS.

S3 Bucket

import * as s3 from "aws-cdk-lib/aws-s3";

const bucket = new s3.Bucket(this, "SiteBucket", {
  blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
  enforceSSL: true,
});

This is where the site artifacts will be stored. Public access is blocked so users cannot bypass CloudFront and hit the S3 bucket directly, preventing unexpected egress costs.

Distribution

import * as cloudfront from "aws-cdk-lib/aws-cloudfront";

const distribution = new cloudfront.Distribution(this, "Distribution", {
  defaultRootObject: "index.html",
  domainNames: ["example.com", "www.example.com"],
  certificate,
  priceClass: cloudfront.PriceClass.PRICE_CLASS_100,
  defaultBehavior: {
    origin: origins.S3BucketOrigin.withOriginAccessControl(bucket, {
      originAccessLevels: [
        cloudfront.AccessLevel.READ,
        cloudfront.AccessLevel.LIST,
      ],
    }),
    viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
    allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD,
    cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD,
    compress: true,
  },
  errorResponses: [
    {
      httpStatus: 404,
      responseHttpStatus: 404,
      responsePagePath: "/404.html",
      ttl: Duration.seconds(10),
    },
  ],
});

This creates a CloudFront distribution that uses the S3 bucket as its origin.

DNS Records (Route 53)

import * as route53 from "aws-cdk-lib/aws-route53";

const target = route53.RecordTarget.fromAlias(
  new route53Targets.CloudFrontTarget(distribution),
);
new route53.ARecord(this, "ApexARecord", { zone, target }); // IPv4
new route53.AaaaRecord(this, "ApexAaaaRecord", { zone, target }); // IPv6
new route53.ARecord(this, "WwwARecord", { zone, recordName: "www", target });
new route53.AaaaRecord(this, "WwwAaaaRecord", {
  zone,
  recordName: "www",
  target,
});

These DNS records route apex and subdomain traffic to our CloudFront distribution.

Deployment

Deploy the infrastructure stack first:

  1. cdk bootstrap — Provisions resources required by CDK on AWS (CloudFormation stack, staging assets)
  2. cdk deploy — Deploys the infrastructure stack

Next, deploy the static site files using the AWS CLI:

  1. Build the site:
    php hyde build
    
  2. Sync the built site to S3:
    aws s3 sync ./hyde/_site s3://$(BUCKET)/ --delete
    
    (where $(BUCKET) is your S3 bucket name)

First-time deployments don't require clearing the cache, but subsequent updates will require a cache invalidation:

aws cloudfront create-invalidation --distribution-id $(DISTRIBUTION_ID) --paths "/*"

(where $(DISTRIBUTION_ID) is your CloudFront distribution ID)

Conclusion

You now have a production-ready static site globally distributed with AWS CloudFront!

Check out the gist for the entire code.

End of article