Go’s text/template package is a surprisingly powerful tool for generating infrastructure code, even when you’re not writing Go applications directly.
Let’s say you want to provision a Kubernetes Service that dynamically points to a Deployment based on its name. Here’s how you might do it with Crossplane and Go templating:
apiVersion: v1
kind: Service
metadata:
name: {{ .Values.appName }}-service
spec:
selector:
app: {{ .Values.appName }}
ports:
- protocol: TCP
port: 80
targetPort: 8080
And here’s the CompositeResourceDefinition (XRD) and CompositeResource (XR) that would feed this template:
# xrd.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: services.example.com
spec:
group: example.com
names:
kind: Service
plural: services
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
appName:
type: string
port:
type: integer
required:
- appName
---
# xr.yaml
apiVersion: example.com/v1alpha1
kind: Service
metadata:
name: my-app-service
spec:
appName: my-app
port: 80
When Crossplane reconciles this XR, it will use the appName: my-app from the XR’s spec to render the Service manifest. The metadata.name will become my-app-service and the spec.selector.app will also be my-app.
This templating capability is built into Crossplane’s CompositeResource mechanism, specifically within the composition field. A composition defines how an XR is translated into a set of Kubernetes resources (or other cloud provider resources managed by Crossplane providers). Within the resources section of a composition, you can define patches that map XR fields to fields in the generated resources, and critically, you can use Go templating for dynamic value generation.
The key lies in the fromComposite or fromFieldPath directives within patches. When you use a Go template expression like {{ .Values.fieldName }}, Crossplane treats the XR’s spec as the Values context for the template. This allows you to parameterize your infrastructure. You can also access nested fields, for example, {{ .Values.database.username }} if your XR had a nested database object.
Beyond simple value substitution, Go’s templating functions offer a lot of power. You can use built-in functions like quote, upper, lower, title, replace, and default. For instance, if you wanted to ensure an environment variable name was always uppercase:
# within a Composition resource definition
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: my-app-composition
spec:
compositeTypeRef:
kind: Service
group: example.com
resources:
- name: my-deployment
base:
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: app
image: nginx:latest
env:
- name: APP_ENV
value: {{ upper .Values.environment }} # Using the 'upper' function
patches:
- fromFieldPath: spec.appName
toFieldPath: spec.template.metadata.labels.app
- fromFieldPath: spec.replicas
toFieldPath: spec.replicas
If your XR had spec.environment: production, the APP_ENV environment variable in the rendered Deployment would be PRODUCTION. You can also chain functions, like {{ replace "." "-" (upper .Values.appName) }}.
The true magic of this system is when you combine it with Crossplane’s Provider and CompositeResource workflow. You define an abstract CompositeResource (like our Service example), and then a Composition that specifies how to render concrete cloud resources (like Kubernetes Service and Deployment objects, or AWS EC2 instances, or Azure SQL databases) from that abstract definition. The Go templating within the Composition is what makes the rendering dynamic and adaptable to the parameters provided in the CompositeResource.
What most people don’t realize is that the {{ .Values }} context in Crossplane’s Go templating also includes fields from the metadata of the Composite Resource, not just its spec. So, if you needed to use the name of the XR itself, you could access it via {{ .Metadata.name }}. This opens up possibilities for self-referential configurations or naming conventions derived directly from the XR’s identity.
The next step in mastering Crossplane’s templating is to explore custom Go template functions, which allow you to encapsulate complex logic and make your compositions even more powerful and reusable.