In this episode we discuss continuous deployment to Kubernetes and Helm, starting from configuring kubeconfig securely with the withKubeConfig step to automatic Helm chart releases. We also learn GitOps integration with ArgoCD and Flux so that deployments always stay in sync with the Git repository.

In episode 15 we delivered the application to a Linux server via SSH and Ansible — a classic approach that remains valid for virtual machines. Now it is time to level up: we deploy to a Kubernetes cluster with Helm, the combination used by almost every company running on containers. Kubernetes provides rolling updates, self-healing, and a uniform environment across nodes. Helm lets dozens of YAML manifest lines be packaged, versioned, and released like a regular application — think of Helm as apt-get for Kubernetes.
In this episode we cover three main things:
withKubeConfig step and kubectl apply.helm upgrade --install.After a successful build and a pushed Docker image, the application still has to be deployed. Manually deploying to hundreds of pods with kubectl run is clearly not scalable. Kubernetes gives us:
Meanwhile Helm solves the problem of repetitive YAML manifests across environments. One chart can serve both staging and production, differing only in the values file. This is analogous to templating, but specifically for infrastructure.
Note
Prerequisites for this episode: a Kubernetes cluster accessible from the Jenkins agent, and kubectl and helm installed on the agent. If not yet present, add both tools to the Docker agent image as we discussed in episode 8.
Access to the cluster is controlled by the kubeconfig file, which contains the server address, token, and context. This file is the key to our house — if it leaks, anyone can destroy the production cluster. That is why a kubeconfig must never be copied into the repository or workspace.
The safe way in Jenkins is to store it as a Secret file credential. In Manage Jenkins then Credentials, add the kubeconfig from the production cluster's ~/.kube/config file as a Secret file with the ID kubeconfig-prod.
Warning
A kubeconfig contains a token equivalent to a cluster password. Make sure this credential is only visible to the jobs that need it, and restrict the context inside it to the required namespace — do not use the cluster admin context for ordinary pipelines.
The Kubernetes CLI plugin provides the withKubeConfig step, which writes the kubeconfig from the credential to a temporary file, then removes it when the block finishes. We do not need to bother exporting variables or storing files in the workspace.
stage('Deploy ke Kubernetes') {
steps {
withKubeConfig(credentialsId: 'kubeconfig-prod', serverUrl: 'https://k8s-prod.example.com') {
sh 'kubectl apply -f k8s/ --namespace production'
}
}
}Let's unpack what happens:
credentialsId points to the Secret file we created earlier.serverUrl tells the step which cluster server is being used.kubectl apply command automatically reads the kubeconfig from that temporary file.The command kubectl apply -f k8s/ applies all manifests in the k8s/ folder — Deployment, Service, and Ingress. This command is idempotent, so it is safe to run repeatedly.
Do not apply straight to production without validation. Add a dry-run as a smoke test:
kubectl apply -f k8s/ --namespace production --dry-run=client -o yamlThis step ensures the YAML is valid and its schema is recognized by Kubernetes. Only then perform the real apply.
After the basic manifests are running, we move to Helm. The core principle: helm upgrade --install is idempotent — it installs if not present, and upgrades if already present. One command for both needs.
helm upgrade --install my-app ./chart \
--namespace production \
--values values.production.yaml \
--set image.repository=registry.example.com/my-app \
--set image.tag=1.0.42The --values flag uses a per-environment configuration file, while --set can override specific values, such as the image tag from the build. In Jenkins, the tag can be taken from the BUILD_NUMBER environment variable so every build produces a new release.
stage('Release Helm Chart') {
steps {
sh 'helm upgrade --install my-app ./chart \
--namespace production \
--values values.production.yaml \
--set image.tag=1.0.${BUILD_NUMBER}'
}
}Jenkins interpolates the BUILD_NUMBER value in that command before passing it to the shell. For charts from public repositories, first add them with helm repo add, then install directly by chart name.
Tip
Always store per-environment values (staging, production) in the repository as separate files, rather than overriding them manually from the console. This preserves the audit trail and makes releases reproducible.
Finally, we discuss the modern pattern: GitOps. The idea is that the Git repository becomes the single source of truth for the cluster state. Instead of Jenkins executing commands directly against the cluster, Jenkins simply updates the Git repository containing the manifests, then ArgoCD or Flux pulls that change and syncs the cluster. This architecture makes cluster operations fully auditable through commit history.
The workflow looks like this:
An example of Jenkins bumping the tag in the gitops repository:
git clone git@github.com:my-org/gitops-repo.git
cd gitops-repo
sed -i 's|imageTag: .*|imageTag: 1.0.42|' apps/my-app/values.yaml
git add apps/my-app/values.yaml
git commit -m "chore: bump my-app to 1.0.42"
git push origin mainBoth ArgoCD and Flux automatically read that change. If you want to trigger synchronization directly (without waiting for the polling interval), Jenkins can call their CLIs:
withCredentials([string(credentialsId: 'argocd-token', variable: 'ARGOCD_TOKEN')]) {
sh 'argocd login argocd.example.com --auth-token=$ARGOCD_TOKEN --grpc-web'
sh 'argocd app sync my-app --grpc-web'
}For Flux, the equivalent command is flux reconcile kustomization my-app.
| Aspect | ArgoCD | Flux |
|---|---|---|
| Sync trigger | argocd CLI or webhook | flux CLI or interval |
| Interface | Mature web UI | CLI plus optional dashboard |
| Backend | Application CRD | Kustomization and HelmRelease |
| Best fit | Teams that like visual UI | Teams that prefer full declarative |
Both are equally robust. Choose based on team culture: ArgoCD excels at visibility, Flux is more minimalist and purely declarative.
Important
In the GitOps pattern, Jenkins no longer needs cluster admin credentials for production deployments — only Git access to the gitops repository. This significantly narrows the attack surface.
In this episode we covered continuous deployment to Kubernetes and Helm:
withKubeConfig step uses the kubeconfig from a Secret file credential without storing it in the workspace.kubectl apply -f k8s/ applies manifests idempotently, and can be validated first with --dry-run.helm upgrade --install installs and upgrades a chart at once, with per-environment values.Automated deployment is great, but without notifications the team will not know whether a build succeeded or failed. In episode 17 we will integrate production notifications to Slack, Microsoft Teams, Telegram, and custom HTML email — so every build event reaches the right people immediately. See you there!