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.