Crossplane
Language Overview¶
Crossplane is a Kubernetes-native framework that enables platform teams to build internal cloud platforms by composing managed cloud resources into higher-level, self-service APIs. It extends Kubernetes with Custom Resource Definitions (CRDs) to manage external infrastructure using the same declarative model as Kubernetes workloads.
Key Characteristics¶
- Kubernetes-Native: Manages infrastructure through the Kubernetes API server
- Composition-Based: Builds higher-level abstractions from lower-level managed resources
- Cloud-Agnostic: Supports AWS, GCP, Azure, and other providers through a unified API
- Self-Service: Platform teams define APIs that application teams consume via Claims
- GitOps-Ready: Declarative resources integrate with ArgoCD, Flux, and other GitOps tools
- Reconciliation Loop: Continuously drifts infrastructure back to declared state
Primary Use Cases¶
- Internal Developer Platforms (IDPs)
- Multi-cloud infrastructure abstraction
- Self-service infrastructure provisioning
- Platform-as-a-Service APIs on Kubernetes
- Standardized cloud resource templates
Quick Reference¶
| Category | Convention | Example | Notes |
|---|---|---|---|
| Resource Naming | |||
| XRDs | x{resource}s.{group} |
xpostgresqlinstances.db.example.org |
Plural, prefixed with x |
| Claims | {Resource} (singular) |
PostgreSQLInstance |
User-facing API name |
| Compositions | x{resource}s.{cloud}.{group} |
xpostgresqlinstances.aws.db.example.org |
Cloud-specific |
| Providers | provider-{cloud} |
provider-aws |
Lowercase, hyphenated |
| File Structure | |||
| XRDs | apis/{group}/definition.yaml |
apis/database/definition.yaml |
One per file |
| Compositions | apis/{group}/compositions/{cloud}.yaml |
apis/database/compositions/aws.yaml |
Per cloud |
| Claims | claims/{namespace}/{resource}.yaml |
claims/team-a/database.yaml |
Per namespace |
| Versioning | |||
| API Versions | v1alpha1 → v1beta1 → v1 |
v1alpha1 |
Follow K8s conventions |
| Provider Versions | Pin major version | version: ">=v1.0.0" |
Use version constraints |
| Labels | |||
| Required | app.kubernetes.io/managed-by: crossplane |
See examples | Standard K8s labels |
| Team | platform.example.org/team |
team-backend |
Ownership tracking |
Project Structure¶
Platform Repository Layout¶
crossplane-platform/
├── apis/ # Composite Resource Definitions
│ ├── database/
│ │ ├── definition.yaml # XRD for database abstraction
│ │ ├── compositions/
│ │ │ ├── aws.yaml # AWS RDS composition
│ │ │ ├── gcp.yaml # GCP Cloud SQL composition
│ │ │ └── azure.yaml # Azure Database composition
│ │ └── examples/
│ │ ├── claim-basic.yaml # Basic usage example
│ │ └── claim-production.yaml # Production configuration
│ ├── network/
│ │ ├── definition.yaml # XRD for network abstraction
│ │ └── compositions/
│ │ ├── aws.yaml
│ │ ├── gcp.yaml
│ │ └── azure.yaml
│ ├── kubernetes/
│ │ ├── definition.yaml # XRD for K8s cluster abstraction
│ │ └── compositions/
│ │ ├── aws-eks.yaml
│ │ ├── gcp-gke.yaml
│ │ └── azure-aks.yaml
│ ├── storage/
│ │ ├── definition.yaml
│ │ └── compositions/
│ │ ├── aws.yaml
│ │ ├── gcp.yaml
│ │ └── azure.yaml
│ └── cache/
│ ├── definition.yaml
│ └── compositions/
│ ├── aws.yaml
│ ├── gcp.yaml
│ └── azure.yaml
├── providers/ # Provider configurations
│ ├── provider-aws.yaml
│ ├── provider-gcp.yaml
│ ├── provider-azure.yaml
│ ├── provider-kubernetes.yaml
│ └── provider-helm.yaml
├── config/ # Provider credentials and config
│ ├── aws/
│ │ ├── providerconfig.yaml
│ │ └── irsa-trust-policy.json
│ ├── gcp/
│ │ ├── providerconfig.yaml
│ │ └── workload-identity.yaml
│ └── azure/
│ ├── providerconfig.yaml
│ └── managed-identity.yaml
├── policies/ # RBAC and usage policies
│ ├── clusterroles.yaml
│ ├── namespace-claims.yaml
│ └── resource-quotas.yaml
├── tests/ # Composition tests
│ ├── database/
│ │ ├── aws-basic.yaml
│ │ ├── gcp-basic.yaml
│ │ └── azure-basic.yaml
│ └── network/
│ ├── aws-basic.yaml
│ └── gcp-basic.yaml
└── crossplane.yaml # Crossplane configuration package
Configuration Package¶
# crossplane.yaml
# Package metadata for Crossplane configuration
apiVersion: meta.pkg.crossplane.io/v1
kind: Configuration
metadata:
name: platform-apis
annotations:
meta.crossplane.io/maintainer: "Platform Team <platform@example.org>"
meta.crossplane.io/source: "github.com/example/crossplane-platform"
meta.crossplane.io/license: "Apache-2.0"
meta.crossplane.io/description: |
Internal platform APIs for self-service cloud infrastructure
provisioning across AWS, GCP, and Azure.
meta.crossplane.io/readme: |
This configuration provides composite resources for databases,
networks, Kubernetes clusters, object storage, and caches.
spec:
crossplane:
version: ">=v1.17.0"
dependsOn:
- provider: xpkg.upbound.io/upbound/provider-family-aws
version: ">=v1.0.0"
- provider: xpkg.upbound.io/upbound/provider-family-gcp
version: ">=v1.0.0"
- provider: xpkg.upbound.io/upbound/provider-family-azure
version: ">=v1.0.0"
Composite Resource Definitions (XRDs)¶
Database Abstraction¶
# apis/database/definition.yaml
# XRD for a cloud-agnostic PostgreSQL database
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xpostgresqlinstances.database.example.org
labels:
platform.example.org/category: data
platform.example.org/api-version: v1alpha1
annotations:
platform.example.org/description: "Self-service PostgreSQL database"
platform.example.org/docs: "https://docs.example.org/apis/database"
spec:
group: database.example.org
names:
kind: XPostgreSQLInstance
plural: xpostgresqlinstances
claimNames:
kind: PostgreSQLInstance
plural: postgresqlinstances
connectionSecretKeys:
- host
- port
- username
- password
- database
- connectionString
defaultCompositionRef:
name: xpostgresqlinstances.aws.database.example.org
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
parameters:
type: object
description: "Database configuration parameters"
properties:
engine:
type: string
description: "Database engine"
enum:
- postgres
default: postgres
engineVersion:
type: string
description: "Engine version"
enum:
- "13"
- "14"
- "15"
- "16"
default: "16"
storageGB:
type: integer
description: "Storage size in GB"
minimum: 20
maximum: 10000
default: 20
size:
type: string
description: "Instance size class"
enum:
- small
- medium
- large
default: small
highAvailability:
type: boolean
description: "Enable multi-AZ deployment"
default: false
backupRetentionDays:
type: integer
description: "Backup retention in days"
minimum: 1
maximum: 35
default: 7
deletionProtection:
type: boolean
description: "Prevent accidental deletion"
default: true
required:
- storageGB
- size
status:
type: object
properties:
connectionDetails:
type: object
properties:
host:
type: string
port:
type: string
database:
type: string
instanceId:
type: string
description: "Cloud provider instance identifier"
state:
type: string
description: "Current instance state"
Network Abstraction¶
# apis/network/definition.yaml
# XRD for a cloud-agnostic virtual network
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xnetworks.network.example.org
labels:
platform.example.org/category: networking
platform.example.org/api-version: v1alpha1
spec:
group: network.example.org
names:
kind: XNetwork
plural: xnetworks
claimNames:
kind: Network
plural: networks
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
parameters:
type: object
properties:
region:
type: string
description: "Cloud region for deployment"
cidr:
type: string
description: "VPC CIDR block"
default: "10.0.0.0/16"
pattern: "^([0-9]{1,3}\\.){3}[0-9]{1,3}/[0-9]{1,2}$"
subnetCount:
type: integer
description: "Number of subnets to create"
minimum: 1
maximum: 6
default: 3
enableNAT:
type: boolean
description: "Enable NAT gateway for private subnets"
default: true
enableDNS:
type: boolean
description: "Enable DNS resolution"
default: true
required:
- region
status:
type: object
properties:
vpcId:
type: string
subnetIds:
type: array
items:
type: string
securityGroupId:
type: string
Kubernetes Cluster Abstraction¶
# apis/kubernetes/definition.yaml
# XRD for a cloud-agnostic Kubernetes cluster
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xkubernetesclusters.compute.example.org
labels:
platform.example.org/category: compute
platform.example.org/api-version: v1alpha1
spec:
group: compute.example.org
names:
kind: XKubernetesCluster
plural: xkubernetesclusters
claimNames:
kind: KubernetesCluster
plural: kubernetesclusters
connectionSecretKeys:
- kubeconfig
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
parameters:
type: object
properties:
version:
type: string
description: "Kubernetes version"
enum:
- "1.28"
- "1.29"
- "1.30"
- "1.31"
default: "1.30"
nodeCount:
type: integer
description: "Number of worker nodes"
minimum: 1
maximum: 100
default: 3
nodeSize:
type: string
description: "Worker node size class"
enum:
- small
- medium
- large
- xlarge
default: medium
region:
type: string
description: "Cloud region"
networkRef:
type: object
description: "Reference to a Network claim"
properties:
name:
type: string
required:
- name
required:
- region
- nodeCount
status:
type: object
properties:
clusterName:
type: string
endpoint:
type: string
oidcIssuer:
type: string
nodeGroupStatus:
type: string
Object Storage Abstraction¶
# apis/storage/definition.yaml
# XRD for cloud-agnostic object storage
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xobjectstores.storage.example.org
labels:
platform.example.org/category: storage
platform.example.org/api-version: v1alpha1
spec:
group: storage.example.org
names:
kind: XObjectStore
plural: xobjectstores
claimNames:
kind: ObjectStore
plural: objectstores
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
parameters:
type: object
properties:
region:
type: string
description: "Cloud region for bucket"
versioning:
type: boolean
description: "Enable object versioning"
default: true
encryption:
type: boolean
description: "Enable server-side encryption"
default: true
publicAccess:
type: boolean
description: "Allow public access (discouraged)"
default: false
lifecycleRules:
type: array
description: "Object lifecycle transition rules"
items:
type: object
properties:
transitionDays:
type: integer
storageClass:
type: string
enum:
- infrequent-access
- archive
- glacier
required:
- region
status:
type: object
properties:
bucketName:
type: string
bucketArn:
type: string
region:
type: string
Cache Abstraction¶
# apis/cache/definition.yaml
# XRD for cloud-agnostic managed cache
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xcacheinstances.cache.example.org
labels:
platform.example.org/category: data
platform.example.org/api-version: v1alpha1
spec:
group: cache.example.org
names:
kind: XCacheInstance
plural: xcacheinstances
claimNames:
kind: CacheInstance
plural: cacheinstances
connectionSecretKeys:
- host
- port
- password
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
parameters:
type: object
properties:
engine:
type: string
description: "Cache engine type"
enum:
- redis
- memcached
default: redis
engineVersion:
type: string
description: "Cache engine version"
default: "7.0"
size:
type: string
description: "Instance size class"
enum:
- small
- medium
- large
default: small
region:
type: string
highAvailability:
type: boolean
description: "Enable replication"
default: false
required:
- region
- size
status:
type: object
properties:
endpoint:
type: string
port:
type: integer
state:
type: string
Compositions¶
AWS Database Composition¶
# apis/database/compositions/aws.yaml
# Composition mapping XPostgreSQLInstance to AWS RDS resources
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: xpostgresqlinstances.aws.database.example.org
labels:
provider: aws
platform.example.org/category: data
annotations:
platform.example.org/description: "PostgreSQL on AWS RDS"
platform.example.org/cloud: aws
spec:
compositeTypeRef:
apiVersion: database.example.org/v1alpha1
kind: XPostgreSQLInstance
mode: Pipeline
pipeline:
- step: patch-and-transform
functionRef:
name: function-patch-and-transform
input:
apiVersion: pt.fn.crossplane.io/v1beta1
kind: Resources
resources:
# Subnet Group for RDS
- name: subnet-group
base:
apiVersion: rds.aws.upbound.io/v1beta1
kind: SubnetGroup
spec:
forProvider:
region: us-east-1
description: "Subnet group for PostgreSQL instance"
tags:
managed-by: crossplane
platform: example-org
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.region
# RDS Instance
- name: rds-instance
base:
apiVersion: rds.aws.upbound.io/v1beta2
kind: Instance
spec:
forProvider:
region: us-east-1
engine: postgres
engineVersion: "16"
instanceClass: db.t3.micro
allocatedStorage: 20
storageType: gp3
storageEncrypted: true
publiclyAccessible: false
skipFinalSnapshot: false
autoMinorVersionUpgrade: true
backupRetentionPeriod: 7
deletionProtection: true
copyTagsToSnapshot: true
performanceInsightsEnabled: true
enabledCloudwatchLogsExports:
- postgresql
- upgrade
tags:
managed-by: crossplane
platform: example-org
writeConnectionSecretToRef:
namespace: crossplane-system
patches:
# Region
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.region
# Engine version
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.engineVersion
toFieldPath: spec.forProvider.engineVersion
# Storage
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.storageGB
toFieldPath: spec.forProvider.allocatedStorage
# Instance size 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
# High availability
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.highAvailability
toFieldPath: spec.forProvider.multiAz
# Backup retention
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.backupRetentionDays
toFieldPath: spec.forProvider.backupRetentionPeriod
# Deletion protection
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.deletionProtection
toFieldPath: spec.forProvider.deletionProtection
# Connection secret name
- type: FromCompositeFieldPath
fromFieldPath: metadata.uid
toFieldPath: spec.writeConnectionSecretToRef.name
transforms:
- type: string
string:
type: Format
fmt: "%s-rds"
# Status patches
- type: ToCompositeFieldPath
fromFieldPath: status.atProvider.address
toFieldPath: status.connectionDetails.host
- type: ToCompositeFieldPath
fromFieldPath: status.atProvider.id
toFieldPath: status.instanceId
connectionDetails:
- name: host
fromFieldPath: status.atProvider.address
type: FromFieldPath
- name: port
type: FromValue
value: "5432"
- name: username
fromConnectionSecretKey: username
type: FromConnectionSecretKey
- name: password
fromConnectionSecretKey: password
type: FromConnectionSecretKey
- name: database
type: FromValue
value: postgres
GCP Database Composition¶
# apis/database/compositions/gcp.yaml
# Composition mapping XPostgreSQLInstance to GCP Cloud SQL
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: xpostgresqlinstances.gcp.database.example.org
labels:
provider: gcp
platform.example.org/category: data
spec:
compositeTypeRef:
apiVersion: database.example.org/v1alpha1
kind: XPostgreSQLInstance
mode: Pipeline
pipeline:
- step: patch-and-transform
functionRef:
name: function-patch-and-transform
input:
apiVersion: pt.fn.crossplane.io/v1beta1
kind: Resources
resources:
- name: cloudsql-instance
base:
apiVersion: sql.gcp.upbound.io/v1beta2
kind: DatabaseInstance
spec:
forProvider:
databaseVersion: POSTGRES_16
region: us-central1
deletionProtection: true
settings:
- tier: db-f1-micro
diskType: PD_SSD
diskSize: 20
diskAutoresize: true
availabilityType: ZONAL
backupConfiguration:
- enabled: true
startTime: "03:00"
pointInTimeRecoveryEnabled: true
backupRetentionSettings:
- retainedBackups: 7
ipConfiguration:
- ipv4Enabled: false
requireSsl: true
insightsConfig:
- queryInsightsEnabled: true
queryPlansPerMinute: 5
databaseFlags:
- name: log_checkpoints
value: "on"
- name: log_connections
value: "on"
userLabels:
managed-by: crossplane
platform: example-org
writeConnectionSecretToRef:
namespace: crossplane-system
patches:
# Region
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.region
# Engine version
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.engineVersion
toFieldPath: spec.forProvider.databaseVersion
transforms:
- type: string
string:
type: Format
fmt: "POSTGRES_%s"
# Storage
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.storageGB
toFieldPath: spec.forProvider.settings[0].diskSize
# Instance size mapping (GCP tiers)
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.size
toFieldPath: spec.forProvider.settings[0].tier
transforms:
- type: map
map:
small: db-f1-micro
medium: db-custom-2-8192
large: db-custom-4-16384
# High availability
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.highAvailability
toFieldPath: spec.forProvider.settings[0].availabilityType
transforms:
- type: map
map:
"true": REGIONAL
"false": ZONAL
# Connection secret name
- type: FromCompositeFieldPath
fromFieldPath: metadata.uid
toFieldPath: spec.writeConnectionSecretToRef.name
transforms:
- type: string
string:
type: Format
fmt: "%s-cloudsql"
connectionDetails:
- name: host
fromFieldPath: status.atProvider.privateIpAddress
type: FromFieldPath
- name: port
type: FromValue
value: "5432"
- name: username
fromConnectionSecretKey: username
type: FromConnectionSecretKey
- name: password
fromConnectionSecretKey: password
type: FromConnectionSecretKey
# Cloud SQL Database
- name: cloudsql-database
base:
apiVersion: sql.gcp.upbound.io/v1beta1
kind: Database
spec:
forProvider:
instanceSelector:
matchControllerRef: true
charset: UTF8
collation: en_US.UTF8
Azure Database Composition¶
# apis/database/compositions/azure.yaml
# Composition mapping XPostgreSQLInstance to Azure Database for PostgreSQL
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: xpostgresqlinstances.azure.database.example.org
labels:
provider: azure
platform.example.org/category: data
spec:
compositeTypeRef:
apiVersion: database.example.org/v1alpha1
kind: XPostgreSQLInstance
mode: Pipeline
pipeline:
- step: patch-and-transform
functionRef:
name: function-patch-and-transform
input:
apiVersion: pt.fn.crossplane.io/v1beta1
kind: Resources
resources:
- name: resource-group
base:
apiVersion: azure.upbound.io/v1beta1
kind: ResourceGroup
spec:
forProvider:
location: East US
tags:
managed-by: crossplane
platform: example-org
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.location
- name: flexible-server
base:
apiVersion: dbforpostgresql.azure.upbound.io/v1beta2
kind: FlexibleServer
spec:
forProvider:
resourceGroupNameSelector:
matchControllerRef: true
location: East US
version: "16"
skuName: B_Standard_B1ms
storageMb: 32768
geoRedundantBackupEnabled: false
backupRetentionDays: 7
publicNetworkAccessEnabled: false
administratorLogin: psqladmin
administratorPasswordSecretRef:
name: db-admin-password
namespace: crossplane-system
key: password
tags:
managed-by: crossplane
platform: example-org
writeConnectionSecretToRef:
namespace: crossplane-system
patches:
# Region
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.location
# Engine version
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.engineVersion
toFieldPath: spec.forProvider.version
# Storage mapping (MB to GB)
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.storageGB
toFieldPath: spec.forProvider.storageMb
transforms:
- type: math
math:
type: Multiply
multiply: 1024
# Instance size mapping (Azure SKUs)
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.size
toFieldPath: spec.forProvider.skuName
transforms:
- type: map
map:
small: B_Standard_B1ms
medium: GP_Standard_D2ds_v4
large: GP_Standard_D4ds_v4
# High availability
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.highAvailability
toFieldPath: spec.forProvider.highAvailability[0].mode
transforms:
- type: map
map:
"true": ZoneRedundant
"false": Disabled
# Backup retention
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.backupRetentionDays
toFieldPath: spec.forProvider.backupRetentionDays
# Connection secret name
- type: FromCompositeFieldPath
fromFieldPath: metadata.uid
toFieldPath: spec.writeConnectionSecretToRef.name
transforms:
- type: string
string:
type: Format
fmt: "%s-flexibleserver"
connectionDetails:
- name: host
fromFieldPath: status.atProvider.fqdn
type: FromFieldPath
- name: port
type: FromValue
value: "5432"
- name: username
type: FromValue
value: psqladmin
- name: password
fromConnectionSecretKey: password
type: FromConnectionSecretKey
# Firewall rule for Azure services
- name: firewall-rule
base:
apiVersion: dbforpostgresql.azure.upbound.io/v1beta1
kind: FlexibleServerFirewallRule
spec:
forProvider:
serverIdSelector:
matchControllerRef: true
startIpAddress: "0.0.0.0"
endIpAddress: "0.0.0.0"
AWS Network Composition¶
# apis/network/compositions/aws.yaml
# Composition mapping XNetwork to AWS VPC resources
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: xnetworks.aws.network.example.org
labels:
provider: aws
platform.example.org/category: networking
spec:
compositeTypeRef:
apiVersion: network.example.org/v1alpha1
kind: XNetwork
mode: Pipeline
pipeline:
- step: patch-and-transform
functionRef:
name: function-patch-and-transform
input:
apiVersion: pt.fn.crossplane.io/v1beta1
kind: Resources
resources:
# VPC
- name: vpc
base:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: VPC
spec:
forProvider:
region: us-east-1
cidrBlock: "10.0.0.0/16"
enableDnsSupport: true
enableDnsHostnames: true
tags:
managed-by: crossplane
Name: platform-vpc
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.region
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.cidr
toFieldPath: spec.forProvider.cidrBlock
- type: ToCompositeFieldPath
fromFieldPath: status.atProvider.id
toFieldPath: status.vpcId
# Internet Gateway
- name: internet-gateway
base:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: InternetGateway
spec:
forProvider:
region: us-east-1
vpcIdSelector:
matchControllerRef: true
tags:
managed-by: crossplane
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.region
# Public Subnet A
- name: subnet-public-a
base:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: Subnet
metadata:
labels:
access: public
zone: a
spec:
forProvider:
region: us-east-1
cidrBlock: "10.0.0.0/24"
vpcIdSelector:
matchControllerRef: true
mapPublicIpOnLaunch: true
tags:
managed-by: crossplane
type: public
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.region
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.availabilityZone
transforms:
- type: string
string:
type: Format
fmt: "%sa"
# Private Subnet A
- name: subnet-private-a
base:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: Subnet
metadata:
labels:
access: private
zone: a
spec:
forProvider:
region: us-east-1
cidrBlock: "10.0.10.0/24"
vpcIdSelector:
matchControllerRef: true
mapPublicIpOnLaunch: false
tags:
managed-by: crossplane
type: private
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.region
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.availabilityZone
transforms:
- type: string
string:
type: Format
fmt: "%sa"
# NAT Gateway (conditional)
- name: nat-gateway
base:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: NATGateway
spec:
forProvider:
region: us-east-1
subnetIdSelector:
matchControllerRef: true
matchLabels:
access: public
zone: a
connectivityType: public
tags:
managed-by: crossplane
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.region
# Security Group
- name: security-group
base:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: SecurityGroup
spec:
forProvider:
region: us-east-1
vpcIdSelector:
matchControllerRef: true
description: "Default security group for platform network"
tags:
managed-by: crossplane
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.region
- type: ToCompositeFieldPath
fromFieldPath: status.atProvider.id
toFieldPath: status.securityGroupId
Provider Configuration¶
AWS Provider with IRSA¶
# providers/provider-aws.yaml
# AWS provider using IAM Roles for Service Accounts
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-family-aws
spec:
package: xpkg.upbound.io/upbound/provider-family-aws:v1.17.0
runtimeConfigRef:
name: aws-runtime-config
---
# Runtime configuration for pod identity
apiVersion: pkg.crossplane.io/v1beta1
kind: DeploymentRuntimeConfig
metadata:
name: aws-runtime-config
spec:
deploymentTemplate:
spec:
selector: {}
template:
spec:
serviceAccountName: crossplane-provider-aws
containers:
- name: package-runtime
args:
- --debug
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 256Mi
---
# Provider configuration using IRSA
apiVersion: aws.upbound.io/v1beta1
kind: ProviderConfig
metadata:
name: default
spec:
credentials:
source: IRSA
GCP Provider with Workload Identity¶
# providers/provider-gcp.yaml
# GCP provider using Workload Identity Federation
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-family-gcp
spec:
package: xpkg.upbound.io/upbound/provider-family-gcp:v1.10.0
runtimeConfigRef:
name: gcp-runtime-config
---
apiVersion: pkg.crossplane.io/v1beta1
kind: DeploymentRuntimeConfig
metadata:
name: gcp-runtime-config
spec:
deploymentTemplate:
spec:
selector: {}
template:
spec:
serviceAccountName: crossplane-provider-gcp
containers:
- name: package-runtime
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 256Mi
---
# Provider configuration using injected credentials
apiVersion: gcp.upbound.io/v1beta1
kind: ProviderConfig
metadata:
name: default
spec:
projectID: my-project-id
credentials:
source: InjectedIdentity
Azure Provider with Managed Identity¶
# providers/provider-azure.yaml
# Azure provider using Pod-managed Identity
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-family-azure
spec:
package: xpkg.upbound.io/upbound/provider-family-azure:v1.10.0
runtimeConfigRef:
name: azure-runtime-config
---
apiVersion: pkg.crossplane.io/v1beta1
kind: DeploymentRuntimeConfig
metadata:
name: azure-runtime-config
spec:
deploymentTemplate:
spec:
selector: {}
template:
spec:
serviceAccountName: crossplane-provider-azure
containers:
- name: package-runtime
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 256Mi
---
apiVersion: azure.upbound.io/v1beta1
kind: ProviderConfig
metadata:
name: default
spec:
credentials:
source: SystemAssignedManagedIdentity
subscriptionID: 00000000-0000-0000-0000-000000000000
tenantID: 00000000-0000-0000-0000-000000000000
Kubernetes and Helm Providers¶
# providers/provider-kubernetes.yaml
# In-cluster Kubernetes provider for secondary resource management
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-kubernetes
spec:
package: xpkg.upbound.io/crossplane-contrib/provider-kubernetes:v0.15.0
---
apiVersion: kubernetes.crossplane.io/v1alpha1
kind: ProviderConfig
metadata:
name: default
spec:
credentials:
source: InjectedIdentity
---
# providers/provider-helm.yaml
# Helm provider for deploying charts as part of compositions
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-helm
spec:
package: xpkg.upbound.io/crossplane-contrib/provider-helm:v0.19.0
---
apiVersion: helm.crossplane.io/v1beta1
kind: ProviderConfig
metadata:
name: default
spec:
credentials:
source: InjectedIdentity
Secret Management¶
Connection Secret Propagation¶
# Connection secret flow: Managed Resource -> XR -> Claim -> Application
# Step 1: Composition writes managed resource secret to crossplane-system
# Step 2: Crossplane propagates connection details to the Claim namespace
# claims/team-a/database.yaml
# Claim with publishConnectionDetailsTo
apiVersion: database.example.org/v1alpha1
kind: PostgreSQLInstance
metadata:
name: orders-db
namespace: team-a
spec:
parameters:
storageGB: 50
size: medium
highAvailability: true
backupRetentionDays: 14
compositionSelector:
matchLabels:
provider: aws
publishConnectionDetailsTo:
name: orders-db-connection
metadata:
labels:
app.kubernetes.io/managed-by: crossplane
app.kubernetes.io/part-of: orders-service
configRef:
name: default
External Secret Store with Vault¶
# config/vault-store.yaml
# Store connection details in HashiCorp Vault
apiVersion: secrets.crossplane.io/v1alpha1
kind: StoreConfig
metadata:
name: vault
spec:
type: Vault
defaultScope: crossplane-system
vault:
mountPath: secret
version: v2
auth:
method: Token
token:
source: Secret
secretRef:
namespace: crossplane-system
name: vault-token
key: token
server: https://vault.example.org
caBundle:
source: Secret
secretRef:
namespace: crossplane-system
name: vault-ca
key: ca.crt
---
# Reference the store in a Claim
apiVersion: database.example.org/v1alpha1
kind: PostgreSQLInstance
metadata:
name: orders-db
namespace: team-a
spec:
parameters:
storageGB: 50
size: medium
publishConnectionDetailsTo:
name: orders-db-vault
configRef:
name: vault
Consuming Connection Secrets in Applications¶
# Application deployment referencing Crossplane connection secret
apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-api
namespace: team-a
spec:
replicas: 3
selector:
matchLabels:
app: orders-api
template:
metadata:
labels:
app: orders-api
spec:
containers:
- name: api
image: registry.example.org/orders-api:v1.2.0
envFrom:
- secretRef:
name: orders-db-connection
env:
- name: DB_HOST
valueFrom:
secretKeyRef:
name: orders-db-connection
key: host
- name: DB_PORT
valueFrom:
secretKeyRef:
name: orders-db-connection
key: port
- name: DB_USER
valueFrom:
secretKeyRef:
name: orders-db-connection
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: orders-db-connection
key: password
- name: DB_NAME
valueFrom:
secretKeyRef:
name: orders-db-connection
key: database
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /health
port: 8080
readinessProbe:
httpGet:
path: /ready
port: 8080
Composition Functions¶
Patch and Transform Strategies¶
# Common patch patterns used across compositions
# Map enum values to provider-specific values
# apis/database/compositions/patches/size-mapping.yaml
apiVersion: pt.fn.crossplane.io/v1beta1
kind: Resources
resources:
- name: example-map-transform
patches:
# Map abstract size to cloud-specific instance type
- 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
xlarge: db.r6g.xlarge
# String formatting for resource names
- type: FromCompositeFieldPath
fromFieldPath: metadata.name
toFieldPath: metadata.annotations["crossplane.io/external-name"]
transforms:
- type: string
string:
type: Format
fmt: "platform-%s"
# Math transform for unit conversion (GB to MB)
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.storageGB
toFieldPath: spec.forProvider.storageMb
transforms:
- type: math
math:
type: Multiply
multiply: 1024
# Convert string boolean to actual boolean
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.highAvailability
toFieldPath: spec.forProvider.multiAz
transforms:
- type: convert
convert:
toType: bool
# Combine multiple fields
- type: CombineFromComposite
combine:
variables:
- fromFieldPath: spec.parameters.region
- fromFieldPath: metadata.name
strategy: string
string:
fmt: "%s-%s-db"
toFieldPath: metadata.annotations["crossplane.io/external-name"]
Go Templating Function¶
# Using function-go-templating for complex logic
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: xnetworks.aws.network.example.org
spec:
compositeTypeRef:
apiVersion: network.example.org/v1alpha1
kind: XNetwork
mode: Pipeline
pipeline:
- step: create-subnets
functionRef:
name: function-go-templating
input:
apiVersion: gotemplating.fn.crossplane.io/v1beta1
kind: GoTemplate
source: Inline
inline:
template: |
{{ $xr := .observed.composite.resource }}
{{ $region := $xr.spec.parameters.region }}
{{ $cidr := $xr.spec.parameters.cidr }}
{{ $count := $xr.spec.parameters.subnetCount | default 3 }}
{{ $zones := list "a" "b" "c" "d" "e" "f" }}
{{ range $i := until (int $count) }}
---
apiVersion: ec2.aws.upbound.io/v1beta1
kind: Subnet
metadata:
name: {{ $xr.metadata.name }}-subnet-{{ index $zones $i }}
annotations:
gotemplating.fn.crossplane.io/composition-resource-name: subnet-{{ index $zones $i }}
labels:
zone: {{ index $zones $i }}
spec:
forProvider:
region: {{ $region }}
availabilityZone: {{ $region }}{{ index $zones $i }}
cidrBlock: "10.0.{{ mul $i 16 }}.0/20"
vpcIdSelector:
matchControllerRef: true
tags:
managed-by: crossplane
zone: {{ index $zones $i }}
{{ end }}
- step: patch-and-transform
functionRef:
name: function-patch-and-transform
input:
apiVersion: pt.fn.crossplane.io/v1beta1
kind: Resources
resources:
- name: vpc
base:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: VPC
spec:
forProvider:
cidrBlock: "10.0.0.0/16"
enableDnsSupport: true
enableDnsHostnames: true
Composition Validation Function¶
# Using function-cel-filter for input validation
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: xpostgresqlinstances.aws.database.example.org
spec:
compositeTypeRef:
apiVersion: database.example.org/v1alpha1
kind: XPostgreSQLInstance
mode: Pipeline
pipeline:
# Step 1: Validate inputs
- step: validate-inputs
functionRef:
name: function-cel-filter
input:
apiVersion: celfilter.fn.crossplane.io/v1beta1
kind: Filters
filters:
# Enforce minimum storage for production
- name: production-min-storage
condition: |
observed.composite.resource.spec.parameters.size == "large" &&
observed.composite.resource.spec.parameters.storageGB < 100
message: "Large instances require at least 100GB storage"
# Enforce HA for large instances
- name: large-requires-ha
condition: |
observed.composite.resource.spec.parameters.size == "large" &&
!observed.composite.resource.spec.parameters.highAvailability
message: "Large instances must enable high availability"
# Step 2: Create resources
- step: patch-and-transform
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/v1beta2
kind: Instance
spec:
forProvider:
engine: postgres
RBAC and Claims¶
Namespace-Scoped Claims¶
# policies/clusterroles.yaml
# RBAC for teams to create Claims in their namespaces
# ClusterRole for database consumers
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: platform:database-consumer
labels:
platform.example.org/role: consumer
rules:
- apiGroups: ["database.example.org"]
resources: ["postgresqlinstances"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch"]
# Only connection secrets
---
# ClusterRole for network consumers
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: platform:network-consumer
labels:
platform.example.org/role: consumer
rules:
- apiGroups: ["network.example.org"]
resources: ["networks"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
# ClusterRole for Kubernetes cluster consumers
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: platform:kubernetes-consumer
labels:
platform.example.org/role: consumer
rules:
- apiGroups: ["compute.example.org"]
resources: ["kubernetesclusters"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
# ClusterRole for storage consumers
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: platform:storage-consumer
labels:
platform.example.org/role: consumer
rules:
- apiGroups: ["storage.example.org"]
resources: ["objectstores"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
# Aggregate role: all platform resources
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: platform:full-consumer
labels:
platform.example.org/role: consumer
aggregationRule:
clusterRoleSelectors:
- matchLabels:
platform.example.org/role: consumer
rules: []
---
# Bind to team namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: platform-consumer
namespace: team-a
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: platform:full-consumer
subjects:
- kind: Group
name: team-a-developers
apiGroup: rbac.authorization.k8s.io
Platform Admin RBAC¶
# policies/platform-admin.yaml
# RBAC for platform team to manage XRDs, Compositions, and Providers
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: platform:admin
rules:
# Manage composite resources and compositions
- apiGroups: ["apiextensions.crossplane.io"]
resources:
- compositeresourcedefinitions
- compositions
- compositionrevisions
verbs: ["*"]
# Manage providers
- apiGroups: ["pkg.crossplane.io"]
resources:
- providers
- providerrevisions
- configurations
- configurationrevisions
- functions
- functionrevisions
verbs: ["*"]
# Manage provider configs
- apiGroups:
- aws.upbound.io
- gcp.upbound.io
- azure.upbound.io
resources: ["*"]
verbs: ["*"]
# View all composite resources
- apiGroups:
- database.example.org
- network.example.org
- compute.example.org
- storage.example.org
- cache.example.org
resources: ["*"]
verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: platform-admin
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: platform:admin
subjects:
- kind: Group
name: platform-team
apiGroup: rbac.authorization.k8s.io
Resource Quotas for Claims¶
# policies/resource-quotas.yaml
# Limit the number and size of Claims per namespace
apiVersion: v1
kind: ResourceQuota
metadata:
name: platform-claims-quota
namespace: team-a
spec:
hard:
# Limit total Claims per resource type
count/postgresqlinstances.database.example.org: "5"
count/networks.network.example.org: "2"
count/kubernetesclusters.compute.example.org: "3"
count/objectstores.storage.example.org: "10"
count/cacheinstances.cache.example.org: "3"
---
# LimitRange for resource sizing constraints
apiVersion: v1
kind: LimitRange
metadata:
name: platform-limits
namespace: team-a
spec:
limits:
- type: Container
default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 100m
memory: 128Mi
Usage Policies¶
Composition Selection¶
# Environment-based composition selection
# claims/team-a/database-dev.yaml
apiVersion: database.example.org/v1alpha1
kind: PostgreSQLInstance
metadata:
name: orders-db
namespace: team-a
labels:
app.kubernetes.io/name: orders-db
app.kubernetes.io/part-of: orders-service
app.kubernetes.io/managed-by: crossplane
platform.example.org/team: team-a
platform.example.org/environment: development
spec:
parameters:
storageGB: 20
size: small
highAvailability: false
backupRetentionDays: 3
deletionProtection: false
compositionSelector:
matchLabels:
provider: aws
publishConnectionDetailsTo:
name: orders-db-connection
# claims/team-a/database-prod.yaml
apiVersion: database.example.org/v1alpha1
kind: PostgreSQLInstance
metadata:
name: orders-db
namespace: team-a-prod
labels:
app.kubernetes.io/name: orders-db
app.kubernetes.io/part-of: orders-service
app.kubernetes.io/managed-by: crossplane
platform.example.org/team: team-a
platform.example.org/environment: production
spec:
parameters:
storageGB: 200
size: large
highAvailability: true
backupRetentionDays: 30
deletionProtection: true
compositionSelector:
matchLabels:
provider: aws
publishConnectionDetailsTo:
name: orders-db-connection
Multi-Cloud Claim Selection¶
# Select GCP composition instead of AWS
apiVersion: database.example.org/v1alpha1
kind: PostgreSQLInstance
metadata:
name: analytics-db
namespace: team-b
spec:
parameters:
storageGB: 100
size: medium
highAvailability: true
engineVersion: "16"
compositionSelector:
matchLabels:
provider: gcp
publishConnectionDetailsTo:
name: analytics-db-connection
---
# Select Azure composition
apiVersion: database.example.org/v1alpha1
kind: PostgreSQLInstance
metadata:
name: reporting-db
namespace: team-c
spec:
parameters:
storageGB: 50
size: medium
highAvailability: false
compositionSelector:
matchLabels:
provider: azure
publishConnectionDetailsTo:
name: reporting-db-connection
Versioning and Upgrades¶
XRD Version Strategy¶
# apis/database/definition-v1beta1.yaml
# Adding a new version alongside the existing one
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xpostgresqlinstances.database.example.org
spec:
group: database.example.org
names:
kind: XPostgreSQLInstance
plural: xpostgresqlinstances
claimNames:
kind: PostgreSQLInstance
plural: postgresqlinstances
connectionSecretKeys:
- host
- port
- username
- password
- database
- connectionString
versions:
# Existing version (still served)
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
parameters:
type: object
properties:
storageGB:
type: integer
default: 20
size:
type: string
enum: [small, medium, large]
default: small
highAvailability:
type: boolean
default: false
# New version with additional fields
- name: v1beta1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
parameters:
type: object
properties:
storageGB:
type: integer
default: 20
size:
type: string
enum: [small, medium, large, xlarge]
default: small
highAvailability:
type: boolean
default: false
# New field in v1beta1
monitoring:
type: object
description: "Monitoring configuration (new in v1beta1)"
properties:
enabled:
type: boolean
default: true
alertEmail:
type: string
format: email
# New field in v1beta1
maintenanceWindow:
type: object
description: "Maintenance window (new in v1beta1)"
properties:
dayOfWeek:
type: string
enum: [MON, TUE, WED, THU, FRI, SAT, SUN]
default: SUN
startHour:
type: integer
minimum: 0
maximum: 23
default: 3
Provider Upgrade Strategy¶
# providers/provider-aws-upgrade.yaml
# Controlled provider upgrade with revision limits
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-family-aws
spec:
package: xpkg.upbound.io/upbound/provider-family-aws:v1.17.0
revisionActivationPolicy: Automatic
revisionHistoryLimit: 3
runtimeConfigRef:
name: aws-runtime-config
# Skip dependency resolution for faster startup
skipDependencyResolution: false
Configuration Package Versioning¶
# Build and push configuration packages with versioning
# Uses the Crossplane CLI (crank)
#!/usr/bin/env bash
# scripts/build-package.sh
# Build and push Crossplane configuration package
set -euo pipefail
readonly REGISTRY="${REGISTRY:-xpkg.upbound.io}"
readonly PACKAGE_NAME="${PACKAGE_NAME:-example/platform-apis}"
readonly VERSION="${1:?Usage: $0 <version>}"
echo "Building configuration package v${VERSION}..."
# Validate all YAML files
find apis/ providers/ -name '*.yaml' -exec kubectl apply --dry-run=client -f {} \;
# Build the package
crossplane xpkg build \
--package-root=. \
--examples-root=apis/ \
--name="platform-apis-v${VERSION}.xpkg"
# Push to registry
crossplane xpkg push \
"${REGISTRY}/${PACKAGE_NAME}:v${VERSION}" \
-f "platform-apis-v${VERSION}.xpkg"
# Tag as latest
crossplane xpkg push \
"${REGISTRY}/${PACKAGE_NAME}:latest" \
-f "platform-apis-v${VERSION}.xpkg"
echo "Published ${REGISTRY}/${PACKAGE_NAME}:v${VERSION}"
Testing Compositions¶
Claim-Based Integration Tests¶
# tests/database/aws-basic.yaml
# Integration test: create and verify a database claim
apiVersion: database.example.org/v1alpha1
kind: PostgreSQLInstance
metadata:
name: test-db-aws
namespace: crossplane-test
labels:
crossplane.io/test: "true"
spec:
parameters:
storageGB: 20
size: small
highAvailability: false
backupRetentionDays: 1
deletionProtection: false
compositionSelector:
matchLabels:
provider: aws
publishConnectionDetailsTo:
name: test-db-aws-connection
#!/usr/bin/env bash
# scripts/test-composition.sh
# Test a composition by creating a Claim and verifying readiness
set -euo pipefail
readonly TEST_FILE="${1:?Usage: $0 <test-yaml>}"
readonly TIMEOUT="${2:-600}"
readonly NAMESPACE="crossplane-test"
# Create test namespace
kubectl create namespace "${NAMESPACE}" --dry-run=client -o yaml | kubectl apply -f -
echo "Applying test claim: ${TEST_FILE}"
kubectl apply -f "${TEST_FILE}"
# Extract resource kind and name
RESOURCE_KIND=$(yq '.kind' "${TEST_FILE}")
RESOURCE_NAME=$(yq '.metadata.name' "${TEST_FILE}")
echo "Waiting for ${RESOURCE_KIND}/${RESOURCE_NAME} to become ready..."
# Wait for Claim to be ready
kubectl wait "${RESOURCE_KIND}/${RESOURCE_NAME}" \
--for=condition=Ready \
--timeout="${TIMEOUT}s" \
--namespace="${NAMESPACE}"
echo "Verifying connection secret..."
kubectl get secret -n "${NAMESPACE}" \
"$(yq '.spec.publishConnectionDetailsTo.name' "${TEST_FILE}")" \
-o jsonpath='{.data}' | jq 'keys'
echo "Test passed: ${RESOURCE_KIND}/${RESOURCE_NAME} is ready"
# Cleanup
echo "Cleaning up test resources..."
kubectl delete -f "${TEST_FILE}" --wait=true --timeout="${TIMEOUT}s"
kubectl delete namespace "${NAMESPACE}" --wait=true
Composition Render Tests¶
#!/usr/bin/env bash
# scripts/render-test.sh
# Offline composition rendering test using crossplane beta render
set -euo pipefail
readonly XR_FILE="${1:?Usage: $0 <xr.yaml> <composition.yaml> <function.yaml>}"
readonly COMPOSITION_FILE="${2:?Missing composition file}"
readonly FUNCTION_FILE="${3:?Missing function file}"
echo "Rendering composition..."
# Render the composition offline (no cluster needed)
crossplane beta render \
"${XR_FILE}" \
"${COMPOSITION_FILE}" \
"${FUNCTION_FILE}" \
--observed-resources=tests/observed/ \
--extra-resources=tests/extra/ |
tee /tmp/rendered-output.yaml
echo ""
echo "Validating rendered resources..."
# Count resources generated
RESOURCE_COUNT=$(yq 'select(.kind != null)' /tmp/rendered-output.yaml | grep -c "^kind:")
echo "Generated ${RESOURCE_COUNT} resources"
# Validate each resource
yq -s '.' /tmp/rendered-output.yaml |
while read -r resource_file; do
KIND=$(yq '.kind' "${resource_file}")
NAME=$(yq '.metadata.name' "${resource_file}")
echo " Validated: ${KIND}/${NAME}"
done
echo "Render test passed"
CI Pipeline for Compositions¶
# .github/workflows/crossplane-test.yml
# CI/CD pipeline for Crossplane platform configurations
name: Crossplane Platform Tests
on:
push:
branches: [main]
paths: ["apis/**", "providers/**", "config/**"]
pull_request:
branches: [main]
paths: ["apis/**", "providers/**", "config/**"]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Crossplane CLI
run: |
curl -sL "https://raw.githubusercontent.com/crossplane/crossplane/master/install.sh" | sh
sudo mv crossplane /usr/local/bin/
- name: Validate YAML syntax
run: |
find apis/ providers/ config/ -name '*.yaml' -exec \
yq eval '.' {} \; > /dev/null
- name: Validate XRD schemas
run: |
for xrd in apis/*/definition.yaml; do
echo "Validating: ${xrd}"
yq eval '.spec.versions[].schema.openAPIV3Schema' "${xrd}" > /dev/null
echo " Schema valid"
done
- name: Lint compositions
run: |
for composition in apis/*/compositions/*.yaml; do
echo "Validating: ${composition}"
# Check required fields
yq eval '.spec.compositeTypeRef' "${composition}" > /dev/null
yq eval '.spec.mode' "${composition}" > /dev/null
echo " Structure valid"
done
- name: Build configuration package
run: |
crossplane xpkg build \
--package-root=. \
--name=platform-apis-test.xpkg
- name: Upload package artifact
uses: actions/upload-artifact@v4
with:
name: crossplane-package
path: platform-apis-test.xpkg
render-test:
runs-on: ubuntu-latest
needs: validate
steps:
- uses: actions/checkout@v4
- name: Setup Crossplane CLI
run: |
curl -sL "https://raw.githubusercontent.com/crossplane/crossplane/master/install.sh" | sh
sudo mv crossplane /usr/local/bin/
- name: Render database compositions
run: |
for provider in aws gcp azure; do
echo "Testing ${provider} database composition..."
crossplane beta render \
tests/database/${provider}-basic.yaml \
apis/database/compositions/${provider}.yaml \
tests/functions.yaml || exit 1
echo "${provider} database composition rendered successfully"
done
- name: Render network compositions
run: |
for provider in aws gcp; do
echo "Testing ${provider} network composition..."
crossplane beta render \
tests/network/${provider}-basic.yaml \
apis/network/compositions/${provider}.yaml \
tests/functions.yaml || exit 1
echo "${provider} network composition rendered successfully"
done
GitOps Integration¶
ArgoCD Application for Crossplane¶
# argocd/crossplane-platform.yaml
# Deploy Crossplane platform APIs via ArgoCD
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: crossplane-platform-apis
namespace: argocd
labels:
app.kubernetes.io/managed-by: argocd
platform.example.org/component: crossplane
annotations:
argocd.argoproj.io/sync-wave: "10"
spec:
project: platform
source:
repoURL: https://github.com/example/crossplane-platform.git
targetRevision: main
path: .
directory:
recurse: true
include: "{apis/**/*.yaml,providers/*.yaml,config/**/*.yaml}"
destination:
server: https://kubernetes.default.svc
namespace: crossplane-system
syncPolicy:
automated:
prune: true
selfHeal: true
allowEmpty: false
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
- PruneLast=true
retry:
limit: 3
backoff:
duration: 30s
factor: 2
maxDuration: 5m
ArgoCD ApplicationSet for Team Claims¶
# argocd/team-claims-appset.yaml
# Auto-discover and deploy team Claims from Git
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: crossplane-team-claims
namespace: argocd
spec:
generators:
- git:
repoURL: https://github.com/example/team-infrastructure.git
revision: main
directories:
- path: "teams/*/claims"
template:
metadata:
name: "claims-{{path.basename}}"
labels:
app.kubernetes.io/managed-by: argocd
platform.example.org/component: claims
spec:
project: teams
source:
repoURL: https://github.com/example/team-infrastructure.git
targetRevision: main
path: "{{path}}"
destination:
server: https://kubernetes.default.svc
namespace: "{{path[1]}}"
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
Flux Kustomization for Crossplane¶
# flux/crossplane-platform.yaml
# Deploy Crossplane platform via Flux
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: crossplane-providers
namespace: flux-system
spec:
interval: 10m
path: ./providers
prune: true
sourceRef:
kind: GitRepository
name: crossplane-platform
healthChecks:
- apiVersion: pkg.crossplane.io/v1
kind: Provider
name: provider-family-aws
- apiVersion: pkg.crossplane.io/v1
kind: Provider
name: provider-family-gcp
dependsOn:
- name: crossplane-install
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: crossplane-apis
namespace: flux-system
spec:
interval: 10m
path: ./apis
prune: true
sourceRef:
kind: GitRepository
name: crossplane-platform
dependsOn:
- name: crossplane-providers
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: crossplane-config
namespace: flux-system
spec:
interval: 10m
path: ./config
prune: true
sourceRef:
kind: GitRepository
name: crossplane-platform
dependsOn:
- name: crossplane-providers
Monitoring and Troubleshooting¶
Health Check Commands¶
#!/usr/bin/env bash
# scripts/health-check.sh
# Crossplane platform health check
set -euo pipefail
echo "=== Crossplane Platform Health Check ==="
echo ""
# Check Crossplane pods
echo "--- Crossplane System Pods ---"
kubectl get pods -n crossplane-system \
-o custom-columns='NAME:.metadata.name,STATUS:.status.phase,RESTARTS:.status.containerStatuses[0].restartCount,AGE:.metadata.creationTimestamp' \
--sort-by='.metadata.name'
echo ""
# Check providers
echo "--- Provider Status ---"
kubectl get providers \
-o custom-columns='NAME:.metadata.name,INSTALLED:.status.conditions[?(@.type=="Installed")].status,HEALTHY:.status.conditions[?(@.type=="Healthy")].status,PACKAGE:.spec.package'
echo ""
# Check provider configs
echo "--- Provider Configs ---"
kubectl get providerconfigs --all-namespaces \
-o custom-columns='NAME:.metadata.name,TYPE:.kind' 2>/dev/null || echo "No provider configs found"
echo ""
# Check XRDs
echo "--- Composite Resource Definitions ---"
kubectl get compositeresourcedefinitions \
-o custom-columns='NAME:.metadata.name,ESTABLISHED:.status.conditions[?(@.type=="Established")].status,OFFERED:.status.conditions[?(@.type=="Offered")].status'
echo ""
# Check Compositions
echo "--- Compositions ---"
kubectl get compositions \
-o custom-columns='NAME:.metadata.name,XR-KIND:.spec.compositeTypeRef.kind,XR-VERSION:.spec.compositeTypeRef.apiVersion'
echo ""
# Check Claims across all namespaces
echo "--- Active Claims ---"
for xrd in $(kubectl get xrd -o jsonpath='{.items[*].spec.claimNames.plural}'); do
echo " ${xrd}:"
kubectl get "${xrd}" --all-namespaces \
-o custom-columns=\
'NAMESPACE:.metadata.namespace,NAME:.metadata.name,READY:.status.conditions[?(@.type=="Ready")].status,SYNCED:.status.conditions[?(@.type=="Synced")].status' \
2>/dev/null || echo " (none)"
done
echo ""
# Check for unhealthy resources
echo "--- Unhealthy Managed Resources ---"
kubectl get managed \
-o custom-columns='KIND:.kind,NAME:.metadata.name,READY:.status.conditions[?(@.type=="Ready")].status,SYNCED:.status.conditions[?(@.type=="Synced")].status' 2>/dev/null |
grep -v "True.*True" |
head -20 || echo "All managed resources healthy"
echo ""
echo "=== Health Check Complete ==="
Troubleshooting Events¶
#!/usr/bin/env bash
# scripts/debug-claim.sh
# Debug a Crossplane claim and its managed resources
set -euo pipefail
readonly KIND="${1:?Usage: $0 <kind> <name> [namespace]}"
readonly NAME="${2:?Usage: $0 <kind> <name> [namespace]}"
readonly NAMESPACE="${3:-default}"
echo "=== Debugging ${KIND}/${NAME} in ${NAMESPACE} ==="
echo ""
# Show Claim status
echo "--- Claim Status ---"
kubectl get "${KIND}" "${NAME}" -n "${NAMESPACE}" -o yaml |
yq '.status'
echo ""
# Show Claim events
echo "--- Claim Events ---"
kubectl events --for="${KIND}/${NAME}" -n "${NAMESPACE}" --sort-by='.lastTimestamp' |
tail -20
echo ""
# Get the composite resource name
XR_NAME=$(kubectl get "${KIND}" "${NAME}" -n "${NAMESPACE}" \
-o jsonpath='{.spec.resourceRef.name}' 2>/dev/null)
if [[ -n "${XR_NAME}" ]]; then
XR_KIND=$(kubectl get "${KIND}" "${NAME}" -n "${NAMESPACE}" \
-o jsonpath='{.spec.resourceRef.kind}')
echo "--- Composite Resource: ${XR_KIND}/${XR_NAME} ---"
kubectl get "${XR_KIND}" "${XR_NAME}" -o yaml |
yq '.status.conditions'
echo ""
echo "--- Composed Resources ---"
kubectl get "${XR_KIND}" "${XR_NAME}" -o yaml |
yq '.spec.resourceRefs[]' 2>/dev/null
echo ""
echo "--- Composed Resource Status ---"
kubectl get "${XR_KIND}" "${XR_NAME}" -o yaml |
yq '.spec.resourceRefs[]' 2>/dev/null |
while IFS= read -r ref; do
REF_KIND=$(echo "${ref}" | yq '.kind')
REF_NAME=$(echo "${ref}" | yq '.name')
echo ""
echo " ${REF_KIND}/${REF_NAME}:"
kubectl get "${REF_KIND}" "${REF_NAME}" -o yaml 2>/dev/null |
yq '.status.conditions' || echo " (not found)"
done
fi
Prometheus Metrics¶
# monitoring/crossplane-metrics.yaml
# Prometheus ServiceMonitor for Crossplane metrics
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: crossplane
namespace: crossplane-system
labels:
app: crossplane
spec:
selector:
matchLabels:
app: crossplane
endpoints:
- port: metrics
interval: 30s
path: /metrics
---
# PrometheusRule for Crossplane alerts
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: crossplane-alerts
namespace: crossplane-system
spec:
groups:
- name: crossplane.rules
rules:
- alert: CrossplaneManagedResourceNotReady
expr: |
crossplane_managed_resource_ready{status="False"} > 0
for: 15m
labels:
severity: warning
annotations:
summary: "Managed resource {{ $labels.name }} not ready"
description: "Crossplane managed resource {{ $labels.kind }}/{{ $labels.name }} has been not ready for 15 minutes"
- alert: CrossplaneProviderUnhealthy
expr: |
crossplane_provider_healthy{status="False"} > 0
for: 5m
labels:
severity: critical
annotations:
summary: "Crossplane provider {{ $labels.name }} unhealthy"
description: "Crossplane provider {{ $labels.name }} has been unhealthy for 5 minutes"
- alert: CrossplaneHighReconcileErrors
expr: |
rate(controller_runtime_reconcile_errors_total{controller=~".*crossplane.*"}[5m]) > 0.1
for: 10m
labels:
severity: warning
annotations:
summary: "High reconciliation error rate for {{ $labels.controller }}"
Anti-Patterns¶
Overly Broad XRD Schemas¶
# Bad - accepts any object without validation
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xdatabases.example.org
spec:
group: example.org
names:
kind: XDatabase
plural: xdatabases
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
# No properties defined - accepts anything
# Good - strict schema with validation, defaults, and descriptions
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xpostgresqlinstances.database.example.org
spec:
group: database.example.org
names:
kind: XPostgreSQLInstance
plural: xpostgresqlinstances
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
parameters:
type: object
properties:
storageGB:
type: integer
description: "Storage size in GB"
minimum: 20
maximum: 10000
default: 20
size:
type: string
description: "Instance size class"
enum: [small, medium, large]
default: small
required:
- storageGB
- size
Hardcoded Cloud-Specific Values in XRDs¶
# Bad - leaking cloud-specific concepts into the abstraction
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xdatabases.example.org
spec:
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
properties:
spec:
properties:
# These are AWS-specific - defeats the purpose of abstraction
instanceClass:
type: string
enum: [db.t3.micro, db.t3.medium, db.r6g.large]
multiAz:
type: boolean
subnetGroupName:
type: string
# Good - cloud-agnostic parameters mapped in compositions
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xpostgresqlinstances.database.example.org
spec:
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
properties:
spec:
properties:
parameters:
properties:
# Abstract size mapped to provider-specific types in composition
size:
type: string
enum: [small, medium, large]
# Generic HA flag mapped per provider
highAvailability:
type: boolean
# Region - each provider maps to its own format
region:
type: string
Missing Connection Secret Keys¶
# Bad - no connectionSecretKeys defined, consumers cannot access credentials
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xdatabases.example.org
spec:
group: example.org
names:
kind: XDatabase
plural: xdatabases
# Missing: connectionSecretKeys
versions:
- name: v1alpha1
served: true
referenceable: true
# Good - explicit connectionSecretKeys for consumer contracts
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xpostgresqlinstances.database.example.org
spec:
group: database.example.org
names:
kind: XPostgreSQLInstance
plural: xpostgresqlinstances
claimNames:
kind: PostgreSQLInstance
plural: postgresqlinstances
connectionSecretKeys:
- host
- port
- username
- password
- database
- connectionString
versions:
- name: v1alpha1
served: true
referenceable: true
Monolithic Compositions¶
# Bad - single composition with everything inlined
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: xapplications.example.org
spec:
compositeTypeRef:
apiVersion: example.org/v1alpha1
kind: XApplication
resources:
# VPC, Subnets, Security Groups, RDS, ElastiCache,
# S3 Bucket, IAM Roles, CloudWatch Alarms,
# EKS Cluster, Node Groups... all in one
# 500+ lines of resources
# Good - compose smaller XRs together
# Step 1: Build focused abstractions
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: xapplications.aws.example.org
spec:
compositeTypeRef:
apiVersion: example.org/v1alpha1
kind: XApplication
mode: Pipeline
pipeline:
- step: patch-and-transform
functionRef:
name: function-patch-and-transform
input:
apiVersion: pt.fn.crossplane.io/v1beta1
kind: Resources
resources:
# Reference existing Network XR
- name: network
base:
apiVersion: network.example.org/v1alpha1
kind: XNetwork
spec:
parameters:
cidr: "10.0.0.0/16"
subnetCount: 3
enableNAT: true
# Reference existing Database XR
- name: database
base:
apiVersion: database.example.org/v1alpha1
kind: XPostgreSQLInstance
spec:
parameters:
storageGB: 50
size: medium
# Reference existing Cache XR
- name: cache
base:
apiVersion: cache.example.org/v1alpha1
kind: XCacheInstance
spec:
parameters:
engine: redis
size: small
References¶
Official Documentation¶
- Crossplane Documentation
- Crossplane GitHub Repository
- Upbound Marketplace
- Composition Functions
- Crossplane CLI Reference