Crossplane Package Development
Crossplane Packages, and specifically the Composition Engine, allow Platform Teams to publish Composite Resources for use by Development Teams or Platform Operators. The end result of a Crossplane Composition is always going to be a set of valid YAML documents which define Crossplane Managed Resources.
Because the output of our compositions is this declarative set of YAML documents, we have the ability to write tests for our configurations. And once we can write tests, we can do test-driven development.
Write the Infrastructure Test First
The Test-Driven workflow for creating new Composite Resources looks like this:
- Create the POC Architecture as Managed Resources, deployed via Crossplane.
- Translate the Managed Resource manifests into tests (using a framework such as kuttl).
- Create the XRD and Compositions for our new Composite Resource.
- Create an Example manifest for the new Composite Resource.
- Iteratively build out the new Composite Resource, using our tests to validate as we write.
- Once the Composite Resource is finalized, commit the tests, package, and example to source control.
This article demonstrates how skaffold can speed up the iterative development loop (step 5) and provide real-time feedback when writing Crossplane Packages. While skaffold’s primary purpose is to support building containers and testing code its deploy functions are just as useful for working with Packages.
Let’s get Started!
You can find a complete example of this tutorial in the A Tour of Crossplane repo.
Prerequisites
We’re going to assume you have the following installed:
Installing skaffold
I am running Mac OS X, so I installed kuttl using homebrew:
brew install skaffold
You can find installation instructions for other platforms on skaffold’s install page.
Step One: Launch Kind
We’re going to quickly spin-up a cluster for local development using kind. You can call it anything, but I tend to call mine “crossplane-tour.”
$ kind create cluster --name crossplane-tour
Creating cluster "crossplane-tour" ...
â Ensuring node image (kindest/node:v1.21.1) đŧ
â Preparing nodes đĻ
â Writing configuration đ
â Starting control-plane đšī¸
â Installing CNI đ
â Installing StorageClass đž
â Waiting ⤠5m0s for control-plane = Ready âŗ
âĸ Ready after 4m2s đ
Now we’re ready to set up skaffold and start our dev loop.
Step Two: Use Skaffold to Helm Install Crossplane and Provider
We need Crossplane running on the cluster to provision Composite Resources and render Compositions. Skaffold supports running helm install in its deploy configuration. Let’s initiate our Skaffold project by creating the skaffold Config file – called skaffold.yaml – at the root of our package repo.
skaffold.yaml:
apiVersion: skaffold/v2beta24
kind: Config
deploy:
helm:
releases:
- name: universal-crossplane
repo: https://charts.upbound.io/stable/
remoteChart: universal-crossplane
namespace: upbound-system
createNamespace: true
version: 1.5.1-up.1
wait: true
We use universal-crossplane at Upbound, but you can use vanilla crossplane. They will both work for this demo.
For this demo we need provider-aws. The helm chart for crossplane supports passing in a list of provider packages during install. We’ll do this using a values file.
Using a values file will make it easier to keep the test dependencies up to date with the actual dependencies, declared in a crossplane.yaml. We’ll keep the values file under the tests folder.
tests/uxp-values.yaml:
provider:
packages:
- crossplane/provider-aws:v0.19.0
We want to wait for the provider to be installed and ready before we attempt to install any compositions. Skaffold supports both pre- and post-deploy hooks. These hooks will run before and after all helm releases are installed. Skaffold does not currently support inserting hooks between helm releases.
skaffold.yaml:
apiVersion: skaffold/v2beta24
kind: Config
deploy:
helm:
hooks:
after:
- host:
command:
[
"sh",
"-c",
"kubectl wait --for condition=healthy --timeout=300s provider/crossplane-provider-aws",
]
releases:
- name: universal-crossplane
repo: https://charts.upbound.io/stable/
remoteChart: universal-crossplane
namespace: upbound-system
createNamespace: true
valuesFiles:
- tests/uxp-values.yaml
version: 1.5.1-up.1
wait: true
With the above, we are ready to run skaffold dev for the first time.
Step Three: Skaffold Dev
The key to our development environment is the skaffold dev command. This command runs our pipeline in a loop, watching our local files and re-running the pipeline whenever changes are saved to disk.
We can see it in action throughout the rest of the demo by starting it now.
One note: we’re going to run with the flag –cleanup=false. This will ensure our resources are not deleted if we hit an error. I have defaulted to using this flag, as my cleanup amounts to deleting the kind cluster. There is no way to set this in skaffold.yaml.
$ skaffold dev --cleanup=false
Listing files to watch...
Generating tags...
Checking cache...
Tags used in deployment:
Starting deploy...
Loading images into kind cluster nodes...
Images loaded in 232ns
Helm release universal-crossplane not installed. Installing...
NAME: universal-crossplane
LAST DEPLOYED: ...
NAMESPACE: upbound-system
STATUS: deployed
REVISION: 1
TEST SUITE: None
...
Waiting for deployments to stabilize...
- upbound-system:deployment/xgql is ready. [3/4 deployment(s) still pending]
- upbound-system:deployment/crossplane is ready. [2/4 deployment(s) still pending]
- upbound-system:deployment/crossplane-rbac-manager is ready. [1/4 deployment(s) still pending]
- upbound-system:deployment/upbound-bootstrapper is ready.
Deployments stabilized in 2.61 seconds
Starting post-deploy hooks...
provider.pkg.crossplane.io/crossplane-provider-aws condition met
Completed post-deploy hooks
Waiting for deployments to stabilize...
Deployments stabilized in 322.442326ms
Press Ctrl+C to exit
Watching for changes...
Leave this terminal open and running skaffold dev.
Step Four: Set Up Tests
Let’s define our test cases before adding any compositions or claims.
Hint: This demo uses the platform-ref-aws Network resource.
Create the tests/compositions/compositenetwork folders and supply a test assertion.
tests/compositions/compositenetwork/01-assert.yaml:
---
apiVersion: ec2.aws.crossplane.io/v1beta1
kind: Subnet
metadata:
labels:
access: private
crossplane.io/claim-name: network
networks.aws.platformref.crossplane.io/network-id: platform-ref-aws-network
zone: us-west-2a
spec:
forProvider:
region: us-west-2
cidrBlock: 192.168.128.0/18
vpcIdSelector:
matchControllerRef: true
availabilityZone: us-west-2a
---
apiVersion: ec2.aws.crossplane.io/v1beta1
kind: Subnet
metadata:
labels:
access: public
crossplane.io/claim-name: network
networks.aws.platformref.crossplane.io/network-id: platform-ref-aws-network
zone: us-west-2a
spec:
forProvider:
region: us-west-2
mapPublicIPOnLaunch: true
cidrBlock: 192.168.0.0/18
vpcIdSelector:
matchControllerRef: true
availabilityZone: us-west-2a
---
apiVersion: ec2.aws.crossplane.io/v1beta1
kind: VPC
metadata:
labels:
crossplane.io/claim-name: network
networks.aws.platformref.crossplane.io/network-id: platform-ref-aws-network
spec:
deletionPolicy: Delete
forProvider:
region: us-west-2
cidrBlock: 192.168.0.0/16
enableDnsSupport: true
enableDnsHostNames: true
tags:
- key: crossplane-kind
value: vpc.ec2.aws.crossplane.io
- key: crossplane-name
- key: crossplane-providerconfig
Next, add a kuttl TestStep to install the resource.
tests/compositions/compositenetwork/01-network.yaml:
apiVersion: kuttl.dev/v1beta1
kind: TestStep
commands:
# Wait for XRD to become "established"
- command: kubectl wait --for condition=established --timeout=20s xrd/compositenetworks.aws.platformref.crossplane.io
# Create the XR/Claim
- command: kubectl apply -f "${PWD}/examples/network.yaml"
And you’ll need to setup a kuttl TestSuite file. Ensure that startKind and skipClusterDelete are set to their proper values. Note, too, that kindContext must match the name of your kind cluster.
kuttl-test.yaml:
apiVersion: kuttl.dev/v1beta1
kind: TestSuite
startKIND: false
testDirs:
- tests/compositions
kindContext: kuttl-test
skipClusterDelete: true
Before we can add this test to our pipeline, we need to configure the XRD and example.
Step Five: Add Composite Resources to the Control Plane
We’ve sourced these Composite Resources from the platform-ref-aws package on github. Because we are demonstrating local development, we’ll save files to disk rather than installing from the web.
Create folders for examples and packages/network. Then download files from the platform-ref-aws repo.
curl https://raw.githubusercontent.com/upbound/platform-ref-aws/main/examples/network.yaml -o examples/network.yaml -s
curl https://raw.githubusercontent.com/upbound/platform-ref-aws/main/network/definition.yaml -o package/network/definition.yaml -s
curl https://raw.githubusercontent.com/upbound/platform-ref-aws/main/network/composition.yaml -o package/network/composition.yaml -s
Deploy Kubernetes Manifests with Skaffold
Earlier we mentioned that Skaffold does not support inserting hooks between helm releases. But Skaffold does allow multiple deployment methods, including applying static kubernetes manifests. We are going to take advantage of this feature to apply our XRD and Compositions after the post-deploy hook for crossplane has completed.
This will only apply kubernetes manifests and cannot be used to run kubectl plugins.
Add a kubectl block to your configuration and add the XRD and Compositions.
skaffold.yaml:
apiVersion: skaffold/v2beta24
kind: Config
deploy:
helm:
hooks:
after:
- host:
command:
[
"sh",
"-c",
"kubectl wait --for condition=healthy --timeout=300s provider/crossplane-provider-aws",
]
releases:
- name: universal-crossplane
repo: https://charts.upbound.io/stable/
remoteChart: universal-crossplane
namespace: upbound-system
createNamespace: true
valuesFiles:
- tests/uxp-values.yaml
version: 1.5.1-up.1
wait: true
kubectl:
manifests:
- package/network/definition.yaml
- package/network/composition.yaml
Once you’ve saved these entries to your file, you should notice skaffold dev picking them up and applying them.
Listing files to watch...
Generating tags...
Checking cache...
Tags used in deployment:
Starting deploy...
Loading images into kind cluster nodes...
Images loaded in 224ns
Waiting for deployments to stabilize...
- upbound-system:deployment/xgql is ready. [3/4 deployment(s) still pending]
- upbound-system:deployment/crossplane is ready. [2/4 deployment(s) still pending]
- upbound-system:deployment/upbound-bootstrapper is ready. [1/4 deployment(s) still pending]
- upbound-system:deployment/crossplane-rbac-manager is ready.
Deployments stabilized in 17.76 seconds
Starting post-deploy hooks...
provider.pkg.crossplane.io/crossplane-provider-aws condition met
Completed post-deploy hooks
Loading images into kind cluster nodes...
Images loaded in 278ns
- compositeresourcedefinition.apiextensions.crossplane.io/compositenetworks.aws.platformref.crossplane.io created
- composition.apiextensions.crossplane.io/compositenetworks.aws.platformref.crossplane.io created
Waiting for deployments to stabilize...
Deployments stabilized in 151.939351ms
Press Ctrl+C to exit
Watching for changes...
It will do this every time you change skaffold.yaml or any file referenced by skaffold.yaml.
Step Six: Add Tests to Skaffold Pipeline
Now that our XRD is established and offered, we can add our kuttl test step.
The same pre- and post-deploy hooks we used on helm are available for kubectl. So we add a post-deploy hook for our resources.
skaffold.yaml:
apiVersion: skaffold/v2beta24
kind: Config
deploy:
helm:
hooks:
after:
- host:
command:
[
"sh",
"-c",
"kubectl wait --for condition=healthy --timeout=300s provider/crossplane-provider-aws",
]
releases:
- name: universal-crossplane
repo: https://charts.upbound.io/stable/
remoteChart: universal-crossplane
namespace: upbound-system
createNamespace: true
valuesFiles:
- tests/uxp-values.yaml
version: 1.5.1-up.1
wait: true
kubectl:
hooks:
after:
- host:
command:
[
"sh",
"-c",
"kubectl kuttl test --test ./tests/compositions/compositenetwork/; exit 0",
]
manifests:
- package/network/definition.yaml
- package/network/composition.yaml
We’ve added ‘; exit 0’ to the end of our command. We want to stop the test from exiting with its normal error code, as this will cause skaffold to completely stop the dev loop.
Success!!
You will get a successful test run.
...
--- PASS: kuttl (19.66s)
--- PASS: kuttl/harness (0.00s)
--- PASS: kuttl/harness/compositenetwork (12.11s)
PASS
Completed post-deploy hooks
Waiting for deployments to stabilize...
Deployments stabilized in 351.199244ms
Press Ctrl+C to exit
Watching for changes...
But there’s one more catch.
Including the Composed Resource
We are applying our examples/network.yaml file from the kuttl TestStep. This means Skaffold is unaware of the file and not watching it for changes. (Go ahead and edit the file, watch skaffold do nothing.)
To get Skaffold to update on changes to our example, we must add it to the list of manifests.
skaffold.yaml:
apiVersion: skaffold/v2beta24
kind: Config
deploy:
helm:
hooks:
after:
- host:
command:
[
"sh",
"-c",
"kubectl wait --for condition=healthy --timeout=300s provider/crossplane-provider-aws",
]
releases:
- name: universal-crossplane
repo: https://charts.upbound.io/stable/
remoteChart: universal-crossplane
namespace: upbound-system
createNamespace: true
valuesFiles:
- tests/uxp-values.yaml
version: 1.5.1-up.1
wait: true
kubectl:
hooks:
after:
- host:
command:
[
"sh",
"-c",
"kubectl kuttl test --test ./tests/compositions/compositenetwork/; exit 0",
]
manifests:
- package/network/definition.yaml
- package/network/composition.yaml
- examples/network.yaml
Be warned: This configuration will fail on a first run because the network resource will not be offered when skaffold applies the example.
To deal with this, we comment out the example when committing to the repo. Only after skaffold dev is running and the resource is healthy do we un-comment it.
The Finished Product
In the end, our folder structure for a configuration package including skaffold dev environment will look like the following:
.
âââ examples # CompositeResource examples
â âââ network.yaml
âââ kuttl-test.yaml # TestSuite configuration
âââ package # XRDs and Composites
â âââ crossplane.yaml # Configuration Package metadata file
â âââ network
â âââ composition.yaml
â âââ definition.yaml
âââ skaffold.yaml # Skaffold Configuration File
âââ tests # TestSteps
âââ compositions
â âââ compositenetwork
â âââ 01-assert.yaml
â âââ 01-network.yaml
âââ uxp-values.yaml # Values for Universal-Crossplane
And this is the complete skaffold.yaml:
apiVersion: skaffold/v2beta24
kind: Config
deploy:
helm:
hooks:
after:
- host:
command:
[
"sh",
"-c",
"kubectl wait --for condition=healthy --timeout=300s provider/crossplane-provider-aws",
]
releases:
- name: universal-crossplane
repo: https://charts.upbound.io/stable/
remoteChart: universal-crossplane
namespace: upbound-system
createNamespace: true
valuesFiles:
- tests/uxp-values.yaml
version: 1.5.1-up.1
wait: true
kubectl:
hooks:
after:
- host:
command:
[
"sh",
"-c",
"kubectl kuttl test --test ./tests/compositions/compositenetwork/; exit 0",
]
manifests:
- package/network/definition.yaml
- package/network/composition.yaml
# This file can only be un-commented once the XRD is healthy in the Control Plane
# - examples/network.yaml