gitlabgitlab-ci

Reducing duplication in GitLab CI with tags per multiple environments


I have a requirement to set tags for the selection of GitLab runners where tags differ per environment. Therefore, I need the tag to be set according to the value of $CI_COMMIT_BRANCH or $CI_MERGE_REQUEST_TARGET_BRANCH_NAME depending on context.

I have the following minimal example of the only pattern I have been able get working:

# Templates for rules
.rules__is_development:
  tags:
    - eks-development
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "development"
      variables:
        TF_WORKSPACE: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME
    - if: $CI_COMMIT_BRANCH == "development"
      variables:
        TF_WORKSPACE: $CI_COMMIT_BRANCH

.rules__is_staging:
  tags:
    - eks-staging
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "staging"
      variables:
        TF_WORKSPACE: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME
    - if: $CI_COMMIT_BRANCH == "staging"
      variables:
        TF_WORKSPACE: $CI_COMMIT_BRANCH

.rules__is_development__terraform_apply:
  tags:
    - eks-development
  rules:
    - if: $CI_COMMIT_BRANCH == "development"

.rules__is_staging__terraform_apply:
  tags:
    - eks-staging
  rules:
    - if: $CI_COMMIT_BRANCH == "staging"

variables:
  ARTIFACTORY_DOCKER_PATH: artifactory.example.com/docker

stages:
  - Quality
  - Terraform

# Development Jobs
Terraform Format - Development:
  extends: .rules__is_development
  stage: Quality
  when: always
  image: $ARTIFACTORY_DOCKER_PATH/hashicorp/terraform:latest
  script: terraform fmt -recursive -check -diff

Terraform Init - Development:
  extends: .rules__is_development
  stage: Terraform
  image: $ARTIFACTORY_DOCKER_PATH/hashicorp/terraform:latest
  script: terraform init -input=false
  artifacts:
    paths:
      - .terraform

Terraform Apply - Development:
  extends: .rules__is_development__terraform_apply
  stage: Terraform
  needs:
    - job: Terraform Init - Development
      artifacts: true
  image: $ARTIFACTORY_DOCKER_PATH/hashicorp/terraform:latest
  script: terraform apply -input=false
  variables:
    TF_WORKSPACE: $CI_COMMIT_BRANCH

# Staging Jobs
Terraform Format - Staging:
  extends: .rules__is_staging
  stage: Quality
  when: always
  image: $ARTIFACTORY_DOCKER_PATH/hashicorp/terraform:latest
  script: terraform fmt -recursive -check -diff

Terraform Init - Staging:
  extends: .rules__is_staging
  stage: Terraform
  image: $ARTIFACTORY_DOCKER_PATH/hashicorp/terraform:latest
  script: terraform init -input=false
  artifacts:
    paths:
      - .terraform

Terraform Apply - Staging:
  extends: .rules__is_staging__terraform_apply
  stage: Terraform
  needs:
    - job: Terraform Init - Staging
      artifacts: true
  image: $ARTIFACTORY_DOCKER_PATH/hashicorp/terraform:latest
  script: terraform apply -input=false
  variables:
    TF_WORKSPACE: $CI_COMMIT_BRANCH

The Problem

The configuration is very 'wet' - not DRY! The job definitions (Terraform Format, Terraform Init, Terraform Apply etc) are essentially duplicated for each environment, with the only differences being:

What I've Tried

For the last 48 hours or so, just about everything I can think of using templates, conditionals, extends and includes. Ideally, I would have 3 layers of inheritance, a base template that defines the tag by environment, a second layer of templates that define each of the steps and includes the base tag template, and then a third layer that defines the pipeline itself.

In the end I found no way to do this without extreme duplication as shown above.

The question

Does anyone know of a way of refactoring the above code in order to ideally avoid, or at least reduce, the duplication?


Solution

  • I am not sure if I have missed something in your post. But GitLab runner tags can be variables. So you could for example just set workflow rules to set the variables. This means you can set your TF_WORKSPACE and runner tag once.

    see https://docs.gitlab.com/ci/runners/configure_runners/#use-cicd-variables-in-tags

    In the .gitlab-ci.yml file, use CI/CD variables with tags for dynamic runner selection

    You can then just define each job once and it will have the right values set based on the workflow rules. I have run an example of this using this structure. I cant use your exact values so I have two different types of Gitlabs online runners, I have also used the alpine image. but hopefully this illustrated the point.

    workflow:
      rules:
        - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "development"
          variables:
            TF_WORKSPACE: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME
            RUNNER_TAG: saas-linux-small-amd64 #eks-development
        - if: $CI_COMMIT_BRANCH == "development"
          variables:
            TF_WORKSPACE: $CI_COMMIT_BRANCH
            RUNNER_TAG: saas-linux-small-amd64 #eks-development
        - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "staging"
          variables:
            TF_WORKSPACE: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME
            RUNNER_TAG: saas-linux-medium-amd64 #eks-staging
        - if: $CI_COMMIT_BRANCH == "staging"
          variables:
            TF_WORKSPACE: $CI_COMMIT_BRANCH
            RUNNER_TAG: saas-linux-medium-amd64 #eks-staging
    
    default:
      image: alpine:latest #artifactory.example.com/docker/hashicorp/terraform:latest
    
    stages:
      - Quality
      - Terraform
    
    Terraform Format:
      tags:
        - $RUNNER_TAG
      stage: Quality
      when: always
      script:
      - echo "$TF_WORKSPACE"
      - echo "$RUNNER_TAG"
      - terraform fmt -recursive -check -diff
    
    Terraform Init:
      tags:
        - $RUNNER_TAG
      stage: Terraform
      script:
        - echo "$TF_WORKSPACE"
        - echo "$RUNNER_TAG"
        - terraform init -input=false
      artifacts:
        paths:
          - .terraform
    
    Terraform Apply:
      tags:
        - $RUNNER_TAG
      stage: Terraform
      needs:
        - job: Terraform Init
          artifacts: true
      script:
        - echo "$TF_WORKSPACE"
        - echo "$RUNNER_TAG"
        - terraform apply -input=false
    

    When run against the development branch

    Running with gitlab-runner 18.3.0~pre.23.gb8a899e1 (b8a899e1)
      on blue-2.saas-linux-medium-amd64.runners-manager.gitlab.com/default ZQ74L_riD, system ID: s_805fb2e5035f
    Preparing the "docker+machine" executor
    00:06
    Using Docker executor with image alpine:latest ...
    ...
    ...
    $ echo "$TF_WORKSPACE"
    staging
    $ echo "$RUNNER_TAG"
    saas-linux-medium-amd64
    

    When run against the staging branch

    Running with gitlab-runner 18.3.0~pre.23.gb8a899e1 (b8a899e1)
      on green-5.saas-linux-small-amd64.runners-manager.gitlab.com/default xS6Vzpvoq, system ID: s_6b1e4f06fcfd
    Preparing the "docker+machine" executor
    00:06
    Using Docker executor with image alpine:latest ...
    ...
    ...
    $ echo "$TF_WORKSPACE"
    development
    $ echo "$RUNNER_TAG"
    saas-linux-small-amd64
    

    You can see here that depending on the rules conditions it will run it on the related runner with the right TF_WORKSPACE.

    If i have missed the point of something feel free to let me know.