for loops, strict type safety, HTTP clients for calling external APIs, and the entire Go ecosystem. Let’s write a function that takes subnetCount: 3 and dynamically outputs 3 Subnet MRs.1. The Anatomy of a Function#
A Crossplane Composition Function is a standard gRPC server.
Crossplane sends a RunFunctionRequest containing the current state of the XR and any existing MRs.
Your function reads the request, executes its logic, and returns a RunFunctionResponse containing the new desired MRs.
Because writing gRPC servers from scratch is tedious, Upbound provides a highly abstracted Go SDK.
2. Setting Up the Go Project#
First, we need to initialize a new Go project and install the Crossplane Function SDK.
mkdir function-dynamic-subnets
cd function-dynamic-subnets
go mod init github.com/acmecorp/function-dynamic-subnets
# Install the Crossplane Function SDK
go get github.com/crossplane/function-sdk-go3. Writing the Go Logic#
Create a file named main.go. This file will contain our gRPC server and the mutation logic.
package main
import (
"context"
"fmt"
"log"
"github.com/crossplane/function-sdk-go/proto/v1beta1"
"github.com/crossplane/function-sdk-go/request"
"github.com/crossplane/function-sdk-go/response"
"github.com/crossplane/function-sdk-go"
)
// RunFunction is the entrypoint that Crossplane calls.
func RunFunction(ctx context.Context, req *v1beta1.RunFunctionRequest) (*v1beta1.RunFunctionResponse, error) {
// 1. Initialize a Response object
rsp := response.To(req, response.DefaultTTL)
// 2. Read the XR from the Request
xr, err := request.GetObservedCompositeResource(req)
if err != nil {
return rsp, err
}
// 3. Extract the `subnetCount` parameter from the Developer's Claim
subnetCount, err := xr.Resource.GetNumber("spec.parameters.subnetCount")
if err != nil {
// If the developer didn't provide it, we gracefully default to 1
subnetCount = 1
}
// 4. The Power of Go: A dynamic FOR loop!
for i := 0; i < int(subnetCount); i++ {
// Generate a dynamic name and CIDR block for each subnet
subnetName := fmt.Sprintf("subnet-%d", i)
cidrBlock := fmt.Sprintf("10.0.%d.0/24", i)
// Create the Managed Resource JSON structure in memory
subnetJson := map[string]interface{}{
"apiVersion": "ec2.aws.upbound.io/v1beta1",
"kind": "Subnet",
"spec": map[string]interface{}{
"forProvider": map[string]interface{}{
"cidrBlock": cidrBlock,
"region": "us-east-1",
},
},
}
// 5. Add the generated Subnet to the Response
if err := response.SetDesiredComposedResource(rsp, subnetName, subnetJson); err != nil {
return rsp, err
}
}
// 6. Return the Response to Crossplane
return rsp, nil
}
func main() {
log.Println("Starting Crossplane Function: dynamic-subnets")
// Start the gRPC Server on port 9443
function.Serve(RunFunction, function.Listen(":9443"))
}This tiny Go program completely replaces the need for the YAML Patcher. It is infinitely scalable, deeply testable, and strictly typed.
4. Packaging and Deploying#
Crossplane Functions are distributed as standard OCI container images.
Step 1: Write a Dockerfile#
Create a Dockerfile to compile and package your Go binary.
# Build Stage
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o function .
# Runtime Stage (Minimal scratch image for security)
FROM scratch
COPY --from=builder /app/function /function
ENTRYPOINT ["/function"]
EXPOSE 9443Step 2: Build and Push#
Build the image and push it to a container registry (like DockerHub or AWS ECR).
docker build -t docker.io/myusername/function-dynamic-subnets:v1 .
docker push docker.io/myusername/function-dynamic-subnets:v1Step 3: Install the Function into Crossplane#
Now we tell Crossplane to download and run our custom function in the Kubernetes cluster.
Create install-function.yaml:
apiVersion: pkg.crossplane.io/v1beta1
kind: Function
metadata:
name: function-dynamic-subnets
spec:
# Point to your DockerHub image
package: docker.io/myusername/function-dynamic-subnets:v1Apply it:
kubectl apply -f install-function.yamlCrossplane will spin up a pod running your Go binary.
5. Using the Function in a Composition#
Finally, we update our Composition YAML to call the custom function using the Pipeline architecture we learned in Episode 10.
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: dynamic-network
spec:
compositeTypeRef:
apiVersion: network.acmecorp.com/v1alpha1
kind: XNetwork
mode: Pipeline
pipeline:
# Call our Custom Go Function!
- step: generate-subnets
functionRef:
name: function-dynamic-subnetsThat’s it. No resources block. No patches block.
When a developer submits a claim with subnetCount: 3, Crossplane sends it to your Go container, the container runs the for loop, and 3 physical AWS Subnets are instantly provisioned.
Troubleshooting & Common Errors#
Function is not HEALTHY- Root Cause: The Pod running your Go binary crashed. Usually, this is because you didn’t compile the Go binary statically (
CGO_ENABLED=0) before putting it in ascratchcontainer. - Solution: Verify your Dockerfile compilation flags.
- Root Cause: The Pod running your Go binary crashed. Usually, this is because you didn’t compile the Go binary statically (
cannot run function: timeout- Root Cause: Your Go code contains an infinite loop, or you are making a slow external API call (like a slow HTTP GET request) and exceeding the Crossplane gRPC timeout (default 5 seconds).
- Solution: Keep your Go logic extremely fast. If you must call external APIs, ensure they respond quickly.
Conclusion & Next Steps#
You have successfully replaced static YAML with a dynamic, Turing-complete Go program. This is the zenith of Platform Engineering. You can now enforce complex organizational policies, generate resources dynamically, and integrate with internal corporate APIs directly inside the infrastructure provisioning loop.
But writing infrastructure is only half of the modern DevOps lifecycle. The other half is deployment. How do we automate the deployment of our XRs and Claims?
In Episode 12: GitOps with ArgoCD and Crossplane, we will marry Crossplane with ArgoCD to achieve a 100% automated, push-to-deploy GitOps workflow for physical cloud infrastructure.

