A complete step‑by‑step tutorial on automating Hugo deployments to AWS S3 and CloudFront using GitHub Actions. No more manual uploads — just push your changes and let CI/CD handle the rest.

How to Auto‑Deploy a Hugo Blog to AWS Using GitHub Actions (Step‑by‑Step Guide)

In the previous blog posts we did the following:

  1. Created a blog post with Hugo
  2. Deployed the Hugo blog to AWS

For background, see:

Every time we add a new blog post or make configuration changes, we need to manually build the website and run a script (or the commands manually) to deploy the application to AWS.

This works, but it’s not ideal. We want deployments to happen automatically whenever we push new content to GitHub.

In this post, we’ll set up a CI/CD pipeline using GitHub Actions that:

  • Builds the Hugo site
  • Uploads it to S3
  • Invalidates the CloudFront cache
  • Makes your blog live within seconds

And we’ll do it step‑by‑step, with no missing pieces.


Prerequisites

Before starting, make sure:

  1. Your Hugo blog lives in a GitHub repository
  2. Your blog is already hosted on:
    • S3 (static website hosting)
    • CloudFront (CDN in front of S3)

If you followed my previous posts, you already have this.


Creating the IAM User (Step‑by‑Step)

We need an IAM user that GitHub Actions can use to deploy your site.
This user should have minimal permissions.

1. Create the IAM user

In the AWS Console:

  1. Open the AWS Console
  2. Search for IAM
  3. In the left menu, click Users
  4. Click Create user
  5. Enter a name:
    hugo-deploy-user
  6. Click Next
  7. Skip adding the user to a group
  8. Click Next again
  9. Click Create user

2. Create access keys

  1. Click the user you just created
  2. Go to the Security credentials tab
  3. Scroll to Access keys
  4. Click Create access key
  5. Choose Command Line Interface (CLI)
  6. Confirm and continue
  7. Copy:
    • Access Key ID
    • Secret Access Key

You will need these for GitHub.


AWS Best Practices (Why We Do It This Way)

🔐 Principle of Least Privilege

We only give the IAM user the permissions it absolutely needs:

  • Upload files to one S3 bucket
  • Invalidate one CloudFront distribution

This protects your AWS account.

🪪 Separate IAM users for automation

Never use your personal IAM user for CI/CD.
Automation users should be isolated and revocable.

🧹 Scoped CloudFront permissions

We explicitly restrict invalidations to a single distribution.

🪣 Avoid wildcard S3 permissions

We do NOT allow access to all buckets — only the one hosting your blog.

🔑 Rotate access keys

GitHub Actions supports multiple keys, so rotate them periodically.


IAM Policies (Copy/Paste Ready)

1. S3 Upload Policy

Replace YOUR_BUCKET_NAME:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowS3Upload",
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:DeleteObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::YOUR_BUCKET_NAME",
        "arn:aws:s3:::YOUR_BUCKET_NAME/*"
      ]
    }
  ]
}

2. CloudFront Invalidation Policy

Replace YOUR_AWS_ACCOUNT_ID and YOUR_DISTRIBUTION_ID:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowSpecificInvalidation",
      "Effect": "Allow",
      "Action": "cloudfront:CreateInvalidation",
      "Resource": "arn:aws:cloudfront::YOUR_AWS_ACCOUNT_ID:distribution/YOUR_DISTRIBUTION_ID"
    }
  ]
}

Where to attach these policies

  1. Go to IAM → Users
  2. Click your user
  3. Go to Permissions
  4. Click Add permissions
  5. Choose Attach policies directly
  6. Search for you created policies and check them
  7. Save and attach it

Adding AWS Credentials to GitHub (Step‑by‑Step)

This is where many tutorials skip steps — so here’s the full walkthrough.

1. Open your GitHub repository

Go to:

https://github.com/<your-username>/<your-repo>

2. Open the repository settings

Top menu → Settings

3. Open Secrets

Left menu → Secrets and variablesActions

4. Add each secret

Click New repository secret for each:

AWS_ACCESS_KEY_ID → Your IAM access key
AWS_SECRET_ACCESS_KEY → Your IAM secret key
AWS_REGION → e.g. eu-west-1
AWS_S3_BUCKET → Your bucket name
AWS_CLOUDFRONT_DISTRIBUTION_ID → Your distribution ID

Where to find the CloudFront distribution ID

AWS Console → CloudFrontDistributions
Look in the ID column.


GitHub Actions Workflow Explained

GitHub Actions uses a special folder:

.github/workflows/

GitHub automatically scans this folder for .yml files and runs them.
This is why the file must be placed there.

Create the workflow file

Create:

.github/workflows/deploy.yml

Full workflow with comments

name: Deploy Hugo site   # Name of the workflow

on:
  push:
    branches: [ "main" ]   # Run this workflow whenever we push to main

jobs:
  deploy:
    runs-on: ubuntu-latest   # GitHub provides a Linux VM for the job

    steps:
      # Step 1: Download your repository code into the VM
      - name: Checkout code
        uses: actions/checkout@v4

      # Step 2: Install Hugo so we can build the site
      - name: Install Hugo
        uses: peaceiris/actions-hugo@v3
        with:
          hugo-version: 'latest'

      # Step 3: Build the Hugo site (output goes to /public)
      - name: Build site
        run: hugo --minify

      # Step 4: Configure AWS credentials so the VM can access AWS
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

      # Step 5: Upload the built site to S3
      - name: Sync to S3
        run: aws s3 sync public/ s3://${{ secrets.AWS_S3_BUCKET }} --delete

      # Step 6: Invalidate CloudFront cache so changes go live immediately
      - name: Invalidate CloudFront cache
        run: |
          aws cloudfront create-invalidation             --distribution-id ${{ secrets.AWS_CLOUDFRONT_DISTRIBUTION_ID }}             --paths "/*"

Deployment Flow Diagram

                ┌──────────────────────┐
                │   GitHub Repository  │
                └──────────┬───────────┘
                           │ Push to main
                           ▼
                ┌──────────────────────┐
                │   GitHub Actions     │
                │  (Build + Deploy)    │
                └──────────┬───────────┘
                           │ Builds Hugo site
                           ▼
                ┌──────────────────────┐
                │        S3 Bucket     │
                │   (Static Website)   │
                └──────────┬───────────┘
                           │ Upload files
                           ▼
                ┌──────────────────────┐
                │     CloudFront       │
                │ (Global CDN Cache)   │
                └──────────┬───────────┘
                           │ Invalidate cache
                           ▼
                ┌──────────────────────┐
                │     Updated Blog     │
                │     robzah.com       │
                └──────────────────────┘

Explanation of each section

name: This is just a label. It appears in the Github Actions UI.

on: Defines when the workflow runs. In this example it runs everytime it is pushed to the main branch. So when merging from your feature branch to the main branch it will run. Unless when you push directly to main, it will directly run.

jobs: A workflow can have multiple jobs. In this example we only have one (deploy), but you can think of having different jobs, like “tests”.

runs-on: Defines the virtual machine type. Github provides multiple different, like:

  • ubuntu-latest
  • windows-latest
  • macos-latest

(ChatGPT tells me that Ubuntu is fastest and cheapest, I did not check this, so take this with a grain of salt).

steps: Each step runs in order

actions/checkout Downloads your code into the VM.

peaceiris/actions-hugo Installs Hugo.

hugo –minify Builds your site into the public/ folder.

aws-actions/configure-aws-credentials Logs into AWS using your GitHub secrets.

aws s3 sync Uploads your site to S3.

aws cloudfront create-invalidation Clears the CDN cache so your new blog post appears instantly.


Final Thoughts

With this setup:

  • You write a blog post
  • Commit and push
  • GitHub builds and deploys automatically

No manual S3 uploads.
No CloudFront clicking.
No forgetting to rebuild the site.

Just clean, automated deployments — exactly how a Hugo blog should work.