Flux, the GitOps tool, can manage Crossplane-provisioned infrastructure declaratively.
Here’s a Crossplane setup managed by Flux:
First, we need a Provider and ProviderConfig for the cloud we’re targeting, let’s say AWS.
# provider.yaml
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-aws
spec:
package: xpkg.upbound.io/upbound/provider-aws:v0.35.0
---
apiVersion: aws.upbound.io/v1beta1
kind: ProviderConfig
metadata:
name: default
spec:
credentials:
source: Secret
secretRef:
namespace: crossplane-system
name: aws-credentials
This tells Crossplane to pull the AWS provider and configure it using credentials from a Kubernetes secret named aws-credentials in the crossplane-system namespace.
Next, we define a Composition and a CompositeResourceDefinition (XRD) for a managed resource, like a managed PostgreSQL instance.
# xrds.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xpginstances.database.example.com
spec:
group: database.example.com
names:
kind: XPostgreSQLInstance
plural: xpginstances
claimNames:
kind: PostgreSQLInstance
plural: postgresqlinstances
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
storageGB:
type: integer
instanceClass:
type: string
required:
- storageGB
- instanceClass
---
# compositions.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: pginstance.database.example.com
labels:
provider: aws
spec:
compositeTypeRef:
apiVersion: database.example.com/v1alpha1
kind: XPostgreSQLInstance
patchSets:
- name: default
patches:
- from: spec.storageGB
to: spec.parameters.storageGB
- from: spec.instanceClass
to: spec.parameters.instanceClass
resources:
- name: rdsinstance
base:
apiVersion: rds.aws.upbound.io/v1beta1
kind: Instance
spec:
allocatedStorage: 20 # Default storage
dbInstanceClass: db.t3.micro # Default instance class
engine: postgres
engineVersion: "13.7"
skipFinalSnapshot: true
patches:
- from: spec.storageGB
to: spec.allocatedStorage
- from: spec.instanceClass
to: spec.dbInstanceClass
The XRD defines the schema for our custom resource XPostgreSQLInstance. The Composition maps this to actual AWS RDS resources. Notice how patchSets automatically maps fields from the composite resource to the managed resource.
Now, we create a CompositeResourceClaim (often just called a claim) to provision an instance.
# claim.yaml
apiVersion: database.example.com/v1alpha1
kind: PostgreSQLInstance
metadata:
name: my-postgres-instance
spec:
storageGB: 100
instanceClass: db.r5.large
This claim, my-postgres-instance, specifies a 100GB PostgreSQL instance of class db.r5.large. Crossplane will use the pginstance.database.example.com composition to create an AWS RDS instance.
To manage all of this with Flux, we’ll set up a Git repository containing these YAML files. Flux will then sync this repository to your Kubernetes cluster.
First, install Flux in your cluster. Assuming you have a Git repository at https://github.com/your-username/crossplane-flux-demo:
flux bootstrap git \
--url=https://github.com/your-username/crossplane-flux-demo \
--branch=main \
--path=./clusters/my-cluster \
--personal
This command configures Flux to watch your Git repository. The --path points to the directory where your Kubernetes manifests are stored.
Inside your Git repository, you’ll have a structure like this:
.
├── clusters
│ └── my-cluster
│ ├── flux-system
│ │ └── kustomization.yaml
│ ├── crossplane
│ │ ├── provider.yaml
│ │ └── providerconfig.yaml
│ ├── xrd
│ │ └── xrds.yaml
│ └── compositions
│ └── compositions.yaml
└── apps
└── postgres-claims
└── claim.yaml
Flux will create a GitRepository and Kustomization object in your cluster. The GitRepository object tells Flux where to get the manifests from, and the Kustomization object tells Flux which paths within that repository to apply to the cluster.
Here’s a sample kustomization.yaml for applying Crossplane resources:
# clusters/my-cluster/crossplane/kustomization.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: crossplane-providers
namespace: flux-system
spec:
interval: 5m
sourceRef:
kind: GitRepository
name: crossplane-flux-demo # Name of the GitRepository object
path: ./crossplane # Path within the Git repo
prune: true
And for the claims:
# apps/postgres-claims/kustomization.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: postgres-claims
namespace: flux-system
spec:
interval: 5m
sourceRef:
kind: GitRepository
name: crossplane-flux-demo
path: ./apps/postgres-claims
prune: true
Flux continuously reconciles the state of your cluster with the Git repository. When you update claim.yaml in Git, Flux detects the change and applies the updated manifest to your cluster. Crossplane then reads this claim and ensures the desired AWS RDS instance is provisioned or updated.
The magic here is that you’re managing cloud infrastructure (like RDS instances) using familiar Kubernetes manifests, and Flux ensures those manifests are always up-to-date with what’s declared in Git. Crossplane acts as the bridge, translating Kubernetes custom resources into cloud-specific API calls.
The most surprising thing about this setup is how seamlessly Crossplane’s abstraction layer integrates with Flux’s GitOps workflow. You’re not just deploying applications; you’re deploying infrastructure definitions that Crossplane then materializes. Flux’s declarative nature ensures that the desired infrastructure state, as defined by your Crossplane resources, is always maintained.
One thing that often trips people up is the interplay between CompositeResourceDefinition (XRD), Composition, and CompositeResourceClaim. The XRD defines the schema of your custom resource, the Composition defines how that custom resource is implemented using Crossplane’s managed resources, and the Claim is the instance of that custom resource that end-users interact with. Flux just sees a list of Kubernetes YAMLs to apply; it doesn’t need to understand the internal Crossplane mechanics, which is the beauty of the separation of concerns.
The next step is typically to manage application deployments (e.g., a web server) that depend on this database, also via Flux, potentially linking their lifecycle to the database’s readiness.