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.

i swear it is encrypted!

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).