Deployments straight from the pipeline to the cluster
Not every team uses GitLab's own Kubernetes integration. Often a plain kubectl call directly from the pipeline is enough, as long as the kubeconfig and access rights are configured cleanly and safely. This article walks through the full path from the kubeconfig variable to a verified rollout.
Table of Contents
- 1. Controlling Kubernetes deployments directly from the pipeline
- 2. Providing kubeconfig safely as a CI variable
- 3. kubectl apply straight from the CI job
- 4. Checking rollout status instead of trusting blind success
- 5. Namespace and context handling per environment
- 6. RBAC and ServiceAccount: restricting access to what's needed
- 7. Comparison to GitLab's own Kubernetes integration
- 8. Rolling back via kubectl on a failed deployment
- 9. Conclusion: kubectl from the pipeline as a pragmatic default
- 10. Summary
- 11. FAQ
1. Controlling Kubernetes deployments directly from the pipeline
As soon as an application runs on Kubernetes, the question arises of how a new image actually reaches the cluster after a successful build. The most direct path is a kubectl call inside a GitLab CI job that applies a deployment manifest or updates an existing deployment to a new image tag. That is technically simple, transparently traceable in the pipeline log, and achievable without additional GitLab features such as the Kubernetes Agent.
The approach suits teams that already have a working kubectl-based deployment routine from the command line and just want to automate it, without fundamentally changing the architecture. What matters is restricting cluster access so a CI job can only do exactly what it actually needs, nothing more.
2. Providing kubeconfig safely as a CI variable
The kubeconfig file contains credentials and certificates for cluster access and must never end up in the repository. The usual path is to store the full kubeconfig, base64-encoded, as a protected, masked CI/CD variable of type File under Settings > CI/CD > Variables. GitLab then writes the value to a temporary file at runtime automatically and exposes its path through the variable itself, so no manual decoding is needed in the script.
For production-grade environments, the variable should be marked protected, so it is only available in pipelines on protected branches or tags. That way a merge request from a feature branch can never accidentally gain access to the production kubeconfig, even if the job name were identical to the production deploy job.
# Prepare the kubeconfig locally and base64-encode it
cat ~/.kube/config-production | base64 -w 0
# In GitLab: Settings > CI/CD > Variables
# Key: KUBECONFIG_PRODUCTION
# Type: File
# Value: <base64-decoded content or the kubeconfig directly>
# Flags: Protected, Masked (if supported)
3. kubectl apply straight from the CI job
With the kubeconfig as a file variable, a lean job using the official bitnami/kubectl image or a similarly small kubectl image and referencing the variable via the KUBECONFIG environment variable is enough. Instead of a full kubectl apply -f manifest.yaml, kubectl set image is often preferable when only a new container image needs to be rolled out without touching the rest of the manifest.
The advantage of kubectl set image over a full apply is that it only changes the image field, offering less surface for accidental configuration changes that should really go through a separate, deliberate manifest commit. For structural changes to the deployment, such as new environment variables or resource limits, kubectl apply -f with a versioned manifest remains the right choice.
deploy-k8s:
stage: deploy
image: bitnami/kubectl:1.29
variables:
KUBECONFIG: $KUBECONFIG_PRODUCTION
script:
- kubectl config current-context
- kubectl set image deployment/api-service
api-service=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
--namespace=production
environment:
name: production
when: manual
4. Checking rollout status instead of trusting blind success
kubectl set image or kubectl apply return as soon as Kubernetes has accepted the change, regardless of whether the new pods actually start successfully. A CI job that counts as successful right after the apply command can therefore wave a broken deployment through as a green pipeline, even while the new pods are stuck in a CrashLoopBackOff.
kubectl rollout status deployment/api-service --timeout=120s closes that gap: the command blocks until the rollout either completes successfully or the timeout is reached, returning a non-zero exit code if the rollout fails. That exit code then correctly fails the CI job, which GitLab shows as a red pipeline result and reports to the team immediately.
deploy-k8s:
stage: deploy
image: bitnami/kubectl:1.29
variables:
KUBECONFIG: $KUBECONFIG_PRODUCTION
script:
- kubectl set image deployment/api-service
api-service=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
--namespace=production
- kubectl rollout status deployment/api-service
--namespace=production --timeout=120s
environment:
name: production
when: manual
5. Namespace and context handling per environment
With multiple environments such as staging and production, it pays to maintain a separate, clearly named kubeconfig variable and namespace for each environment, rather than switching a single kubeconfig context via script. That significantly reduces the risk of accidentally working against the wrong cluster with the wrong context, because each job only ever has access to exactly one environment from the start.
Environment names in GitLab's environment: should match the Kubernetes namespaces or at least map to them unambiguously, so the GitLab Environments dashboard clearly shows which job touches which cluster namespace. That consistency later also makes it easier to use GitLab features like environment URLs or deployment freezes meaningfully.
6. RBAC and ServiceAccount: restricting access to what's needed
A common mistake is giving the CI pipeline a kubeconfig with cluster-admin rights, because that is the fastest way to keep every conceivable kubectl command working. Safer is a dedicated ServiceAccount with a tightly scoped RBAC role that only grants read and write access to deployments, pods, and replica sets in the relevant namespace, with no access to secrets in other namespaces or cluster-wide resources like nodes.
A Role and RoleBinding pair scoped to the target namespace is fully sufficient for most deployment pipelines. The associated ServiceAccount token then becomes part of the kubeconfig stored as a CI variable. Should that variable ever be compromised, the damage stays limited to the rights of that restricted role instead of endangering the entire cluster.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: ci-deployer
namespace: production
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "patch", "update"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
7. Comparison to GitLab's own Kubernetes integration
GitLab offers an alternative, more deeply integrated connection with the Kubernetes Agent, where an agent pod runs inside the cluster and establishes an outbound connection to GitLab, instead of the pipeline actively reaching the cluster with credentials. This has the advantage that no kubeconfig with credentials needs to be maintained as a CI variable, and the cluster does not need to allow inbound connections from GitLab runners, which is preferable from a security standpoint in many network setups.
The direct kubectl approach from this article remains relevant nonetheless, because it works without installing an additional agent in the cluster, is easier to debug since every command is explicitly visible in the pipeline log, and fits seamlessly into existing, already-established kubectl workflows. For smaller teams or projects where the effort of installing the agent outweighs the benefit, the direct variant remains the more pragmatic choice.
8. Rolling back via kubectl on a failed deployment
Kubernetes keeps a history of previous ReplicaSet revisions of a deployment by default, which means kubectl rollout undo deployment/api-service --namespace=production reverts the deployment to the previous working version without manually resetting the image tag. That is significantly faster than re-applying with the old image tag, because Kubernetes itself retains the last known working configuration.
A manual rollback job in the .gitlab-ci.yml using when: manual makes this command available with a click at any time, without needing to create a new pipeline. After rollout undo, running kubectl rollout status again is recommended to confirm the rollback itself completed successfully instead of ending up stuck in another broken state.
rollback-k8s:
stage: deploy
image: bitnami/kubectl:1.29
variables:
KUBECONFIG: $KUBECONFIG_PRODUCTION
script:
- kubectl rollout undo deployment/api-service --namespace=production
- kubectl rollout status deployment/api-service
--namespace=production --timeout=120s
environment:
name: production
when: manual
9. Conclusion: kubectl from the pipeline as a pragmatic default
Controlling kubectl directly from GitLab CI is a simple, transparent, and easily debuggable approach that requires no additional infrastructure in the cluster. What matters for security is a restrictive RBAC role instead of cluster-admin rights, along with a protected, file-based CI variable for the kubeconfig.
Teams already running several clusters or environments and seeking additional security without inbound cluster connections should evaluate the GitLab Kubernetes Agent as an alternative. For most small to mid-sized projects, though, the direct kubectl route is fully sufficient and quicker to set up.
| Method | Setup effort | Security model | Recommended for |
|---|---|---|---|
| Direct kubectl from a CI job | Low | Kubeconfig as a protected variable, outbound to the cluster API | Smaller teams, simple setups |
| GitLab Kubernetes Agent | Medium to high | Agent inside the cluster, outbound connection to GitLab | Multiple clusters, higher security requirements |
| GitOps tool (e.g. ArgoCD, Flux) | High | Declarative sync, no direct CI access to the cluster | Larger teams with many deployments, high degree of automation |
| Manual kubectl on the CLI | None | Personal kubeconfig, no audit trail in GitLab | Debugging only, not production |
Mironsoft
CI/CD pipelines, zero-downtime deployments and release automation
Deployments that run without downtime and without the nail-biting?
We review existing GitLab pipelines for fragile deployment steps and missing safeguards, then build a release process with zero-downtime deployments, automated checks and a rollback you can actually trust in an emergency.
Pipeline Review
Checking an existing .gitlab-ci.yml for fragility, missing stages and security gaps.
Zero-Downtime Deployment
Building symlink releases, health checks and rollback strategies for Magento stores.
CI/CD Automation
Connecting tests, security scans and deployments into one reliable pipeline.
10. Summary
kubectl from GitLab CI: The Essentials at a Glance
Kubeconfig as a File variable
Store a base64-encoded kubeconfig as a protected, file-based CI/CD variable.
Check rollout status
kubectl rollout status after every deploy prevents falsely green pipelines on failed rollouts.
Restrictive RBAC
A ServiceAccount with a tightly scoped role instead of cluster-admin rights limits damage on compromise.
Agent as an alternative
The GitLab Kubernetes Agent avoids inbound cluster connections but costs more setup effort.