This is the third and final post of the Create and deploy you Blog with Hugo, GitLab and AWS series.

This post will focus on setting up a CICD pipeline and automating deployments to the AWS infrastructure we created in the previous post.

If you want to read through the AWS setup, read the second post in this series.

Tech Stack

  • Hugo for our SSG
  • GitLab to host our code
  • GitLab CI/CD for packaging and releasing
  • AWS S3 to host our static site
  • Cloudfront as our CDN

GitLab Project

In this series’s first post, we created a project on GitLab. We will use that same repository in this post. If you haven’t made one, this is the time to take care of it.

AWS IAM Policy

We need to create an AWS user with proper permissions for our deployments.

Head over to your AWS web console; from the services list, choose IAM. In the left sidebar, select policies and look for a button to create one. The screen defaults to a UI form to build the policy, but we’ll be using JSON for this, so switch over to the relevant tab and copy the following policy:

    "Version": "2012-10-17",
    "Statement": [
            "Sid": "ListBuckets",
            "Effect": "Allow",
            "Action": "s3:ListBucket",
            "Resource": ""
            "Sid": "CreateInvalidation",
            "Effect": "Allow",
            "Action": "cloudfront:CreateInvalidation",
            "Resource": "arn:aws:cloudfront::<account-id>:distribution/<distribution-id>"
            "Sid": "ReadObjects",
            "Effect": "Allow",
            "Action": "s3:*Object",
            "Resource": "*"

Remember to substitute the ARNs with your bucket’s ARN. For the CreateDistribution statement you need to provide:

  • A distribution id, find this in CloudFront distribution screen
  • The account id which you can find in any ARN or in your username’s dropdown (no dashes) Save the policy and give a name such as gitlab-s3-deploy in the review screen.


In the same IAM service, head over to Users and create a new one with the Access Key - Programmatic access option selected. In the Permission screen, choose `Attach existing policies directly. Search for the policy we created and select it. Now review and save. Make sure to store your access and secret key.


Create a .gitlab-ci.yml file in the root directory on your local repository.

    - build
    - deploy


  - public

    stage: build
    - master
    - git submodule sync --recursive
    - git submodule update --init --recursive
    - hugo

    stage: deploy
    - master
    - aws s3 sync public $BUCKET
    - aws cloudfront create-invalidation --distribution-id $DISTRIBUTION --paths /

In your GitLab project, head over to Settings, CICD, and expand the Variables section.

Add your AWS access and secret keys as well as your bucket address and region. Make sure the variables are protected and masked. The AWS variables follow a naming convention: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and, AWS_DEFAULT_REGION. You will also need to include your distribution id, you can find this on the CloudFront distributions page.

GitLab Variables

Some notes on the two images we are using:

  • I’ve created a Hugo image which is easier to maintain. It’s an update on other images available, changing the version is can be done through the .gitlab-ci.yml file. The repo can be found here.
  • GitLab maintains an AWS image which pulls the AWS CICD variables from our settings by default.

The AWS script syncs the bucket with your latest changes but this won’t update your site until the cache expires. The create-invalidation call will request an update to the cache although it does take a few minutes to complete.

Now commit the changes and push them to GitLab, triggering the CICD pipeline. You can view the pipeline status in the CICD > pipelines screen. Any errors you might encounter are also available in each job’s log.


I like showing a badge of my pipeline status on the project’s main page. It’s a quick way to view if my repo is up to date with my published site. This is a quick change, head over to Settings > General and expand the Badges section.

For name you can use Pipeline Status, Build, Pipeline, etc. Use the following for the link{project_path}/-/commits/%{default_branch} and for the image URL use{project_path}/badges/%{default_branch}/pipeline.svg.

You can view the badge on the main page right after saving the changes.

Final Thoughts

This post ties together the previous two posts in the series. We can also see how having this ecosystem of tools improves our development experience. Don’t underestimate the value of automation even if you’re publishing once a month; maintaining the cognitive load of the deployment procedure can catch up with you quite fast.

This was the final step in our series; the goal was to create a blog, set up proper infrastructure to host it, and have an automated, easy-to-use process to deploy changes. The initial effort might require some time investment, but it is a scalable, secure and maintainable component and workflow which we’ve set up.