Continuous deployment (CD) is all the rage these days. And, if you ask me, it’s a good feeling to push to a branch, wait for some time, and eventually see your changes in production. Getting this to work within the confines of Gitlab CI can be a little tricky, so I’m going to share my preferred set-up in this post.
I’ll be talking about the following scenario:
The reason I would always include an intermediate environment (e.g. “staging”), is that I would need somewhere to run my automated acceptance tests against, which try to make sure that the changes made do not brake any existing functionality. So in reality, the pipeline would look a little more like this:
Looking at the part of the pipeline after the unit test stage (blue) completes, it looks the same for both staging and production. First we want to deploy to the stage, then run our acceptance tests against said stage.
As far as I’m aware, for Gitlab pipeline definitions, we have three ways to handle this duplication:
I won’t go into detail about any of them, but I’ve found (3) to be the one that I prefer. It allows me to (mostly) separate the “deployment / acceptance test” logic into its own building block, making it easy to, for example, add new stages without having to copy and paste boilerplate.
Okay, so let’s look at some
.gitlab-ci.yml (full source available here). It starts out simple enough:
stages: - "Analyze/Test" - "Deploy Staging" - "Deploy Production" workflow: # For jobs which contain `rules`, removing this causes strange behaviour # when merge requests are created. In that scenario, additional detached # pipeline runs are started. # See also: https://gitlab.com/gitlab-org/gitlab/issues/34756#note_282820625 rules: - if: $CI_COMMIT_BRANCH test: stage: "Analyze/Test" script: - echo "Running test suite ..."
Not many surprises here, except for the
workflow workaround (which will hopefully be fixed in the future). In this file you would add all stages that are independent of deployment, that is, all the checks that you wouldn’t want to re-run for each stage you’re deploying to. This would likely include your unit tests, integration tests, format checks, etc.
In addition, this is where we trigger the child pipelines, which will deploy to the individual environments:
deploy_staging: stage: "Deploy Staging" rules: - if: '$CI_COMMIT_BRANCH == "main"' - when: never trigger: include: .gitlab-ci/deploy-environment.yml strategy: depend variables: ENVIRONMENT_NAME: staging
rules section we define that the deployment should only be run when updating the
main branch. In
variables, we pass whatever information is needed for the deployment, which in this example case is only the name. Finally, the
trigger starts the child pipeline, in a way that the current (parent) pipeline will
depend on the result.
strategy: dependis omitted, the child pipeline would be started, but the parent would not wait for its completion. As a result, deployment to all environments would happen concurrently.
Finally, the child pipeline we’re triggering in the example above will be as simple or complex as your deployment / acceptance testing logic:
# Contents of `.gitlab-ci/deploy-environment.yml` stages: - deploy - acceptance deploy_static_environment: stage: deploy environment: name: "$ENVIRONMENT_NAME" variables: WORKSPACE: $CI_ENVIRONMENT_NAME rules: - if: '$CI_COMMIT_BRANCH == "main"' - when: never script: - echo "Deploying $ENVIRONMENT_NAME" acceptance_tests_branch: stage: acceptance script: - echo "Deploying $ENVIRONMENT_NAME"
Because we’re triggering the child pipeline with
strategy: depend, the parent will not continue to run until the child has completed successfully.
You may think that this is a lot of effort for a relatively simple pipeline. And, if we’re just looking at the example I wrote, I would agree. However, CI/CD pipelines tend to grow more complex over time, and, in my experience, a 1000+ line
.gitlab-ci.yml file tends to be a bit cumbersome to understand, and especially to extend.
The ISTQB defines them as:
Formal testing with respect to user needs, requirements, and business processes conducted to determine whether a system satisfies the acceptance criteria and […] to determine whether to accept the system.