I've done this migration three times now. Each time, I learn something new about what NOT to do. This guide is the documentation I wish I'd had the first time: the complete migration from a multi-stage GitHub Actions deployment pipeline to a fully declarative GitOps workflow with ArgoCD including the parts every other tutorial glosses over.
What you'll build: Production ready ArgoCD installation, ApplicationSet for multi cluster management, Image Updater for automated rollouts, RBAC configuration for multi-team environments, Rollback procedures that work under pressure.
Prerequisites: Kubernetes cluster (1.24+), kubectl, Helm 3, existing GitHub Actions CI pipeline. Estimated time: 4-6 hours for learning setup (30 days for production migration).
Phase 1: Assessment — Mapping Your Existing GitHub Actions Pipeline
Every successful migration begins with clarity. Before introducing GitOps, the existing CI/CD topology must be dissected—methodically, not casually.
Start by inventorying workflows
# .github/workflows/deploy.yml
name: Deploy Service
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: docker build -t myapp .
deploy:
needs: build
steps:
- run: kubectl apply -f k8s/
This pipeline reveals implicit assumptions:
Deployment is tightly coupled with CI
Kubernetes manifests are applied imperatively
No state reconciliation exists
Map each component
| Component | Purpose |
|---|---|
| Build Step | Artifact creation |
| Deploy Step | Cluster mutation |
| Secrets | Runtime configuration |
The goal is not replication but transformation. GitOps demands declarative intent, not procedural execution.
Phase 2: ArgoCD Installation (Production-Grade Helm Values)
A default installation is rarely sufficient. Production demands rigor secure ingress, RBAC, and resource constraints.
Install ArgoCD using Helm
helm repo add argo https://argoproj.github.io/argo-helm
helm install argocd argo/argo-cd -n argocd --create-namespace -f values.yaml
A hardened values.yaml might resemble
server:
service:
type: ClusterIP
ingress:
enabled: true
hosts:
- argocd.example.com
configs:
params:
server.insecure: false
cm:
url: https://argocd.example.com
redis:
enabled: true
controller:
resources:
limits:
cpu: 500m
memory: 512Mi
Security is not ornamental. It is foundational. Misconfigured access at this stage invites future instability.
Phase 3: Structuring Your GitOps Repository
Git becomes the single source of truth. Structure, therefore, becomes paramount.
A canonical layout
gitops-repo/
├── apps/
│ ├── payments/
│ ├── auth/
├── environments/
│ ├── dev/
│ ├── staging/
│ └── prod/
└── infrastructure/
├── ingress/
└── monitoring/
Each directory encapsulates intent.
Example application manifest
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: payments-service
spec:
source:
repoURL: https://github.com/org/gitops-repo
path: apps/payments
targetRevision: HEAD
destination:
server: https://kubernetes.default.svc
namespace: payments
syncPolicy:
automated:
prune: true
selfHeal: true
Declarative. Predictable. Auditable.
Phase 4: ApplicationSet Configuration for Multi-Team
At scale, manually defining applications becomes untenable. ApplicationSet introduces dynamic orchestration.
A Git generator example
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: team-apps
spec:
generators:
- git:
repoURL: https://github.com/org/gitops-repo
revision: HEAD
directories:
- path: apps/*
template:
metadata:
name: '{{path.basename}}'
spec:
project: default
source:
repoURL: https://github.com/org/gitops-repo
targetRevision: HEAD
path: '{{path}}'
destination:
server: https://kubernetes.default.svc
namespace: '{{path.basename}}'
Teams simply add a folder. The system reacts autonomously.
This is where GitOps begins to feel almost sentient responsive, adaptive, yet deterministic.
Phase 5: Migrating Your First Service (with Rollback Plan)
Start small. Select a non-critical service. Minimize risk while maximizing insight.
Step 1: Remove Deploy Step from CI
# Keep build, remove kubectl apply
- run: docker build -t myapp .
Step 2: Push Kubernetes Manifests to GitOps Repo
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 2
Step 3: Let ArgoCD Sync
ArgoCD reconciles desired state with actual state.
Rollback Strategy
Rollback becomes trivial
git revert <commit-hash>
git push origin main
No manual intervention. No frantic patching. Just version control.
Phase 6: Image Updater for CI Integration
CI still builds images. But deployment is decoupled.
ArgoCD Image Updater bridges the gap
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
annotations:
argocd-image-updater.argoproj.io/image-list: myapp=repo/myapp
argocd-image-updater.argoproj.io/update-strategy: latest
Pipeline pushes image
docker push repo/myapp:1.2.3
Image Updater modifies Git
image: repo/myapp:1.2.3
ArgoCD syncs automatically.
Elegant. Autonomous.
Phase 7: RBAC and Multi-Tenant Configuration
As teams proliferate, governance becomes essential.
Define roles
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-rbac-cm
data:
policy.csv: |
p, role:dev, applications, get, */*, allow
p, role:dev, applications, sync, */*, allow
g, team-dev, role:dev
Namespace isolation
spec:
destination:
namespace: team-a
Each team operates within boundaries autonomous yet controlled.
Phase 8: Observability for Your GitOps Pipelines
Visibility transforms guesswork into certainty.
Enable metrics
controller:
metrics:
enabled: true
Scrape with Prometheus
- job_name: 'argocd'
static_configs:
- targets: ['argocd-metrics:8082']
Key metrics
Sync status
Reconciliation duration
Drift detection
Logs provide narrative
kubectl logs deployment/argocd-application-controller
Without observability, GitOps becomes a black box. With it, a glass box.
The 5 Things No Tutorial Mentions (Learned the Hard Way)
1. Git Becomes Your Bottleneck
Frequent commits can overwhelm repositories. Optimize branching strategies. Avoid unnecessary churn.
2. Drift Happens More Than Expected
Manual changes in clusters still occur. ArgoCD corrects them—but not always instantly. Expect transient inconsistencies.
3. Secrets Are a Persistent Challenge
Plain YAML is insufficient. Integrate tools like sealed secrets or external secret managers.
4. ApplicationSet Can Spiral into Complexity
Dynamic generation is powerful—but dangerous. Poor templates create cascading misconfigurations.
5. Human Behavior Is the Hardest Problem
Engineers accustomed to imperative deployments resist change. Training is not optional. It is essential.
Migrating to ArgoCD is not merely a tooling shift. It is a philosophical transition—from imperative control to declarative intent.
The rewards are substantial. Stability. Traceability. Confidence.
But the journey demands discipline. And a willingness to relinquish old habits in favor of something far more resilient.
Top comments (0)