DEV Community

Jamil Shaikh
Jamil Shaikh

Posted on

πŸš€ Building a GitOps Infrastructure Pipeline with Crossplane and Argo CD

From manual kubectl commands to fully automated infrastructure management - here's how I built a production-ready GitOps pipeline


TL;DR

I built a complete GitOps infrastructure management system using:

  • 🎯 Argo CD for GitOps automation
  • ⚑ Crossplane for infrastructure provisioning
  • πŸ”„ App-of-Apps pattern for scalable application management
  • πŸ“¦ MetalLB as the infrastructure example
  • 🎭 Sync waves for dependency management

Result: Infrastructure changes now happen through Git commits, with full automation and zero manual intervention.


The Problem I Solved

Managing Kubernetes infrastructure traditionally sucks:

# The old way - manual and error-prone kubectl apply -f metallb-config.yaml kubectl apply -f ingress-controller.yaml kubectl apply -f monitoring-stack.yaml # Oh no! Order matters... πŸ’₯ # Which version is running in production? πŸ€·β€β™‚οΈ # Who made that change? πŸ•΅οΈβ€β™‚οΈ 
Enter fullscreen mode Exit fullscreen mode

I wanted infrastructure that:

  • βœ… Lives in Git (version controlled)
  • βœ… Deploys automatically (no manual steps)
  • βœ… Handles dependencies (no ordering issues)
  • βœ… Self-heals (drift detection & correction)
  • βœ… Provides audit trails (who, what, when)

The Solution Architecture

graph LR A[Git Commit] --> B[Argo CD] B --> C[Crossplane] C --> D[Kubernetes Resources] B --> E[Sync Waves] E --> F[Ordered Deployment] 
Enter fullscreen mode Exit fullscreen mode

🎯 Step 1: App-of-Apps Pattern

Instead of managing 50+ individual Argo CD applications, I use the App-of-Apps pattern:

# One app to rule them all apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: homelab-root annotations: argocd.argoproj.io/sync-wave: "-1" spec: source: repoURL: https://github.com/jamilshaikh07/homelab-gitops.git path: apps # πŸ‘ˆ All child apps live here syncPolicy: automated: prune: true # πŸ—‘οΈ Clean up deleted resources selfHeal: true # πŸ”§ Fix manual changes 
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • One root app manages everything
  • New apps = just add YAML files
  • Automatic discovery and deployment

⚑ Step 2: Crossplane for Infrastructure

Crossplane lets me manage infrastructure through Kubernetes APIs. Here's the magic:

# Instead of direct kubectl apply... apiVersion: kubernetes.crossplane.io/v1alpha1 kind: Object metadata: name: metallb-ipaddresspool annotations: argocd.argoproj.io/sync-wave: "2" # πŸ‘ˆ Depends on provider spec: providerConfigRef: name: in-cluster forProvider: manifest: apiVersion: metallb.io/v1beta1 kind: IPAddressPool metadata: name: homelab-pool namespace: metallb-system spec: addresses: - 10.20.0.81-10.20.0.99 # 🎯 My LoadBalancer IP range 
Enter fullscreen mode Exit fullscreen mode

Why Crossplane Objects?

  • πŸ”„ Continuous reconciliation (drift detection)
  • πŸ“Š Rich status reporting (health, errors)
  • 🎭 Dependency management (waits for providers)
  • πŸ”’ RBAC integration (secure access)

🎭 Step 3: Sync Waves for Dependencies

Order matters in infrastructure! I use sync waves to ensure proper sequencing:

# Wave -1: Root app-of-apps argocd.argoproj.io/sync-wave: "-1" # Wave 0: Install Crossplane providers argocd.argoproj.io/sync-wave: "0" # Wave 1: Provider configs + RBAC argocd.argoproj.io/sync-wave: "1" # Wave 2: Infrastructure resources argocd.argoproj.io/sync-wave: "2" 
Enter fullscreen mode Exit fullscreen mode

Result: No more "CRD not found" or "provider not ready" errors! πŸŽ‰


πŸ” Step 4: RBAC - The Critical Missing Piece

Crossplane needs permissions to manage your infrastructure. This is often overlooked:

apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: crossplane-provider-kubernetes-metallb rules: - apiGroups: ["metallb.io"] resources: ["ipaddresspools", "l2advertisements"] verbs: ["*"] --- # Bind to the provider's service account apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: crossplane-provider-kubernetes-metallb roleRef: kind: ClusterRole name: crossplane-provider-kubernetes-metallb subjects: - kind: ServiceAccount name: provider-kubernetes-xxxxx # πŸ‘ˆ Get from kubectl namespace: crossplane-system 
Enter fullscreen mode Exit fullscreen mode

Pro tip: Restart provider pods after RBAC changes!


πŸ§ͺ Testing the Complete Pipeline

Time for the moment of truth:

# 1. Bootstrap (one-time manual step) kubectl apply -f argocd/app-of-apps.yaml # 2. Check GitOps automation kubectl -n argocd get applications NAME SYNC STATUS HEALTH STATUS crossplane-provider-kubernetes Synced Healthy βœ… homelab-root Synced Healthy βœ… metallb-config Synced Healthy βœ… # 3. Verify infrastructure was created kubectl -n metallb-system get ipaddresspools.metallb.io NAME ADDRESSES homelab-pool ["10.20.0.81-10.20.0.99"] βœ… # 4. Test LoadBalancer functionality kubectl create deployment nginx --image=nginx kubectl expose deployment nginx --type=LoadBalancer --port=80 kubectl get svc nginx NAME TYPE EXTERNAL-IP PORT(S) nginx LoadBalancer 10.20.0.82 80:30114/TCP βœ… # 5. Verify connectivity curl http://10.20.0.82 <!DOCTYPE html> <html> <head><title>Welcome to nginx!</title></head> # πŸŽ‰ SUCCESS! 
Enter fullscreen mode Exit fullscreen mode

It works! Infrastructure deployed entirely through GitOps! πŸš€


πŸ“ Repository Structure

homelab-gitops/ β”œβ”€β”€ argocd/ β”‚ └── app-of-apps.yaml # 🏠 Root application β”œβ”€β”€ apps/ β”‚ β”œβ”€β”€ crossplane-provider-kubernetes-app.yaml β”‚ └── metallb-config-app.yaml # πŸ‘Ά Child applications β”œβ”€β”€ crossplane/ β”‚ └── provider-kubernetes/ β”‚ β”œβ”€β”€ provider.yaml # ⚑ Crossplane provider β”‚ β”œβ”€β”€ providerconfig.yaml # βš™οΈ Configuration β”‚ └── rbac.yaml # πŸ” Permissions └── metallb/ β”œβ”€β”€ metallb-ipaddresspool.yaml # 🌐 Infrastructure └── metallb-l2advertisement.yaml # πŸ“¦ Resources 
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ Key Lessons Learned

1. API Versions Are Critical

Different provider versions use different APIs:

# v0.13.0 uses v1alpha1 apiVersion: kubernetes.crossplane.io/v1alpha1 # Newer versions use v1alpha2  apiVersion: kubernetes.crossplane.io/v1alpha2 
Enter fullscreen mode Exit fullscreen mode

2. Bootstrap is Still Manual

Even with full GitOps, you need one manual step:

kubectl apply -f argocd/app-of-apps.yaml 
Enter fullscreen mode Exit fullscreen mode

After this, everything else is automated!

3. RBAC Debugging

If Crossplane objects stay "NotReady":

# Check provider permissions kubectl -n crossplane-system describe object metallb-ipaddresspool # Common fix: restart provider after RBAC changes kubectl -n crossplane-system delete pod -l pkg.crossplane.io/provider=provider-kubernetes 
Enter fullscreen mode Exit fullscreen mode

4. YAML Formatting Matters

Watch your indentation! This kept my root app OutOfSync:

# ❌ Wrong destination: server: https://kubernetes.default.svc namespace: crossplane-system # βœ… Correct  destination: server: https://kubernetes.default.svc namespace: crossplane-system 
Enter fullscreen mode Exit fullscreen mode

πŸš€ What's Next?

This foundation scales to manage ANY infrastructure:

# Database clusters kind: PostgreSQLCluster # Service meshes  kind: Istio # Monitoring stacks kind: PrometheusStack # Certificate management kind: ClusterIssuer # Storage solutions kind: StorageClass 
Enter fullscreen mode Exit fullscreen mode

The pattern stays the same:

  1. Define in YAML
  2. Commit to Git
  3. Argo CD syncs automatically
  4. Crossplane provisions infrastructure
  5. Profit! πŸ’°

πŸŽ‰ Results

Before:

  • Manual kubectl commands
  • Configuration drift
  • No audit trail
  • Deployment anxiety 😰

After:

  • Infrastructure as Code
  • Git-driven deployments
  • Automatic drift correction
  • Pull request reviews
  • Confidence in production 😎

Infrastructure changes are now as simple as creating a pull request!


πŸ”— Resources


What infrastructure will you GitOps next? Drop a comment and let me know what you're planning to automate! πŸ‘‡


Follow me for more cloud-native and DevOps content! πŸš€

Top comments (1)

Collapse
 
anik_sikder_313 profile image
Anik Sikder

This is a textbook example of how GitOps can evolve from a deployment strategy into full-stack infrastructure orchestration. The use of Crossplane as a control plane abstraction paired with Argo CD’s sync waves is a brilliant way to tame dependency hell and enforce sequencing without brittle scripts.

The App-of-Apps pattern adds clarity and scalability, especially in environments where infra and app lifecycles diverge. And the RBAC integration with Crossplane? That’s the kind of operational hygiene that often gets overlooked until it breaks something critical.

Appreciate how you’ve framed this not just as tooling, but as a mindset shift from manual ops to declarative, auditable infrastructure. Posts like this help teams move from β€œGitOps curious” to production-ready.