Deploy applications to Kubernetes without opening the API server to the public. You'll connect your cluster via the GitLab Agent, run kubectl and helm upgrade directly from the pipeline, and apply GitOps patterns with ArgoCD and Flux.

In the previous episode 15 we deployed applications to Linux servers via SSH and Ansible. Now we go up a level: Kubernetes. The difference is fundamental — we're no longer talking about one host with one process manager, but a cluster that can move pods between nodes, perform rolling updates, and keep applications alive. But this cluster also has a much larger attack surface: the Kubernetes API server is a door that, if opened to the internet, can be targeted by scanning bots within minutes.
That's why the "old" method — exposing the API server to a public IP and placing a kubeconfig on runners — is no longer recommended. This episode covers the GitLab Agent for Kubernetes (KAS): a secure bridge between GitLab and your cluster, how to deploy manifests and Helm charts through the pipeline, and how ArgoCD and Flux round out the GitOps flow.
Imagine your house: the Kubernetes API server is its front door. Anyone who can touch that door can try picking the lock or knocking repeatedly (brute force). Keeping the door closed is a fundamental principle: the control plane and worker nodes should be on a private network, and kubeconfig credentials should never stay in runners used by many projects.
The GitLab Agent solves this with an outbound connection pattern. The agent runs as a pod inside the cluster and makes outbound connections to the GitLab server — not the other way around. The firewall keeps the API server port closed to the internet, yet GitLab pipelines can still send kubectl commands to the cluster. Think of a call center: instead of your team having to come to the company building, the company's staff are ready to be reached over an always-open channel.
| Aspect | kubeconfig + Public API Server | GitLab Agent (KAS) |
|---|---|---|
| Connection direction | Inbound from runner to cluster | Outbound from cluster to GitLab |
| API server port | Open to the internet | Closed from the internet |
| Credentials | kubeconfig stored on the runner | Agent token in a Kubernetes Secret |
| Audit trail | Minimal | Visible on the GitLab agent page |
The first step is registering the agent in GitLab. Open Operate → Kubernetes clusters, select Connect a cluster, and choose the project that will use the agent. GitLab generates an agent token. Agent configuration is stored in the repository as a YAML file:
ci_access:
projects:
- id: my-group/my-appThe ci_access block determines which projects are allowed to use this agent through CI. Without this block, no project can call kubectl from the pipeline — this is what keeps the agent usable only by authorized teams.
Next, install the agent in the cluster using the official Helm chart:
helm repo add gitlab https://charts.gitlab.io
helm repo update
helm upgrade --install my-agent gitlab/gitlab-agent \
--namespace gitlab-agent \
--create-namespace \
--set config.kasAddress=wss://kas.gitlab.com \
--set config.token=<isi-dengan-token-agent>The config.kasAddress value is the GitLab KAS server address — for GitLab.com it's wss://kas.gitlab.com, while for a self-hosted installation, adjust it to your KAS domain. Once the agent pod appears in the gitlab-agent namespace, the agent status on the Operate page changes to Connected.
Warning
The agent token is privileged credentials. Don't commit the token to a repository, don't put it in logs, and don't write it directly in the install command — use a Kubernetes Secret and reference it via config.secretName. If the token leaks, revoke it on the agent page and repeat the installation.
With the agent connected, GitLab pipelines automatically receive the KUBE_CONTEXT and KUBE_NAMESPACE variables — as long as the job runs in a project with ci_access access. These are the magic keys that make kubectl work without storing a kubeconfig anywhere:
deploy_manifest:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl apply -f k8s/deployment.yaml
- kubectl rollout status deployment/my-app --timeout=120s
environment:
name: production
url: https://myapp.example.comNotice there's no kubectl config set-cluster or kubeconfig file upload — everything is handled through the agent's built-in context. The kubectl rollout status command makes the job wait until the Deployment is truly stable, so if the new image fails to start (CrashLoopBackOff), the job fails and the pipeline stops before declaring success.
For applications with many components (service, ingress, configmap, secret), Helm is a much cleaner choice. We can use the same agent to run helm upgrade --install:
deploy_helm:
stage: deploy
image: alpine/helm:latest
variables:
KUBE_CONTEXT: my-group/my-app:prod-agent
KUBE_NAMESPACE: production
script:
- helm dependency update ./chart
- helm upgrade --install my-app ./chart
--namespace production
--set image.tag=$CI_COMMIT_SHA
environment:
name: production
url: https://myapp.example.comThe KUBE_CONTEXT variable format is path/main-project:<agent-name> — in this example project my-group/my-app uses the prod-agent agent. The release name is my-app, the chart is in the ./chart folder, and the image tag comes from $CI_COMMIT_SHA so every commit produces a unique version. Because --install is included, the same command works for both the first deploy and subsequent upgrades.
Tip
Use one agent per important environment (for example separate agents for staging and production). That way each agent's ci_access boundary can differ, and a mistake in staging never grants access to production.
Besides deploying directly from the pipeline (push-based), there's the GitOps pattern: the Git repository becomes the single source of truth for the cluster state. Operators inside the cluster pull changes from Git, rather than the pipeline pushing them to the cluster.
ArgoCD and Flux CD are the two most popular implementations. Both can take their source directly from a GitLab repository:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
namespace: argocd
spec:
project: default
source:
repoURL: https://gitlab.com/my-group/my-app.git
targetRevision: main
path: helm-chart
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: trueArgoCD will compare the cluster state with the state in the GitLab repository. Any change merged into main is pulled and synced to the cluster — making GitLab the home of the application code while also serving as the "remote config" for the cluster.
Flux CD uses similar custom resources named GitRepository and Kustomization, with the same principle. This is where the push-based pattern (helm upgrade from the pipeline) and the pull-based pattern (GitOps operators) can be combined: the pipeline keeps building images and tagging versions, while the GitOps operator decides when to apply that version — full control in the operator's hands, not a bot's.
This episode connected GitLab to Kubernetes in a secure way:
ci_access config in .gitlab/agents/my-agent/config.yaml restricts who may use the agent.KUBE_CONTEXT and KUBE_NAMESPACE variables make kubectl and helm work without a kubeconfig on the runner.Now you can deploy to Kubernetes. But deploying alone isn't enough — big releases still carry risk. In the next episode 17 we cover progressive delivery: canary and blue-green deployments, plus automatic rollback when metrics warn of failure. See you there!