DEV Community

Mubashir Taj
Mubashir Taj

Posted on

GitOps with ArgoCD: Pull-Based Deploys Explained

Most deployment pipelines push. A CI job builds an image, then runs a command that reaches into the cluster and applies it. That works until the day it does not, and the failure modes are familiar: a deploy script that only exists on one person's laptop, a kubectl command run from the wrong terminal, a cluster state that no longer matches anything written down anywhere. GitOps replaces the push with a pull. The cluster watches a Git repository and makes itself match what the repository says. The deploy is no longer an action someone performs. It is a state the system converges toward.

The definition is simple enough to state in one sentence. Git is the single source of truth for the desired state of the system, and an automated agent inside the cluster continuously reconciles the live state with that desired state. Everything else in GitOps follows from that sentence. If a change is not in Git, it is not a real change. If a change is in Git, it will reach the cluster without anyone running a deploy command.

ArgoCD is the most widely used implementation of that idea for Kubernetes, and it helps to understand what it actually does before reaching for its configuration. ArgoCD runs inside your cluster as a set of controllers. Its Application controller watches the repositories you register and the Application resources you define. For each application it compares two things: the manifests rendered from Git, and the live resources currently in the cluster. When they differ, the application is out of sync, and ArgoCD can either report that fact or fix it, depending on the sync policy you chose. That comparison loop is the whole product. Everything else, the UI, the CLI, the API, is a way of looking at the loop or steering it.

The first concept to get right is the Application resource. It is a small Kubernetes custom resource that tells ArgoCD three things: where the desired state lives, what it should be applied to, and how strictly to enforce it. Here is a minimal example that deploys a service from a Helm chart stored in Git.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: checkout-api
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/example/platform.git
    targetRevision: main
    path: apps/checkout-api
  destination:
    server: https://kubernetes.default.svc
    namespace: checkout
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
Enter fullscreen mode Exit fullscreen mode

Each field carries a decision. The source points at a repository, a branch, and a path inside it, so the desired state is a specific directory at a specific revision. The destination names the cluster and namespace. The sync policy says what ArgoCD should do when it finds drift. With automated sync enabled, a commit to main is enough to trigger a deploy. With prune enabled, resources that were deleted from Git are deleted from the cluster. With self heal enabled, a manual change made directly in the cluster is reverted on the next reconciliation. Those last two options are what turn ArgoCD from a deployment tool into a guarantee: the cluster cannot quietly drift away from the repository.

This pull model changes the security picture in a useful way. In a push model, the CI system needs credentials that can write to the cluster, which means the credentials for production live in the same system that runs untrusted pull request builds. In a pull model, the cluster reaches out to Git and pulls. The CI system only needs permission to write to a Git repository, which is a far smaller blast radius. The cluster's credentials never leave the cluster. This is one of the strongest practical arguments for GitOps, and it is independent of any tooling preference. It follows from the direction of the connection.

The second concept is the separation between the application code repository and the configuration repository. A common beginner mistake is to put Kubernetes manifests in the same repository as the application source and let CI update image tags there. That works, but it mixes two concerns with different review rules and different owners. The cleaner pattern is two repositories, or at least two clearly separated directories with different ownership. The application repository holds source code and a CI pipeline that builds and publishes an image. The configuration repository holds manifests, and the only thing CI writes into it is the new image tag. A deploy is therefore a small, reviewable commit that changes one line in one file, and the full history of what ran in production is the Git history of that repository. Rollback becomes a revert, which is a workflow every developer already knows.

The third concept is the app of apps pattern, which solves the problem of managing ArgoCD itself with GitOps. If every application is defined by an Application resource, then those resources can themselves live in a Git repository, managed by a single parent Application. The parent watches a directory of Application manifests and creates the children. Adding a new service to the platform becomes a pull request that adds one file to that directory. This is how teams scale from three applications to three hundred without the ArgoCD configuration becoming a manual chore. The bootstrap problem, how the first Application gets created, is solved once by hand, and everything after that is declarative.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root-apps
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/example/platform.git
    targetRevision: main
    path: argocd/apps
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
Enter fullscreen mode Exit fullscreen mode

A question that comes up immediately is secrets. Git is a terrible place for plaintext secrets, and GitOps does not change that. The standard answers are Sealed Secrets, which encrypt secrets into the repository so only a controller in the cluster can decrypt them, or the External Secrets Operator, which keeps secrets in a vault and syncs them into the cluster at runtime. Either way the rule is the same: the repository holds a reference to a secret, never the secret itself. Teams that ignore this rule usually discover the problem when a repository becomes public or a contractor gains read access, and by then the rotation work is painful. Decide the secrets strategy before the first application goes live, not after.

Drift detection deserves a closer look, because it is where GitOps earns its keep in day to day operations. Someone will, at some point, run kubectl edit against a production deployment to test a quick fix and forget to revert it. In a push model that change lives on silently until the next deploy overwrites it or, worse, until nobody remembers it existed. With self heal enabled, ArgoCD notices the difference within minutes and reverts it, and the event is visible in the ArgoCD UI and in its logs. The manual change is not punished. It is simply not durable. That property makes the cluster boring in the best sense: the live state is always explainable from the repository, and an incident review can start from Git history instead of from guesswork.

Environments are the next natural question. The usual approach is one directory per environment, with a shared base and per environment overlays, using Kustomize or Helm values. A staging directory points at the same charts with different replicas, resource limits, and configuration values. Promotion from staging to production is a pull request that copies the tested image tag from one directory to another, which gives you a review step and an audit trail for free. Avoid the temptation to give each environment its own branch. Branches drift apart and merge conflicts in YAML are miserable. One branch, multiple directories, is the pattern that survives contact with a real team.

There are limits, and honest coverage of them matters. GitOps is a poor fit for state that is inherently dynamic, like database migrations that must run exactly once in order, or secrets that rotate on their own schedule. ArgoCD has sync hooks and waves for ordering, and tools like Argo Rollouts add progressive delivery, but a migration that needs a human decision is better handled outside the sync loop. GitOps is also not a substitute for observability. Knowing that the cluster matches Git tells you nothing about whether the application is healthy. Pair ArgoCD with monitoring and alerting, and treat a sync failure as a page worthy event, not a UI badge to glance at later.

Getting started is deliberately small. Install ArgoCD in a test cluster, register one repository, create one Application with manual sync first, and watch the comparison view until you trust what it reports. Then enable automated sync, add prune and self heal, and move one low risk service to the pull model. The team will learn the workflow from that one service: change the manifest, open a pull request, get a review, merge, watch the sync. After a week of that, expanding to the rest of the platform is a mechanical exercise, because the hard part was never the tool. It was agreeing that Git is the truth, and then letting the cluster enforce it.

The payoff is quiet but real. Deploys stop being events that need a runbook and a war room. They become commits. Rollbacks become reverts. The question "what is running in production right now?" stops being a research project and becomes a git log command. That is what pull based deploys buy you, and ArgoCD is a solid, proven way to get there.

Top comments (0)