22 KiB
title | weight |
---|---|
Get Started With Composition | 200 |
This guide shows how to create a new kind of custom resource named App
. When a
user calls the custom resource API to create an App
, Crossplane creates a
Deployment
and a Service
.
Crossplane calls this composition. The App
is composed of the
Deployment
and the Service
.
{{<hint "tip">}} The guide shows how to configure composition using YAML, templated YAML, Python, and KCL. You can pick your preferred language. {{}}
An App
custom resource looks like this:
apiVersion: example.crossplane.io/v1
kind: App
metadata:
namespace: default
name: my-app
spec:
image: nginx
status:
replicas: 2 # Copied from the Deployment's status
address: 10.0.0.1 # Copied from the Service's status
The App
is the custom API Crossplane users use to configure an app.
When users create an App
Crossplane creates this Deployment
and Service
:
---
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: default
name: my-app-dhj3a
labels:
example.crossplane.io/app: my-app # Copied from the App's name
spec:
replicas: 2
selector:
matchLabels:
example.crossplane.io/app: my-app # Copied from the App's name
template:
metadata:
labels:
example.crossplane.io/app: my-app # Copied from the App's name
spec:
containers:
- name: app
image: nginx # Copied from the App's spec
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
namespace: default
name: my-app-03mda
labels:
example.crossplane.io/app: my-app # Copied from the App's name
spec:
selector:
example.crossplane.io/app: my-app # Copied from the App's name
ports:
- protocol: TCP
port: 8080
targetPort: 80
Crossplane builds on Kubernetes, so users can use kubectl
or any other tool
from the Kubernetes ecosystem to work with apps.
{{<hint "tip">}} Kubernetes custom resources are just JSON REST APIs, so users can use any tool that supports REST APIs to work with apps. {{}}
Prerequisites
This guide requires:
- A Kubernetes cluster with at least 2 GB of RAM
- The Crossplane v2 preview [installed on the Kubernetes cluster]({{<ref "install">}})
Create the custom resource
Follow these steps to create a new kind of custom resource using Crossplane:
- Define the schema of the
App
custom resource - Install the function you want to use to configure how Crossplane composes apps
- Configure how Crossplane composes apps
After you complete these steps you can
use the new App
custom resource.
Define the schema
Crossplane calls a custom resource that's powered by composition a composite resource, or XR.
{{<hint "note">}} Kubernetes calls user-defined API resources custom resources.
Crossplane calls user-defined API resources that use composition composite resources.
A composite resource is a kind of custom resource. {{}}
Create this composite resource definition (XRD) to define the schema of the
new App
composite resource (XR).
apiVersion: apiextensions.crossplane.io/v2alpha1
kind: CompositeResourceDefinition
metadata:
name: apps.example.crossplane.io
spec:
scope: Namespaced
group: example.crossplane.io
names:
kind: App
plural: apps
versions:
- name: v1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
image:
description: The app's OCI container image.
type: string
required:
- image
status:
type: object
properties:
replicas:
description: The number of available app replicas.
type: integer
address:
description: The app's IP address.
type: string
Save the XRD as xrd.yaml
and apply it:
kubectl apply -f xrd.yaml
Check that Crossplane has established the XRD:
kubectl get -f xrd.yaml
NAME ESTABLISHED OFFERED AGE
apps.example.crossplane.io True 21s
Now that Crossplane has established the XRD, Kubernetes is serving API requests
for the new App
XR.
Crossplane now knows it's responsible for the new App
XR, but it doesn't know
what to do when you create or update one. You tell Crossplane what to do by
installing a function and
configuring a composition.
Install the function
You can use different composition functions to configure what Crossplane does when someone creates or updates a composite resource (XR). Composition functions are like configuration language plugins.
Pick what language to use to configure how Crossplane turns an App
XR into a
Deployment
and a Service
.
{{< tabs >}}
{{< tab "YAML" >}} YAML is a good choice for small, static compositions. It doesn't support loops or conditionals.
Create this composition function to install YAML support:
apiVersion: pkg.crossplane.io/v1
kind: Function
metadata:
name: crossplane-contrib-function-patch-and-transform
spec:
package: xpkg.crossplane.io/crossplane-contrib/function-patch-and-transform:v0.8.2
Save the function as fn.yaml
and apply it:
kubectl apply -f fn.yaml
Check that Crossplane installed the function:
kubectl get -f fn.yaml
NAME INSTALLED HEALTHY PACKAGE AGE
crossplane-contrib-function-patch-and-transform True True xpkg.crossplane.io/crossplane-contrib/function-patch-and-transform:v0.8.2 10s
{{< /tab >}}
{{< tab "Templated YAML" >}} Templated YAML is a good choice if you're used to writing Helm charts.
Create this composition function to install templated YAML support:
apiVersion: pkg.crossplane.io/v1
kind: Function
metadata:
name: crossplane-contrib-function-go-templating
spec:
package: xpkg.crossplane.io/crossplane-contrib/function-go-templating:v0.9.2
Save the function as fn.yaml
and apply it:
kubectl apply -f fn.yaml
Check that Crossplane installed the function:
kubectl get -f fn.yaml
NAME INSTALLED HEALTHY PACKAGE AGE
crossplane-contrib-function-go-templating True True xpkg.crossplane.io/crossplane-contrib/function-go-templating:v0.9.2 9s
{{< /tab >}}
{{< tab "Python" >}} Python is a good choice for compositions with dynamic logic. You can use the full Python standard library.
Create this composition function to install Python support:
apiVersion: pkg.crossplane.io/v1
kind: Function
metadata:
name: crossplane-contrib-function-python
spec:
package: xpkg.crossplane.io/crossplane-contrib/function-python:v0.1.0
Save the function as fn.yaml
and apply it:
kubectl apply -f fn.yaml
Check that Crossplane installed the function:
kubectl get -f fn.yaml
NAME INSTALLED HEALTHY PACKAGE AGE
crossplane-contrib-function-python True True xpkg.crossplane.io/crossplane-contrib/function-python:v0.1.0 12s
{{< /tab >}}
{{< tab "KCL" >}} KCL is a good choice for compositions with dynamic logic. It's fast and sandboxed.
Create this composition function to install KCL support:
apiVersion: pkg.crossplane.io/v1
kind: Function
metadata:
name: crossplane-contrib-function-kcl
spec:
package: xpkg.crossplane.io/crossplane-contrib/function-kcl:v0.11.2
Save the function as fn.yaml
and apply it:
kubectl apply -f fn.yaml
Check that Crossplane installed the function:
kubectl get -f fn.yaml
NAME INSTALLED HEALTHY PACKAGE AGE
crossplane-contrib-function-kcl True True xpkg.crossplane.io/crossplane-contrib/function-kcl:v0.11.2 6s
{{< /tab >}}
{{</ tabs >}}
Configure the composition
A composition tells Crossplane what functions to call when you create or update a composite resource (XR).
Create a composition to tell Crossplane what to do when you create or update an
App
XR.
{{< tabs >}}
{{< tab "YAML" >}} Create this composition to use YAML to configure Crossplane:
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: app-yaml
spec:
compositeTypeRef:
apiVersion: example.crossplane.io/v1
kind: App
mode: Pipeline
pipeline:
- step: create-deployment-and-service
functionRef:
name: crossplane-contrib-function-patch-and-transform
input:
apiVersion: pt.fn.crossplane.io/v1beta1
kind: Resources
resources:
- name: deployment
base:
apiVersion: apps/v1
kind: Deployment
spec:
replicas: 2
template:
spec:
containers:
- name: app
ports:
- containerPort: 80
patches:
- type: FromCompositeFieldPath
fromFieldPath: metadata.name
toFieldPath: metadata.labels[example.crossplane.io/app]
- type: FromCompositeFieldPath
fromFieldPath: metadata.name
toFieldPath: spec.selector.matchLabels[example.crossplane.io/app]
- type: FromCompositeFieldPath
fromFieldPath: metadata.name
toFieldPath: spec.template.metadata.labels[example.crossplane.io/app]
- type: FromCompositeFieldPath
fromFieldPath: spec.image
toFieldPath: spec.template.spec.containers[0].image
- type: ToCompositeFieldPath
fromFieldPath: status.availableReplicas
toFieldPath: status.replicas
readinessChecks:
- type: MatchCondition
matchCondition:
type: Available
status: "True"
- name: service
base:
apiVersion: v1
kind: Service
spec:
ports:
- protocol: TCP
port: 8080
targetPort: 80
patches:
- type: FromCompositeFieldPath
fromFieldPath: metadata.name
toFieldPath: metadata.labels[example.crossplane.io/app]
- type: FromCompositeFieldPath
fromFieldPath: metadata.name
toFieldPath: spec.selector[example.crossplane.io/app]
- type: ToCompositeFieldPath
fromFieldPath: spec.clusterIP
toFieldPath: status.address
readinessChecks:
- type: NonEmpty
fieldPath: spec.clusterIP
{{< /tab >}}
{{< tab "Templated YAML" >}} Create this composition to use templated YAML to configure Crossplane:
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: app-templated-yaml
spec:
compositeTypeRef:
apiVersion: example.crossplane.io/v1
kind: App
mode: Pipeline
pipeline:
- step: create-deployment-and-service
functionRef:
name: crossplane-contrib-function-go-templating
input:
apiVersion: gotemplating.fn.crossplane.io/v1beta1
kind: GoTemplate
source: Inline
inline:
template: |
---
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
gotemplating.fn.crossplane.io/composition-resource-name: deployment
{{ if eq (.observed.resources.deployment | getResourceCondition "Available").Status "True" }}
gotemplating.fn.crossplane.io/ready: "True"
{{ end }}
labels:
example.crossplane.io/app: {{ .observed.composite.resource.metadata.name }}
spec:
replicas: 2
selector:
matchLabels:
example.crossplane.io/app: {{ .observed.composite.resource.metadata.name }}
template:
metadata:
labels:
example.crossplane.io/app: {{ .observed.composite.resource.metadata.name }}
spec:
containers:
- name: app
image: {{ .observed.composite.resource.spec.image }}
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
annotations:
gotemplating.fn.crossplane.io/composition-resource-name: service
{{ if (get (getComposedResource . "service").spec "clusterIP") }}
gotemplating.fn.crossplane.io/ready: "True"
{{ end }}
labels:
example.crossplane.io/app: {{ .observed.composite.resource.metadata.name }}
spec:
selector:
example.crossplane.io/app: {{ .observed.composite.resource.metadata.name }}
ports:
- protocol: TCP
port: 8080
targetPort: 80
---
apiVersion: example.crossplane.io/v1
kind: App
status:
replicas: {{ get (getComposedResource . "deployment").status "availableReplicas" | default 0 }}
address: {{ get (getComposedResource . "service").spec "clusterIP" | default "" | quote }}
{{< /tab >}}
{{< tab "Python" >}} Create this composition to use Python to configure Crossplane:
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: app-python
spec:
compositeTypeRef:
apiVersion: example.crossplane.io/v1
kind: App
mode: Pipeline
pipeline:
- step: create-deployment-and-service
functionRef:
name: crossplane-contrib-function-python
input:
apiVersion: python.fn.crossplane.io/v1beta1
kind: Script
script: |
def compose(req, rsp):
observed_xr = req.observed.composite.resource
rsp.desired.resources["deployment"].resource.update({
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"labels": {"example.crossplane.io/app": observed_xr["metadata"]["name"]},
},
"spec": {
"replicas": 2,
"selector": {"matchLabels": {"example.crossplane.io/app": observed_xr["metadata"]["name"]}},
"template": {
"metadata": {
"labels": {"example.crossplane.io/app": observed_xr["metadata"]["name"]},
},
"spec": {
"containers": [{
"name": "app",
"image": observed_xr["spec"]["image"],
"ports": [{"containerPort": 80}]
}],
},
},
},
})
observed_deployment = req.observed.resources["deployment"].resource
if "status" in observed_deployment:
if "availableReplicas" in observed_deployment["status"]:
rsp.desired.composite.resource.get_or_create_struct("status")["replicas"] = observed_deployment["status"]["availableReplicas"]
if "conditions" in observed_deployment["status"]:
for condition in observed_deployment["status"]["conditions"]:
if condition["type"] == "Available" and condition["status"] == "True":
rsp.desired.resources["deployment"].ready = True
rsp.desired.resources["service"].resource.update({
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"labels": {"example.crossplane.io/app": observed_xr["metadata"]["name"]},
},
"spec": {
"selector": {"example.crossplane.io/app": observed_xr["metadata"]["name"]},
"ports": [{"protocol": "TCP", "port": 8080, "targetPort": 80}],
},
})
observed_service = req.observed.resources["service"].resource
if "spec" in observed_service and "clusterIP" in observed_service["spec"]:
rsp.desired.composite.resource.get_or_create_struct("status")["address"] = observed_service["spec"]["clusterIP"]
rsp.desired.resources["service"].ready = True
{{<hint "tip">}} You can write your own function in Python.
It's a good idea to write your own function for larger configurations. When you write your own function you can write multiple files of Python. You don't embed the Python in YAML, so it's easier to use a Python IDE.
Read the [guide to writing a composition function in Python]({{<ref "../guides/write-a-composition-function-in-python">}}). {{}} {{< /tab >}}
{{< tab "KCL" >}} Create this composition to use KCL to configure Crossplane:
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: app-kcl
spec:
compositeTypeRef:
apiVersion: example.crossplane.io/v1
kind: App
mode: Pipeline
pipeline:
- step: create-deployment-and-service
functionRef:
name: crossplane-contrib-function-kcl
input:
apiVersion: krm.kcl.dev/v1alpha1
kind: KCLInput
spec:
source: |
observed_xr = option("params").oxr
_desired_deployment = {
apiVersion = "apps/v1"
kind = "Deployment"
metadata = {
annotations = {
"krm.kcl.dev/composition-resource-name" = "deployment"
}
labels = {"example.crossplane.io/app" = observed_xr.metadata.name}
}
spec = {
replicas = 2
selector.matchLabels = {"example.crossplane.io/app" = observed_xr.metadata.name}
template = {
metadata.labels = {"example.crossplane.io/app" = observed_xr.metadata.name}
spec.containers = [{
name = "app"
image = observed_xr.spec.image
ports = [{containerPort = 80}]
}]
}
}
}
observed_deployment = option("params").ocds["deployment"]?.Resource
if any_true([c.type == "Available" and c.status == "True" for c in observed_deployment?.status?.conditions or []]):
_desired_deployment.metadata.annotations["krm.kcl.dev/ready"] = "True"
_desired_service = {
apiVersion = "v1"
kind = "Service"
metadata = {
annotations = {
"krm.kcl.dev/composition-resource-name" = "service"
}
labels = {"example.crossplane.io/app" = observed_xr.metadata.name}
}
spec = {
selector = {"example.crossplane.io/app" = observed_xr.metadata.name}
ports = [{protocol = "TCP", port = 8080, targetPort = 80}]
}
}
observed_service = option("params").ocds["service"]?.Resource
if observed_service?.spec?.clusterIP:
_desired_service.metadata.annotations["krm.kcl.dev/ready"] = "True"
_desired_xr = {
**option("params").dxr
status.address = observed_service?.spec?.clusterIP or ""
status.replicas = observed_deployment?.status?.availableReplicas or 0
}
items = [_desired_deployment, _desired_service, _desired_xr]
{{< /tab >}}
{{</ tabs >}}
Save the composition as composition.yaml
and apply it:
kubectl apply -f composition.yaml
{{<hint "note">}} A composition can include multiple functions.
Functions can change the results of earlier functions in the pipeline. Crossplane uses the result returned by the last function. {{}}
Use the custom resource
Crossplane now understands App
custom resources.
Create an App
:
apiVersion: example.crossplane.io/v1
kind: App
metadata:
namespace: default
name: my-app
spec:
image: nginx
Save the App
as app.yaml
and apply it:
kubectl apply -f app.yaml
Check that the App
is ready:
kubectl get -f app.yaml
NAME SYNCED READY COMPOSITION AGE
my-app True True app-yaml 56s
{{<hint "note">}}
The COMPOSITION
column shows what composition the App
is using.
You can create multiple compositions for each kind of XR. [Read the XR page]({{<ref "../composition/composite-resources">}}) to learn how to select which composition Crossplane uses. {{}}
Check that Crossplane created a Deployment
and a Service
:
kubectl get deploy,service -l example.crossplane.io/app=my-app
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/my-app-2r2rk 2/2 2 2 11m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/my-app-xfkzg ClusterIP 10.96.148.56 <none> 8080/TCP 11m
{{<hint "tip">}}
Use kubectl edit -f app.yaml
to edit the App
's image. Crossplane updates
the Deployment
's image to match.
{{}}
Delete the App
.
kubectl delete -f app.yaml
When you delete the App
, Crossplane deletes the Deployment
and Service
.
Next steps
Managed resources (MRs) are ready-made Kubernetes custom resources.
Crossplane has an extensive library of managed resources you can use to manage almost any cloud provider, or cloud native software.
[Get started with managed resources]({{<ref "get-started-with-managed-resources">}}) to learn more about them.
You can use MRs with composition. Try updating your App
composition to include
an MR.