Topic 6.5
Deploying to Kubernetes: Push from CI vs GitOps
In one line
There are two common ways for a pipeline to deploy to Kubernetes. Push: CI authenticates to the cluster and runs kubectl apply, helm upgrade, or kustomize. GitOps (pull): CI only updates the desired state in a Git repository (image tag or digest), and a controller in the cluster (Argo CD, Flux) applies it and reports health. GitOps keeps cluster credentials out of CI and gives drift detection and an audit trail, at the cost of an extra repository and controller.
Think of it like this
Updating a shop's price list. Push: head office sends someone with the shop keys to change the prices themselves. Pull: head office updates the master price list, and the shop manager checks it every few minutes and updates the shop. The second way, head office never needs the shop's keys.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Push-based deployment
- CI connects to the cluster and applies changes directly.
- Pull-based deployment (GitOps)
- A controller in the cluster pulls desired state from Git and applies it.
- Config repository
- A Git repository holding deployment manifests per environment, separate from app code.
- Image automation
- Tools that detect new image versions and update manifests automatically.
- Sync
- A GitOps controller applying Git's desired state to the cluster.
Step by step
01Push: a CI job deploying with Helm
For the catering services, still on push deploys, the GitHub Actions job gets short-lived AWS credentials via OIDC, gets a kubeconfig for EKS, and runs Helm with --atomic. The CI role maps to a Kubernetes role that can only deploy into the catering namespace.
deploy:
runs-on: ubuntu-latest
environment: production
permissions: { id-token: write, contents: read }
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::111122223333:role/ci-deploy-catering
aws-region: ap-south-1
- run: aws eks update-kubeconfig --name tiffin-prod
- run: |
helm upgrade --install catering-api oci://ghcr.io/tiffin-team/charts/tiffin-service \
--version 1.6.0 -n catering -f deploy/values-prod.yaml \
--set image.tag=${{ github.sha }} --atomic --wait --timeout 5m02Pull: CI updates Git, Argo CD deploys
For Tiffin's main services, CI never touches the cluster. After building the image, it commits the new digest to the deploy repository's staging overlay. Argo CD syncs it, and CI waits for the result.
promote-staging:
needs: image
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
repository: tiffin-team/tiffin-deploy
token: ${{ secrets.DEPLOY_REPO_TOKEN }} # fine-grained: contents write on one repo
- run: |
cd apps/web/overlays/staging
kustomize edit set image ghcr.io/tiffin-team/web@${{ needs.image.outputs.digest }}
git config user.name tiffin-ci && git config user.email ci@tiffin.in
git commit -am "staging: web ${GITHUB_SHA::7}" && git push03Comparing the two
Both work. Tiffin chose GitOps for its main services and keeps push deploys for the acquired catering services until they migrate.
push (CI applies) pull (GitOps)
cluster credentials in CI (scoped, short-lived) only in the cluster
source of truth pipeline runs Git repository
drift detection none continuous, with self-heal
rollback rerun with old version git revert
audit trail CI logs Git history + controller events
setup simple config repo + controllerBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Push deploys fighting GitOps
A service managed by Argo CD (with self-heal) also has an old CI job that runs kubectl set image on every merge.
Myth vs fact
Myth
GitOps means no CI.
Fact
CI still builds, tests, scans, and publishes artifacts. GitOps replaces only the last step: applying changes to clusters.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
With push deploys, give CI a Kubernetes role limited to one namespace and the resource kinds it deploys, mapped from a short-lived OIDC identity. Never store a cluster-admin kubeconfig as a CI secret.
Remember this
- 1
Push model: the CI job has cluster credentials (ideally short-lived via OIDC and a narrowly scoped Kubernetes role) and applies manifests, then waits for
rollout status. - 2
Pull model (GitOps): CI builds the image and opens or merges a commit changing the image reference in a config repository. Argo CD or Flux in the cluster syncs it.
- 3
Separating repositories: app code repository (source, tests, Dockerfile) and config repository (manifests per environment), so deployment changes have their own review and history.
- 4
Image updates: CI edits the manifest (
kustomize edit set image,yq), or tools like Argo CD Image Updater / Flux image automation watch the registry and commit new tags. - 5
Feedback to CI: with GitOps, CI can wait on Argo CD (
argocd app wait) or rely on notifications, so a failed deploy still turns the pipeline red. - 6
Security: GitOps means CI never holds cluster-admin credentials. A compromised CI can only propose changes to Git, which branch protection and reviews still guard.
Explain it without notes
What's the main security difference between push and pull deployments?
How does CI know a GitOps deployment succeeded?
Practice
Deploy a service with a push job using short-lived credentials.
Change a pipeline to update a GitOps repository instead of applying manifests.
Trade-offs
- ↔
Push deploys are simple and immediate but put powerful credentials in CI. GitOps adds a repository and controller to run, in exchange for better security, drift correction, and a clear audit trail.
Done when you can
I can deploy from CI with scoped, short-lived credentials.
I can implement GitOps image promotion.
Each service has exactly one deployment path.