End-to-End GitOps: CI with GitHub Actions + CD with ArgoCD on Kubernetes
Most engineering teams have some form of CI — tests run on pull requests, Docker images get built. The CD side is where things fall apart: manual kubectl applies, Helm commands run by hand, no audit trail of who deployed what. GitOps with ArgoCD fixes this by making your Git repository the single source of truth for cluster state, with ArgoCD continuously reconciling what's running against what's committed.
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:
- Every deployment is a Git commit — you have a full history with author, timestamp, and diff.
- Drift detection — if someone manually changes a resource, ArgoCD detects the discrepancy and can auto-heal.
- Rollback is a
git revert. - Your CI system no longer needs cluster credentials.
Repository Structure
Separate your application code from your deployment manifests. A common pattern uses two repositories:
- app-repo — application source code; GitHub Actions runs CI here.
- gitops-repo — Kubernetes manifests or Helm values; ArgoCD watches this.
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:
- Sealed Secrets — encrypt secrets with a cluster-side public key; the encrypted
SealedSecretresource is safe to commit. The Sealed Secrets controller decrypts and creates the underlyingSecretat sync time. - External Secrets Operator — references AWS Secrets Manager or SSM Parameter Store directly. The ESO controller fetches the value and creates a Kubernetes
Secret. Nothing sensitive ever touches Git.
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