Skip to content
Published on

Implementing Kubernetes-Native IaC with Crossplane: A Cloud Control Plane That Replaces Terraform

Authors
  • Name
    Twitter

Introduction

Terraform has been the de facto standard for IaC, but it has limitations in Kubernetes-centric environments. State file management, manual Plan/Apply execution, and difficulty with drift detection are common challenges. Crossplane solves these problems using Kubernetes' declarative model and controller pattern.

Having graduated to CNCF Graduated project status in October 2025, Crossplane is now thoroughly validated for production environments.

Crossplane vs Terraform

CategoryTerraformCrossplane
Execution ModelCLI (Plan/Apply)Kubernetes Controller (Continuous Reconciliation)
State Managementtfstate file (external backend needed)etcd (Kubernetes native)
Drift DetectionManual plan requiredAutomatic (reconciliation loop)
AbstractionModuleComposition
Access ControlSeparate IAM configurationRBAC integration
GitOpsSeparate pipeline neededNative ArgoCD/Flux integration

Installing Crossplane

# Install with Helm
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update

helm install crossplane crossplane-stable/crossplane \
  --namespace crossplane-system \
  --create-namespace \
  --set args='{"--enable-usages"}'

# Verify installation
kubectl get pods -n crossplane-system
kubectl api-resources | grep crossplane

AWS Provider Configuration

# Install AWS Provider
cat <<EOF | kubectl apply -f -
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
  name: provider-aws-s3
spec:
  package: xpkg.upbound.io/upbound/provider-aws-s3:v1.18.0
EOF

# Verify Provider installation
kubectl get providers

Authentication Configuration

# Create AWS credentials Secret
kubectl create secret generic aws-creds \
  -n crossplane-system \
  --from-file=credentials=$HOME/.aws/credentials

# Create ProviderConfig
cat <<EOF | kubectl apply -f -
apiVersion: aws.upbound.io/v1beta1
kind: ProviderConfig
metadata:
  name: default
spec:
  credentials:
    source: Secret
    secretRef:
      namespace: crossplane-system
      name: aws-creds
      key: credentials
EOF

Managed Resources — Direct Cloud Resource Management

# s3-bucket.yaml
apiVersion: s3.aws.upbound.io/v1beta2
kind: Bucket
metadata:
  name: my-crossplane-bucket
spec:
  forProvider:
    region: ap-northeast-2
    tags:
      Environment: production
      ManagedBy: crossplane
  providerConfigRef:
    name: default
kubectl apply -f s3-bucket.yaml

# Check status
kubectl get bucket my-crossplane-bucket
# NAME                    READY   SYNCED   EXTERNAL-NAME           AGE
# my-crossplane-bucket    True    True     my-crossplane-bucket    2m

# Detailed status
kubectl describe bucket my-crossplane-bucket

Creating an RDS Instance

# rds-instance.yaml
apiVersion: rds.aws.upbound.io/v1beta2
kind: Instance
metadata:
  name: my-postgres
spec:
  forProvider:
    region: ap-northeast-2
    allocatedStorage: 20
    engine: postgres
    engineVersion: '16.4'
    instanceClass: db.t3.micro
    dbName: myapp
    masterUsername: admin
    masterPasswordSecretRef:
      name: rds-password
      namespace: default
      key: password
    skipFinalSnapshot: true
    publiclyAccessible: false
    vpcSecurityGroupIdSelector:
      matchLabels:
        app: my-rds
  providerConfigRef:
    name: default
  writeConnectionSecretToRef:
    name: rds-connection
    namespace: default
kubectl apply -f rds-instance.yaml

# Connection info is automatically stored in the Secret
kubectl get secret rds-connection -o yaml

Composition — The Core of Abstraction

Composition bundles multiple Managed Resources into a single high-level API. It is similar to Terraform Modules but Kubernetes-native.

CompositeResourceDefinition (XRD)

# xrd-database.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xdatabases.platform.example.com
spec:
  group: platform.example.com
  names:
    kind: XDatabase
    plural: xdatabases
  claimNames:
    kind: Database
    plural: databases
  versions:
    - name: v1alpha1
      served: true
      referenceable: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                parameters:
                  type: object
                  properties:
                    size:
                      type: string
                      enum: ['small', 'medium', 'large']
                      default: 'small'
                    engine:
                      type: string
                      enum: ['postgres', 'mysql']
                      default: 'postgres'
                    region:
                      type: string
                      default: 'ap-northeast-2'
                  required:
                    - size

Composition

# composition-database.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: aws-database
  labels:
    provider: aws
spec:
  compositeTypeRef:
    apiVersion: platform.example.com/v1alpha1
    kind: XDatabase
  resources:
    - name: rds-instance
      base:
        apiVersion: rds.aws.upbound.io/v1beta2
        kind: Instance
        spec:
          forProvider:
            region: ap-northeast-2
            engine: postgres
            engineVersion: '16.4'
            skipFinalSnapshot: true
            publiclyAccessible: false
          providerConfigRef:
            name: default
      patches:
        # size to instanceClass mapping
        - type: FromCompositeFieldPath
          fromFieldPath: spec.parameters.size
          toFieldPath: spec.forProvider.instanceClass
          transforms:
            - type: map
              map:
                small: db.t3.micro
                medium: db.t3.medium
                large: db.r6g.large
        # size to allocatedStorage mapping
        - type: FromCompositeFieldPath
          fromFieldPath: spec.parameters.size
          toFieldPath: spec.forProvider.allocatedStorage
          transforms:
            - type: map
              map:
                small: '20'
                medium: '100'
                large: '500'
        - type: FromCompositeFieldPath
          fromFieldPath: spec.parameters.engine
          toFieldPath: spec.forProvider.engine
        - type: FromCompositeFieldPath
          fromFieldPath: spec.parameters.region
          toFieldPath: spec.forProvider.region
    - name: security-group
      base:
        apiVersion: ec2.aws.upbound.io/v1beta1
        kind: SecurityGroup
        spec:
          forProvider:
            region: ap-northeast-2
            description: 'Managed by Crossplane'
            ingress:
              - fromPort: 5432
                toPort: 5432
                protocol: tcp
                cidrBlocks:
                  - '10.0.0.0/8'

Claim — The Developer Interface

# claim-database.yaml
apiVersion: platform.example.com/v1alpha1
kind: Database
metadata:
  name: my-app-db
  namespace: team-alpha
spec:
  parameters:
    size: medium
    engine: postgres
    region: ap-northeast-2
  writeConnectionSecretToRef:
    name: db-credentials
kubectl apply -f claim-database.yaml

# This is all the developer needs to know
kubectl get database -n team-alpha
# NAME        READY   CONNECTION-SECRET   AGE
# my-app-db   True    db-credentials      5m

# Platform engineer verification
kubectl get composite
kubectl get managed

Functions — Advanced Composition Logic

Crossplane Functions allow you to implement complex patching logic in Go, Python, and other languages.

# Using function-go-templating
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: dynamic-database
spec:
  compositeTypeRef:
    apiVersion: platform.example.com/v1alpha1
    kind: XDatabase
  mode: Pipeline
  pipeline:
    - step: render-resources
      functionRef:
        name: function-go-templating
      input:
        apiVersion: gotemplating.fn.crossplane.io/v1beta1
        kind: GoTemplate
        source: Inline
        inline:
          template: |
            apiVersion: rds.aws.upbound.io/v1beta2
            kind: Instance
            metadata:
              annotations:
                gotemplating.fn.crossplane.io/composition-resource-name: rds
            spec:
              forProvider:
                region: {{ .observed.composite.resource.spec.parameters.region }}
                engine: {{ .observed.composite.resource.spec.parameters.engine }}
                instanceClass: {{ if eq .observed.composite.resource.spec.parameters.size "small" }}db.t3.micro{{ else if eq .observed.composite.resource.spec.parameters.size "medium" }}db.t3.medium{{ else }}db.r6g.large{{ end }}
    - step: auto-ready
      functionRef:
        name: function-auto-detect-ready

GitOps Integration with ArgoCD

Crossplane resources are standard Kubernetes manifests, so they integrate naturally with ArgoCD.

# argocd-application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: infrastructure
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/myorg/infra-repo.git
    targetRevision: main
    path: crossplane/claims
  destination:
    server: https://kubernetes.default.svc
    namespace: default
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
# Push a Claim to Git and infrastructure is automatically provisioned
git add claim-database.yaml
git commit -m "feat: provision medium postgres for team-alpha"
git push origin main

# ArgoCD syncs automatically
argocd app get infrastructure

Usages — Resource Dependency Protection

The Usage resource protects deletion order.

apiVersion: apiextensions.crossplane.io/v1alpha1
kind: Usage
metadata:
  name: db-used-by-app
spec:
  of:
    apiVersion: platform.example.com/v1alpha1
    kind: Database
    resourceRef:
      name: my-app-db
  by:
    apiVersion: apps/v1
    kind: Deployment
    resourceRef:
      name: my-app
      namespace: team-alpha
  reason: 'Database is used by the application'

Troubleshooting

# Check Provider status
kubectl get providers
kubectl describe provider provider-aws-s3

# Check Managed Resource events
kubectl describe bucket my-crossplane-bucket

# Debug Composition
kubectl get composite -o wide
kubectl describe xdatabase my-app-db-xxxxx

# Crossplane logs
kubectl logs -n crossplane-system deploy/crossplane -f

# Specific Provider logs
kubectl logs -n crossplane-system \
  $(kubectl get pods -n crossplane-system -l pkg.crossplane.io/revision -o name | head -1) -f

Summary

Crossplane is not competing with Terraform -- it evolves IaC to the next level in Kubernetes-native environments:

  • Continuous Reconciliation: Just declare and the controller manages everything, no Plan/Apply needed
  • Self-Service Platform: Abstract complex infrastructure with Compositions and provide it to developers
  • GitOps Native: Natural integration with ArgoCD/Flux
  • RBAC Integration: Leverage Kubernetes' existing permission system as-is
  • Automatic Drift Recovery: Even if someone manually changes resources in the console, they are automatically restored

Quiz: Crossplane Comprehension Check (7 Questions)

Q1. Why is Crossplane stronger than Terraform at drift detection?

The Kubernetes controller's reconciliation loop continuously compares the actual state with the declared state and automatically recovers any drift.

Q2. What is the difference between Managed Resource and Composite Resource?

A Managed Resource maps 1:1 to an actual cloud resource (S3, RDS, etc.), while a Composite Resource bundles multiple Managed Resources into a single high-level API.

Q3. What is the role of a Claim?

A Claim is the namespace-scoped interface through which developers request infrastructure. It uses an abstracted API without directly handling Composite Resources.

Q4. Where is Crossplane's state stored?

It is stored in Kubernetes' etcd. There is no need for separate state file management like Terraform.

Q5. What advantage does mode: Pipeline offer in Composition?

It allows chaining various Functions such as Go Templating and Python to implement complex resource creation logic.

Q6. What is the role of the Usage resource?

It declares dependencies between resources to prevent resources that are in use from being accidentally deleted.

Q7. Does Crossplane require special configuration to integrate with ArgoCD?

No. Crossplane resources are standard Kubernetes manifests, so ArgoCD can sync them as-is.