7 minutes
Continuous Integration and Continuous Deployment for my blog
Good evening everyone, tonight I want to talk about building a Continuous Integration and Continuous Deployment pipeline for my blog. Okay, this feels way too overkill but as I was “strategizing” on how I would migrate my blog into a new AWS account (which I have already done a few hours ago! more on that on my next blog post!) I keep having this feeling of disgust towards myself if I didn’t implement a CI/CD pipeline for my blog and also a voice from the back of my head kept screaming “DO IT FOR THE CONTENT!” so I guess I just went and did it. Before I proceed I want to say that I use Hugo to build my static blog, it is a framework for building static websites, it is open source and written in Go (I stumbled across this framework during one of my late nights learning the Go language and I am still learning).
I decided to use Hugo because it’s so easy to use, I didn’t have to use Go, HTML, js, CSS, or any language to build the website (well I did have to do a bit of those to add asciinema support), the framework handles all of that with a few short and easy commands. All I needed to do was pick my starter theme and write content. Writing content is very easy as long as one knows how to write in Markdown. The most amazing thing is I can build and deploy my static blog very easily to my hosting platform of choice and fortunately, Hugo has out-of-the-box support for deploying to S3 and for invalidating Cloudfront cache.
See how easy it is to build and deploy using Hugo? Now let’s put all of that into a CI/CD pipeline. The first thing I did was I generated AWS access and secret keys and uploaded them into my Github account’s Secrets storage (don’t worry it’s all encrypted), this ensures that my Github Actions workflows/pipelines will be able to access my AWS account in a restricted manner, in this case, the pipeline will need to run hugo deploy
and this command needs those secrets to deploy to my S3 bucket in AWS.

The next thing I did is to create a workflow directory inside my blog’s code directory, any yaml
files I drop in here will be automatically read by Github Workflows.
mkdir .github/workflows
Continuous Integration
I then created a ci.yml
file inside .github/workflows
, this will contain instructions that will automate my Continuous Integration workflow. I wanted Continuous Integration and Continuous Deployment to be separate workflows so I put them in separate yaml
files. Take note that workflow filenames are arbitrary.
touch ./github/workflows/ci.yml
I wanted my Continuous Integration to only trigger when changes are introduced to any branch except main
and when a pull request
to main
is opened
or edited
. Since Hugo’s build command already does a lot of checks it already serves as a good way to look for errors.
Of course, even if Hugo’s build passes it could potentially render botched static content, broken links, and images so at the very end of the CI workflow I run htmlproofer. Below is the full content of my ci.yml
and does what I just described in this section.
name: Continuous Integration
# Controls when the workflow will run
on:
# Triggers the workflow on push to any branch except main
push:
branches: [ "**", "!main" ]
# Triggers the workflow when a pull request to main is opened or edited
pull_request:
branches: [ "main" ]
types: [ opened, edited ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
# Setup Hugo
- name: Setup Hugo
# You may pin to the exact commit or the version.
# uses: peaceiris/actions-hugo@2e89aa66d0093e4cd14751b3028fc1a179452c2e
uses: peaceiris/actions-hugo@v2.4.13
with:
# The Hugo version to download (if necessary) and use. Example: 0.58.2
hugo-version: latest
# Download (if necessary) and use Hugo extended version. Example: true
extended: false
# Hugo build
- name: Hugo build
run: |
echo "Build website"
hugo
- name: Proof HTML
# You may pin to the exact commit or the version.
# uses: anishathalye/proof-html@47a787591515a207d6fc8ef13e016ac42cb877c8
uses: anishathalye/proof-html@v1.1.0
with:
# The directory to scan
directory: public/
# Check whether external anchors exist
check_external_hash: true
# Validate HTML
check_html: true
# Enforce that images use HTTPS
check_img_http: true
# Check images and URLs in Open Graph metadata
check_opengraph: true
# Check whether favicons are valid
check_favicon: true
# Allow images with empty alt tags
empty_alt_ignore: false
# Require that links use HTTPS
enforce_https: true
# JSON-encoded map of domains to authorization tokens
# tokens: # optional
# Maximum number of concurrent requests
max_concurrency: 50
# HTTP connection timeout
connect_timeout: 30
# HTTP request timeout
timeout: 120
# Newline-separated list of URLs to ignore
url_ignore: |
https://www.linkedin.com/in/jrpospos
# Newline-separated list of URL regexes to ignore
#url_ignore_re: # optional
This ensures that the CI workflow will run every time and I will be able to catch any failures before deploying my blog. I will add more tests as I go along.
Continuous Delivery
Great! Now that I have a Continuous Integration workflow running nicely, I now need a Continuous Deployment workflow that will deploy my blog to S3 only if a pull request to main
is closed. This part is relatively simpler because I just need to tell the workflow to do a fresh build and deploy. I first created the cd.yml
file inside .github/workflows
which will contain the instructions to deploy my blog to S3 and invalidate CloudFront cache.
touch ./github/workflows/cd.yml
The only thing to take note of here is I use an AWS published Github action from the marketplace to inject my AWS secrets (the secrets I uploaded to Github earlier) into my workflow. Below is the full content of my cd.yml
and does what I just described in this section.
name: Continuous Deployment
# Controls when the workflow will run
on:
# Triggers the workflow when a pull request to main is closed
pull_request:
branches: [ "main" ]
types: [ closed ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- name: Configure AWS Credentials
# You may pin to the exact commit or the version.
# uses: aws-actions/configure-aws-credentials@0d9a5be0dceea74e09396820e1e522ba4a110d2f
uses: aws-actions/configure-aws-credentials@v1
with:
# AWS Access Key ID. This input is required if running in the GitHub hosted environment. It is optional if running in a self-hosted environment that already has AWS credentials, for example on an EC2 instance.
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
# AWS Secret Access Key. This input is required if running in the GitHub hosted environment. It is optional if running in a self-hosted environment that already has AWS credentials, for example on an EC2 instance.
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
# AWS Region, e.g. us-east-2
aws-region: ap-southeast-2
# Setup Hugo
- name: Setup Hugo
# You may pin to the exact commit or the version.
# uses: peaceiris/actions-hugo@2e89aa66d0093e4cd14751b3028fc1a179452c2e
uses: peaceiris/actions-hugo@v2.4.13
with:
# The Hugo version to download (if necessary) and use. Example: 0.58.2
hugo-version: latest
# Download (if necessary) and use Hugo extended version. Example: true
extended: false
# Hugo build
- name: Hugo build
run: |
echo "Build website"
hugo
# Hugo deploy
- name: Hugo deploy
run: |
echo "Deploy website"
hugo deploy
With this in place, every time I close a pull request this CD workflow will trigger and it will just automatically deploy and invalidate cache as long as the pull request has a passing CI. In my next post, I will share how I got this blog up and running in AWS in less than an hour like the technologies used, the architecture, and design thought behind it (now that I’ve finished the migration to the new AWS account and that this is now out of the way).