Crossplane 2026: Manage AWS Infrastructure as Kubernetes YAML (Terraform Alternative)

Crossplane 2026: Manage AWS Infrastructure as Kubernetes YAML

Infrastructure-as-code has been dominated by tools like Terraform for years. But what if your entire cloud infrastructure — S3 buckets, RDS databases, VPCs, IAM roles — could be declared as Kubernetes YAML and managed by the same control loop that keeps your pods running? That is exactly what Crossplane delivers.

This crossplane tutorial walks you through the complete workflow: installing Crossplane on a Kubernetes cluster, configuring the AWS provider, creating real cloud resources with kubectl apply, building Composite Resource Definitions (XRDs) to create internal abstractions, and comparing Crossplane vs Terraform across ten practical criteria.


1. What Crossplane Is

Crossplane is a CNCF (Cloud Native Computing Foundation) graduated project that extends Kubernetes with Custom Resource Definitions (CRDs) for managing cloud infrastructure. When you install Crossplane and a cloud provider plugin, your Kubernetes API server learns new resource types such as Bucket, RDSInstance, VPC, and SecurityGroup.

The Control Loop Model

Kubernetes reconciles every object continuously: it reads the desired state from etcd, compares it to the actual state of the cluster, and acts to close any gap. Crossplane applies exactly the same pattern to cloud infrastructure.

Declare desired state (YAML) → Crossplane reads object
                              → Crossplane calls cloud API
                              → Cloud resource created/updated
                              → Crossplane watches for drift
                              → Drift detected → reconcile again

This is continuous reconciliation, not a one-time apply. If someone manually deletes an S3 bucket through the AWS Console, Crossplane notices and recreates it — without you running any command.

Crossplane vs Terraform

Terraform uses a plan/apply workflow: you run terraform plan to preview changes, then terraform apply to execute them. State is stored in a .tfstate file (or remote backend). Drift detection requires explicit terraform refresh or terraform plan runs.

Crossplane uses continuous reconciliation: state is stored in Kubernetes etcd, drift detection is built-in and always active, and there is no separate CLI step to apply changes — pushing YAML to the cluster is enough.


2. Install Crossplane with Helm

Prerequisites: a running Kubernetes cluster (EKS, GKE, kind, or k3s all work) and Helm 3.

helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update

helm install crossplane crossplane-stable/crossplane \
  --namespace crossplane-system \
  --create-namespace

Verify the installation:

kubectl get pods -n crossplane-system
# NAME                                       READY   STATUS    RESTARTS
# crossplane-7d8f9b4d5c-xkqzp                1/1     Running   0
# crossplane-rbac-manager-6c9f7b8d4-mnjkl    1/1     Running   0

Crossplane is now running and waiting for provider plugins.


3. Install the AWS Provider

Crossplane providers are Kubernetes packages that ship CRDs and a controller for a specific cloud. Install the official AWS provider:

# aws-provider.yaml
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
  name: provider-aws-s3
spec:
  package: xpkg.upbound.io/upbound/provider-aws-s3:v1.7.0
kubectl apply -f aws-provider.yaml
kubectl get providers
# NAME               INSTALLED   HEALTHY   PACKAGE
# provider-aws-s3    True        True      upbound/provider-aws-s3:v1.7.0

Configure AWS Credentials

Store your AWS credentials in a Kubernetes Secret, then create a ProviderConfig that references it:

kubectl create secret generic aws-secret \
  -n crossplane-system \
  --from-literal=creds="[default]
aws_access_key_id = AKIAIOSFODNN7EXAMPLE
aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
# provider-config.yaml
apiVersion: aws.upbound.io/v1beta1
kind: ProviderConfig
metadata:
  name: default
spec:
  credentials:
    source: Secret
    secretRef:
      namespace: crossplane-system
      name: aws-secret
      key: creds
kubectl apply -f provider-config.yaml

4. First Resource: Create an S3 Bucket

With the provider installed, you can create an S3 bucket using a Kubernetes manifest — no AWS CLI or Console needed.

# my-bucket.yaml
apiVersion: s3.aws.upbound.io/v1beta1
kind: Bucket
metadata:
  name: my-crossplane-bucket-2026
spec:
  forProvider:
    region: us-east-1
  providerConfigRef:
    name: default
kubectl apply -f my-bucket.yaml

Check the sync status:

kubectl get bucket my-crossplane-bucket-2026
# NAME                          READY   SYNCED   EXTERNAL-NAME                   AGE
# my-crossplane-bucket-2026     True    True      my-crossplane-bucket-2026       45s

READY=True means the bucket exists in AWS. SYNCED=True means Crossplane's last reconciliation succeeded. The bucket now appears in your AWS Console exactly as if you had created it directly.

To delete the bucket, simply delete the Kubernetes object:

kubectl delete bucket my-crossplane-bucket-2026

Crossplane calls the AWS API and removes the actual bucket. No orphaned resources.


5. Create an RDS PostgreSQL Instance

The AWS RDS provider creates managed databases with the same declarative pattern:

# rds-postgres.yaml
apiVersion: rds.aws.upbound.io/v1beta1
kind: RDSInstance
metadata:
  name: my-postgres-db
spec:
  forProvider:
    region: us-east-1
    dbInstanceClass: db.t3.micro
    engine: postgres
    engineVersion: "15.4"
    masterUsername: adminuser
    allocatedStorage: 20
    skipFinalSnapshot: true
    publiclyAccessible: false
  writeConnectionSecretToRef:
    namespace: default
    name: my-postgres-db-conn
  providerConfigRef:
    name: default
kubectl apply -f rds-postgres.yaml

Notice writeConnectionSecretToRef: Crossplane automatically retrieves the database endpoint, port, username, and password from AWS and writes them into a Kubernetes Secret named my-postgres-db-conn. Your application pods can mount this secret without any manual credential distribution step.

kubectl get secret my-postgres-db-conn -o yaml
# data:
#   endpoint: bXktcG9zdGdyZXMtZGIuY3h4eHh4LnVzLWVhc3QtMS5yZHMuYW1hem9uYXdzLmNvbQ==
#   password: UGFzc3dvcmQxMjM0NTY3OA==
#   port: NTQzMg==
#   username: YWRtaW51c2Vy

6. Composite Resources (XRDs) — The Killer Feature

Individual managed resources are useful, but the real power of Crossplane is infrastructure as code for platform teams: you define internal abstractions that hide cloud complexity from application developers.

Define a CompositeResourceDefinition

A CompositeResourceDefinition (XRD) declares a new custom API type in your cluster:

# xrd-database-cluster.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xdatabaseclusters.platform.example.com
spec:
  group: platform.example.com
  names:
    kind: XDatabaseCluster
    plural: xdatabaseclusters
  claimNames:
    kind: DatabaseCluster
    plural: databaseclusters
  versions:
    - name: v1alpha1
      served: true
      referenceable: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                parameters:
                  type: object
                  properties:
                    storageGB:
                      type: integer
                    instanceClass:
                      type: string
                    environment:
                      type: string
                      enum: [dev, staging, prod]
                  required: [storageGB, instanceClass, environment]

Define a Composition

A Composition maps the abstract type to real AWS resources. One XDatabaseCluster can create an RDS instance, a security group, a parameter group, and enable Enhanced Monitoring — all wired together automatically:

# composition-database-cluster.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: database-cluster-aws
spec:
  compositeTypeRef:
    apiVersion: platform.example.com/v1alpha1
    kind: XDatabaseCluster
  resources:
    - name: rds-instance
      base:
        apiVersion: rds.aws.upbound.io/v1beta1
        kind: RDSInstance
        spec:
          forProvider:
            region: us-east-1
            engine: postgres
            engineVersion: "15.4"
            masterUsername: adminuser
            skipFinalSnapshot: true
      patches:
        - type: FromCompositeFieldPath
          fromFieldPath: spec.parameters.storageGB
          toFieldPath: spec.forProvider.allocatedStorage
        - type: FromCompositeFieldPath
          fromFieldPath: spec.parameters.instanceClass
          toFieldPath: spec.forProvider.dbInstanceClass
    - name: security-group
      base:
        apiVersion: ec2.aws.upbound.io/v1beta1
        kind: SecurityGroup
        spec:
          forProvider:
            region: us-east-1
            description: RDS security group managed by Crossplane

7. Claims: Developer Self-Service Without AWS Knowledge

With the XRD and Composition in place, any developer on any team can request a database by creating a Claim — a namespace-scoped resource that hides all implementation details:

# my-postgres.yaml
apiVersion: platform.example.com/v1alpha1
kind: DatabaseCluster
metadata:
  name: my-app-postgres
  namespace: team-backend
spec:
  parameters:
    storageGB: 50
    instanceClass: db.t3.small
    environment: prod
  writeConnectionSecretToRef:
    name: my-app-postgres-conn
kubectl apply -f my-postgres.yaml

The developer does not know which AWS region the database lives in, what security groups are attached, whether monitoring is enabled, or how the parameter group is tuned. The platform team controls all of that through the Composition. The developer gets a connection secret in their namespace and a running database.


8. Crossplane vs Terraform: Detailed Comparison

CriterionCrossplaneTerraform
Drift detectionContinuous, automaticManual (terraform plan)
State fileStored in Kubernetes etcd.tfstate file or remote backend
Kubernetes-nativeYes — full kubectl/RBAC/GitOps supportNo — separate CLI and state
Multi-team self-serviceClaims + XRDs enable namespace isolationRequires module design + workspace conventions
Secrets handlingWritten directly to Kubernetes SecretsStored in state file (sensitive data exposed)
GitOps compatibleFirst-class (ArgoCD, Flux work natively)Requires wrapper tooling (Atlantis, etc.)
LanguageYAML / Kubernetes APIHCL (HashiCorp Configuration Language)
Provider ecosystemGrowing (AWS, GCP, Azure, Helm, SQL)Mature (thousands of community providers)
Learning curveSteep if new to KubernetesModerate (HCL is purpose-built)
Deletion safetyDeleting the K8s object deletes the resourceterraform destroy required explicitly

For teams already running Kubernetes and using GitOps, Crossplane's continuous reconciliation and native RBAC integration offer significant operational advantages over Terraform. For teams without Kubernetes, or those managing resources outside of any cluster, Terraform remains the more practical choice.


9. Crossplane vs AWS CDK, Pulumi, and Ansible

AWS CDK: CDK generates CloudFormation templates using TypeScript, Python, or Java. It is tightly coupled to AWS and requires the CloudFormation service plane. Crossplane is cloud-agnostic and Kubernetes-native.

Pulumi: Pulumi uses general-purpose languages (TypeScript, Go, Python) and supports many clouds. Like Terraform, it uses a separate state backend and CLI workflow. Crossplane has no separate state backend — Kubernetes etcd is the source of truth.

Ansible: Ansible is a procedural configuration management tool. It can create cloud resources but has no concept of desired-state reconciliation or drift detection. Each playbook run is a one-time imperative execution.

The key differentiator for Crossplane is the control loop: as long as your cluster is running, Crossplane continuously ensures that your declared infrastructure matches reality. None of the alternatives above provide this without external tooling.


10. FAQ

Do I need a running Kubernetes cluster to use Crossplane? Yes. Crossplane runs as a set of controllers inside Kubernetes. It cannot run standalone. A small managed cluster (EKS, GKE, AKS) or even a local kind cluster works fine.

What happens to my AWS resources if I delete the Crossplane controller? Existing AWS resources are not deleted when Crossplane itself is removed. Crossplane adds a finalizer to each managed resource to prevent accidental deletion, but if the controller stops running, it simply stops reconciling. Resources remain intact in AWS.

Can Crossplane import existing AWS resources? Yes. Set the crossplane.io/external-name annotation on a managed resource to the existing AWS resource ID. Crossplane will adopt the resource rather than create a new one.

Is Crossplane production-ready? Crossplane graduated from the CNCF incubator in 2024. Major users include Deutsche Telekom, Grafana Labs, and Upbound (the primary maintainer). The AWS, GCP, and Azure providers are stable and widely used.

How does Crossplane handle cost management? Crossplane does not manage cost directly. However, because all infrastructure is declared in Git and audited through Kubernetes RBAC, platform teams can enforce policies (using tools like OPA/Gatekeeper) that prevent developers from requesting oversized instances.

Can I use Crossplane alongside Terraform? Yes. Many organizations adopt Crossplane incrementally — managing new resources with Crossplane while keeping existing Terraform-managed infrastructure in place. There is no technical conflict between the two.


Next Steps

  • Install Crossplane locally with kind to experiment without AWS costs
  • Explore the Upbound Marketplace for pre-built provider families
  • Read the Crossplane Composition documentation to learn advanced patching and transforms
  • Integrate with ArgoCD or Flux to achieve full GitOps for both application and infrastructure manifests

Leonardo Lazzaro

Software engineer and technical writer. 10+ years experience in DevOps, Python, and Linux systems.

More articles by Leonardo Lazzaro