Provisioning S3 buckets with specific policies across multiple cloud providers, all managed by a single Kubernetes API, is surprisingly straightforward once you understand how Crossplane compositions tie together.

Imagine you need to create S3 buckets for your application, but each environment (dev, staging, prod) requires a different access policy. Traditionally, this means managing separate AWS, Azure, or GCP configurations. Crossplane lets you define a single "managed resource" in Kubernetes that represents your S3 bucket with its policy, and then it handles the translation to the underlying cloud provider’s API.

Here’s a look at a composition in action. We’ll define a S3BucketWithPolicy custom resource that, when applied, will create an S3 bucket in AWS with a policy restricting access to a specific VPC.

apiVersion: s3.example.com/v1beta1
kind: S3BucketWithPolicy
metadata:
  name: my-app-data-bucket
spec:
  forProvider:
    bucketName: my-app-data-bucket-unique
    region: us-east-1
    policy: |
      {
        "Version": "2012-10-17",
        "Statement": [
          {
            "Sid": "AllowVPCAccess",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::my-app-data-bucket-unique/*",
            "Condition": {
              "StringEquals": {
                "aws:SourceVpc": "vpc-0123456789abcdef0"
              }
            }
          }
        ]
      }

When you kubectl apply -f my-s3-bucket-with-policy.yaml, Crossplane’s s3.example.com/v1beta1.S3BucketWithPolicy controller kicks in. It looks at the spec.forProvider section and, based on the providerConfigRef (which tells it which AWS credentials to use), it calls the AWS API to create a bucket named my-app-data-bucket-unique in us-east-1 and applies the provided JSON policy.

The mental model here involves two key components: Managed Resources and Compositions.

  • Managed Resources are the Kubernetes Custom Resource Definitions (CRDs) that represent cloud infrastructure. Examples include AWSBucket, AzureStorageAccount, GCPBucket. Crossplane provides these out-of-the-box for many cloud services.
  • Compositions are your own custom CRDs that define how to assemble one or more Managed Resources into a higher-level abstraction. In our case, S3BucketWithPolicy is a Composition. It’s a blueprint that says, "When someone asks for an S3BucketWithPolicy, I will create an AWSBucket and an AWSBucketPolicy (or a single resource that handles both, depending on the provider and Crossplane version)."

The Composition specifies a patch section that maps fields from your custom resource (S3BucketWithPolicy.spec.forProvider) to the underlying Managed Resources. For example, the bucketName from your custom resource would be directly mapped to the spec.forProvider.bucketName of the AWSBucket Managed Resource. The policy would be mapped similarly.

The beauty of this is that you can create multiple Compositions for the same base Managed Resource. You could have an S3BucketWithPublicRead Composition, an S3BucketWithEncryption Composition, or our S3BucketWithPolicy Composition. All of them would use the underlying AWSBucket Managed Resource, but present a different, opinionated interface to your users.

The providerConfigRef is crucial. It’s a reference to a ProviderConfig resource, which tells Crossplane how to authenticate with a specific cloud provider. Without it, Crossplane wouldn’t know which AWS account or which set of credentials to use for creating the bucket. This allows you to manage buckets in different AWS accounts or Azure subscriptions simply by referencing different ProviderConfig resources in your Compositions.

What most people don’t realize is that the policy field in the S3BucketWithPolicy custom resource isn’t just a string; it’s a JSON document that gets directly translated into the S3 bucket policy. Crossplane’s AWS provider is smart enough to know that a policy field within an S3 bucket definition should be applied as an S3 bucket policy, rather than something else. This allows for very direct and powerful mapping of cloud-native features into Kubernetes resources without needing to define separate AWSBucketPolicy resources in your composition, which simplifies the composition definition significantly.

Once your S3 buckets are provisioned with their policies, the next natural step is to explore how to manage their lifecycle, such as implementing versioning or configuring lifecycle rules for object deletion.

Want structured learning?

Take the full Crossplane course →