How do you deploy to Kubernetes from a CI/CD pipeline?

A CI/CD pipeline deploys to Kubernetes in three steps: it builds a container image, pushes it to a registry and records its digest, then updates the cluster. The update is either push-based, where the pipeline runs kubectl or Helm against the cluster, or pull-based, where the pipeline commits the new image reference to Git and a GitOps controller in the cluster applies it. A safe pipeline authenticates with short-lived OIDC tokens instead of a stored kubeconfig and waits for the rollout to succeed before it reports success.

The pipeline shape

Most Kubernetes pipelines, in GitHub Actions, GitLab CI or any other system, follow the same order:

  1. Build and test the application.
  2. Build the image and push it to a container registry.
  3. Reference the image by digest (registry.example.com/app@sha256:...), not only by tag. Tags can be moved to another image; a digest always identifies the same bytes.
  4. Update the cluster, either directly or through Git.
  5. Verify the rollout and roll back if it fails.

Push-based deploys vs GitOps

Push-based (kubectl or Helm from CI)Pull-based (GitOps)
Who changes the clusterThe pipelineA controller running in or next to the cluster
Where cluster credentials liveIn the CI systemOnly inside the cluster
Drift from manual changesStays until the next deployDetected and reverted on reconciliation
RollbackRerun an older pipeline, or kubectl rollout undo / helm rollbackRevert the commit in Git
Good fitFew services, one or two clusters, fast feedbackMany services or clusters, audit requirements

Push-based deploys are simpler to start with and show deploy errors directly in the CI log. GitOps keeps cluster credentials out of CI and makes Git the record of what runs where. The article on GitOps and push-based CI/CD covers the pull model in detail.

Authenticating to the cluster without a stored kubeconfig

The common shortcut is to save a kubeconfig with a long-lived service account token as a CI secret. Anyone who can read or exfiltrate that secret can reach the cluster until someone rotates it.

The alternative is OIDC federation. When a GitHub Actions workflow has permissions: id-token: write, it can request a short-lived JSON Web Token from GitHub’s issuer, https://token.actions.githubusercontent.com. The token carries claims such as the repository, branch and environment. The Kubernetes API server can be configured to accept JWTs from that issuer, or a cloud provider can exchange the token for a cloud identity that maps to Kubernetes permissions. GitLab CI offers the same pattern with its own ID tokens.

Two rules keep this safe:

  • Bind RBAC to the most specific subject the workflow produces, such as a protected branch or a deployment environment, and grant only the namespace it deploys to. The article on Kubernetes RBAC explains Roles and bindings.
  • Never grant write access to the subject that pull requests produce, because pull requests from forks run with it.

The broader pattern of workload identity for CI jobs applies to cloud APIs as well as to clusters.

Building images without a Docker socket

On GitHub-hosted runners, Docker and Buildx are installed, so docker build works. On self-hosted runners inside Kubernetes, the usual choices are:

  • Rootless BuildKit, which builds images without a privileged daemon.
  • Buildah, which builds OCI images without a daemon.
  • Docker-in-Docker, which works but needs a privileged container.

Kaniko was a common choice for in-cluster builds, but Google archived the repository on June 3, 2025, and it is no longer maintained.

Tools on hosted runners

The ubuntu-24.04 GitHub-hosted image includes kubectl, Helm, kind and minikube, as listed in the runner-images README. GitHub updates these images regularly, so the preinstalled versions change without notice. Pin the versions your pipeline depends on with setup actions such as azure/setup-kubectl and azure/setup-helm. Kubernetes supports kubectl within one minor version of the API server, according to the version skew policy, so match the pinned version to your cluster.

A minimal push-based workflow

yaml
permissions:
  id-token: write   # lets the job request an OIDC token
  contents: read

jobs:
  deploy:
    needs: build    # a build job that pushes the image and outputs its digest
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: azure/setup-kubectl@v5
        with:
          version: v1.34.2   # match the cluster's minor version
      # Authenticate with the OIDC token here (provider-specific step)
      - run: kubectl -n production set image deployment/app app=registry.example.com/app@${{ needs.build.outputs.digest }}
      - run: kubectl -n production rollout status deployment/app --timeout=5m

Verifying and rolling back

A deploy step that returns as soon as the API server accepts the change can report success while new Pods crash. Wait for the result instead:

  • kubectl rollout status with a timeout fails the job if the new ReplicaSet does not become available.
  • helm upgrade --install --wait waits until resources are ready.
  • A short smoke test against the new version catches errors that readiness probes miss.

On failure, kubectl rollout undo or helm rollback returns to the previous revision. For gradual rollouts with automatic analysis, progressive delivery controllers such as Argo Rollouts and Flagger shift traffic in steps and roll back on failing metrics.

Deploying to Cloudfleet from CI

Cloudfleet Kubernetes Engine (CFKE) clusters trust GitHub Actions and GitLab CI OIDC tokens by default, so pipelines need no stored kubeconfig or API key. The GitHub Actions integration guide shows the configure-kubectl action and how to bind RBAC to a workflow’s branch or environment.

On this page