Introduction
As you may have noticed this site is built using Hugo and Papermod theme. After playing around with Hugo and getting comfortable with it, I wanted to push the initial version of this site to the git repository and set up an automatic deployment pipeline to be able to easily publish updates to the web.
Since I am the lucky owner of a self-hosted GitLab instance which I’ve been using for more than a decade for my projects, it was an obvious decision to push this site there too. Same story with Cloudflare - I use their DNS services, and since they also provide static content hosting services - Pages, decided to give it a try.
Fast googling revealed that Cloudflare supports automatic deployments of Hugo-based sites from GitHub and GitLab…, but only for cloud-based SaaS version.
Luckily Cloudflare has Wrangler - command-line interface to manage Worker projects. You may ask what workers have to do with static pages? Well, they are managed by the same tool, see pages command documentation of Wrangler tool.
Below I will explain how to tie Wrangler together with GitLab CI/CD to perform deployment to Cloudflare Pages. Further steps assume you have knowledge and experience of using Cloudflare services, git, Hugo and GitLab CI/CD. I will not go into details for some of the steps.
Setting and Testing Locally
Below are the steps to prepare project, repository and test everything locally:
- Create Hugo project
- Change into project directory
- Login with Wrangler CLI into your Cloudflare account:
npx wrangler login
, follow instructions - Create new Pages project, I will call mine
pages-for-article
, make sure your project git default branch matches with what you pass in--production-branch
argument. Cloudflare will use it later to determine if you are publishing toproduction
orpreview
environments:npx wrangler pages project create pages-for-article --production-branch main
- Initialize git repository, do this before deploying to Pages, as Wrangler uses git metadata for deployments
- Build Hugo project:
hugo
- Now, run deployment:
npx wrangler pages deploy public --project-name pages-for-article
- This is what you should see from Cloudflare dashboard
Workers & Pages
section, noticemain
branch andProduction
environment in the screenshot: - Add
public/
to.gitignore
- Commit and push your project to git
Now we are all set to move to the GitLab CI/CD configuration.
Setting GitLab CI/CD
Requirements
Before going into configuration details, I like to define my requirements for CI/CD pipeline:
- I want to have two environments -
production
andpreview
(staging) - I want to deploy changes from default branch, tags or merge requests only
- I want to be able to see draft content in the
preview
environment - I want to be able to deploy any changes except tags to
preview
environment - I want to be able to deploy to
production
environment changes only from default branch or tags
GitLab Project Configuration
Wrangler requires certain authentication environment variables to be present to operate from CI/CD. See Run Wrangler in CI/CD guide. Add these variables in the GitLab project under Settings
-> CI/CD
-> Variables
. Make variables masked, unprotected and non-expandable.
Make sure that the project has at least one runner with a docker execution environment under Settings
-> CI/CD
-> Runners
.
Pipeline Definition
GitLab uses .gitlab-ci.yml
to describe jobs that make up the CI/CD pipeline. I will not go into detail of every aspect of that file, but rather highlight parts that are made up to fulfill above requirements. Complete .gitlab-ci.yml
file is available at GitHub.
workflow:rules
describes the requirement to create and run a pipeline only for the default branch, merge requests commits and tags, omitting commits to branches without merge requests.
Because of the requirement to have draft content available in the preview
environment, two distinct build jobs are needed - build:staging
and build:production
, first one will build a site with drafts, second - without, see script
section. Both jobs inherit from common .build
template, which describes common build configuration - using alpine
image for runtime environment and installing hugo using apk
package manager, collecting build artifacts from public
folder.
The most interesting part of .gitlab-ci.yml
is the .deploy
template. What it does - it runs Wrangler deploy command with additional arguments - project name, branch, commit hash, and commit message. Values for the latter three arguments are taken from corresponding variables defined in the same template, which in turn reference pre-defined GitLab CI/CD variables by default.
variables:
CLOUDFLARE_BRANCH: "$CI_COMMIT_BRANCH"
CLOUDFLARE_COMMIT_HASH: "$CI_COMMIT_SHA"
CLOUDFLARE_COMMIT_MESSAGE: "$CI_COMMIT_MESSAGE"
You may ask - why is it so complex? As mentioned earlier, Wrangler can extract this information from git itself. Yes, indeed, it can, but Wrangler also makes a decision on where to deploy - production
or preview
environment based on the branch name. Default branch always goes to production, period, you can’t affect it, this basically breaks requirements 4, 5.
Luckily, Wrangler has these arguments and you can pass whatever you want making it possible to workaround behavior of always publishing default branch changes to production
. And this is what is being done in deploy:staging
and deploy:production
which inherit from the .deploy
template.
Here is a closer look at deploy:staging
variables
and rules
section:
variables:
CLOUDFLARE_BRANCH: "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME"
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
when: manual
variables:
CLOUDFLARE_BRANCH: "$CI_COMMIT_BRANCH-preview"
- if: '$CI_COMMIT_TAG'
when: never
- when: manual
- First rule makes it possible to deploy default branch changes to
preview
environment by adding-preview
postfix to the default branch name, this way Wrangler will treat it as a non-default branch. - Second rule disallows deployment of tags to
preview
, since I will use tags as release milestones for this project. - For all other cases, i.e. merge request commits - merge request source branch name will be used, as defined in
variables
section.
Please note that preview
deployments are manual actions and require launching these jobs from pipeline view in the GitLab by hand. Also for pipelines not to appear as “blocked” in GitLab, deploy:staging
has allow_failure: true
set.
Cool thing about Cloudflare Pages is that preview deployments can be accessed via <branch-name>.pages-for-article.pages.dev
address. For default branch preview for this project it is https://main-preview.pages-for-article.pages.dev.
These are deploy:production
rules:
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
when: manual
- if: '$CI_COMMIT_TAG'
when: always
variables:
CLOUDFLARE_BRANCH: "$CI_DEFAULT_BRANCH"
CLOUDFLARE_COMMIT_MESSAGE: "$CI_COMMIT_TAG - $CI_COMMIT_TAG_MESSAGE"
- when: never
- First rule allows manually deploying anything from the default branch to the
production
environment; this normally is not used, but sometimes may come in handy. - Second rule - automatically deploys to
production
environment when tag is created. Since Wrangler has no support for tags, passing actual tag name into branch name would result in tag being deployed intopreview
environment instead ofproduction
, to workaround this - default branch name is fed into branch name, and actual tag name along with tag message is set in commit message.
That’s it, these rules satisfy my requirements set above, and I can use GitLab to track and publish my blog changes to Cloudflare pages.
Further Improvements
- I am using bare
alpine
andnode
docker images which downloadhugo
andwrangler
for each job invocation. This negatively affects pipeline speed and generates excess traffic, a better approach would be to use pre-built images containing these tools. On the other hand, impact is negligible, as whole pipeline takes around 30 seconds to build and deploy. - Add a separate deployment job to the
preview
environment without draft content to make it identical to what is going to be deployed to theproduction
environment.
Conclusion
GitLab and Cloudflare are great services with lots of useful features and customization options. With some exploration and trial and error approach I was able to create CI/CD pipeline configuration that meets my requirements for publishing Hugo blog to Cloudflare Pages from a self-hosted GitLab instance.
Complete .gitlab-ci.yml
file is available on GitHub.