Deploying to Kubernetes
GitLab Agent
kubectl & Helm
Environments
GitOps with Flux
Learn how to connect a Kubernetes cluster to GitLab using the agent, run kubectl and Helm commands from CI/CD pipelines, configure KUBE_CONTEXT for environments, and choose between CI/CD push deployments and GitOps with Flux.
Kubernetes
GitOps
GitLab CI/CD
How GitLab Connects to Kubernetes
GitLab does not talk to Kubernetes directly. Instead, it uses an agent — a small process installed inside your cluster that maintains a bidirectional connection to GitLab. The agent is registered in a GitLab project, receives configuration from a YAML file in that project, and exposes the Kubernetes API to GitLab CI/CD jobs through a secure proxy. This design means your cluster can sit behind a firewall or NAT, and GitLab can still deploy to it without exposing the Kubernetes API to the public internet [citation:19].
Once the agent is connected, GitLab CI/CD jobs can run kubectl and helm commands against the cluster. The key is the KUBE_CONTEXT variable, which tells the job which agent connection to use. GitLab automatically injects a kubeconfig into every job that has access to the agent, and setting KUBE_CONTEXT selects the right cluster from that kubeconfig. A single project can deploy to multiple clusters by defining multiple KUBE_CONTEXT variables scoped to different environments [citation:1][citation:4].
GitLab supports two deployment workflows. The CI/CD workflow is push-based: a pipeline job runs kubectl apply or helm upgrade, and the changes are applied immediately. The GitOps workflow is pull-based: a Git repository is the source of truth, and Flux (running in the cluster) continuously reconciles the cluster state with the repository. GitLab recommends GitOps for production because it has a stronger security model and provides automatic drift remediation, while the CI/CD workflow is useful for pipeline-driven processes and migration scenarios [citation:19].
Core Principle: The agent is the bridge. Install it once per cluster, register it in a project, and then any authorized CI/CD job can use kubectl or helm against that cluster using KUBE_CONTEXT.
GitLab Agent
Runs in the cluster, connects to GitLab via KAS. Enables CI/CD access to the Kubernetes API without exposing it publicly.
KUBE_CONTEXT
Selects which agent connection to use. Format: path/to/project:agent-name. Can be scoped per environment.
CI/CD Workflow
Push-based. Pipeline runs kubectl/helm. Simpler but weaker security. Not recommended for production.
GitOps (Flux)
Pull-based. Flux reconciles cluster from Git/OCI. Recommended for production. Automatic drift remediation.
Agent Authorization
Projects/groups must be authorized in the agent config (ci_access). Controls which pipelines can use the agent.
RBAC
The agent's service account permissions determine what kubectl can do. Default is cluster-admin; restrict for production.
Installing and Registering the Agent
Installing the agent is a three-step process: create a configuration file in your repository, register the agent in GitLab to get an access token, and install the agent in your cluster using Helm. The configuration file lives at .gitlab/agents//config.yaml in the default branch of the project where you register the agent. It can be empty at first, but it's where you'll later define ci_access rules to authorize other projects and user_access rules for dashboard access [citation:6].
Registration happens in the GitLab UI: navigate to Operate → Kubernetes clusters, click "Connect a cluster," and choose an agent name. GitLab generates an access token and a Helm install command. The token is shown only once — store it securely. The Helm command installs the agent into a namespace (by default gitlab-agent) and connects it to GitLab's KAS (Kubernetes Agent Server) endpoint [citation:3][citation:6].
The default Helm installation grants the agent's service account cluster-admin rights. This is convenient for getting started but dangerous in production: any pipeline authorized to use the agent can do anything in the cluster. For production, customize the Helm installation to use a service account with only the permissions your deployments need — typically CRUD on Deployments, Services, ConfigMaps, and Secrets in specific namespaces [citation:3][citation:11].
# Step 1: Create agent configuration file
# Path: .gitlab/agents/my-agent/config.yaml
# (Can be empty initially)
# Step 2: Register agent in GitLab UI
# Operate > Kubernetes clusters > Connect a cluster
# Enter agent name: my-agent
# Click "Create and register"
# Copy the token and Helm command
# Step 3: Install agent with Helm (from the command GitLab gave you)
helm repo add gitlab https://charts.gitlab.io
helm repo update
helm upgrade --install my-agent gitlab/gitlab-agent \
--namespace gitlab-agent-my-agent \
--create-namespace \
--set image.tag=v17.10.1 \
--set config.token=glagent-xxxxxxxxxxxx \
--set config.kasAddress=wss://kas.gitlab.com
# For self-managed GitLab:
# --set config.kasAddress=wss://gitlab.example.com/-/kubernetes-agent/
# Production: restrict RBAC instead of cluster-admin
helm upgrade --install my-agent gitlab/gitlab-agent \
--namespace gitlab-agent-my-agent \
--create-namespace \
--set config.token=$AGENT_TOKEN \
--set config.kasAddress=wss://kas.gitlab.com \
--set serviceAccount.create=false \
--set serviceAccount.name=gitlab-deployer \
--set rbac.create=false
# Then create a ServiceAccount + Role + RoleBinding manually
# granting only the permissions your deployments need
# Verify connection
# Operate > Kubernetes clusters
# Status should show "Connected"
# Agent config file example (authorize a project)
# .gitlab/agents/my-agent/config.yaml
ci_access:
projects:
- id: my-group/my-application
# Or authorize a whole group:
ci_access:
groups:
- id: my-group
# Or all projects on the instance (admin only):
ci_access:
instance: {}
Agent Security:
- Token is sensitive: A bad actor with the agent token can access source code and potentially the cluster
- Default is cluster-admin: Restrict RBAC for production
- Authorize narrowly: Use
ci_access to allow only the projects that need to deploy
- One agent per cluster: Do not install multiple agents in the same cluster unless you have a specific multi-tenancy need
CI/CD Deployment with kubectl
Once the agent is connected and authorized, deploying with kubectl from a GitLab CI/CD job is straightforward. The job needs a container image with kubectl installed (like bitnami/kubectl or alpine/k8s), and it needs to set KUBE_CONTEXT to the agent's context path. GitLab injects a kubeconfig into the job environment automatically; setting the context selects the right cluster from that kubeconfig. After that, kubectl apply, kubectl set image, and kubectl rollout status work exactly as they would locally [citation:4][citation:12].
The typical deployment pattern is to build an image, push it to the GitLab Container Registry, and then deploy it using kubectl set image or kubectl apply with the new image tag. For manifest-based deployments, you can use sed to substitute the image placeholder with the actual image reference, or use Kustomize to patch the image tag. For a more robust approach, kubectl apply -k with Kustomize overlays lets you manage environment-specific configuration cleanly [citation:14].
It's important to use kubectl rollout status after applying changes. This command blocks until the deployment is complete or fails, which gives the pipeline an accurate success/failure signal. Without it, the job succeeds as soon as the API accepts the request, even if the pods crash immediately afterward. For rollbacks, GitLab's environment tracking (covered in the previous guide) lets you redeploy a previous commit, which re-runs the job with the older image tag.
# CI/CD deployment with kubectl
deploy_production:
stage: deploy
image: bitnami/kubectl:latest
variables:
KUBE_CONTEXT: my-group/my-agent-project:my-agent
before_script:
# KUBE_CONTEXT selects the agent from the injected kubeconfig
- kubectl config use-context $KUBE_CONTEXT
script:
# Update the deployment image
- kubectl set image deployment/myapp \
myapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA \
-n production
# Wait for rollout to complete
- kubectl rollout status deployment/myapp -n production --timeout=5m
environment:
name: production
url: https://example.com
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
# Manifest-based deployment with sed
deploy_manifest:
stage: deploy
image: bitnami/kubectl:latest
variables:
KUBE_CONTEXT: my-group/my-agent-project:my-agent
before_script:
- kubectl config use-context $KUBE_CONTEXT
script:
# Replace image placeholder in manifest
- sed -i "s|IMAGE_PLACEHOLDER|$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA|g" k8s/deployment.yaml
- kubectl apply -f k8s/deployment.yaml -n production
- kubectl rollout status deployment/myapp -n production
# Kustomize-based deployment
deploy_kustomize:
stage: deploy
image: bitnami/kubectl:latest
variables:
KUBE_CONTEXT: my-group/my-agent-project:my-agent
before_script:
- kubectl config use-context $KUBE_CONTEXT
script:
- cd k8s/overlays/production
- kustomize edit set image myapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
- kubectl apply -k .
- kubectl rollout status deployment/myapp -n production
# KUBE_CONTEXT format:
# path/to/agent/project:agent-name
# Example: my-group/my-agent-project:my-agent
# For environment-scoped KUBE_CONTEXT (multiple clusters):
# Settings > CI/CD > Variables
# Variable: KUBE_CONTEXT
# Value: my-group/my-agent-project:production-agent
# Environment scope: production
#
# Variable: KUBE_CONTEXT
# Value: my-group/my-agent-project:staging-agent
# Environment scope: staging
kubectl Deployment Best Practices:
- Always set
KUBE_CONTEXT before running kubectl
- Use
kubectl rollout status to wait for deployment completion
- Pin image tags to commit SHAs for rollback safety
- Use Kustomize overlays for environment-specific configuration
- Scope
KUBE_CONTEXT per environment for multi-cluster setups
CI/CD Deployment with Helm
Helm is the package manager for Kubernetes, and GitLab CI/CD supports it natively. Instead of applying raw manifests, you package your application as a Helm chart, version it, and deploy it with helm upgrade --install. The CI/CD job sets KUBE_CONTEXT, authenticates to the registry if the chart is stored there, and runs the Helm command with the new image tag as a value override. This pattern is more maintainable for complex applications with many configurable parameters [citation:10].
A typical Helm deployment job uses an image with Helm installed (like alpine/helm or dtzar/helm-kubectl), sets the context, and runs helm upgrade --install with --set image.tag=$CI_COMMIT_SHORT_SHA. The --wait flag makes Helm wait for the deployment to become ready, and --timeout prevents it from hanging indefinitely. The --atomic flag automatically rolls back if the deployment fails, which is valuable for production [citation:10].
Helm also supports OCI-based chart registries, which means you can store your charts in the GitLab Container Registry alongside your images. This keeps everything in one place and lets you version charts the same way you version images. The job authenticates with helm registry login using the CI registry credentials, then pulls and installs the chart. For production, pin the chart version to a specific release rather than using latest.
# Helm deployment from a chart in the repository
deploy_helm:
stage: deploy
image: alpine/helm:latest
variables:
KUBE_CONTEXT: my-group/my-agent-project:my-agent
before_script:
- helm repo add myrepo https://charts.example.com # If using a repo
- helm repo update
- kubectl config use-context $KUBE_CONTEXT
script:
- helm upgrade --install myapp ./charts/myapp \
--namespace production \
--create-namespace \
--set image.repository=$CI_REGISTRY_IMAGE \
--set image.tag=$CI_COMMIT_SHORT_SHA \
--wait \
--timeout 5m \
--atomic
environment:
name: production
url: https://example.com
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
# Helm deployment from OCI registry (GitLab Container Registry)
deploy_helm_oci:
stage: deploy
image: alpine/helm:latest
variables:
KUBE_CONTEXT: my-group/my-agent-project:my-agent
HELM_EXPERIMENTAL_OCI: "1"
before_script:
- echo $CI_REGISTRY_PASSWORD | helm registry login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin
- kubectl config use-context $KUBE_CONTEXT
script:
- helm upgrade --install myapp \
oci://$CI_REGISTRY_IMAGE/charts/myapp \
--version $CI_COMMIT_SHORT_SHA \
--namespace production \
--set image.tag=$CI_COMMIT_SHORT_SHA \
--wait \
--atomic
# Publish chart to OCI registry (build job)
publish_chart:
stage: build
image: alpine/helm:latest
script:
- echo $CI_REGISTRY_PASSWORD | helm registry login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin
- helm package ./charts/myapp --version $CI_COMMIT_SHORT_SHA
- helm push myapp-$CI_COMMIT_SHORT_SHA.tgz oci://$CI_REGISTRY_IMAGE/charts
# Helm flags explained:
# --wait: Wait for resources to be ready
# --timeout: Maximum wait time
# --atomic: Rollback on failure
# --create-namespace: Create namespace if missing
# --set: Override values
# --version: Chart version (for OCI)
GitOps with Flux
GitLab recommends GitOps for production deployments because it has a stronger security model than the CI/CD push workflow. In GitOps, the cluster state is defined by a Git repository (or an OCI artifact built from a repository), and Flux — running inside the cluster — continuously reconciles the actual state with the desired state. GitLab CI/CD is used for continuous integration (building and testing), while Flux handles continuous deployment. This separation means the cluster never needs to expose its API to GitLab, and any manual change to the cluster is automatically reverted to match the repository [citation:17][citation:19].
GitLab's agent integrates with Flux to enable immediate reconciliation. Normally, Flux polls the source at a configured interval, which means a delay between a push and the change being applied. The GitLab agent detects Flux GitRepository objects that reference GitLab projects and configures a Receiver webhook. When a push happens, the receiver triggers Flux to reconcile immediately, reducing deployment latency. This works for public projects and the agent's configuration project; private project support is not yet available [citation:17].
For production, GitLab recommends using OCI images as the Flux source rather than Git repositories. The GitLab Container Registry supports OCI images, which are immutable and can be security-scanned. The CI/CD pipeline builds the manifests into an OCI artifact and pushes it to the registry; Flux pulls from the OCI registry, not from Git. This avoids the problem of Flux treating every Git push as a change (including documentation changes that don't affect the cluster) and provides a clean, versioned artifact for each deployment [citation:17].
# GitOps setup overview
# 1. Install Flux in the cluster (flux bootstrap)
# 2. Configure the agent with Flux integration
# 3. Push manifests to Git or build OCI artifacts
# 4. Flux reconciles automatically
# Agent config for Flux (optional)
# .gitlab/agents/my-agent/config.yaml
flux:
webhook_receiver_url: http://webhook-receiver.flux-system.svc.cluster.local
# Flux GitRepository (Git source)
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: myapp
namespace: flux-system
spec:
interval: 5m
url: https://gitlab.com/my-group/myapp.git
ref:
branch: main
secretRef:
name: gitlab-auth
# Flux Kustomization (applies manifests)
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: myapp
namespace: flux-system
spec:
interval: 10m
sourceRef:
kind: GitRepository
name: myapp
path: ./k8s/production
prune: true
targetNamespace: production
# Flux OCIRepository (OCI source, recommended for production)
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: OCIRepository
metadata:
name: myapp
namespace: flux-system
spec:
interval: 5m
url: oci://registry.gitlab.com/my-group/myapp/manifests
ref:
tag: latest
secretRef:
name: gitlab-registry-auth
# CI job to build and push OCI artifact
build_manifests:
stage: build
image: fluxcd/flux-cli:latest
script:
- |
flux create source oci myapp \
--url oci://$CI_REGISTRY_IMAGE/manifests \
--tag $CI_COMMIT_SHORT_SHA \
--secret-ref gitlab-registry-auth \
--export > clusters/production/myapp.yaml
- flux push artifact oci://$CI_REGISTRY_IMAGE/manifests:$CI_COMMIT_SHORT_SHA \
--path ./k8s \
--source="$(git config --get remote.origin.url)" \
--revision="$CI_COMMIT_SHORT_SHA"
# GitOps benefits:
# - Cluster state is always reconciled from source
# - No cluster API exposed to GitLab
# - Automatic drift remediation
# - Audit trail in Git
# - Separation of CI (build) and CD (reconcile)
Complete Kubernetes Deployment Pipeline
Here's a complete pipeline that builds an image, deploys to staging with kubectl, and deploys to production with Helm — using the agent for both.
# Complete Kubernetes deployment pipeline
stages:
- build
- deploy
variables:
KUBE_CONTEXT: my-group/my-agent-project:my-agent
# Build and push image
build:
stage: build
image: docker:24
services:
- docker:24-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
# Deploy to staging (kubectl, automatic)
deploy_staging:
stage: deploy
image: bitnami/kubectl:latest
variables:
KUBE_CONTEXT: my-group/my-agent-project:my-agent
before_script:
- kubectl config use-context $KUBE_CONTEXT
script:
- kubectl set image deployment/myapp \
myapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA \
-n staging
- kubectl rollout status deployment/myapp -n staging --timeout=5m
environment:
name: staging
url: https://staging.example.com
rules:
- if: $CI_COMMIT_BRANCH == "main"
# Deploy to production (Helm, manual)
deploy_production:
stage: deploy
image: alpine/helm:latest
variables:
KUBE_CONTEXT: my-group/my-agent-project:my-agent
before_script:
- kubectl config use-context $KUBE_CONTEXT
script:
- helm upgrade --install myapp ./charts/myapp \
--namespace production \
--create-namespace \
--set image.repository=$CI_REGISTRY_IMAGE \
--set image.tag=$CI_COMMIT_SHORT_SHA \
--wait \
--timeout 10m \
--atomic
environment:
name: production
url: https://example.com
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
# Result:
# - Every push to main builds and deploys to staging
# - Production deploy is manual (approval required)
# - Helm --atomic ensures rollback on failure
# - Environment tracking provides deployment history
Frequently Asked Questions
What is the GitLab agent and why do I need it?
The agent is a process that runs in your Kubernetes cluster and connects to GitLab. It lets GitLab CI/CD jobs run kubectl and helm against the cluster without exposing the Kubernetes API publicly. You need it to deploy from GitLab to Kubernetes securely.
What is KUBE_CONTEXT and how do I format it?
KUBE_CONTEXT selects which agent connection to use. Format: path/to/agent/project:agent-name. Example: my-group/my-agent-project:my-agent. You can scope it per environment to deploy to different clusters.
Should I use kubectl or Helm?
Use Helm for complex applications with many configurable values and reusable templates. Use kubectl for simple, straightforward deployments. Many teams use both: kubectl for quick fixes, Helm for the main application.
What's the difference between CI/CD and GitOps workflows?
CI/CD is push-based: a pipeline job runs kubectl/helm. GitOps is pull-based: Flux in the cluster reconciles from Git/OCI. GitOps has a stronger security model and is recommended for production.
How do I authorize a project to use the agent?
Add the project path to the agent's config.yaml under ci_access.projects. Example: ci_access: projects: - id: my-group/my-application. Wait a few minutes for the config to sync.
How do I restrict the agent's permissions?
The default Helm install grants cluster-admin. For production, create a ServiceAccount with only the needed permissions (Deployments, Services, ConfigMaps, Secrets in specific namespaces), and install the agent with --set serviceAccount.create=false --set serviceAccount.name=gitlab-deployer --set rbac.create=false.
How do I roll back a Kubernetes deployment?
Use GitLab environments: go to Operate → Environments, find the last successful deployment, and click Rollback. This re-runs the deploy job with the old commit, which applies the old image tag. For Helm, you can also use helm rollback.
Can I deploy to multiple clusters from one project?
Yes. Define multiple KUBE_CONTEXT variables scoped to different environments (staging, production). Each environment's jobs use the context for the corresponding cluster.