The GitOps Model

Traditional CD pushes changes to a cluster: a pipeline runs kubectl apply or helm upgrade and the cluster state lives in the pipeline's memory. GitOps flips this: the desired state lives in Git, and a controller running inside the cluster pulls and reconciles continuously.

This gives you several properties that are hard to achieve with push-based CD:

Repository Structure

Separate your application code from your deployment manifests. A common pattern uses two repositories:

Keeping them separate means a new image tag (from CI) triggers a manifest update commit in the gitops-repo, which ArgoCD picks up — without mixing application history with deployment history.

Step 1: The GitHub Actions CI Pipeline

This workflow triggers on pushes to main, builds and tests the app, pushes the Docker image to ECR, then updates the image tag in the gitops-repo:

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]

env:
  AWS_REGION: ap-south-1
  ECR_REPOSITORY: my-app
  IMAGE_TAG: ${{ github.sha }}

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      id-token: write   # required for OIDC
      contents: read

    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-ecr
          aws-region: ${{ env.AWS_REGION }}

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Run tests
        run: |
          docker build --target test -t $ECR_REPOSITORY:test .
          docker run --rm $ECR_REPOSITORY:test

      - name: Build and push image
        run: |
          ECR_REGISTRY=${{ steps.login-ecr.outputs.registry }}
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG

      - name: Update image tag in gitops-repo
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITOPS_PAT }}
          script: |
            const { execSync } = require('child_process');
            execSync('git clone https://x-access-token:${{ secrets.GITOPS_PAT }}@github.com/org/gitops-repo.git');
            process.chdir('gitops-repo');
            execSync(`sed -i 's|image: .*my-app:.*|image: ${{ steps.login-ecr.outputs.registry }}/my-app:${{ env.IMAGE_TAG }}|' apps/my-app/deployment.yaml`);
            execSync('git config user.email "ci@omphoratech.com"');
            execSync('git config user.name "GitHub Actions"');
            execSync(`git commit -am "chore: update my-app to ${{ env.IMAGE_TAG }}"`);
            execSync('git push');

Use OIDC (id-token: write) instead of long-lived AWS access keys. OIDC issues short-lived credentials per workflow run — no secrets to rotate or accidentally leak.

Step 2: Install ArgoCD on Your Cluster

ArgoCD runs as a set of Kubernetes controllers and a UI. Install it into a dedicated namespace:

kubectl create namespace 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=available deployment \
  -l app.kubernetes.io/name=argocd-server \
  -n argocd --timeout=120s

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

For production, put the ArgoCD server behind an ingress or AWS Load Balancer. For initial setup, port-forward is fine:

kubectl port-forward svc/argocd-server -n argocd 8080:443

Step 3: Define an ArgoCD Application

An Application resource tells ArgoCD which Git repo and path to watch, and which cluster/namespace to deploy into:

# argocd-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/org/gitops-repo.git
    targetRevision: main
    path: apps/my-app
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true       # delete resources removed from Git
      selfHeal: true    # revert manual changes in cluster
    syncOptions:
      - CreateNamespace=true
kubectl apply -f argocd-app.yaml

With automated.selfHeal: true, ArgoCD checks every 3 minutes and reconciles any drift. With prune: true, resources deleted from Git get deleted from the cluster — preventing ghost resources from accumulating.

Step 4: Kubernetes Manifests in the GitOps Repo

The gitops-repo needs standard Kubernetes manifests. A minimal deployment:

# apps/my-app/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  namespace: production
spec:
  replicas: 2
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: my-app
          image: 123456789012.dkr.ecr.ap-south-1.amazonaws.com/my-app:abc1234
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
            limits:
              cpu: "500m"
              memory: "512Mi"
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10

Environment Promotion

For multiple environments, use separate directories or branches in the gitops-repo:

apps/
  my-app/
    base/           # shared manifests
      deployment.yaml
      service.yaml
    overlays/
      dev/          # kustomization.yaml with dev-specific patches
      staging/
      production/

Kustomize overlays let you patch image tags, replica counts, resource limits, and config maps per environment without duplicating the full manifest. ArgoCD has native Kustomize support — set path: apps/my-app/overlays/production in the Application spec.

Health Checks and Notifications

ArgoCD surfaces health status for standard Kubernetes resources out of the box (Deployments, StatefulSets, Services, Ingresses). For custom resources, define a Lua health check script. Connect Slack or PagerDuty via the argocd-notifications add-on to alert on sync failures or degraded health.

Secrets Management

ArgoCD syncs everything in Git — which means secrets cannot live in Git as plaintext. The two most common solutions:

Need a production-ready CI/CD pipeline?

We design and implement GitOps workflows with GitHub Actions and ArgoCD, including multi-environment promotion, secrets management, and observability.

Talk to Us