Implementing CI/CD to a Kubernetes Deployment

Raphael Ndonga | Oct 25, 2025 min read

I was deploying my containers onto kubernetes through this article I wrote a while back. What happens if I want to change a bit of my code ? Would I have to locally rebuild the docker container and then push it to the docker container registry manually ? There had to be a better way.

Approach

  • Enforce a ci/cd pipeline through Github Actions that automatically pushes code from the main branch to the docker container registry (e.g dockerhub or ghcr)
  • Connect the uploaded container image to the Kubernetes Application.

Enforcing CI/CD

CI/CD stands for Continuous Integration and Continuous Delivery (or Continuous Deployment). It is a software engineering practice designed to automate and streamline the process of building, testing, and releasing code.

GitHub Actions is a continuous integration and continuous delivery (CI/CD) platform built directly into GitHub. It allows developers to automate workflows that build, test, and deploy code whenever certain events occur in a GitHub repository.

Github Actions mainly deals with events & actions in a workflow

  • events are anything that happen in or to your repository.
  • actions are the defined operations that should be done as a result of events being triggered.
  • workflow is a sequence of events and actions that are configured for a specific purpose e.g publishing a container to a docker container registry

Prebuilt Workflows

Github Actions has many templates you can use to implement github actions for your use case. In this case, you can search for Publish Docker Container as below:

Publish Docker Workflow

It adds a docker-publish.yml file under .github/workflows as below:

name: Docker

# Define the events using "on"
on:
  push:
    branches: [ "main" ]
    # Publish semver (semantic versioning) tags as releases.
    tags: [ 'v*.*.*' ]
  pull_request:
    branches: [ "main" ]

env:
  # Use docker.io for Docker Hub if empty
  REGISTRY: ghcr.io
  # github.repository as <account>/<repo>
  IMAGE_NAME: ${{ github.repository }}


jobs:
  build:

    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      # This is used to complete the identity challenge
      # with sigstore/fulcio when running outside of PRs.
      id-token: write

    # Define the actions using "steps"
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      # Install the cosign tool except on PR
      # https://github.com/sigstore/cosign-installer
      - name: Install cosign
        if: github.event_name != 'pull_request'
        uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 #v3.5.0
        with:
          cosign-release: 'v2.2.4'

      # Set up BuildKit Docker container builder to be able to build
      # multi-platform images and export cache
      # https://github.com/docker/setup-buildx-action
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0

      # Login against a Docker registry except on PR
      # https://github.com/docker/login-action
      # 
      - name: Log into registry ${{ env.REGISTRY }}
        if: github.event_name != 'pull_request'
        uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      # Extract metadata (tags, labels) for Docker
      # https://github.com/docker/metadata-action
      - name: Extract Docker metadata
        id: meta
        uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

      # Build and push Docker image with Buildx (don't push on PR)
      # https://github.com/docker/build-push-action
      - name: Build and push Docker image
        id: build-and-push
        uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      # Sign the resulting Docker image digest except on PRs.
      # This will only write to the public Rekor transparency log when the Docker
      # repository is public to avoid leaking data.  If you would like to publish
      # transparency data even for private images, pass --force to cosign below.
      # https://github.com/sigstore/cosign
      - name: Sign the published Docker image
        if: ${{ github.event_name != 'pull_request' }}
        env:
          # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable
          TAGS: ${{ steps.meta.outputs.tags }}
          DIGEST: ${{ steps.build-and-push.outputs.digest }}
        # This step uses the identity token to provision an ephemeral certificate
        # against the sigstore community Fulcio instance.
        run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}

Key areas to note

Events

The events in the above workflow are defined in the on section:

on:
  push:
    branches: [ "main" ]
    # Publish semver (semantic versioning) tags as releases.
    tags: [ 'v*.*.*' ]
  pull_request:
    branches: [ "main" ]

When there is a push on the main branch or a pull request is made on the main branch

Actions

The actions in the above workflow are defined in the jobs.build.steps section:

jobs:
  build:
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Install cosign
        if: github.event_name != 'pull_request'
        uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 #v3.5.0
        with:
          cosign-release: 'v2.2.4'

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0

      - name: Log into registry ${{ env.REGISTRY }}
        if: github.event_name != 'pull_request'
        uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract Docker metadata
        id: meta
        uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

      - name: Build and push Docker image
        id: build-and-push
        uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Sign the published Docker image
        if: ${{ github.event_name != 'pull_request' }}
        env:
          TAGS: ${{ steps.meta.outputs.tags }}
          DIGEST: ${{ steps.build-and-push.outputs.digest }}
        run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}

There are seven steps namely:

  • Checkout repository - Reads the code in the repository.
  • Install cosign - Cosign is a tool that authenticates your container in the registry.
  • Setup Docker Buildx - Docker buildx will build your container from code and push it to the container registry.
  • Log into a container registry - Log in to your preferred container registry.
  • Extract Docker metadata - Extract information related to the docker image.
  • Build and push Docker image - Done using the buildx tool installed earlier.
  • Sign the published Docker image = Done using cosign setup earlier.

This setup ensures that once you push your code onto github, the container is built and packaged for the container registry.

Connecting to Kubernetes

With the CI/CD connecting our code to our container in the container registry, it is important to note that changes in the container image do not reflect automatically on the pods or deployment. Since it is easier to rollout changes to a deployment rather than a pod, it is better to use that in our CI/CD pipeline

Approach

  • Create the docker secret for the container registry
  • Create the relevant deployments
  • Add the secret for the container registry
  • Add the necessary env variables
  • Rollout the deployments on changes made

To create a docker secret for the ghcr.io container registry, run the below command:

kubectl create secret docker-registry ghcr-secret --docker-server=ghcr.io --docker-username=<YOUR-USERNAME> --docker-password=<YOUR-GITHUB-PERSONAL-ACCESS-TOKEN>--docker-email=<YOUR-EMAIL>--dry-run=client -o yaml > ghcr-registry-key.yaml

and then apply the file:

kubectl apply -f ghcr-registry-key.yaml

Create the relevant deployments as below: wazijobs-frontend-deploy.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: wazijobs-frontend
  name: wazijobs-frontend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: wazijobs-frontend
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: wazijobs-frontend
    spec:
      imagePullSecrets:
        - name: ghcr-secret
      containers:
      - image: ghcr.io/raphaelndonga/wazijobs-frontend:main
        name: wazijobs-frontend
        ports:
        - containerPort: 3000
        env:
        - name: NEXT_PUBLIC_API_URL_PROD
          value: "http://wazijobs-backend-svc"
status: {}

wazijobs-backend-deploy.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: wazijobs-backend
  name: wazijobs-backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: wazijobs-backend
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: wazijobs-backend
    spec:
      imagePullSecrets:
      - name: ghcr-secret
      containers:
      - image: ghcr.io/raphaelndonga/wazijobs-backend:main
        name: wazijobs-backend
        ports:
        - containerPort: 8000
        ## Add the env variables here
        env:
        - name: PORT
          value: "8000"
        - name: jwt_key
          value: "smartjobsolutions"
        - name: DATABASE_URL
          value: ""
status: {}

run:

kubectl apply -f wazijobs-frontend-deploy.yaml
kubectl apply -f wazijobs-backend-deploy.yaml

Upon further changes, you can rollout your deployments as below:

kubectl rollout restart deploy wazijobs-frontend
kubectl rollout restart deploy wazijobs-backend

Conclusion

There you have it. Your CI/CD pipeline implemented on Kubernetes through a CI/CD pipeline by Github Actions