Welcome to this hands-on tutorial! Together, we will explore how to dynamically provision and manage the lifecycle of a PostgreSQL cluster using the power of Crossplane, kube-green and CloudNativePG.
The main goal is to create a reusable, on-demand PostgreSQL cluster definition that can be provisioned whenever needed and automatically scaled down or "put to sleep" during inactive hours to save resources and costs. This is a common pattern for creating ephemeral development or testing environments.
We will cover:
- Setting up a local control plane with Kind.
- Installing and configuring Crossplane and its Kubernetes providers.
- Defining our own cloud infrastructure abstractions using Composition.
- Provisioning a complete PostgreSQL cluster.
- Integrating kube-green to schedule hibernation for our cluster, scaling down the pods during off-hours.
Let's get started!
First, we need to set up our local environment with all the necessary tools. We'll use a local Kubernetes cluster created with kind as our control plane where Crossplane will run.
-
Install Kind Kind lets you run local Kubernetes clusters using Docker container "nodes". It's perfect for development and testing. If you're on macOS and use Homebrew, you can run:
brew install kind
For other installation options, please refer to the official Kind documentation.
-
Install Helm Helm is the package manager for Kubernetes, which helps you manage complex applications. We'll use it to install Crossplane.
brew install helm
For other installation options, see the Helm installation guide.
-
Install kubectl
kubectlis the command-line tool for interacting with Kubernetes clusters.brew install kubectl
For other installation options, see the kubectl installation guide.
-
Create a Local Cluster Now, let's create our control plane cluster using Kind.
kind create cluster --name ephemeral-environments-demo
-
Verify the Cluster Check that your local cluster is up and running.
kubectl get ns
You should see a similar output:
NAME STATUS AGE default Active 4m52s kube-node-lease Active 4m52s kube-public Active 4m52s kube-system Active 4m53s local-path-storage Active 4m47s
With our local cluster ready, it's time to install the core components: Crossplane for infrastructure provisioning and kube-green for lifecycle management.
-
Add Crossplane Helm Repository First, we add the official Helm chart repository for Crossplane.
helm repo add crossplane-stable https://charts.crossplane.io/stable helm repo update
-
Install Crossplane Now, we install Crossplane into its own namespace,
crossplane-system.helm install crossplane --namespace crossplane-system --create-namespace crossplane-stable/crossplane
-
Verify Crossplane Installation Let's make sure the Crossplane pods are running correctly.
kubectl get pods -n crossplane-system
The output should look like this (pod names may vary):
NAME READY STATUS RESTARTS AGE crossplane-67b976bbf4-hn9kk 1/1 Running 0 81s crossplane-rbac-manager-594757659d-dhr97 1/1 Running 0 81s
-
Install Cert-Manager Cert-manager is a dependency for kube-green. It's used to manage certificates for webhooks.
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.5/cert-manager.yaml
-
Install kube-green Finally, we install kube-green, which will manage the sleep schedule for our cluster.
kubectl apply -f https://github.com/kube-green/kube-green/releases/latest/download/kube-green.yaml
-
Verify kube-green Installation Check that the kube-green controller is running.
kubectl get pods -n kube-green
You should see a running pod:
NAME READY STATUS RESTARTS AGE kube-green-controller-manager-6c677846bb-vxs64 1/1 Running 0 85s
Crossplane uses Providers to interact with external APIs like Kubernetes. We need to install and configure the specific providers for Kubernetes.
-
**Install the Kubernetes Provider ** This provider manages Kubernetes resources. You can find it here: manifests/providers/provider-kubernetes.yaml
manifests/providers/provider-kubernetes.yamlapiVersion: pkg.crossplane.io/v1 kind: Provider metadata: name: provider-kubernetes spec: package: xpkg.upbound.io/crossplane-contrib/provider-kubernetes:v0.18.0
kubectl apply -f manifests/providers/provider-kubernetes.yaml
-
Create a ProviderConfig The
ProviderConfigtells the Kubernetes providers how to authenticate. It references the secret we just created. You can find it here: manifests/providers/provider-kubernetes-config.yamlmanifests/providers/provider-kubernetes-config.yamlapiVersion: kubernetes.crossplane.io/v1alpha1 kind: ProviderConfig metadata: name: kubernetes-in-cluster spec: credentials: source: InjectedIdentity
kubectl apply -f manifests/providers/provider-kubernetes-config.yaml
-
Install the CloudNativePG Operator This operator manages PostgreSQL clusters.
kubectl apply --server-side -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.26/releases/cnpg-1.26.0.yaml
-
Check the operator installation
kubectl rollout status deployment -n cnpg-system cnpg-controller-manager
You should get this output:
deployment "cnpg-controller-manager" successfully rolled out
This is where the magic of Crossplane shines. We will define our own custom API for provisioning a "PostgresCluster". This involves creating a few key components:
- CompositeResourceDefinition (XRD): This defines the schema for our custom API—what inputs it accepts and what outputs it returns.
- Composition: This maps our custom API to the actual cloud resources that need to be created.
-
Create the PostgresCluster XRD (
xrd-postgrescluster.yaml) This defines an API to request a standard networking stack. You can find it here: manifests/apis/xrd-postgrescluster.yamlmanifests/apis/xrd-postgrescluster.yamlapiVersion: apiextensions.crossplane.io/v1 kind: CompositeResourceDefinition metadata: name: xpostgresclusters.k8s.crossplane.grazdev.io spec: group: k8s.crossplane.grazdev.io names: kind: XPostgresCluster plural: xpostgresclusters versions: - name: v1alpha1 served: true referenceable: true schema: openAPIV3Schema: type: object properties: spec: type: object properties: instances: type: integer description: "Number of PostgreSQL instances in the cluster." default: 1 hibernation: type: string description: "Set to 'on' to hibernate the cluster, 'off' to wake it up." default: "off" required: - instances
kubectl apply -f manifests/apis/xrd-postgrescluster.yaml
-
Create the PostgresCluster Composition (
composition-postgrescluster.yaml) ThisCompositionmap the abstract API to anObjectresource of the crossplane kubernetes-provider that defines a CloudNativePGCluster. You can find it here: manifests/apis/composition-postgrescluster.yamlmanifests/apis/composition-postgrescluster.yamlapiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: name: postgrescluster.k8s.crossplane.grazdev.io spec: compositeTypeRef: apiVersion: k8s.crossplane.grazdev.io/v1alpha1 kind: XPostgresCluster resources: - name: postgres-cluster-object base: apiVersion: kubernetes.crossplane.io/v1alpha2 kind: Object spec: managementPolicies: ["*"] forProvider: manifest: apiVersion: postgresql.cnpg.io/v1 kind: Cluster metadata: namespace: default spec: storage: size: 1Gi bootstrap: initdb: database: appdb owner: appuser providerConfigRef: name: kubernetes-in-cluster patches: - fromFieldPath: "metadata.name" toFieldPath: "spec.forProvider.manifest.metadata.name" - fromFieldPath: "spec.instances" toFieldPath: "spec.forProvider.manifest.spec.instances" - type: FromCompositeFieldPath fromFieldPath: spec.hibernation toFieldPath: spec.forProvider.manifest.metadata.annotations[cnpg.io/hibernation] policy: fromFieldPath: Optional
kubectl apply -f manifests/apis/composition-postgrescluster.yaml
-
Give the permission to the kubernetes-provider service account to act as a cluster-admin
SA=$(kubectl -n crossplane-system get sa -o name | grep provider-kubernetes | sed -e 's|serviceaccount\/|crossplane-system:|g')Then run:
kubectl create clusterrolebinding provider-kubernetes-admin-binding --clusterrole cluster-admin --serviceaccount="${SA}"
Now that we have defined our custom PostgresCluster API, let's use it to provision a real cluster.
-
Create a Claim A
Claimis a request for a resource defined by our XRD. This simple YAML is all a user needs to provision a complete PostgreSQL cluster. You can find it here: manifests/claims/claim-postgrescluster.yamlmanifests/claims/claim-postgrescluster.yamlapiVersion: k8s.crossplane.grazdev.io/v1alpha1 kind: XPostgresCluster metadata: name: my-production-db namespace: default spec: instances: 1 hibernation: "off"
kubectl apply -f manifests/claims/claim-postgrescluster.yaml
-
Monitor Provisioning Provisioning a PostgreSQL cluster is fast. You can monitor the status with the following command:
kubectl get xpostgrescluster my-production-db
Wait until
SYNCEDandREADYare bothTrue.NAME SYNCED READY COMPOSITION AGE my-production-db True True postgrescluster.k8s.crossplane.grazdev.io 3m50s
-
Access the New PostgreSQL Cluster Once ready, you can check the status of the CloudNativePG cluster:
kubectl get clusters.postgresql.cnpg.io -n default my-production-db
You should see the cluster in a healthy state:
NAME AGE INSTANCES READY STATUS PRIMARY my-production-db 43s 1 1 Cluster in healthy state my-production-db-1
And the database pods:
kubectl get pods -n default -l cnpg.io/cluster=my-production-db
You should see the running pod:
NAME READY STATUS RESTARTS AGE my-production-db-1 1/1 Running 0 65s
Now, let's configure kube-green to automatically scale down our cluster's node pool to save costs during inactive hours.
-
Grant kube-green Permissions We need to give kube-green permission to modify our custom
XPostgresClusterresources. You can find it here: manifests/kube-green/kube-green-postgrescluster.yamlmanifests/kube-green/kube-green-postgrescluster.yamlapiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: kube-green-xpostgrescluster-patcher rules: - apiGroups: - "k8s.crossplane.grazdev.io" resources: - "xpostgresclusters" verbs: - "get" - "list" - "watch" - "patch" --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: kube-green-patch-xpostgrescluster roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: kube-green-xpostgrescluster-patcher subjects: - kind: ServiceAccount name: kube-green-controller-manager namespace: kube-green
kubectl apply -f manifests/kube-green/kube-green-postgrescluster.yaml
-
Create a SleepInfo Resource The
SleepInforesource tells kube-green when to sleep and wake up, and what to patch. In this case, we patch thehibernationparameter of ourXPostgresClusterto scale it down. You can find it here: manifests/kube-green/sleepinfo.yamlmanifests/kube-green/sleepinfo.yamlapiVersion: kube-green.com/v1alpha1 kind: SleepInfo metadata: name: sleep-schedule-for-postgres namespace: default spec: weekdays: "*" timeZone: "Europe/Rome" sleepAt: "18:47" # Adjust to a few minutes from now for testing wakeUpAt: "18:49" # Adjust to a few minutes from now for testing patches: - target: group: k8s.crossplane.grazdev.io kind: XPostgresCluster patch: | - op: replace path: /spec/hibernation value: "on"
kubectl apply -f manifests/kube-green/sleepinfo.yaml
-
Verify Hibernation (Sleep) At the
sleepAttime, kube-green will patch ourXPostgresClusterresource, and Crossplane will hibernate the PostgreSQL cluster.You can watch the
XPostgresClusterresource to see the change:watch "kubectl get xpostgrescluster my-production-db -n default -o yaml"You should see the
hibernationproperty change toon.Then, check the pods:
watch kubectl get pods -n default -l cnpg.io/cluster=my-production-db
You should see no pods running:
No resources found in default namespace. -
Verify Hibernation (Wake Up) At the
wakeUpAttime, kube-green will revert the patch, and Crossplane will wake up the PostgreSQL cluster.You will see the pod being recreated:
watch kubectl get pods -n default -l cnpg.io/cluster=my-production-db
And the cluster will return to a healthy state.
Congratulations! You have successfully created a self-service, ephemeral PostgreSQL cluster with scheduled hibernation.