2. Build your composition logic
In the previous guide, you created a brand new project and reviewed the foundational components of your project. This guide walks through how to update those components to create real resources in your Upbound organization.
Prerequisites​
Make sure you've completed the previous guide and have:
- An Upbound account
- The Up CLI installed
- kubectl installed
- Docker Desktop running
- A project with the basic structure (
upbound.yaml,apis/,examples/) - Provider dependencies added
- An XRD and Composition generated from your example claim
If you missed any of the previous steps, go to the project foundations guide to get started.
Generate a function​
Composition functions allow you to write the logic for creating cloud resources with programming languages like KCL, Python, or Go.
With composition functions you can:
- Write code with familiar programming concepts like variables, loops, and conditions
- Catch errors with type safety before deployment
- Keep related logic together in an easier to parse format
- Test your infrastructure logic like any other code
Your composition function is a program you write that translates your user's requests as the inputs and returns specific cloud resources as the outputs.
Generate the composition function scaffolding and choose your preferred language:
up function generate example-function apis/storagebuckets/composition.yaml --language=kcl
This command creates a function directory and creates a new file based on your chosen language.
Create your function logic​
Next, create the actual program logic that builds your cloud resources.
Paste the following into main.k:
import models.io.upbound.awsm.s3.v1beta1 as awsms3v1beta1
oxr = option("params").oxr # observed composite resource
params = oxr.spec.parameters # extract parameter values from XR
_metadata = lambda name: str -> any {
{
generateName = name # due to global S3 naming restrictions we'll have
# Crossplane generate a name to garauntee uniqueness
annotations = {
"krm.kcl.dev/composition-resource-name" = name
}
}
}
_items: [any] = [
# Create S3 Bucket
awsms3v1beta1.Bucket {
metadata: _metadata("{}-bucket".format(oxr.metadata.name))
spec = {
forProvider = {
region = params.region
}
}
},
# Bucket BOC
awsms3v1beta1.BucketOwnershipControls {
metadata: _metadata("{}-boc".format(oxr.metadata.name))
spec = {
forProvider = {
bucketSelector: {
matchControllerRef: True
}
region = params.region
rule = {
objectOwnership = "BucketOwnerPreferred"
}
}
}
},
# Bucket PAB
awsms3v1beta1.BucketPublicAccessBlock {
metadata: _metadata("{}-pab".format(oxr.metadata.name))
spec = {
forProvider = {
bucketSelector: {
matchControllerRef: True
}
region = params.region
blockPublicAcls: False
ignorePublicAcls: False
restrictPublicBuckets: False
blockPublicPolicy: False
}
}
},
# Bucket ACL
awsms3v1beta1.BucketACL {
metadata: _metadata("{}-acl".format(oxr.metadata.name))
spec = {
forProvider = {
bucketSelector: {
matchControllerRef: True
}
region = params.region
acl = params.acl
}
}
},
# Default encryption for the bucket
awsms3v1beta1.BucketServerSideEncryptionConfiguration {
metadata: _metadata("{}-encryption".format(oxr.metadata.name))
spec = {
forProvider = {
region = params.region
bucketSelector: {
matchControllerRef: True
}
rule = [
{
applyServerSideEncryptionByDefault = {
sseAlgorithm = "AES256"
}
bucketKeyEnabled = True
}
]
}
}
}
]
# Set up versioning for the bucket if desired
if params.versioning:
_items += [
awsms3v1beta1.BucketVersioning{
metadata: _metadata("{}-versioning".format(oxr.metadata.name))
spec = {
forProvider = {
region = params.region
bucketSelector: {
matchControllerRef: True
}
versioningConfiguration = {
status = "Enabled"
}
}
}
}
]
items = _items
Save your composition file.
Review your function​
Resource imports and user inputs​
import models.io.upbound.awsm.s3.v1beta1 as awsms3v1beta1
oxr = option("params").oxr # observed composite resource
params = oxr.spec.parameters # extract parameter values from XR
This section:
- Imports the cloud resource definitions required to create the resource
- Extracts the user-specified values in the claim (
params = oxr.spec.parameters)
Metadata and helper functions​
_metadata = lambda name: str -> any {
{
generateName = name # due to global S3 naming restrictions we'll have
# Crossplane generate a name to garauntee uniqueness
annotations = {
"krm.kcl.dev/composition-resource-name" = name
}
}
}
This section:
- Standardizes resource naming
- Adds required labels and annotations where needed
- Leverages helper functions to sanitize naming where applicable
- Reduces metadata duplication
Cloud resource definition​
# Create S3 Bucket
awsms3v1beta1.Bucket {
metadata: _metadata("{}-bucket".format(oxr.metadata.name))
spec = {
forProvider = {
region = params.region
}
}
},
This section:
- Creates the primary resource for your chosen cloud provider
- Inserts your claim parameters as required fields
- Applies the metadata function to the resource
Security configuration​
# Bucket BOC
awsms3v1beta1.BucketOwnershipControls {
metadata: _metadata("{}-boc".format(oxr.metadata.name))
spec = {
forProvider = {
bucketSelector: {
matchControllerRef: True
}
region = params.region
rule = {
objectOwnership = "BucketOwnerPreferred"
}
}
}
},
# Bucket PAB
awsms3v1beta1.BucketPublicAccessBlock {
metadata: _metadata("{}-pab".format(oxr.metadata.name))
spec = {
forProvider = {
bucketSelector: {
matchControllerRef: True
}
region = params.region
blockPublicAcls: False
ignorePublicAcls: False
restrictPublicBuckets: False
blockPublicPolicy: False
}
}
},
This section:
- Applies object ownership to the bucket
- Allows for public access to the bucket
Access control and encryption​
# Bucket ACL
awsms3v1beta1.BucketACL {
metadata: _metadata("{}-acl".format(oxr.metadata.name))
spec = {
forProvider = {
bucketSelector: {
matchControllerRef: True
}
region = params.region
acl = params.acl
}
}
},
# Default encryption for the bucket
awsms3v1beta1.BucketServerSideEncryptionConfiguration {
metadata: _metadata("{}-encryption".format(oxr.metadata.name))
spec = {
forProvider = {
region = params.region
bucketSelector: {
matchControllerRef: True
}
rule = [
{
applyServerSideEncryptionByDefault = {
sseAlgorithm = "AES256"
}
bucketKeyEnabled = True
}
]
}
}
}
This section:
- Sets the access level using the user's ACL parameter
- Automatically enables encryption for all objects
Bucket versioning​
# Set up versioning for the bucket if desired
if params.versioning:
_items += [
awsms3v1beta1.BucketVersioning{
metadata: _metadata("{}-versioning".format(oxr.metadata.name))
spec = {
forProvider = {
region = params.region
bucketSelector: {
matchControllerRef: True
}
versioningConfiguration = {
status = "Enabled"
}
}
}
}
]
This section:
- Only creates versioning if requested in the claim
- Captures the items defined in the function as a single variable.
Save your changes.
Render your composition locally​
The up composition render command allows you to review your desired composed
resources.
Render the composition against your XR file:
up composition render apis/storagebuckets/composition.yaml examples/storagebucket/example.yaml
This process ensures the build, configuration, and orchestration runs as expected before you deploy to a development control plane.
Errors in the render command can indicate a malformed function or other issues within the composition itself.
Next steps​
You constructed a new embedded function that allows user input from your claim file. This function uses your composition to create a fully configured storage resources to your specifications.
The next guide walks through how to test your composition function logic with the built-in test suite.