Kubernetes CI/CD is the pipeline that validates, tests and rolls out your Kubernetes manifests automatically, so you do not have to run every step by hand. Unlike a plain application pipeline, it also takes care of the syntax and structure of your YAML files, the right order for rolling resources out, and often an approval step before anything reaches the production environment. What comes out the other end is a deployment that runs just as often and just as reliably as your application build.
I run Kubernetes clusters in production and wrote the Kubernetes practical guide published by Rheinwerk (2024). What follows are pipelines I built myself, tore apart and rebuilt, not a tutorial demo. You can find all my Kubernetes articles collected on the Kubernetes page.
What a Kubernetes CI/CD pipeline needs on top
A typical application pipeline compiles, tests and builds an image. For Kubernetes, a second layer joins in: the manifests themselves. A Deployment with a typo in the replicas field, or a Service pointing at the wrong label, will not show up in a normal build. It shows up during rollout, and in the worst case, in production.
That is why a mature Kubernetes pipeline checks three things a plain application pipeline never sees: whether the YAML files are syntactically and structurally correct, in which order resources need to be created, and whether a rollout actually reached the desired state after being applied. How much of this a team automates depends heavily on how the manifests are organized in Git. That is where every pipeline starts.
Manifests in Git: folder layout and branching
Before you build a pipeline, you decide how your manifests live in Git. Two repository models have worked well for me. In a monorepo, application code and manifests sit together, one team owns everything from commit to rollout, and a change to code and deployment lands in a single pull request. With separate repositories, application teams work independently from an operations team that owns the manifests. That raises control, but costs coordination between teams, especially when a new environment variable has to land on both sides.
Which model fits you almost always comes down to how your team is organized: a team with full ownership of one application tends toward a monorepo, while separate development and operations teams tend toward separate repositories with their own access rights.
The folder layout is followed by the branching strategy. I have used all three common workflows across projects, with quite different results.
| Workflow |
Fits well with |
Biggest downside |
| Git Flow |
long release cycles, several versions running in parallel |
a history that is hard to read, and a heavier pipeline due to two permanent branches |
| GitHub Flow |
small teams, continuous deployment straight from the main branch |
the main branch must stay production ready at all times, which takes discipline |
| GitLab Flow |
staged environments such as development, staging and production |
extra merge work between the environment branches |
For operations-heavy teams with different release cadences between staging and production, GitLab Flow usually works best for me, because the environment branches give you an honest view: whatever sits in staging is genuinely running there. For smaller teams with short cycles, GitHub Flow is the simpler path, as long as the pipeline is strict enough to catch problems before the merge.
Linting and testing before the cluster ever sees it
The cheapest bug is the one your pipeline catches before anything is rolled out at all. A linter for Kubernetes manifests checks syntax and structure against the schema of the Kubernetes API before a kubectl apply ever runs. A widely used tool for this is Kubeconform: because it needs no running cluster and drops into any pipeline in seconds.
A simple lint step in GitLab CI looks like this:
stages:
- validate
- deploy
lint-manifests:
stage: validate
image:
name: ghcr.io/yannh/kubeconform:latest-alpine
entrypoint: [""]
script:
- kubeconform -strict -summary manifests/
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
The validate stage runs before every deploy and stops the pipeline as soon as a manifest fails the schema, for example because replicas is a string instead of a number. The alpine variant of the image and the empty entrypoint matter here: GitLab CI runs the script lines in a shell, and the slim default image does not ship one. The rule under rules makes this step fire on every merge request, not only once it merges. That way a reviewer already sees the failure before approving anything.
This example is assembled from the Kubeconform documentation and the GitLab CI reference, not copied out of one of my running pipelines. Let it run once in your own project and adjust the image tag and the path before you rely on it.
Linting only checks the shape, not the behavior after rollout. For that I additionally use Kuttl, a testing tool that actually rolls a manifest out into a test cluster and then checks whether the expected state was reached, for example whether a pod with a given label is really running. I run tests like this in their own stage after deploying to a staging environment, not before the merge, because they need a running cluster and take noticeably longer.
Pipeline architectures: one pipeline or several
Once linting and testing are in place, the next question is the architecture of the pipeline itself. I look at two axes. The first is monolithic versus split: does everything, from build to rollout, run as a single pass, or are the build and deployment pipelines separate and trigger each other? The second is centralized versus decentralized: is there one shared pipeline template for several projects, or does each repository fully own its own pipeline?
A split build and deployment pipeline is something I have used mainly where a development team should not have direct access to the Kubernetes cluster. The build pipeline compiles, builds the image, pushes it to a registry, and only hands the new version number over to the deployment pipeline. The deployment pipeline then updates the image tag in the manifest and rolls out, often only after a manual approval for the production environment. The upside is a clean separation of responsibility, the downside is the extra handoff between the two pipelines, which I have usually solved with a webhook carrying the version number as a parameter.
A centralized pipeline template pays off once several projects need the same steps: linting, image build, rollout. Instead of writing each pipeline from scratch, you pull a shared template from a central repository and only pass parameters such as the application name or the target environment. That saves maintenance effort, but brings a new problem with it: an update to the central template has to be checked against every project that uses it, or the pipeline breaks somewhere else without anyone noticing.
Where GitOps takes over from a pipeline
Everything described so far is imperative: your pipeline actively runs kubectl apply or something similar as soon as a merge happens. With GitOps, the direction flips. A controller inside the cluster watches your repository itself and continuously reconciles the actual state of the cluster with what is stored in Git, instead of waiting for a push from a pipeline.
The difference is more than a technicality. A pipeline that deploys actively needs a technical user with write access to the cluster, sitting somewhere in the pipeline configuration. The GitOps controller does not need that, because it pulls the changes from the repository itself. It also catches drift someone introduced by hand with kubectl edit and reverts it back to what is in Git automatically. How you set this up concretely with Argo CD, including the repository layout, is covered in GitOps with ArgoCD: Deploy from Git.
In practice, GitOps usually replaces only the last step of a pipeline, the rollout itself. Linting, image builds and tests still run in your regular CI, only the rollout moves from a kubectl apply in the pipeline to a commit that the cluster controller then picks up.
Frequently asked questions
How do I set up a Kubernetes pipeline in GitLab CI?
You need at least two stages: one to validate your manifests, for example with Kubeconform, and one for the actual rollout with kubectl apply or helm upgrade. Validation should run on every merge request, while the rollout usually runs only after the merge into the relevant branch.
What tools do I need for a Kubernetes CI/CD pipeline?
At the core, a linter such as Kubeconform for syntax checks, a testing tool such as Kuttl for checks after rollout, and a CI system of your choice, such as GitLab CI, GitHub Actions or Jenkins, are enough. Depending on your setup, Helm, Kustomize or plain kubectl handle the actual deployment.
What are the most important Kubernetes CI/CD best practices?
From my experience, four things matter most: lint manifests before the merge, run tests after deploy rather than only before it, make a deliberate choice about repository and branching structure, and keep a clear separation between automatic deploys to staging and approval-gated deploys to production.
Do I still need a CI pipeline if I use GitOps?
Yes, in most cases. GitOps typically takes over only the last step, applying the manifests in the cluster. Linting, image builds and tests before the rollout still run in a classic pipeline that, at the end, only writes a commit to your GitOps repository instead of deploying directly.
Where to go next
Before you build a pipeline, it is worth looking at the manifests themselves: which fields you actually need and where common mistakes creep in is covered in Kubernetes YAML: Understanding Manifests. Once several environments with similar but not identical values come into play, Helm Charts Explained: Packages for K8s is worth a look too, since Helm takes over the parameterization you would otherwise have to rebuild in the pipeline.
My suggestion for getting started: add the lint step to your existing pipeline first, before worrying about testing tools or GitOps. It takes a few minutes to set up and already catches the mistakes that happen most often.
In full detail, from the folder layout to the pipeline architectures, this is covered in chapter 4 of my Kubernetes Practical Guide (Rheinwerk Computing).