GitOps Infrastructure as Code: Managing Kubernetes Infrastructure with Crossplane and Argo CD
5 min read
·
Aug 29, 2025
--
Share
Press enter or click to view image in full size
How I built a fully automated GitOps pipeline for infrastructure management using Crossplane, Argo CD, and the App-of-Apps pattern
The Problem: Manual Infrastructure Management
Managing Kubernetes infrastructure traditionally involves:
- Manual
kubectl applycommands - Configuration drift between environments
- No version control for infrastructure changes
- Difficulty tracking who changed what and when
- Complex dependency management between components
What if we could treat infrastructure the same way we treat application code? Enter GitOps for Infrastructure.
The Solution: GitOps + Crossplane + Argo CD
In this post, I’ll show you how I built a complete GitOps infrastructure pipeline that:
- ✅ Manages infrastructure through Git commits
- ✅ Automatically syncs changes to Kubernetes
- ✅ Handles complex dependencies with sync waves
- ✅ Provides a clear audit trail of all changes
- ✅ Enables easy rollbacks and disaster recovery
Architecture Overview
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Git Repo │───▶│ Argo CD │───▶│ Crossplane │───▶│ Kubernetes │
│ │ │ │ │ │ │ Resources │
│ YAML Files │ │ GitOps Sync │ │ Provider │ │ (MetalLB) │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
Step 1: The App-of-Apps Pattern
The foundation of scalable GitOps is the App-of-Apps pattern. Instead of managing dozens of individual Argo CD applications, we create a root application that manages child applications.
# argocd/app-of-apps.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: homelab-root
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "-1"
spec:
project: default
source:
repoURL: https://github.com/jamilshaikh07/homelab-gitops.git
targetRevision: main
path: apps
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
This single application automatically discovers and manages all child applications in the apps/ directory.
Step 2: Crossplane Provider Setup
Crossplane allows us to manage any infrastructure through Kubernetes APIs. We start by installing the provider-kubernetes:
# crossplane/provider-kubernetes/provider.yaml
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-kubernetes
annotations:
argocd.argoproj.io/sync-wave: "0"
spec:
package: xpkg.upbound.io/crossplane-contrib/provider-kubernetes:v0.13.0
The key insight is using sync waves to ensure proper ordering:
- Wave 0: Install the provider
- Wave 1: Configure the provider + RBAC
- Wave 2: Create infrastructure resources
Step 3: Infrastructure as Code with Crossplane Objects
Here’s where the magic happens. Instead of directly applying Kubernetes resources, we use Crossplane Object resources to manage them:
# metallb/metallb-ipaddresspool.yaml
apiVersion: kubernetes.crossplane.io/v1alpha1
kind: Object
metadata:
name: metallb-ipaddresspool
namespace: crossplane-system
annotations:
argocd.argoproj.io/sync-wave: "2"
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
This approach provides several benefits:
- Dependency management : Crossplane ensures the provider is ready before creating resources
- Drift detection : Crossplane continuously reconciles the desired state
- Status reporting : Rich status information about infrastructure health
Step 4: RBAC - The Missing Piece
A critical aspect often overlooked is RBAC. Crossplane needs permissions to manage your infrastructure:
# crossplane/provider-kubernetes/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: crossplane-provider-kubernetes-metallb
rules:
- apiGroups: ["metallb.io"]
resources: ["ipaddresspools", "l2advertisements"]
verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: crossplane-provider-kubernetes-metallb
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: crossplane-provider-kubernetes-metallb
subjects:
- kind: ServiceAccount
name: provider-kubernetes-xxxxx # Dynamic name from provider
namespace: crossplane-system
Step 5: Testing the Complete Pipeline
The moment of truth! Let’s verify our GitOps infrastructure pipeline:
# Check Argo CD applications
kubectl -n argocd get applications
NAME SYNC STATUS HEALTH STATUS
crossplane-provider-kubernetes Synced Healthy
homelab-root Synced Healthy
metallb-config Synced Healthy
# Verify MetalLB resources were created
kubectl -n metallb-system get ipaddresspools.metallb.io
NAME AUTO ASSIGN AVOID BUGGY IPS ADDRESSES
homelab-pool true false ["10.20.0.81-10.20.0.99"]# Test LoadBalancer IP assignment
kubectl create deployment nginx --image=nginx
kubectl expose deployment nginx --type=LoadBalancer --port=80
kubectl get svc nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
nginx LoadBalancer 10.104.155.73 10.20.0.82 80:30114/TCP# Verify connectivity
curl <http://10.20.0.82>
<!DOCTYPE html>
<html>
<head><title>Welcome to nginx!</title></head>
...
🎉 Success! Our infrastructure was created entirely through GitOps!
Key Benefits Achieved
1. Declarative Infrastructure
- Infrastructure is defined in YAML files
- Version controlled alongside application code
- Easy to review changes through pull requests
2. Automated Deployment
- Git commits automatically trigger infrastructure changes
- No manual
kubectl applycommands needed - Consistent deployments across environments
3. Dependency Management
- Sync waves ensure correct installation order
- Crossplane handles resource dependencies automatically
- No more “resource not found” errors
4. Audit Trail
- Every infrastructure change is tracked in Git
- Easy to see who changed what and when
- Simple rollbacks using Git history
5. Self-Healing
- Argo CD continuously monitors for drift
- Automatically corrects manual changes
- Ensures desired state is maintained
Repository Structure
homelab-gitops/
├── argocd/
│ └── app-of-apps.yaml # Root GitOps application
├── apps/
│ ├── crossplane-provider-kubernetes-app.yaml
│ └── metallb-config-app.yaml # Child applications
├── crossplane/
│ └── provider-kubernetes/
│ ├── provider.yaml # Crossplane provider
│ ├── providerconfig.yaml # Provider configuration
│ └── rbac.yaml # RBAC permissions
└── metallb/
├── metallb-ipaddresspool.yaml # Infrastructure resources
└── metallb-l2advertisement.yaml # managed by Crossplane
Lessons Learned
1. API Versions Matter
Different Crossplane provider versions use different API versions. Always check the provider documentation:
kubernetes.crossplane.io/v1alpha1for provider-kubernetes v0.13.0kubernetes.crossplane.io/v1alpha2for newer versions
2. RBAC is Critical
Crossplane providers need explicit permissions to manage resources. Don’t forget to:
- Create appropriate ClusterRoles
- Bind them to the provider’s ServiceAccount
- Restart provider pods after RBAC changes
3. Sync Waves Prevent Race Conditions
Use sync waves to ensure proper ordering:
-1: Root applications0: Providers and CRDs1: Provider configurations and RBAC2+: Infrastructure resources
4. Bootstrap Process
Even with GitOps, you need one manual step:
kubectl apply -f argocd/app-of-apps.yaml
After this, everything else is automated!
What’s Next?
This foundation enables managing any infrastructure through GitOps:
- Databases : PostgreSQL, MongoDB via operators
- Networking : Ingress controllers, service meshes
- Storage : Persistent volumes, backup solutions
- Security : Certificate management, policy engines
- Monitoring : Prometheus, Grafana, alerting
The same pattern scales to manage entire infrastructure portfolios across multiple clusters and environments.
Conclusion
By combining Crossplane with Argo CD and the App-of-Apps pattern, we’ve created a powerful GitOps infrastructure pipeline that:
- Treats infrastructure as code
- Provides automated deployments
- Ensures consistent state management
- Enables easy scaling and maintenance
The result? Infrastructure changes are now as simple as creating a pull request. No more manual commands, no more configuration drift
Want to see the complete implementation? Check out the homelab-gitops repository for all the code and configurations used in this post.