GitLab CI/CD to Kubernetes: The Agent, the Pipeline, and the Parts That Bite

Quick answer
The certificate-based cluster integration is gone. Deploying from GitLab to Kubernetes now means the agent — agentk in your cluster, KAS on the GitLab side, and a config file that has to live on the default branch. Here is the whole path, including the context-name format that silently breaks every first pipeline.
- Why the old integration had to go
- The two components
- Registering and installing the agent
- Granting your pipeline access
- Using the agent from .gitlab-ci.yml
14 min read · DevOps & Platform
Deploying from GitLab to Kubernetes means installing an agent in your cluster. There is no longer a supported way to hand GitLab a kubeconfig and let it connect inward. The modern path is agentk running as a pod in your cluster, dialling out to GitLab's agent server (KAS), and holding that connection open. Your pipeline then talks to the cluster through that tunnel.
This is a better design than what it replaced, and it is also the source of nearly every "why can't my pipeline see the cluster" question. The failure mode is not an error message about authentication — it is a kubectl command that runs against entirely the wrong context and fails with something unhelpful.
What follows is the full path from nothing to a working deployment, the exact identifiers involved, and the four things that reliably bite on the first attempt.
Why the old integration had to go
The original GitLab-to-Kubernetes integration was certificate-based: you gave GitLab a cluster API endpoint and a service account token, and GitLab connected to your cluster.
That model has three problems that get worse as you grow:
It requires an inbound path. Your Kubernetes API server has to be reachable from GitLab. For a managed cluster with a public endpoint that is merely uncomfortable. For a cluster behind a NAT gateway or a corporate firewall it is impossible without punching a hole you will later regret.
The credential is long-lived and over-scoped. A service account token sitting in GitLab's database, typically bound to cluster-admin because scoping it properly was fiddly, valid until someone remembers to rotate it.
It inverts the trust direction. Your cluster ends up trusting an external SaaS to hold a key to its front door. Every security review asks about this, and there is no good answer.
The agent flips all three. The connection is outbound-only, so no inbound firewall rule exists. The credential lives in your cluster, not GitLab's. And the cluster decides what GitLab is allowed to do, rather than GitLab deciding what it feels like doing.
The two components
The agent is two pieces with confusingly similar names:
| Component | Where it runs | Job |
|---|---|---|
agentk | In your Kubernetes cluster, as a pod | Maintains the outbound connection to GitLab and executes work on the cluster |
kas (GitLab agent server for Kubernetes) | On the GitLab side — bundled with self-managed, hosted for GitLab.com | Terminates agent connections and routes operations to them |
agentk connects to kas. Everything else — your pipeline, the GitLab UI, GitOps reconciliation — goes through kas and down the tunnel that agentk is holding open.
Note the direction of that arrow between agentk and KAS. It points out. Nothing from GitLab ever initiates a connection to your cluster.
Registering and installing the agent
An agent is always registered inside a GitLab project. That project owns the agent's configuration, and — importantly — is the project whose path you will use later when selecting the agent from a pipeline.
Step one: create the configuration file. In the agent's project, on the default branch, create:
.gitlab/agents/<agent-name>/config.yaml
The <agent-name> directory name is the agent name. There is no name: field inside the file that overrides it.
The file may legitimately be empty at first. An empty config produces a working agent that can do nothing useful yet, which is a reasonable starting point.
Step two: register it in the UI, under the project's Operate → Kubernetes clusters. GitLab generates an access token. This token is the cluster's credential — treat it like one.
Step three: install agentk with Helm:
1helm repo add gitlab https://charts.gitlab.io
2helm repo update
3
4helm upgrade --install test gitlab/gitlab-agent \
5 --namespace gitlab-agent-test \
6 --create-namespace \
7 --set image.tag=<current agentk version> \
8 --set config.token=<your_token> \
9 --set config.kasAddress=<address_to_GitLab_KAS_instance>In practice, do not pass config.token on the command line — it lands in your shell history and in any CI log that echoes the command. The documented alternative is to bring your own secret: create it yourself, omit the token flag, and pass --set config.secretName=<your secret name> instead.
For config.kasAddress on GitLab.com the value is wss://kas.gitlab.com. Self-managed instances expose their own KAS address, which the registration UI shows you.
Granting your pipeline access
Here is the part people skip. Installing the agent does not let your pipelines use it. The agent starts life accessible to nothing. You grant CI/CD access explicitly, in the agent's config.yaml:
ci_access:
projects:
- id: path/to/project
groups:
- id: path/to/group/subgroupTwo things about this block:
- It lives on the default branch of the agent's project. Editing it on a feature branch does nothing. This catches people constantly — they add
ci_access, push to a branch, open an MR, and wonder why the pipeline still cannot see the cluster. - Granting a
groupgrants every project underneath it, now and in future. That is convenient and it is also how a cluster quietly becomes reachable from forty repositories. Prefer listing projects until you have a reason not to.
Using the agent from .gitlab-ci.yml
With access granted, the pipeline selects the cluster by switching kubectl context. The context name is not something you invent — it is derived, in this exact format:
<path/to/agent/project>:<agent-name>
So an agent named production registered in a project at platform/infra gives you the context platform/infra:production.
deploy:
script:
- kubectl config use-context path/to/agent/project:agent-name
- kubectl get podsThis is the number one thing that bites. The context string bundles two identifiers that live in different places — a repository path from GitLab's project tree, and a directory name from inside that repository. Get either wrong and kubectl does not tell you the agent is missing; it reports that the context does not exist, or worse, falls through to a default context that points somewhere else entirely.
If you are using Auto DevOps, set the variable instead of running the command:
deploy:
variables:
KUBE_CONTEXT: path/to/agent/project:agent-nameA pipeline that is actually shaped like a pipeline
The snippet above is the minimum. Real deployment jobs need gating, ordering, and environment tracking:
1stages:
2 - build
3 - test
4 - deploy
5
6build:
7 stage: build
8 script:
9 - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" .
10 - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
11
12test:
13 stage: test
14 script:
15 - make test
16
17deploy:staging:
18 stage: deploy
19 needs: ["build", "test"]
20 environment:
21 name: staging
22 url: https://staging.example.com
23 script:
24 - kubectl config use-context platform/infra:staging
25 - kubectl set image deployment/api api="$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
26 - kubectl rollout status deployment/api --timeout=180s
27 rules:
28 - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
29
30deploy:production:
31 stage: deploy
32 needs: ["deploy:staging"]
33 environment:
34 name: production
35 url: https://example.com
36 script:
37 - kubectl config use-context platform/infra:production
38 - kubectl set image deployment/api api="$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
39 - kubectl rollout status deployment/api --timeout=180s
40 rules:
41 - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
42 when: manualThree details worth calling out:
kubectl rollout status is not optional. Without it, kubectl set image returns success the instant the API server accepts the patch. Your pipeline goes green while the new pods are still pulling, or crash-looping. The --timeout matters too — without one, a genuinely broken rollout hangs the job until GitLab's job timeout kills it, which is usually an hour of a runner doing nothing.
needs decouples ordering from stages. Jobs with needs start as soon as their dependencies finish rather than waiting for the whole preceding stage. On a wide pipeline that is the difference between a four-minute and a twelve-minute deploy.
environment: is what populates GitLab's deployment history. Skip it and you lose the environment view, the "deployed to production 3 hours ago" annotations on merge requests, and the rollback affordance. It costs two lines.
Terraform Day-2 Operations Checklist
State hygiene, drift, imports, policy checks, and upgrade routines — everything after `terraform apply` works. Plain Markdown, commit it to your repo.
Free. Instant download. You'll also get the occasional deep-dive from the newsletter — unsubscribe anytime.
Killing the long-lived cloud credential
The agent solves cluster credentials. It does nothing about the other secrets your deploy job needs — the registry pull token, the AWS keys for the S3 bucket your migration script touches, the Vault token.
The mechanism for that is OIDC, via the id_tokens keyword. GitLab mints a short-lived JWT scoped to an audience you specify, and the third-party service is configured to trust GitLab as an identity provider:
1job_with_id_tokens:
2 id_tokens:
3 FIRST_ID_TOKEN:
4 aud: https://first.service.com
5 SECOND_ID_TOKEN:
6 aud: https://second.service.com
7 script:
8 - first-service-authentication-script.sh $FIRST_ID_TOKEN
9 - second-service-authentication-script.sh $SECOND_ID_TOKENEach token gets its own aud claim. This is the point of the design: a service configured to accept only https://first.service.com will reject a token minted for the second service, so a token leaked from one job cannot be replayed against another. Bind audiences narrowly.
The result is a deploy job with no static cloud credentials in it at all — the cluster reached through the agent tunnel, everything else reached with a JWT that expires when the job does. That is the configuration worth aiming at.
Push or pull? And what happened to the agent's own GitOps
Everything above is push-based: the pipeline holds the deployment logic and drives kubectl.
The agent used to have its own pull-based GitOps mode, configured with a gitops key in config.yaml, where agentk reconciled manifests from a repository by itself. That is gone. It was deprecated in GitLab 16.2 and the module was removed in GitLab 18.0. If you find a tutorial showing a gitops: block in agent configuration, it describes a feature that no longer exists.
The replacement is Flux plus the agent, with a clear division of labour:
- Flux does the reconciling. It is the thing that keeps cluster state synchronised with the source.
agentksimplifies the Flux setup, manages cluster-to-GitLab access, and surfaces cluster state in the GitLab UI.
The agent detects Flux GitRepository objects and wires up reconciliation for them. You can also point Flux's webhook receiver at a custom endpoint so a push triggers immediate reconciliation instead of waiting for the next interval:
flux:
webhook_receiver_url: http://webhook-receiver.another-flux-namespace.svc.cluster.localSo the real choice is not "agent push or agent pull" — it is whether you drive deployments from the pipeline or run a GitOps controller alongside the agent. That is the same argument as Argo CD versus Flux, one layer up:
Push (pipeline drives kubectl) | Pull (Flux reconciles) | |
|---|---|---|
| Drift correction | None — cluster stays wrong until the next pipeline | Continuous |
| Deploy visibility | Full pipeline log, one place | Split between git and Flux logs, plus the agent's cluster view |
| Ordering with build steps | Trivial — same pipeline | Needs image tag propagation |
| Blast radius of a bad commit | Gated by pipeline rules and manual jobs | Applied as fast as reconciliation runs |
| Works when GitLab is down | No | Yes, keeps reconciling |
Push is easier to reason about and integrates naturally with build steps. Pull gives you drift correction and survives your CI provider having a bad day. Most teams start with push because it is a smaller step from what they already have, and that is a defensible place to stay. If you want the pull model, that means running Flux — or Argo CD — rather than expecting the agent to do it.
The four things that bite
1. The config file is on the wrong branch. ci_access — and every other agent setting — is read from the default branch only. Merge it.
2. The context name is wrong. <path/to/agent/project>:<agent-name>. The project path is the agent's own project, not the project running the pipeline. These are frequently different, and the error message will not point you at this.
3. kubectl isn't in the image. The agent gives you a tunnel, not a toolchain. Your job's image needs kubectl (and helm, if you use it) at a version compatible with your cluster. Pin it — a latest tag that drifts two minor versions ahead of your API server produces deprecation failures that look like cluster problems.
4. Group-level ci_access is broader than it looks. It covers every project in the group and every project added later. On a group that maps to a whole department, that is a lot of repositories with production cluster access, granted silently.
When this is the wrong tool
Be honest about the cases where the agent is overhead:
- A single cluster, one team, one repository. You will spend more time on agent registration than you save. A scoped service account and a stored kubeconfig is defensible at this size, as long as you are honest that it is a stopgap.
- You are already all-in on a GitOps controller. If Argo CD or Flux already reconciles your cluster from git, adding a push path from GitLab gives you two systems that can both change production and will eventually disagree. Pick one writer.
- Ephemeral clusters per merge request. The agent is a long-lived, registered object. Clusters that live for twenty minutes fight that model. Provision credentials dynamically instead.
Frequently asked questions
Do I need one agent per cluster?
Yes — agentk runs inside a cluster and represents that cluster. Multiple clusters means multiple agents. You can register them all in a single project (each gets its own .gitlab/agents/<name>/ directory), which keeps cluster configuration in one reviewable place and makes the context names predictable.
Can several projects share one agent?
Yes. An agent is registered in one project but its connection can be shared with other projects, groups, and users. For CI/CD specifically, that sharing is the ci_access block in the agent's config.yaml. Grant projects where you can and groups only when you genuinely mean every repository beneath it.
Why does my pipeline say the context does not exist?
Almost always one of two things: the ci_access grant is not on the agent project's default branch, or the context string is wrong. The format is <path/to/agent/project>:<agent-name> — the project path belongs to the agent's project, and the agent name is the directory name under .gitlab/agents/. Run kubectl config get-contexts in the job to see exactly what GitLab injected.
Is the certificate-based cluster integration still usable?
It has been superseded by the agent, and GitLab documents a migration path from the legacy certificate-based integration to it. Treat any remaining certificate-based setup as technical debt with a deadline rather than a supported architecture, and plan the move.
Does the agent replace Argo CD or Flux?
No. It used to have a built-in GitOps mode that overlapped with them, but that was deprecated in GitLab 16.2 and removed in 18.0. The current model is explicitly complementary: Flux reconciles, and the agent simplifies Flux setup, manages cluster access, and visualises cluster state in GitLab. If you already run Argo CD, keep it and use the agent purely as a CI/CD tunnel — what you must avoid is two systems writing to the same Deployment.
How do I avoid storing cloud credentials in the pipeline?
Use id_tokens with a narrowly bound aud claim and configure the target service to trust GitLab as an OIDC provider. The job receives a JWT that expires with it, so there is no long-lived secret to rotate or leak. This is worth doing for the cloud provider, the registry, and the secret store — see secrets management patterns for how the cluster side of that fits together.
Where to go next
The shortest useful path: register one agent against a non-production cluster, put an empty config.yaml on the default branch, install agentk, then add ci_access for exactly one project and get a kubectl get pods job green. That sequence exercises every identifier that can be wrong, on a cluster where being wrong costs nothing.
Once that works, the rest is ordinary pipeline design — and the interesting decisions move to whether you want push or pull, how you structure deployments across environments, and how you keep the credential surface at zero.
Official References
- GitLab CI/CD documentation — pipeline syntax, runners and the Kubernetes agent
- AWS IAM User Guide — policies, roles and trust relationships
- IAM roles for service accounts — how IRSA maps a Kubernetes SA to an IAM role
See also
Was this article helpful?
Be the first to rate this article
Related Topics
Found this useful? Share it.


