Skip to content

Set up infrastructure

This guide shows you how to create a static website hosted on S3 with CloudFront distribution, SSL certificate, and custom domain.

Before you begin

Step 1: Add the package

Add the CloudFront static website package, replace web-example with a stack name that fits your project:

repo-iac/environments/dev/
ok pkg add cloudfront-static-website web-example

Naming convention

We recommend using the web- prefix when naming a static web application.

Step 2: Configure the package

Configure the package. The settings depend on your application type:

  • Single Page Application — the application has one HTML entrypoint. Client-side JavaScript handles routing.
  • Static Website — the application maps each URL to a file. /about/ serves /about/index.html.
repo-iac/environments/dev/web-example/package-config.yml
StackName: "web-example"
cloudfront-static-website-data.StackName: "web-example-data"
Name: "example"
RedirectAllToIndex: true # (1)!
DeploymentPipelineV2:
  Enable: true
  1. By default, CloudFront returns a 403 Forbidden error when someone requests a file that doesn't exist in your S3 bucket. Enable this setting to serve index.html instead of the error page. This allows your Single Page Application (SPA) to handle client-side routing - the SPA receives the request and can display the correct content based on the URL path
repo-iac/environments/dev/web-example/package-config.yml
StackName: "web-example"
cloudfront-static-website-data.StackName: "web-example-data"
Name: "example"
ErrorObject: 404.html # (1)!
AppendIndexToDirectories: true # (2)!
DeploymentPipelineV2:
  Enable: true
  1. Configure where you will upload your default error page.
  2. When someone visits a directory URL like /getting-started/, CloudFront needs to know which file to serve. Enable this to automatically append index.html to directory requests, so /getting-started/ serves /getting-started/index.html.

Batteries included

This template is configured with defaults that work out of the box for Single Page Applications and Static Websites. The configuration options, their descriptions and default values can be viewed in the cloudfront static website README.

Step 3: Install the package

Install the package to generate Terraform files:

repo-iac/environments/dev/web-example/
ok pkg install

The command also creates a data stack (web-example-data) that provisions S3 buckets for your static files and access logs.

Step 4: Apply

We will now create and merge a PR containing our changes. Because of the dependencies between the two stacks one of the plans will expectedly fail. The plan for web-example will show errors reading SSM parameters — this is expected because the data stack hasn't created them yet. On merge, Terraform applies the data stack first, so web-example succeeds.

  1. Create a pull request with the changes in both web-example and web-example-data.
  2. Verify that the errors in web-example match the pattern below. The plan fails because it references S3 resources (via SSM parameters) that the data stack hasn't created yet. For a real example, see this plan comment.
  3. Verify that the plan for web-example-data has no errors.
  4. Apply both stacks by bypassing merge rules and merging to main. Applying the changes takes about 5 minutes. The majority of the time is spent setting up the CloudFront distribution.
  5. Verify that Terraform apply from main runs without errors. You should see the creation of S3 buckets for logs and content from web-example-data, and that the web-example stack completed without errors. There will be a Terraform output, service_url, pointing to your website.
  6. Verify the distribution is reachable at the service url from the previous step (e.g. https://example.pirates-dev.oslo.systems). With no files uploaded yet, you should see an AccessDenied response — this confirms CloudFront and DNS are wired up correctly:
    Expected response
    <Error>
      <Code>AccessDenied</Code>
      <Message>Access Denied</Message>
    </Error>
    

Step 5: Verify the distribution works (optional)

Upload a placeholder index.html so you can confirm S3, CloudFront, and DNS are configured correctly before you add the deployment pipeline.

  1. Authenticate to the dev AWS account (e.g., aws sso login --profile pirates-dev).
  2. Upload an index.html file:

    echo '<html><body><h1>Hello world!</h1></body></html>' \
      | aws s3 cp - s3://<environment>-<name>/index.html --content-type "text/html" --cache-control "no-cache"
    
    Replace these values:

    Field Description Example
    <environment> Your environment name pirates-dev
    <name> Your website name example
  3. Refresh the URL from Step 4. You should see the Hello world page instead of the AccessDenied error.

The deployment pipeline will overwrite this file later, so no cleanup is needed.

Step 6: Repeat for production

Repeat the above steps for your production environment.

Next step

Create the deployment pipeline to automate deployment of your static site from GitHub Actions.