Skip to content

Continuous deployment with Kustomizer and Flux

This guide shows you how to continuously deploy applications to Kubernetes clusters with Flux using OCI artifacts produced by Kustomizer.

This guide offers a better alternative to deploying applications with GitHub CI (as showcased in the Deploy from Git guide). Instead of connecting to each Kubernetes cluster from GitHub Actions, we'll use CI for pushing OCI artifacts to a container registry, and from there, the Kubernetes clusters (running Flux) will drive the app deployment themselves. One major advantage to this approach, is that you no longer have to deal with securing the access from CI to your production systems.

Before you begin

  • Install Kustomizer and the Flux CLI.
  • Have a Kubernetes cluster version 1.20 or newer.
  • Have a GitHub account.

Install with Homebrew

brew install stefanprodan/tap/kustomizer fluxcd/tap/flux

Login to GitHub Container Registry

Export you GitHub username:

export GITHUB_USER="YOUR-GITHUB-USERNAME"

Generate a personal access token (PAT) with read and write access to GitHub Container Registry.

export GITHUB_TOKEN="YOUR-GITHUB-PAT"

Use the token to sign in to the container registry service at ghcr.io:

$ echo $GITHUB_TOKEN | docker login ghcr.io -u ${GITHUB_USER} --password-stdin
> Login Succeeded

Other container registries

Besides GHCR, both Kustomizer and Flux are compatible with Docker Hub, ACR, ECR, GCR, self-hosted Docker Registry v2 and any other registry that conforms to the Open Container Initiative.

Clone the demo app repository

Clone the Kustomizer Git repository locally:

git clone https://github.com/stefanprodan/kustomizer
cd kustomizer

You'll be using a sample web application composed of two podinfo instances called frontend and backend, and a redis instance called cache. The web application's Kubernetes configuration is located at ./examples/demo-app.

Publish the app manifests

Export the repository URL and app version:

export APP_REPO="ghcr.io/${GITHUB_USER}/kustomizer-demo-app"
export APP_VERSION="1.0.0"

Build and push the app manifests to GitHub container registry:

$ kustomizer push artifact oci://${APP_REPO}:${APP_VERSION} \
    -k ./examples/demo-app \
    --source="$(git config --get remote.origin.url)" \
    --revision="$(git branch --show-current)/$(git rev-parse HEAD)"
building manifests...
Namespace/kustomizer-demo-app
ConfigMap/kustomizer-demo-app/redis-config-bd2fcfgt6k
Service/kustomizer-demo-app/backend
Service/kustomizer-demo-app/cache
Service/kustomizer-demo-app/frontend
Deployment/kustomizer-demo-app/backend
Deployment/kustomizer-demo-app/cache
Deployment/kustomizer-demo-app/frontend
HorizontalPodAutoscaler/kustomizer-demo-app/backend
HorizontalPodAutoscaler/kustomizer-demo-app/frontend
pushing image ghcr.io/stefanprodan/kustomizer-demo-app:1.0.0
published digest ghcr.io/stefanprodan/kustomizer-demo-app@sha256:91d2bd8e0f1620e17e9d4c308ab87903644a952969d8ff52b601be0bffdca096

Tag the config image as latest:

kustomizer tag artifact oci://${APP_REPO}:${APP_VERSION} latest

Configure Flux to deploy the app

First install Flux on your cluster with:

flux install

GitOps

For Flux to manage your cluster in a GitOps manner, you could use the flux bootstrap instead of flux install.

Create an image pull secret for ghcr.io with:

flux create secret oci ghcr-auth \
  --url=ghcr.io \
  --username=flux \
  --password=${GITHUB_TOKEN}

Create a Flux OCIRepository for pulling the latest artifact from GitHub container registry:

flux create source oci demo-app \
  --secret-ref=ghcr-auth \
  --url=oci://${APP_REPO} \
  --tag=latest \
  --interval=1m

Automated updates

At every minute, Flux verifies if the latest OCI artifact digest differs from the digest of the last downloaded artifact. When a new artifact is tagged as latest, Flux will detect the new version and will pull it inside the cluster.

Create a Flux Kustomization for applying the manifests from the artifact on the cluster:

flux create kustomization demo-app \
--source=OCIRepository/demo-app \
--prune=true \
--wait=true \
--health-check-timeout=3m \
--interval=10m

Automated reconciliation

Every time a new version of the OCI artifact is downloaded, Flux reconciles the changes in the Kubernetes manifests from the artifact with the cluster state.

During a reconciliation, Flux performs these tasks:

  • Validates the manifests against the Kubernetes API (server-side apply dry run)
  • Applies the Kubernetes objects that changed in order (namespaces and other global objects first)
  • Deletes the objects that were removed from the latest artifact version
  • Waits for the changes to be successfully rollout (Helm upgrades, deployments, jobs, etc)
  • Reports the apply diff or any error as Kubernetes events

You can see the apply diff and any other Flux events with:

kubectl alpha events --for kustomization/demo-app -n flux-system

Drift detection and correction

Even if nothing changed in the OCI source, Flux verifies if the cluster state has drifted from the desired state. If a drift is detected, Flux re-applies the Kubernetes objects that changed and waits for the drift to be corrected. Then it emits a Kubernetes events with the list of objects that were corrected.

Promote changes to production

Assuming you're deploying the latest version to staging, you could introduce a dedicated tag for production e.g. stable.

On the production cluster, you'll configure Flux to reconcile the artifacts tagged as stable:

flux create source oci demo-app \
  --secret-ref=ghcr-auth \
  --url=oci://${APP_REPO} \
  --tag=stable \
  --interval=1m

To promote a version tested on staging, you would tag it as stable with:

kustomizer tag artifact oci://${APP_REPO}:${APP_VERSION} stable

Automate the artifact publishing

You can automate the publishing process by running Kustomizer in CI.

Here is an example of a GitHub Actions workflow that pushes an OCI artifact to GHCR every time there is a change to the Kubernetes configuration:

name: publish
on:
  push:
    branches:
      - 'main'
    paths:
      - 'examples/demo-app/**'

permissions:
  contents: read
  id-token: write
  packages: write

env:
  ARTIFACT: oci://ghcr.io/${{github.repository_owner}}/${{github.event.repository.name}}

jobs:
  kustomizer:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Login to GitHub Container Registry
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Setup kustomizer
        uses: stefanprodan/kustomizer/action@main
      - name: Push
        run: |
          kustomizer push artifact ${ARTIFACT}:${GITHUB_REF_NAME} \
            -k=examples/demo-app \
            --source=${{ github.repositoryUrl }} \
            --revision="${{ github.ref_name }}/${{ github.sha }}"
      - name: Tag latest
        run: |
          kustomizer tag artifact ${ARTIFACT}:${GITHUB_REF_NAME} latest

For more details on how to use Kustomizer within GitHub workflows, please see the GitHub Actions documentation.