Post

Feature branch deployments with Flux and GitLab CI

Flux has no inbuilt functionality at the moment to deploy feature branches/previews from a GitLab merge request but it can be “easily” managed with already existing options.

The workflow will be the following:

  • Creating a merge request in GitLab.
  • CI pipeline starts and a job will commit to a gitops repository the flux configuration for the feature branch and a kustomization.yml.
  • Flux deploys the feature branch.
  • After merging, a GitLab CI stop job for the environment will delete files again.
  • Flux will remove the resources.

Please note that this is not a step-by-step guide covering all aspects and assumes already some knowledge in these topics.

Overview

In this example I’ll deploy an application silly-demo which is also the name of the GitLab project and holds the source of the app itself.

A second GitLab project called gitops-config has the Flux resources which is managed by the developer team.

Flux repository structure

I’ll use the Repo per team approach described in the Flux documentation examples with Kustomize and one environment/cluster, development, where feature branches will be deployed.

The GitOps repository managed by the team will look like this:

1
2
3
4
5
6
7
8
9
10
11
12
├── apps
│   ├── base
│   │   ├── silly-demo
│   │   │   ├── deployment.yml # <!-- k8s deployment
│   │   │   └── kustomization.yml
│   ├── development
│   │   ├── silly-demo
│   │   │   ├── main # <!-- default branch
│   │   │   │   └── kustomization.yml
├── gitops-config
│   ├── development
│   │   ├── silly-demo.yml # <!-- Flux Kustomization of "silly-demo"

GitLab CI

The CI pipeline with these jobs will be run in the application repository which holds the source code of the app itself.

Create Flux resouces for feature branch

Let’s assume a pipeline will start when a merge request is created and we pushed following branch -> feature/a-new-test.

First it will create a Flux Kustomization resource with the name of silly-demo-feature-a-new-test and then an image policy which will apply in our case for something like this: feature-a-new-test-cd7954c2-1 (I’ve omitted the building image part).

The last part creates a Kustomization file with the kustomization.yml from the main folder as resources, a nameSuffix and the images part for the image policy setter.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
create_flux_sources:
  image:
    name: fluxcd/flux-cli:v2.1.2
    entrypoint: [""]
  script:
    ### flux - kustomize
    - flux create kustomization ${CI_PROJECT_NAME}-${CI_COMMIT_REF_SLUG}
      --source GitRepository/gitops-config --wait true
      --path "./apps/development/${CI_PROJECT_NAME}/${CI_COMMIT_REF_SLUG}" --health-check-timeout 5m --prune true
      --interval 10m
      --export > ${CI_PROJECT_NAME}-${CI_COMMIT_REF_SLUG}.yml
    ### flux - image policy
    - flux create image policy ${CI_PROJECT_NAME}-${CI_COMMIT_REF_SLUG} --image-ref=${CI_PROJECT_NAME} --select-numeric asc
      --filter-regex "^${CI_COMMIT_REF_SLUG}-[a-fA-F0-9]+-(?P<CI_PIPELINE_IID>\d+)$"
      --filter-extract '$CI_PIPELINE_IID' --export >> ${CI_PROJECT_NAME}-${CI_COMMIT_REF_SLUG}.yml
    ### kustomization
    - |
      cat <<EOF > ${CI_PROJECT_NAME}-${CI_COMMIT_REF_SLUG}-kustomization.yml
      apiVersion: kustomize.config.k8s.io/v1beta1
      kind: Kustomization
      resources:
      - ../main
      nameSuffix: -${CI_COMMIT_REF_SLUG}
      images:
      - name: repo.example.com/${CI_PROJECT_NAME}
        newTag: "${CI_COMMIT_REF_SLUG}-${CI_COMMIT_SHORT_SHA}-${CI_PIPELINE_IID}" # {"\$imagepolicy": "flux-system:${CI_PROJECT_NAME}-${CI_COMMIT_REF_SLUG}:tag"}
  artifacts:
    paths:
      - ${CI_PROJECT_NAME}-${CI_COMMIT_REF_SLUG}.yml
      - ${CI_PROJECT_NAME}-${CI_COMMIT_REF_SLUG}-kustomization.yml

Second job commits the resources via the GitLab API to the Teams Flux repository (gitops-config).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
commit_flux_resources:
  image:
    name: curlimages/curl:8.5.0
    entrypoint: [""]
  script:
    - 'curl --fail-with-body -X POST -H "PRIVATE-TOKEN: ${FLUX_TOKEN_API}"
      --form "branch=main"
      --form "commit_message=preview - add ${CI_PROJECT_NAME} feature branch ${CI_COMMIT_REF_NAME}"
      --form "actions[][action]=create"
      --form "actions[][file_path]=gitops-config/development/${CI_PROJECT_NAME}-${CI_COMMIT_REF_SLUG}.yml"
      --form "actions[][content]=<${CI_PROJECT_NAME}-${CI_COMMIT_REF_SLUG}.yml"
      --form "actions[][action]=create"
      --form "actions[][file_path]=apps/development/${CI_PROJECT_NAME}/${CI_COMMIT_REF_SLUG}/kustomization.yml"
      --form "actions[][content]=<${CI_PROJECT_NAME}-${CI_COMMIT_REF_SLUG}-kustomization.yml"
      "https://gitlab.com/api/v4/projects/${GITOPS_PROJECT_ID}/repository/commits"'
  needs:
    - job: create_flux_sources
      artifacts: true
  environment:
    name: development/${CI_COMMIT_REF_SLUG}
    action: prepare

After committing these files the tree in the Teams Flux repository looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
├── apps
│   ├── base
│   │   ├── silly-demo
│   │   │   ├── deployment.yml
│   │   │   └── kustomization.yml
│   ├── development
│   │   ├── silly-demo
│   │   │   ├── main
│   │   │   │   └── kustomization.yml
│   │   │   └── feature-a-new-test # <!--- feature branch
│   │   │       └── kustomization.yml
├── gitops-config
│   ├── development
│   │   ├── silly-demo.yml
│   │   └── silly-demo-feature-a-new-test.yml # <!--- Flux kustomization and image policy

Flux will pick up the new resources from silly-demo-feature-a-new-test.yml and reconciles them on the cluster.

After that we’ll have 2 Kubernetes deployments running, silly-demo from the main branch and silly-demo-feature-a-new-test 🚀🚀.

Merge feature branch and delete Flux resources

To remove the Flux resources again after the merge request has been merged we can use a stop job for the environment in GitLab CI.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
stop_preview:
  image:
    name: curlimages/curl:8.5.0
    entrypoint: [""]
  script:
    - 'curl --fail-with-body -X POST -H "PRIVATE-TOKEN: ${FLUX_TOKEN_API}"
      --form "branch=main"
      --form "commit_message=preview - deleting ${CI_PROJECT_NAME} feature branch ${CI_COMMIT_REF_NAME}"
      --form "actions[][action]=delete"
      --form "actions[][file_path]=gitops-config/development/${CI_PROJECT_NAME}-${CI_COMMIT_REF_SLUG}.yml"
      --form "actions[][action]=delete"
      --form "actions[][file_path]=apps/development/${CI_PROJECT_NAME}/${CI_COMMIT_REF_SLUG}/kustomization.yml"
      "https://gitlab.com/api/v4/projects/${GITOPS_PROJECT_ID}/repository/commits"'
  environment:
    name: development/${CI_COMMIT_REF_SLUG}
    action: stop
  dependencies: []
  when: manual
  variables:
      GIT_STRATEGY: none

Flux will notice the absence of the resources and removes them from the cluster with next reconciliation.

This post is licensed under CC BY 4.0 by the author.