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