Skip to content
Codeloom
DevOps

GitOps with ArgoCD for Kubernetes

A hands-on guide to implementing GitOps with ArgoCD: declarative deployments, sync policies, and multi-environment management.

·6 min read · By Codeloom
Intermediate 11 min read

What you'll learn

  • What GitOps is and why it improves Kubernetes deployments
  • Installing and configuring ArgoCD
  • Creating Applications that sync from Git
  • Handling multi-environment setups with Kustomize overlays

Prerequisites

  • Basic Kubernetes knowledge (deployments, services, namespaces)
  • Familiarity with Git and YAML

What GitOps Is

GitOps is a deployment model where Git is the single source of truth for your infrastructure and application state. You describe the desired state of your cluster in YAML files stored in a Git repository. A controller running inside the cluster continuously compares the desired state (Git) to the actual state (cluster) and reconciles any drift.

The key shift is that you never run kubectl apply manually. You push a commit, and the controller applies it. You roll back by reverting a commit. Every change is auditable, reviewable, and reversible through Git history.

ArgoCD is the most widely adopted GitOps controller for Kubernetes. It provides a web UI, CLI, RBAC, health checks, and multi-cluster support.

Installing ArgoCD

# Create the namespace
kubectl create namespace argocd

# Install ArgoCD
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# Wait for pods to be ready
kubectl wait --for=condition=ready pod -l app.kubernetes.io/part-of=argocd -n argocd --timeout=120s

# Get the initial admin password
kubectl get secret argocd-initial-admin-secret -n argocd -o jsonpath='{.data.password}' | base64 -d

# Port-forward the UI
kubectl port-forward svc/argocd-server -n argocd 8080:443

Access the UI at https://localhost:8080. Log in with username admin and the password you extracted. Change the password immediately.

Install the CLI:

# macOS
brew install argocd

# Login
argocd login localhost:8080 --username admin --password <initial-password> --insecure

Repository Structure

A GitOps repo separates application manifests from application code. A common pattern:

gitops-repo/
  apps/
    api-service/
      base/
        deployment.yaml
        service.yaml
        hpa.yaml
        kustomization.yaml
      overlays/
        staging/
          kustomization.yaml
          replicas-patch.yaml
        production/
          kustomization.yaml
          replicas-patch.yaml
          resources-patch.yaml
    worker/
      base/
        deployment.yaml
        kustomization.yaml
      overlays/
        staging/
          kustomization.yaml
        production/
          kustomization.yaml

Base Manifests

# apps/api-service/base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-service
spec:
  replicas: 2
  selector:
    matchLabels:
      app: api-service
  template:
    metadata:
      labels:
        app: api-service
    spec:
      containers:
        - name: api
          image: registry.example.com/api-service:v1.0.0
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 512Mi
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10
# apps/api-service/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - deployment.yaml
  - service.yaml
  - hpa.yaml

Production Overlay

# apps/api-service/overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - ../../base
namespace: production
patches:
  - path: replicas-patch.yaml
  - path: resources-patch.yaml
# apps/api-service/overlays/production/replicas-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-service
spec:
  replicas: 5
# apps/api-service/overlays/production/resources-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-service
spec:
  template:
    spec:
      containers:
        - name: api
          resources:
            requests:
              cpu: 500m
              memory: 512Mi
            limits:
              cpu: "2"
              memory: 2Gi

Creating an ArgoCD Application

You can create applications through the UI, CLI, or declaratively with YAML. Declarative is the GitOps way:

# argocd/applications/api-service-production.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: api-service-production
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: default
  source:
    repoURL: https://github.com/your-org/gitops-repo.git
    targetRevision: main
    path: apps/api-service/overlays/production
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
    retry:
      limit: 3
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 1m

The important fields:

  • source.path points to the Kustomize overlay directory.
  • syncPolicy.automated enables automatic syncing when Git changes.
  • prune: true deletes resources from the cluster that no longer exist in Git.
  • selfHeal: true reverts manual kubectl changes to match Git.

Sync Waves and Hooks

When deploying, some resources must be created before others. A database migration job should run before the new deployment rolls out. Sync waves control ordering:

apiVersion: batch/v1
kind: Job
metadata:
  name: db-migrate
  annotations:
    argocd.argoproj.io/sync-wave: "-1"
    argocd.argoproj.io/hook: PreSync
    argocd.argoproj.io/hook-delete-policy: HookSucceeded
spec:
  template:
    spec:
      containers:
        - name: migrate
          image: registry.example.com/api-service:v1.1.0
          command: ["./migrate", "up"]
      restartPolicy: Never
  backoffLimit: 3

Lower sync wave numbers execute first. The PreSync hook runs the job before the main sync starts. HookSucceeded cleans up the job after it completes successfully.

The Deployment Workflow

With GitOps and ArgoCD in place, the deployment flow becomes:

  1. CI pipeline builds and pushes a new container image (api-service:v1.1.0).
  2. CI pipeline opens a pull request on the GitOps repo, updating the image tag in the Kustomize overlay.
  3. A teammate reviews the PR. The diff shows exactly what will change in the cluster.
  4. PR is merged. ArgoCD detects the change within its polling interval (default 3 minutes) or via a webhook.
  5. ArgoCD applies the manifests. The deployment rolls out with Kubernetes’ rolling update strategy.
  6. ArgoCD reports the sync status. If health checks fail, the deployment is marked as degraded.

To roll back, revert the merge commit. ArgoCD syncs the previous state.

Image Updater

Manually updating image tags in Git is tedious. ArgoCD Image Updater automates this:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: api-service-production
  annotations:
    argocd-image-updater.argoproj.io/image-list: api=registry.example.com/api-service
    argocd-image-updater.argoproj.io/api.update-strategy: semver
    argocd-image-updater.argoproj.io/api.allow-tags: "regexp:^v[0-9]+\\.[0-9]+\\.[0-9]+$"
    argocd-image-updater.argoproj.io/write-back-method: git

Image Updater watches the container registry, and when a new semver tag appears, it commits the updated tag to the GitOps repo. The entire flow stays in Git.

App of Apps Pattern

Managing dozens of Application resources by hand does not scale. The App of Apps pattern creates a single root Application that manages all other Applications:

# argocd/root-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/your-org/gitops-repo.git
    targetRevision: main
    path: argocd/applications
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

Every YAML file in argocd/applications/ defines an Application. ArgoCD syncs them all. Adding a new service means adding one YAML file and pushing a commit.

Common Pitfalls

Storing secrets in Git. Even in a private repo, plaintext secrets in Git are a security risk. Use Sealed Secrets, External Secrets Operator, or Vault to inject secrets at runtime.

Disabling self-heal. Without self-heal, someone can kubectl edit a deployment and the change persists until the next Git push. This breaks the GitOps contract.

Monolithic GitOps repos. If every team commits to the same repo, merge conflicts and slow CI become problems. Consider one repo per team or per domain.

Start with one non-critical service. Set up the GitOps repo, create the ArgoCD Application, and deploy through a Git push. Once the workflow is comfortable, expand to more services and enable automated sync.