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.

Want structured learning?

Take the full Crossplane course →