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.

Want structured learning?

Take the full Crossplane course →