Crossplane Integration
This document provides an example deployment walkthrough showing how to integrate kube-bind with Crossplane and how to deploy a sample managed MySQL resource using two kind clusters: a provider cluster (where Crossplane runs and kube-bind backend to export APIs) and a consumer cluster (which allows to bind those APIs using kube-bind konnector).
Note
Currently for permission claims to work properly, it is required to run namespaced Crossplane resources.

Setup
The following sections will guide you through the one-time setup that is required for providing MySQL databases using Crossplane and kube-bind.
Install Crossplane
Install Crossplace in your Kubernetes cluster where the kube-bind backend will run. You can follow the official installation guide from the Crossplane documentation.
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update
helm install crossplane crossplane-stable/crossplane \
--namespace crossplane-system \
--create-namespace
Install Crossplane provider-sql
In this example, we will set up MySQL database:
kubectl apply -f - <<EOF
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-sql
spec:
package: xpkg.upbound.io/crossplane-contrib/provider-sql:v0.13.0
EOF
Deploy also Crossplane function for Go templating:
kubectl apply -f - <<EOF
apiVersion: pkg.crossplane.io/v1
kind: Function
metadata:
name: function-go-templating
spec:
package: xpkg.crossplane.io/crossplane-contrib/function-go-templating:v0.9.2
EOF
Setup the MySQL Deployment
Create and set up Deployment, PersistentVolume, PersistentVolumeClaim and Service for the
MySQL instance.
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql
spec:
selector:
matchLabels:
app: mysql
strategy:
type: Recreate
template:
metadata:
labels:
app: mysql
spec:
containers:
- image: mysql:9
name: mysql
env:
# Use secret in real usage
- name: MYSQL_ROOT_PASSWORD
value: password
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: mysql-persistent-storage
mountPath: /var/lib/mysql
volumes:
- name: mysql-persistent-storage
persistentVolumeClaim:
claimName: mysql-pv-claim
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: mysql-pv-volume
labels:
type: local
spec:
storageClassName: manual
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
hostPath:
path: "/mnt/data"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-pv-claim
spec:
storageClassName: manual
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
ports:
- port: 3306
selector:
app: mysql
clusterIP: None
EOF
Configure Crossplane
Time to create a Crossplane XRD and Composition for a managed MySQL database. Apply both manifests:
kubectl apply -f - <<EOF
apiVersion: apiextensions.crossplane.io/v2
kind: CompositeResourceDefinition
metadata:
name: mysqldatabases.mangodb.com
labels:
kube-bind.io/exported: "true"
spec:
scope: Namespaced
group: mangodb.com
names:
kind: MySQLDatabase
plural: mysqldatabases
versions:
- name: v1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
name:
description: The name of the database to create
type: string
required:
- name
status:
type: object
properties:
ready:
description: Whether the database setup is ready
type: boolean
connectionSecret:
description: Name of the connection secret
type: string
EOF
kubectl apply -f - <<'EOF'
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: mysql-database-simple
spec:
compositeTypeRef:
apiVersion: mangodb.com/v1
kind: MySQLDatabase
mode: Pipeline
pipeline:
- functionRef:
name: function-go-templating
input:
apiVersion: gotemplating.fn.crossplane.io/v1beta1
inline:
template: |
{{ $objName := .observed.composite.resource.metadata.name }}
{{ $dbName := .observed.composite.resource.spec.name }}
{{ $objNamespace := .observed.composite.resource.metadata.namespace }}
{{ $userName := printf "%s-user" $dbName }}
{{ $secretName := printf "%s-secret" $dbName }}
{{ $credentials := printf "%s-credentials" $objName }}
---
apiVersion: mysql.sql.m.crossplane.io/v1alpha1
kind: Database
metadata:
annotations:
gotemplating.fn.crossplane.io/composition-resource-name: database
{{ if eq (.observed.resources.database | getResourceCondition "Synced").Status "True" }}
gotemplating.fn.crossplane.io/ready: "True"
{{ end }}
name: {{ $dbName }}
namespace: default
spec:
forProvider: {}
providerConfigRef:
kind: ProviderConfig
name: mysql-cfg
---
apiVersion: v1
kind: Secret
metadata:
annotations:
gotemplating.fn.crossplane.io/composition-resource-name: secret-exposed
gotemplating.fn.crossplane.io/ready: "True"
labels:
kube-bind.io/selector: consumer-database
namespace: default
name: {{ $credentials }}
{{ if eq $.observed.resources nil }}
stringData: {}
{{ else }}
stringData:
username: {{ ( index $.observed.resources "user" ).connectionDetails.username }}
password: {{ ( index $.observed.resources "user" ).connectionDetails.password }}
port: {{ ( index $.observed.resources "user" ).connectionDetails.port }}
endpoint: {{ ( index $.observed.resources "user" ).connectionDetails.endpoint }}
{{ end }}
---
apiVersion: v1
kind: Secret
metadata:
annotations:
gotemplating.fn.crossplane.io/composition-resource-name: secret
gotemplating.fn.crossplane.io/ready: "True"
namespace: default
name: {{ $secretName }}
data:
password: {{ randAlphaNum 16 | b64enc }}
---
# Hardcoded demo Secret used by ProviderConfig (in default namespace)
apiVersion: v1
kind: Secret
metadata:
annotations:
gotemplating.fn.crossplane.io/composition-resource-name: provider-db-conn
gotemplating.fn.crossplane.io/ready: "True"
namespace: default
name: db-conn
type: Opaque
stringData:
endpoint: mysql.default.svc.cluster.local
port: "3306"
username: root
password: password
---
apiVersion: mysql.sql.m.crossplane.io/v1alpha1
kind: User
metadata:
annotations:
gotemplating.fn.crossplane.io/composition-resource-name: user
{{ if eq (.observed.resources.user | getResourceCondition "Synced").Status "True" }}
gotemplating.fn.crossplane.io/ready: "True"
{{ end }}
name: {{ $userName }}
namespace: default
spec:
forProvider:
passwordSecretRef:
name: {{ $secretName }}
key: password
writeConnectionSecretToRef:
name: {{ printf "%s-connection-secret" $dbName }}
providerConfigRef:
kind: ProviderConfig
name: mysql-cfg
---
apiVersion: mangodb.com/v1
kind: MySQLDatabase
metadata:
name: {{ $objName }}
namespace: default
status:
ready: {{ and (eq (.observed.resources.database | getResourceCondition "Synced").Status "True") (eq (.observed.resources.user | getResourceCondition "Synced").Status "True") }}
connectionSecret: {{ printf "%s-connection-secret" $dbName }}
---
apiVersion: mysql.sql.m.crossplane.io/v1alpha1
kind: ProviderConfig
metadata:
name: mysql-cfg
annotations:
gotemplating.fn.crossplane.io/composition-resource-name: provider-cfg
gotemplating.fn.crossplane.io/ready: "True"
spec:
credentials:
source: MySQLConnectionSecret
connectionSecretRef:
name: db-conn
tls: preferred
kind: GoTemplate
source: Inline
step: create-mysql-resources
EOF
Export the Database API
Create an APIServiceExportTemplate for the mysqldatabase.mangodb.com resource:
kubectl apply -f - <<EOF
apiVersion: kube-bind.io/v1alpha2
kind: APIServiceExportTemplate
metadata:
name: mysqldatabase
spec:
resources:
- group: mangodb.com
resource: mysqldatabases
versions:
- v1
permissionClaims:
- group: ""
resource: secrets
selector:
labelSelector:
matchLabels:
kube-bind.io/selector: consumer-database
scope: Namespaced
EOF
Usage
Now that everything is set up, users can begin to bind to your backend and begin consuming the new API.
Login to kube-bind
Request a Binding
Wait for the Binding to be Established
Once the binding is active, you can create MySQLDatabase resources in your consumer cluster,
and you will get MySQLDatabase objects synced from the provider cluster.
kubectl bind
🌐 Opening kube-bind UI in your browser...
https://kube-bind.example.com?redirect_url=....
Browser opened successfully
Waiting for binding completion from UI...
(Press Ctrl+C to cancel)
Binding completed successfully!
🔒 Updated secret kube-bind/kubeconfig-zxrdn for host https://kube-bind.example.com, namespace kube-bind-bp52k
🚀 Deploying konnector v0.6.0 to namespace kube-bind with custom image "ghcr.io/kube-bind/konnector:v0.6.0".
✅ Created APIServiceBinding mysqldatabase-6rvjt for 1 resources
Created 1 APIServiceBinding(s):
- mysqldatabase-6rvjt
Resources bound successfully!
Create a Managed Database
Verify that a mysqldatabases.mangodb.com CRD is synced to the consumer cluster:
k get crd mysqldatabases.mangodb.com
NAME CREATED AT
mysqldatabases.mangodb.com 2025-11-27T14:22:18Z
Order a new consumer database instance in the provider cluster:
kubectl apply -f - <<EOF
apiVersion: mangodb.com/v1
kind: MySQLDatabase
metadata:
name: consumer-database
namespace: default
spec:
name: consumer-database
EOF
Wait for Provisioning
The kube-bind konnector and the CloudNativePG operator should now be busy provisioning your database. You can observe the provisioned database and connection Secret in the provider cluster:
kubectl get mysqldatabases.mangodb.com kube-bind-bp52k-consumer-database
NAME SYNCED READY COMPOSITION AGE
kube-bind-bp52k-consumer-database True True mysql-database-simple 18m
kubectl get secrets -n default
NAME TYPE DATA AGE
consumer-database-connection-secret connection.crossplane.io/v1alpha1 4 18m
consumer-database-secret Opaque 1 18m
db-conn Opaque 4 20m
kube-bind-bp52k-consumer-database-credentials Opaque 4 18m
apiVersion: mangodb.com/v1
kind: MySQLDatabase
metadata:
annotations:
kube-bind.io/cluster-namespace: kube-bind-bp52k
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"mangodb.com/v1","kind":"MySQLDatabase","metadata":{"annotations":{},"name":"consumer-database"},"spec":{"name":"consumer-database"}}
creationTimestamp: "2025-11-270T15:39:31Z"
finalizers:
- composite.apiextensions.crossplane.io
generation: 4
labels:
crossplane.io/composite: kube-bind-bp52k-consumer-database
name: kube-bind-bp52k-consumer-database
ownerReferences:
- apiVersion: v1
kind: Namespace
name: kube-bind-bp52k
uid: d20ee5be-e9d7-41e3-95ee-76897a554750
resourceVersion: "136223"
uid: 5d510666-f5d3-4bf5-98e0-384364a81170
spec:
crossplane:
compositionRef:
name: mysql-database-simple
compositionRevisionRef:
name: mysql-database-simple-c36a727
compositionUpdatePolicy: Automatic
resourceRefs:
- apiVersion: mysql.sql.m.crossplane.io/v1alpha1
kind: Database
name: consumer-database
- apiVersion: mysql.sql.m.crossplane.io/v1alpha1
kind: User
name: consumer-database-user
- apiVersion: v1
kind: Secret
name: consumer-database
namespace: default
name: consumer-database
status:
conditions:
- lastTransitionTime: "2025-11-27T15:39:32Z"
observedGeneration: 4
reason: ReconcileSuccess
status: "True"
type: Synced
- lastTransitionTime: "2025-11-27T15:39:32Z"
observedGeneration: 4
reason: Available
status: "True"
type: Ready
connectionSecret: consumer-database
ready: true
You should see your MySQL instance created in the provider cluster and a secret with connection details, once Crossplane finishes provisioning of the database.
Observe that the requested Secret with connection details for user is synced to consumer cluster.
kubectl get secrets
NAMESPACE NAME TYPE DATA AGE
default consumer-database-credentials Opaque 4 5m21s
For troubleshooting and more information, check the kube-bind documentation.