Skip to content
Insights
Cloud & Infrastructure7 min read ·

Crossplane for Multi-Cloud Infrastructure: Extending Kubernetes as a Universal Control Plane for AWS and GKE

Crossplane for Multi-Cloud Infrastructure: Extending Kubernetes as a Universal Control Plane for AWS and GKE

Crossplane is an open source Kubernetes add-on that turns a Kubernetes cluster into a control plane for cloud infrastructure. With Crossplane, Kubernetes stops being only a place to run containers and becomes the API you use to create and manage AWS RDS databases, S3 buckets, GKE clusters, and other cloud resources. You declare those resources as Kubernetes objects. Crossplane controllers then reconcile them continuously against the real cloud APIs, the same way Kubernetes reconciles Deployments and Pods. For teams running across AWS and Google Cloud, this gives you one declarative interface, one access model, and one drift-correction loop for both providers.

The rest of this post explains how that works, where it fits next to tools you already use, and what to watch for before you adopt it.

The problem Crossplane solves

If you operate on more than one cloud, you have probably run into some version of this:

  • Terraform state files split across teams, each with its own backend, locking, and pipeline conventions.
  • Application teams filing tickets for a database because the infrastructure repo is owned by a platform team.
  • Drift that nobody notices until the next terraform plan, which may be weeks away.
  • Different credential and approval flows for AWS and GCP, each audited separately.

None of these are failures of any single tool. They come from treating infrastructure provisioning as a series of one-off runs instead of a continuously reconciled system. Kubernetes already solved the continuous reconciliation problem for workloads. Crossplane applies that same model to infrastructure.

How Crossplane Kubernetes control planes work

Crossplane installs into any conformant Kubernetes cluster via Helm. It adds custom resource definitions (CRDs) and controllers. Four concepts carry most of the weight.

Providers

A Provider is a package that teaches Crossplane how to talk to an external API. Installing the AWS RDS provider, for example, adds CRDs like Instance and controllers that call the RDS API. Providers are typically split by service family, so you install only what you need.

apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
  name: provider-aws-rds
spec:
  package: xpkg.upbound.io/upbound/provider-aws-rds:v1
---
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
  name: provider-gcp-container
spec:
  package: xpkg.upbound.io/upbound/provider-gcp-container:v1

Pin to the specific provider versions you have tested. The tags above are illustrative.

ProviderConfigs

A ProviderConfig tells a provider which credentials to use. We strongly prefer workload identity over static keys: IAM Roles for Service Accounts (IRSA) or EKS Pod Identity on AWS, and Workload Identity on GKE.

apiVersion: aws.upbound.io/v1beta1
kind: ProviderConfig
metadata:
  name: aws-prod
spec:
  credentials:
    source: IRSA
---
apiVersion: gcp.upbound.io/v1beta1
kind: ProviderConfig
metadata:
  name: gcp-prod
spec:
  projectID: my-gcp-project
  credentials:
    source: InjectedIdentity

Managed resources

A managed resource is a one-to-one representation of a real cloud resource. Here is a GKE cluster declared as a Kubernetes object:

apiVersion: container.gcp.upbound.io/v1beta1
kind: Cluster
metadata:
  name: analytics-gke
spec:
  forProvider:
    location: us-central1
    initialNodeCount: 1
    removeDefaultNodePool: true
    releaseChannel:
      - channel: REGULAR
  providerConfigRef:
    name: gcp-prod

Apply it with kubectl apply, and the controller creates the cluster. If someone changes a setting in the console, the controller notices on its next reconcile and moves it back to the declared state. That continuous correction is the core behavioral difference from run-based tools.

Composite resources and Compositions

Managed resources alone are too low-level to hand to application teams. Crossplane lets you define your own higher-level APIs:

  1. A CompositeResourceDefinition (XRD) defines the schema, for example a Database with fields for size and engine.
  2. A Composition defines what that abstraction expands into: an RDS instance, a subnet group, a security group, and a secret, or the Cloud SQL equivalent on GCP.
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xdatabases.platform.example.org
spec:
  group: platform.example.org
  names:
    kind: XDatabase
    plural: xdatabases
  versions:
    - name: v1alpha1
      served: true
      referenceable: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                size:
                  type: string
                  enum: [small, medium, large]
                cloud:
                  type: string
                  enum: [aws, gcp]
              required: [size, cloud]

Compositions use a function pipeline. Functions such as function-patch-and-transform, function-go-templating, or function-kcl take the composite resource as input and produce the managed resources it should own.

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: xdatabase-aws
  labels:
    provider: aws
spec:
  compositeTypeRef:
    apiVersion: platform.example.org/v1alpha1
    kind: XDatabase
  mode: Pipeline
  pipeline:
    - step: render-resources
      functionRef:
        name: function-patch-and-transform
      input:
        apiVersion: pt.fn.crossplane.io/v1beta1
        kind: Resources
        resources:
          - name: rds-instance
            base:
              apiVersion: rds.aws.upbound.io/v1beta1
              kind: Instance
              spec:
                forProvider:
                  region: us-east-1
                  engine: postgres
                  allocatedStorage: 20
                  instanceClass: db.t3.micro
                providerConfigRef:
                  name: aws-prod
            patches:
              - type: FromCompositeFieldPath
                fromFieldPath: spec.size
                toFieldPath: spec.forProvider.instanceClass
                transforms:
                  - type: map
                    map:
                      small: db.t3.micro
                      medium: db.m6g.large
                      large: db.m6g.2xlarge

The application team never sees the RDS-specific fields. They request a small database on aws, and the platform team's Composition encodes the opinionated defaults: encryption, backup retention, network placement, tagging.

A note on versions: Crossplane v2 introduced namespaced composite resources and namespaced managed resources, and reduced the need for the separate "claim" objects that v1 used for namespace-scoped requests. Check the documentation for the version you install, since API groups and scoping differ between major versions.

Managing AWS and GKE from one control plane

The universal control plane pattern looks like this in practice:

  • A dedicated management cluster (often small and locked down) runs Crossplane and the AWS and GCP providers.
  • Platform engineers publish XRDs like XDatabase, XBucket, or XKubernetesCluster, with one Composition per cloud.
  • Application teams submit composite resources through Git. A GitOps tool such as Argo CD or Flux syncs them into the management cluster.
  • Kubernetes RBAC governs who can create which abstractions in which namespaces. Admission policies (Kyverno, OPA Gatekeeper) add guardrails such as allowed regions or mandatory tags.

The cloud-specific detail lives in Compositions, not in application repos. When you change a default, for example raising backup retention, you change it once and every resource reconciles toward it.

What this does not do is erase the differences between clouds. An RDS instance and a Cloud SQL instance have different failure modes, networking models, and maintenance behavior. A portable XDatabase API is useful for common cases. We recommend not forcing every cloud-specific feature through a lowest-common-denominator schema. It is fine to have XAuroraCluster alongside a generic XDatabase.

Where Crossplane fits next to Terraform

Crossplane and Terraform solve overlapping problems with different execution models.

  • Terraform runs on demand, typically in CI, and compares a state file to the real world at plan time.
  • Crossplane runs continuously inside Kubernetes, and the Kubernetes API server (backed by etcd) is effectively the state store.

Many organizations run both. A common split is to keep foundational, rarely changed infrastructure (organization accounts, core networking, the management cluster itself) in Terraform, and use Crossplane for the self-service layer that application teams consume. The Upbound AWS and GCP providers are generated from the corresponding Terraform providers, so resource coverage is broad.

What to evaluate before adopting

We have found these questions useful to settle early:

  1. Who operates the management cluster? It becomes critical infrastructure. Plan for backups, upgrades, and high availability.
  2. What is your credential model? Workload identity per ProviderConfig, scoped by least privilege, is far easier to audit than shared static keys.
  3. How will you handle deletion? Managed resources default to deleting the cloud resource when the Kubernetes object is deleted. Use deletionPolicy: Orphan or management policies for stateful resources where that is too risky.
  4. How will you import existing resources? Crossplane can observe and adopt existing resources using the external-name annotation and observe-only management policies. Pilot this on non-critical resources first.
  5. How many CRDs can your cluster tolerate? Large provider families install many CRDs, which affects API server memory. Install only the provider families you use.
  6. How will you test Compositions? Use crossplane render locally and run Compositions in a non-production control plane before promoting.

If you are working through these decisions for a regulated or high-availability environment, our cloud and platform engineering capabilities cover this kind of control-plane design, and we have written about how requirements shift across different industries.

Sources

FAQ

Is Crossplane a replacement for Terraform?

Not necessarily. Crossplane reconciles infrastructure continuously through the Kubernetes API, while Terraform applies changes on demand. Many teams use Terraform for foundational infrastructure and Crossplane for self-service platform APIs. The right split depends on how often resources change and who needs to request them.

Do I need to run my applications on Kubernetes to use Crossplane?

No. Crossplane needs a Kubernetes cluster to run in, but the resources it manages can be anything with a supported provider: databases, buckets, IAM roles, DNS records, or other clusters. Your applications can run on VMs, serverless platforms, or elsewhere.

How does Crossplane handle drift in AWS and GKE?

Each managed resource controller periodically compares the desired state in Kubernetes with the actual state in the cloud API. If they differ, the controller updates the cloud resource to match the declared spec. You can limit this behavior with management policies, for example making a resource observe-only.

Can Crossplane manage resources that already exist?

Yes. You can create a managed resource with the crossplane.io/external-name annotation set to the existing resource's identifier, and Crossplane will adopt it. Start with observe-only management policies to confirm the mapping before allowing Crossplane to make changes.

What is the difference between a managed resource and a composite resource?

A managed resource maps one-to-one to a single cloud resource, such as one RDS instance. A composite resource is a custom, higher-level API you define with an XRD. A Composition expands it into multiple managed resources, which lets platform teams hide provider-specific detail behind a simpler interface.