Continuing from my last post about how I setup my website, this digs specifically into multi environment roll-out with a manual gate on Gitlab CI.

Where are we at

If you followed along from my previous post about my CI/CD pipeline, you will recall that we had two repositories and, therefore, two pipelines. For the gated release, we’re going to extend the pipeline for the site layout repository and leave the content repository alone.

Here is what the layout YAML currently looks like:

stages:
  - build
  - deploy

build:
  stage: build
  image: dettmering/hugo-build
  before_script:
    - mkdir content
    - "wget -O content.tar.gz https://gitlab.com/some-user/content/-/archive/master/content-master.tar.gz?private_token=${GITLAB_PRIVATE_TOKEN}"
    - tar --strip-components=1 -zxf content.tar.gz -C content
    - rm -rf content/.git content/.gitignore content/.gitlab-ci.yml
    - rm content.tar.gz
    - rm -rf /var/cache/apk/*
  script:
    - "hugo --config staging-config.toml"
  artifacts:
    expire_in: 1 week
    paths:
      - public
  only:
    - master

deploy:
  stage: deploy
  image: alpine:3.7
  dependencies:
    - build
  before_script:
    - apk update
    - apk add git
    - apk add rsync
    - apk add openssh-client
    - mkdir "${HOME}/.ssh"
    - echo "$VPS_SSH_KEY" > "${HOME}/.ssh/id_rsa"
    - chmod 600 "${HOME}/.ssh/id_rsa"
    - eval "$(ssh-agent -s)"
    - ssh-add "${HOME}/.ssh/id_rsa"
    - echo $VPS_HOST >> "${HOME}/.ssh/known_hosts"
    - rm -rf /var/cache/apk/*
  script:
    - rsync -hrvz --delete --exclude=_ public/ some-user@192.168.1.1:/var/www/my-site/

Multiple Environments

Ok, no one really likes going directly to Production. What would be really nice is to build and automatically deploy to a test or staging environment and then gate the production build/deploy. To do this, we need another two steps in our YAML file. These steps are just copies of what exist with small tweaks.

stages:
  - build-stage
  - deploy-stage
  - build-prod
  - deploy-prod

build-stage:
  stage: build-stage
  image: dettmering/hugo-build
  before_script:
    - mkdir content
    - "wget -O content.tar.gz https://gitlab.com/some-user/content/-/archive/master/content-master.tar.gz?private_token=${GITLAB_PRIVATE_TOKEN}"
    - tar --strip-components=1 -zxf content.tar.gz -C content
    - rm -rf content/.git content/.gitignore content/.gitlab-ci.yml
    - rm content.tar.gz
    - rm -rf /var/cache/apk/*
  script:
    - "hugo --config staging-config.toml"
  artifacts:
    expire_in: 1 week
    paths:
      - public
  only:
    - master

deploy-stage:
  stage: deploy-stage
  image: alpine:3.7
  dependencies:
    - build
  before_script:
    - apk update
    - apk add git
    - apk add rsync
    - apk add openssh-client
    - mkdir "${HOME}/.ssh"
    - echo "$VPS_SSH_KEY" > "${HOME}/.ssh/id_rsa"
    - chmod 600 "${HOME}/.ssh/id_rsa"
    - eval "$(ssh-agent -s)"
    - ssh-add "${HOME}/.ssh/id_rsa"
    - echo $VPS_HOST >> "${HOME}/.ssh/known_hosts"
    - rm -rf /var/cache/apk/*
  script:
    - rsync -hrvz --delete --exclude=_ public/ some-user@192.168.1.1:/var/www/my-site-stg/

build-prod:
  stage: build-prod
  image: dettmering/hugo-build
  before_script:
    - mkdir content
    - "wget -O content.tar.gz https://gitlab.com/some-user/content/-/archive/master/content-master.tar.gz?private_token=${GITLAB_PRIVATE_TOKEN}"
    - tar --strip-components=1 -zxf content.tar.gz -C content
    - rm -rf content/.git content/.gitignore content/.gitlab-ci.yml
    - rm content.tar.gz
    - rm -rf /var/cache/apk/*
  script:
    - "hugo --config production-config.toml"
  artifacts:
    expire_in: 1 week
    paths:
      - public
  only:
    - master
  when: manual
  allow_failure: false

deploy-prod:
  stage: deploy-prod
  image: alpine:3.7
  dependencies:
    - build
  before_script:
    - apk update
    - apk add git
    - apk add rsync
    - apk add openssh-client
    - mkdir "${HOME}/.ssh"
    - echo "$VPS_SSH_KEY" > "${HOME}/.ssh/id_rsa"
    - chmod 600 "${HOME}/.ssh/id_rsa"
    - eval "$(ssh-agent -s)"
    - ssh-add "${HOME}/.ssh/id_rsa"
    - echo $VPS_HOST >> "${HOME}/.ssh/known_hosts"
    - rm -rf /var/cache/apk/*
  script:
    - rsync -hrvz --delete --exclude=_ public/ some-user@192.168.1.1:/var/www/my-site/

And that is the worst thing ever. Stages are almost exactly the same except for two lines. That is bad because it is really hard to spot.

Gating

The new lines we added are for the gating process and go at the end of the production build stage.

  # in the production build stage
  when: manual
  allow_failure: false

The manual setting makes sure the stage does not run without user input. Then the allow_failure flag is set to false so that stages after the current one do not run until this stage has completed.

That is the manual gating process.

Special features on Gitlab

Thankfully, we don’t have to continue with this duplication madness. Gitlab provides two features that will simplify reading this file: hidden keys and anchors.

Combining these features allows us to create template stages and include them in regular stages.

Build template

The important parts that shouldn’t change much are the before_script, image, and artifacts. Let’s pull those out and see what it looks like.

.build-template: &build-def
  image: dettmering/hugo-build
  before_script:
    - mkdir content
    - "wget -O content.tar.gz https://gitlab.com/some-user/content/-/archive/master/content-master.tar.gz?private_token=${GITLAB_PRIVATE_TOKEN}"
    - tar --strip-components=1 -zxf content.tar.gz -C content
    - rm -rf content/.git content/.gitignore content/.gitlab-ci.yml
    - rm content.tar.gz
    - rm -rf /var/cache/apk/*
  artifacts:
    expire_in: 1 week
    paths:
      - public

Next, we use the template in our build stages instead of being so verbose:

build-stage:
  <<: *build-def
  stage: build-stage
  script:
    - "hugo --config staging-config.toml"
  only:
    - master

build-prod:
  <<: *build-def
  stage: build-prod
  script:
    - "hugo --config production-config.toml"
  only:
    - master
  when: manual
  allow_failure: false

Bubbles, a character from the Trailer Park Boys, saying his catchphrase: 'Decent'

Deploy template

Thankfully the deploy stages look very similar so we can template them as well.

.deploy-tenplate: &deploy-def
  image: alpine:3.7
  before_script:
    - apk update
    - apk add git
    - apk add rsync
    - apk add openssh-client
    - mkdir "${HOME}/.ssh"
    - echo "$VPS_SSH_KEY" > "${HOME}/.ssh/id_rsa"
    - chmod 600 "${HOME}/.ssh/id_rsa"
    - eval "$(ssh-agent -s)"
    - ssh-add "${HOME}/.ssh/id_rsa"
    - echo $VPS_HOST >> "${HOME}/.ssh/known_hosts"
    - rm -rf /var/cache/apk/*

# now the stages
deploy-stage:
  <<: *deploy-def
  stage: deploy-stage
  dependencies:
    - build-stage
  script:
    - rsync -hrvz --delete --exclude=_ public/ some-user@192.168.1.1:/var/www/my-site-test/

deploy-prod:
  <<: *deploy-def
  stage: deploy-prod
  dependencies:
    - build-prod
  script:
    - rsync -hrvz --delete --exclude=_ public/ some-user@192.168.1.1:/var/www/my-site/

Conclusion

I am sure that was a bit of a slog to read through. To be honest, that was kind of the point. To see the final version, you can download the full file from this website. In the final part of this series, I will cover testing and pre-processing of any files.