From a passing build to a running pod
A pipeline is only valuable once its output reaches the cluster reliably. Many teams get as far as “the image is built” and then deploy with a script on someone’s laptop, a kubectl apply of hand-edited YAML, or a long-lived cluster admin kubeconfig stored in CI. These approaches work until a person is on holiday, a cluster is rebuilt, or an auditor asks who changed what.
This page covers how we connect CI/CD pipelines to Kubernetes using Helm, Argo CD or both, and how to decide between the two main models.
Two models: push and pull
Push-based deployment. The pipeline holds credentials, connects to the cluster and runs helm upgrade --install. It is easy to understand and quick to set up. The pipeline needs network access and permissions to the cluster, which makes it a high-value target. Drift is not corrected: if someone edits a resource by hand, nothing notices.
Pull-based deployment (GitOps). The pipeline never touches the cluster. It builds the image and commits the new version to a configuration repository. A controller in the cluster, such as Argo CD or Flux, notices the change and applies it. Git becomes the source of truth, and the controller reverts manual changes. See GitOps for the concept in more detail.
| Aspect | Push (Helm from CI) | Pull (Argo CD) |
|---|---|---|
| Setup effort | Low | Moderate |
| Cluster credentials in CI | Required | Not required |
| Drift correction | None | Continuous |
| Audit trail | Pipeline logs | Git history |
| Multi-cluster at scale | Gets awkward | Designed for it |
| Extra component to run | No | Yes |
We do not favour GitOps as dogma. For one cluster, one team and a handful of services, push with Helm and a tightly scoped role is perfectly sound. Pull earns its keep with multiple clusters, several teams, compliance requirements, or a history of drift problems. Our GitOps service goes deeper on that path, and the Argo CD on EKS tutorial shows a working setup.
Structuring Helm charts
A Helm chart bundles templates for Deployments, Services, Ingress and so on, with configuration supplied through values. The structure that causes least pain:
- One chart per service, or one shared base chart that many services use with different values. Shared charts reduce duplication but tie services to one release cadence, so version the chart.
- Per-environment values files (
values-staging.yaml,values-production.yaml) layered over defaults. Environments differ in values, not in templates. - Sensible defaults for safety: resource requests and limits, readiness and liveness probes, a PodDisruptionBudget and a non-root security context.
- Lint and test in CI:
helm lint,helm templatepiped to a schema validator such as kubeconform, and optionally unit tests for templates.
# values-production.yaml
image:
repository: 123456789012.dkr.ecr.eu-west-1.amazonaws.com/checkout
digest: sha256:REPLACED_BY_PIPELINE
replicaCount: 4
resources:
requests: { cpu: 250m, memory: 256Mi }
limits: { memory: 512Mi }
The digest value is filled in by the pipeline when it promotes a build. A real digest is a 64-character hash, so the placeholder above is only a marker for illustration.
Push deployment with guardrails
If you choose push, the pipeline should deploy like this:
helm upgrade --install checkout ./charts/checkout \
--namespace shop \
--values charts/checkout/values-production.yaml \
--set image.digest="$IMAGE_DIGEST" \
--atomic --timeout 5m
with the following protections around it: credentials obtained by OIDC and mapped to a Kubernetes role limited to the namespace, the job gated by a protected environment, and the --atomic flag to revert a failed upgrade. Namespaced roles matter. A pipeline that can only touch shop cannot damage the rest of the cluster, whereas one with cluster-admin can.
Pull deployment with Argo CD
With Argo CD, an Application declares which repository path should be synced to which cluster and namespace:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: checkout-production
namespace: argocd
spec:
project: production
source:
repoURL: https://github.com/your-org/deploy-config.git
targetRevision: main
path: apps/checkout/production
destination:
server: https://kubernetes.default.svc
namespace: shop
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
selfHeal reverts manual changes and prune removes resources deleted from Git. For production we sometimes turn automated sync off and keep a manual sync step as the approval, which fits with promotion gates. For many services and clusters we use ApplicationSet to generate Applications from a template rather than hand-writing each.
The CI side is then small: after the image is pushed, a job updates the tag or digest in the configuration repository (directly for dev, through a pull request for production). The pull request itself is the approval.
Handling the awkward parts
Database migrations. Run them as a Helm hook or an Argo CD pre-sync job, and write them to be backward-compatible so a rollback is possible. See automated rollback.
Secrets. Never commit plaintext. Use an operator that syncs from your secret store or an encrypted-in-Git approach, covered in secrets management.
Ordering. Use sync waves or hooks when resources depend on each other, for example CRDs before the resources that use them.
Config repository layout. A separate repository for deployment configuration keeps application commits and deploy commits apart, and permits different review rules. A monorepo of both is simpler for small teams.
Decisions and trade-offs
Helm versus Kustomize. Helm is better for distributing configurable packages and has a broad ecosystem. Kustomize is simpler for patching plain manifests and has no templating language. Many teams use Helm for third-party software and either for their own.
Argo CD versus Flux. Both implement GitOps well. Argo CD offers a UI that developers appreciate, and Flux is lighter and more composable. We choose by team preference rather than ideology.
When not to adopt GitOps. If your team has two engineers, one cluster and rare deploys, the controller, repository structure and extra concepts may cost more than they save.
Common failure modes
- Cluster-admin kubeconfig stored in CI.
- Mutable tags causing nodes to run different versions of the same tag.
- Charts with no resource limits or probes.
- Manual
kubectl editin production that GitOps then reverts, surprising the person who made it. - Rollback in the cluster only, immediately undone by the controller.
- One giant values file for all environments.
Delivery and handover
We agree the model, build charts or review yours, wire CI to deployment, and install and configure Argo CD if chosen. Everything is stored in your repositories, and the Kubernetes side often links with our Kubernetes consulting work. You receive an architecture diagram, a how-to for adding a service, and a rehearsed promotion and rollback.
How to tell it is working
Look at deployment frequency, lead time from merge to running in production, the proportion of deploys that need manual intervention, drift detected and corrected, and time to restore after a bad release. If a new service can go live by adding a values file and an Application, and nobody needs cluster credentials on their laptop to ship, the model is working. To discuss which fits your team, book a consultation.