Overview

To automate deployments of my Hugo-based blog to AWS, I built a CI/CD pipeline using GitHub Actions and AWS IAM Roles with OIDC (OpenID Connect) for secure, short-lived credentials.

Here's a step-by-step breakdown of the setup:

Step 1: Set Up GitHub Actions to Build Your Hugo Blog

  1. Create a GitHub Actions workflow in .github/workflows/deploy.yml.

2. Use the peaceiris/actions-hugo GitHub Action to install Hugo and build the blog, or you can build your own yaml file.

name: Build and Deploy Hugo Blog

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repo
        uses: actions/checkout@v3

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v2
        with:
          hugo-version: '0.111.3' # or your preferred version

      - name: Build Hugo site
        run: hugo --minify

Step 2: Set Up OIDC Between GitHub and AWS

  1. In your development AWS account, create a custom IAM Identity Provider for GitHub:

2. This integration allows GitHub to assume IAM roles without storing long-term AWS credentials.

Step 3: Create a Least-Privilege IAM Policy

Create a policy (e.g., GitHubActions) with permissions restricted to the target S3 bucket:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject", "s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::your-blog-bucket",
        "arn:aws:s3:::your-blog-bucket/*"
      ]
    }
  ]
}

Step 4: Create an IAM Role with Web Identity Trust

  1. Create a new IAM Role with Web Identity as the trusted entity type.
  2. Trust the GitHub OIDC Identity Provider you just set up.
  3. Add a trust policy that allows only your specific GitHub repository to assume the role:
{
  "Effect": "Allow",
  "Principal": {
    "Federated": "arn:aws:iam::<your-account-id>:oidc-provider/token.actions.githubusercontent.com"
  },
  "Action": "sts:AssumeRoleWithWebIdentity",
  "Condition": {
    "StringEquals": {
      "token.actions.githubusercontent.com:sub": "repo:your-username/your-repo-name:ref:refs/heads/main"
    }
  }
}

4. Attach the GitHubActions policy to this role.

Step 5: Store the Role ARN in GitHub Secrets

  • In your GitHub repository, go to Settings > Secrets and variables > Actions
  • Add a new secret:
  • AWS_ROLE_ARN: with the ARN of your IAM role

Step 6: Update GitHub Actions Workflow to Assume the IAM Role

Add steps to your workflow to assume the role using aws-actions/configure-aws-credentials:

      - name: Configure AWS credentials from IAM Role
        uses: aws-actions/configure-aws-credentials@v2
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: us-east-1

Step 7: Deploy to S3 and Invalidate CloudFront Cache

Add deployment and cache invalidation steps:

      - name: Sync public/ to S3
        run: aws s3 sync public/ s3://your-blog-bucket --delete

      - name: Invalidate CloudFront cache
        run: |
          aws cloudfront create-invalidation \
            --distribution-id YOUR_DISTRIBUTION_ID \
            --paths "/*"

Step 8: Grant CloudFront Invalidation Permissions

Update your IAM Policy (GitHubActions) to include:

{
  "Effect": "Allow",
  "Action": "cloudfront:CreateInvalidation",
  "Resource": "arn:aws:cloudfront::your-account-id:distribution/YOUR_DISTRIBUTION_ID"
}

Done! CI/CD Complete 🎉

Every time a change is pushed to the main branch:

  • Hugo builds the blog
  • GitHub Actions assumes the AWS role via OIDC
  • The public/ folder is synced to S3
  • CloudFront cache is invalidated so visitors see the latest content instantly

💡 Key Takeaways

  • 🔐 OIDC means no more long-term AWS keys in GitHub
  • 📦 Using Hugo with GitHub Actions is lightweight and fast
  • 🚀 CloudFront + S3 = a scalable, serverless blog architecture