Crossplane helps platform engineers develop abstractions for developers. It is an open-source, multicloud control plane that handles interactions with cloud providers’ APIs for you.
In this post, I’ll show how developers can create an AWS network (VPC
, Subnet
, etc.) with just a single YAML request to the Kubernetes API.
Crossplane leverages Kubernetes CRD
(Custom Resource Definition), which extends Kubernetes APIs with new custom objects. Along with that, Crossplane creates controllers for these objects, implementing the reconciliation loop to ensure the system’s actual state always matches the declared state.
For example, if you define a Table
custom resource with spec.legs = 3
, Kubernetes continuously checks:
“Does this table have 3 legs?”
- If it finds only 2, it builds another.
- If someone manually adds a 4th, it removes the extra one.
This ensures the resource has exactly 3 legs—no more, no less.
Pre-Requisites
- Docker & Kubernetes setup
- AWS account with user or IAM role credentials (below, I provide steps to set up user in AWS)
Docker Kubernetes Setup
I am assuming you already have a local Kubernetes setup. If not, you can install Docker Desktop from https://docs.docker.com/desktop/setup/install/mac-install/ and enable Kubernetes.
Confirm the setup by checking Kubernetes version.
kubectl version
Crossplane Installation
Now, we will install Crossplane using helm, helm is a packaging tool for Kubernetes resources.
Install helm using
brew install helm
confirm, using version check
helm version
Let’s add helm repo for Crossplane using commands:
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update
And then, the command below will install Crossplane.
helm install crossplane --namespace crossplane-system --create-namespace crossplane-stable/crossplane
You can change the namespace to one of your choice, and then check the installation status using:
helm list -n crossplane-system
Provider
In Crossplane, a provider acts like a software package or driver that enables functionality. It bundles the necessary Custom Resource Definitions (CRDs) and controllers to interact with external systems, most often, cloud provider APIs. In our case, we’ll use provider-aws-ec2
, which allows us to manage AWS EC2 instances and networking resources.
To install provider, save the lines below as .yaml
file,
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-aws-ec2
spec:
package: xpkg.upbound.io/upbound/provider-aws-ec2:v1
and apply using:
kubectl create secret generic aws-creds
--from-file=creds=./aws-creds
To link this secret with Crossplane, we need something called a ProviderConfig.
Create the provider config by saving the yaml content below into a .yaml
file,
apiVersion: aws.upbound.io/v1beta1
kind: ProviderConfig
metadata:
name: default
spec:
credentials:
source: Secret
secretRef:
namespace: crossplane-system
name: aws-creds
key: creds
and using kubectl apply -f <your-file-name>.yaml
. Note that we used the name of the secret aws-creds as a reference to this provider.
Crossplane Composite Resource Definition (XRD)
This is where Crossplane leverages Kubernetes Custom Resource Definitions (CRDs) to define custom resources that represent infrastructure abstractions, essentially the interface. A Composite Resource (XR) bundles and abstracts the underlying cloud resources, so instead of worrying about configuring VPCs, subnets, and routing, developers can simply declare that they need an AWS network. Think of it like ordering a dish: You care about the outcome, not the individual ingredients. To create an XRD, save the following content into a .yaml
file:
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xnetworks.aws.platformref.crossplane.io
spec:
group: aws.platformref.crossplane.io
names:
kind: XNetwork
plural: xnetworks
claimNames:
kind: Network
plural: networks
connectionSecretKeys:
- vpcId
- subnetIds
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
parameters:
type: object
properties:
vpcCidr:
type: string
required: [vpcCidr]
required: [parameters]
status:
type: object
properties:
vpcId:
type: string
subnetIds:
type: array
items:
type: string
and apply
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: network.aws.platformref.crossplane.io
labels:
crossplane.io/xrd: xnetworks.aws.platformref.crossplane.io
spec:
compositeTypeRef:
apiVersion: aws.platformref.crossplane.io/v1alpha1
kind: XNetwork
resources:
- name: vpc
base:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: VPC
spec:
forProvider:
cidrBlock: 10.0.0.0/16
enableDnsSupport: true
enableDnsHostNames: true
region: us-east-1
providerConfigRef:
name: default
patches:
- fromFieldPath: spec.parameters.vpcCidr
toFieldPath: spec.forProvider.cidrBlock
type: FromCompositeFieldPath
- fromFieldPath: spec.claimRef.name
toFieldPath: spec.forProvider.tags.Name
type: FromCompositeFieldPath
transforms:
- type: string
string:
fmt: "%s-vpc"
- fromFieldPath: status.atProvider.id
toFieldPath: status.vpcId
type: ToCompositeFieldPath
- name: subnet
base:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: Subnet
spec:
forProvider:
cidrBlock: 10.0.1.0/24
availabilityZone: us-east-1a
mapPublicIpOnLaunch: true
region: us-east-1
providerConfigRef:
name: default
patches:
- fromFieldPath: status.vpcId
toFieldPath: spec.forProvider.vpcId
type: FromCompositeFieldPath
- fromFieldPath: spec.claimRef.name
toFieldPath: spec.forProvider.tags.Name
type: FromCompositeFieldPath
transforms:
- type: string
string:
fmt: "%s-subnet"
- fromFieldPath: status.atProvider.id
toFieldPath: status.subnetIds[0]
type: ToCompositeFieldPath
apiVersion: aws.platformref.crossplane.io/v1alpha1
kind: Network
metadata:
name: crossplane-demo-network
namespace: default
spec:
parameters:
vpcCidr: "10.0.0.0/16"
compositionRef:
name: network.aws.platformref.crossplane.io
and apply.