Your Infrastructure Has an API Now, and It's Not Terraform
Crossplane control plane, Composition Functions, Claims vs Terraform, Kyverno guardrails, and debugging the XR chain
In Issue #4, we talked about Platform Engineering and briefly mentioned Crossplane as a Terraform alternative. Time to go deep. What if your infrastructure had a real API - not a CLI tool you run from a CI pipeline, but a Kubernetes controller that never stops reconciling?
That’s exactly what Crossplane does. And if you’re still wrapping Terraform in CI jobs, you might be solving 2025’s problems with 2016’s architecture.
The Pattern: Crossplane as Cloud Control Plane
Your infrastructure is a Kubernetes resource now.
Crossplane extends the K8s API with CRDs for cloud resources. RDS instances, S3 buckets, VPCs, IAM roles - all of them become objects in your cluster. You kubectl apply them just like you would a Deployment or a ConfigMap.
This sounds like Terraform with extra steps. It’s not. The difference is fundamental. Terraform runs as a CLI - you execute terraform apply, it creates resources, writes state to a file, and walks away. If someone modifies the resource manually in the console? Terraform doesn’t know until you run terraform plan again. That’s drift, and it’s your problem.
Crossplane runs reconciliation controllers. Continuously. Every 10 minutes (configurable), it checks actual cloud state against desired state and fixes any drift. No human intervention. No CI job. It just works - exactly like how a Deployment controller makes sure your pods are always running.
The architecture has four layers. A Provider is a controller that talks to a specific cloud API (AWS, GCP, Azure). An XRD (Composite Resource Definition) is the schema for your custom abstraction. A Composition is the implementation - it maps your abstraction to actual cloud resources. And a Claim is what developers interact with.
Here’s the workflow. A developer runs kubectl apply -f postgres-claim.yaml. Crossplane creates an RDS instance, attaches security groups, configures parameter groups, and writes the connection credentials into a Kubernetes Secret in the developer’s namespace. One YAML file. No Terraform, no tickets, no waiting.
Crossplane graduated in the CNCF in 2024. It’s not an experiment anymore.
Links
Hidden Gem: Composition Functions
When YAML patching isn’t enough, bring code.
Standard Crossplane Compositions use patch-and-transform - you map fields from the Claim to the managed resources. It works, but it’s limited. No loops. No conditionals. No “create this resource only if the environment is production.”
Composition Functions change everything. Instead of static YAML patching, your Composition runs a pipeline of gRPC functions. Each function is a container - written in Go, Python, whatever you like - that receives the composite resource and desired state, then returns modified desired state.
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
spec:
mode: Pipeline
pipeline:
- step: create-resources
functionRef:
name: function-go-templating
- step: add-tags
functionRef:
name: function-auto-tagger
- step: validate
functionRef:
name: function-compliance-checkThis unlocks things that were impossible before. Dynamic naming based on environment. Conditional resource creation. External API calls during rendering - like fetching the latest AMI ID or checking a CMDB. You’re writing real logic, not fighting YAML indentation.
Links
The Showdown: Crossplane vs Terraform
Terraform: CLI-driven (terraform apply). State in .tfstate file - drift possible. Manual drift detection (terraform plan). Self-service requires CI/CD wrapper. Abstraction via modules. Secrets live inside .tfstate.
Crossplane: Continuous reconciliation loop. State in K8s etcd - auto-healing. Automatic drift detection via controller. Native self-service through K8s RBAC + Claims. Abstraction via XRD + Compositions. Secrets in K8s Secrets + Vault integration.
The verdict: Terraform isn’t going away. It’s still great for bootstrapping clusters, managing DNS zones, and one-off infrastructure. But for day-2 operations - the “developers need databases and don’t want to file tickets” problem - Crossplane fits the Kubernetes-native model better. Continuous reconciliation beats run-and-pray.
Deep Dive: Crossplane + Kyverno = Safe Self-Service
Claims are K8s resources. Kyverno can validate them.
Here’s where it gets interesting. A Claim is just a Kubernetes custom resource. That means every policy tool that works with K8s admission control - Kyverno, OPA Gatekeeper, Kubewarden - works with Claims out of the box.
Want to limit dev namespaces to db.t3.small instances with max 100GB storage? Kyverno ClusterPolicy. Want to auto-add billing tags and cost-center labels to every Claim? Kyverno mutation rule. Want to block db.r6g.16xlarge in anything that isn’t the production namespace? Validation policy.
The beauty is layered enforcement. Validate at the Claim level - that’s the fastest feedback loop, developers see errors immediately on kubectl apply. Then validate again at the MR (Managed Resource) level as a safety net. Two layers, same tool.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: limit-rds-size
spec:
rules:
- name: restrict-instance-class
match:
resources:
kinds:
- PostgreSQLInstanceClaim
validate:
message: "Dev namespaces limited to db.t3.small"
pattern:
spec:
parameters:
instanceClass: "db.t3.small"Self-service without guardrails is just chaos with a UI. Crossplane + Kyverno gives you both.
Links
The One-Liner: Secret Management Chain
kubectl get secret my-app-db-conn -n my-app -o jsonpath='{.data.endpoint}' | base64 -dWhere did that secret come from? Nobody created it manually. Here’s the chain.
When you define a Claim with writeConnectionSecretToRef, Crossplane creates the cloud resource (say, an RDS instance), reads the generated credentials from the cloud API, and writes them into a Kubernetes Secret in the developer’s namespace. The developer’s app mounts that secret - no copy-pasting passwords, no Slack DMs with credentials.
The chain goes deeper. The Managed Resource (MR) writes its secret. The Composite Resource (XR) aggregates secrets from multiple MRs. The Claim exposes the final secret to the developer’s namespace. Three levels, fully automated.
For production, you don’t want raw K8s Secrets. Crossplane’s StoreConfig integrates with HashiCorp Vault, AWS Secrets Manager, or any external secret store. The developer’s workflow doesn’t change - they still reference a secret name in their Claim. Where it’s stored is an infrastructure concern, not a developer concern.
Links
Questions? Feedback? Reply to this email. I read every one.
Podo Stack - Ripe for Prod.







