Hugo Site CI CD Deployment Guide (GitHub Actions + AWS)

Hugo Site CI/CD Deployment Guide (GitHub Actions + AWS)

Overview

This document describes the complete process for deploying a Hugo static website using:

  • GitHub
  • GitHub Actions
  • Amazon S3
  • Amazon CloudFront
  • AWS Certificate Manager (ACM)
  • Amazon Route53

The resulting architecture is:

GitHub Repository
        |
        v
GitHub Actions
        |
        v
S3 Bucket (Private)
        |
        v
CloudFront Distribution
        |
        v
CloudFront Function
(Index.html Rewrite)
        |
        v
notes.chintanspace.com

Phase 1 - Prepare Hugo Site

Verify Hugo Build

Run locally:

hugo

Verify:

tree public

Example:

public
├── index.html
├── aws
│   └── fargate
│       └── index.html
├── hugo
│   └── setup-hugo
│       └── index.html

Configure Base URL

Update hugo.toml

baseURL = "https://notes.chintanspace.com/"
title = "Alok's Notes"
locale = "en-us"

[module]
[[module.imports]]
path = "github.com/google/docsy"

[params]
copyright = "Alok Modak"

[markup]
  [markup.goldmark]
    [markup.goldmark.renderer]
      unsafe = true

Phase 2 - Create GitHub Repository

Push repository:

git init

git add .

git commit -m "Initial commit"

git branch -M main

git remote add origin https://github.com/<user>/<repo>.git

git push -u origin main

Phase 3 - Create S3 Bucket

Navigate to:

AWS Console
→ S3
→ Create Bucket

Bucket Settings

Bucket Name

notes-prod-bucket

Bucket Type

General Purpose

Object Ownership

ACLs Disabled (Recommended)
Bucket owner enforced

Block Public Access

Keep all enabled

The bucket remains private.

Versioning

Enabled

Recommended.

Encryption

SSE-S3

Create bucket.


Phase 4 - Create CloudFront Distribution

Navigate to:

CloudFront
→ Create Distribution

Origin

Origin Domain

Select:

notes-prod-bucket.s3.us-east-1.amazonaws.com

Origin Access

Origin Access Control (OAC)

Create a new OAC.

Origin Path

Leave blank.

Default Root Object

index.html

Create distribution.


Phase 5 - Configure Origin Access Control

CloudFront automatically generates a bucket policy.

Verify bucket policy:

{
  "Version": "2008-10-17",
  "Statement": [
    {
      "Sid": "AllowCloudFrontServicePrincipal",
      "Effect": "Allow",
      "Principal": {
        "Service": "cloudfront.amazonaws.com"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::notes-prod-bucket/*",
      "Condition": {
        "StringEquals": {
          "AWS:SourceArn": "arn:aws:cloudfront::<ACCOUNT_ID>:distribution/<DISTRIBUTION_ID>"
        }
      }
    }
  ]
}

Phase 6 - Configure CloudFront Function

Without this function, Hugo pages return:

AccessDenied

because S3 cannot resolve:

/docs/aws/fargate/

to:

/docs/aws/fargate/index.html

Create Function

Navigate:

CloudFront
→ Functions
→ Create Function

Name:

rewrite-index

Code:

function handler(event) {
    var request = event.request;
    var uri = request.uri;

    if (uri.endsWith('/')) {
        request.uri += 'index.html';
    } else if (!uri.includes('.')) {
        request.uri += '/index.html';
    }

    return request;
}

Publish function.


Attach Function

Navigate:

CloudFront
→ Distribution
→ Behaviors
→ Edit

Attach function:

Viewer Request
→ rewrite-index

Save.


Phase 7 - Create ACM Certificate

CloudFront requires certificates in:

us-east-1

Navigate:

Certificate Manager
(us-east-1)
→ Request Certificate

Domain

notes.chintanspace.com

Validation

DNS Validation

Request certificate.


Phase 8 - Validate Certificate

If Route53 hosts the domain:

Create records in Route53

Wait until:

Status = Issued

Phase 9 - Configure Route53

Navigate:

Route53
→ Hosted Zone
→ chintanspace.com

Create record.

Record Name

notes

Type

A

Alias

Yes

Alias Target

Select CloudFront distribution.

Example:

de81eyupvfzho.cloudfront.net

Save.


Phase 10 - Update CloudFront Domain Settings

Navigate:

CloudFront
→ Distribution
→ General
→ Edit

Alternate Domain Name

notes.chintanspace.com

SSL Certificate

Select:

notes.chintanspace.com

SSL Support

SNI Only

Save.

Wait for deployment.


Phase 11 - Create GitHub OIDC Provider

Navigate:

IAM
→ Identity Providers
→ Add Provider

Provider Type

OpenID Connect

Provider URL

https://token.actions.githubusercontent.com

Audience

sts.amazonaws.com

Create provider.


Phase 12 - Create Deployment Role

Navigate:

IAM
→ Roles
→ Create Role

Trusted Entity

Web Identity

Provider:

token.actions.githubusercontent.com

Audience:

sts.amazonaws.com

Role Name:

GitHubNotesDeployRole

Trust Policy

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::<ACCOUNT_ID>:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:<github-user>/<repo>:*"
        }
      }
    }
  ]
}

Phase 13 - Create Deployment Permissions

Attach policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "s3:PutObject",
        "s3:DeleteObject",
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Effect": "Allow",
      "Resource": [
        "arn:aws:s3:::notes-prod-bucket",
        "arn:aws:s3:::notes-prod-bucket/*"
      ]
    },
    {
      "Action": [
        "cloudfront:CreateInvalidation"
      ],
      "Effect": "Allow",
      "Resource": "*"
    }
  ]
}

Phase 14 - Configure GitHub Secrets

Navigate:

GitHub
→ Repository
→ Settings
→ Secrets and Variables
→ Actions

Create:

Secret Value
AWS_ROLE_ARN IAM Role ARN
AWS_REGION us-east-1
NOTES_BUCKET notes-prod-bucket
NOTES_CF_DISTRIBUTION_ID CloudFront Distribution ID

Phase 15 - GitHub Actions Workflow

Create:

.github/workflows/deploy-notes.yml

Key workflow stages:

Checkout Repository

Setup Go

Setup Hugo

Setup Node.js

Install Dependencies

hugo mod tidy

hugo

Configure AWS Credentials (OIDC)

aws s3 sync public/ s3://bucket --delete

aws cloudfront create-invalidation --paths "/*"

Phase 16 - Verify Deployment

After deployment:

Verify:

https://notes.chintanspace.com

Verify pages:

https://notes.chintanspace.com/docs/aws/fargate/
https://notes.chintanspace.com/docs/hugo/setup-hugo/

Troubleshooting

Access Denied on Pages

Cause:

CloudFront → S3
cannot resolve
directory URLs

Fix:

CloudFront Function
rewrite-index

OIDC Authentication Failure

Verify:

IAM Identity Provider exists

Verify:

permissions:
  id-token: write

exists in workflow.


Unable to Locate Credentials

Occurs when:

Configure AWS Credentials
step not executed

Ensure:

uses: aws-actions/configure-aws-credentials@v4

runs before AWS CLI commands.


Hugo Build Works Locally but Fails in GitHub

Ensure Node version is modern:

node-version: '22'

Older Node versions fail with:

bad option: --permission

during PostCSS processing.


Final Architecture

GitHub Repository
        |
        v
GitHub Actions
        |
        v
Private S3 Bucket
        |
        v
CloudFront Distribution
        |
        v
CloudFront Function
(Index Rewrite)
        |
        v
ACM Certificate
        |
        v
Route53 Alias
        |
        v
notes.chintanspace.com