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 test-function apis/xstoragebuckets/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.aws.s3.v1beta1 as s3v1beta1
oxr = option("params").oxr # observed composite resource
params = oxr.spec.parameters
bucketName = "{}-bucket".format(oxr.metadata.name)
_metadata = lambda name: str -> any {
{
name = name
annotations = {
"krm.kcl.dev/composition-resource-name" = name
}
}
}
_items: [any] = [
# Bucket in the desired region
s3v1beta1.Bucket{
metadata: _metadata(bucketName)
spec = {
forProvider = {
region = params.region
}
}
},
s3v1beta1.BucketOwnershipControls{
metadata: _metadata("{}-boc".format(oxr.metadata.name))
spec = {
forProvider = {
bucketRef = {
name = bucketName
}
region = params.region
rule:[{
objectOwnership:"BucketOwnerPreferred"
}]
}
}
},
s3v1beta1.BucketPublicAccessBlock{
metadata: _metadata("{}-pab".format(oxr.metadata.name))
spec = {
forProvider = {
bucketRef = {
name = bucketName
}
region = params.region
blockPublicAcls: False
ignorePublicAcls: False
restrictPublicBuckets: False
blockPublicPolicy: False
}
}
},
# ACL for the bucket
s3v1beta1.BucketACL{
metadata: _metadata("{}-acl".format(oxr.metadata.name))
spec = {
forProvider = {
bucketRef = {
name = bucketName
}
region = params.region
acl = params.acl
}
}
},
# Default encryption for the bucket
s3v1beta1.BucketServerSideEncryptionConfiguration{
metadata: _metadata("{}-encryption".format(oxr.metadata.name))
spec = {
forProvider = {
region = params.region
bucketRef = {
name = bucketName
}
rule = [
{
applyServerSideEncryptionByDefault = [
{
sseAlgorithm = "AES256"
}
]
bucketKeyEnabled = True
}
]
}
}
}
]
# Set up versioning for the bucket if desired
if params.versioning:
_items += [
s3v1beta1.BucketVersioning{
metadata: _metadata("{}-versioning".format(oxr.metadata.name))
spec = {
forProvider = {
region = params.region
bucketRef = {
name = bucketName
}
versioningConfiguration = [
{
status = "Enabled"
}
]
}
}
}
]
items = _items
Save your composition file.
Review your function
Resource imports and user inputs
import models.io.upbound.aws.s3.v1beta1 as s3v1beta1
oxr = option("params").oxr # observed composite resource
params = oxr.spec.parameters
bucketName = "{}-bucket".format(oxr.metadata.name)
This section:
- Imports the cloud resource definitions required to create the resource
- Extracts the user-specified values in the claim (
params = oxr.spec.parameters
) - Creates a resource name with the claim name and appropriate suffix.
Metadata helper function
_metadata = lambda name: str -> any {
{
name = name
annotations = {
"krm.kcl.dev/composition-resource-name" = name
}
}
}
This section:
- Standardizes resource naming
- Adds required annotations
- Reduces metadata duplication
Cloud resource definition
_items: [any] = [
# Main S3 bucket
s3v1beta1.Bucket{
metadata: _metadata(bucketName)
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
s3v1beta1.BucketOwnershipControls{
metadata: _metadata("{}-boc".format(oxr.metadata.name))
spec = {
forProvider = {
bucketRef = {
name = bucketName
}
region = params.region
rule:[{
objectOwnership:"BucketOwnerPreferred"
}]
}
}
},
s3v1beta1.BucketPublicAccessBlock{
metadata: _metadata("{}-pab".format(oxr.metadata.name))
spec = {
forProvider = {
bucketRef = {
name = bucketName
}
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
# ACL for the bucket
s3v1beta1.BucketACL{
metadata: _metadata("{}-acl".format(oxr.metadata.name))
spec = {
forProvider = {
bucketRef = {
name = bucketName
}
region = params.region
acl = params.acl
}
}
},
# Default encryption for the bucket
s3v1beta1.BucketServerSideEncryptionConfiguration{
metadata: _metadata("{}-encryption".format(oxr.metadata.name))
spec = {
forProvider = {
region = params.region
bucketRef = {
name = bucketName
}
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 += [
s3v1beta1.BucketVersioning{
metadata: _metadata("{}-versioning".format(oxr.metadata.name))
spec = {
forProvider = {
region = params.region
bucketRef = {
name = bucketName
}
versioningConfiguration = [
{
status = "Enabled"
}
]
}
}
}
]
items = _items
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. To render a composition, create a Composite Resource (XR) file.
Create a new file called examples/storagebucket/xr.yaml
:
apiVersion: platform.example.com/v1alpha1
kind: XStorageBucket
metadata:
name: example
spec:
parameters:
region: us-west-1
versioning: true
acl: public-read
Render the composition against your XR file:
up composition render apis/xstoragebuckets/composition.yaml examples/storagebucket/xr.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.