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.