Before you continue, rule out Kubernetes fundamentals as the source of friction. You should be comfortable reading and applying Deployments, Services, Secrets, CustomResourceDefinitions, and controller status conditions, because Crossplane uses the same declarative reconciliation model for cloud infrastructure. You will also need an AWS account where you are allowed to create the resources used in this walkthrough. From Part 1, confirm that Crossplane is installed in the cluster, the AWS provider is installed and healthy, a valid ProviderConfig exists, and the credentials behind it have enough AWS permissions for managed resources to move from requested state to ready state.
This walkthrough builds on Part 1, Deploy AWS Resources using Crossplane on Kubernetes. Before you add an application, make sure the control plane is actually healthy: the AWS provider should be installed and reporting a good status, its credentials should work, your ProviderConfig should be referenced by the managed resources you create, and Crossplane should reconcile without looping on permission or validation errors. Skipping that check makes troubleshooting messy. A missing bucket, an IAM denial, or a managed resource stuck waiting on AWS can easily look like a bad application rollout, even though the real failure is in provider configuration, permissions, or an infrastructure dependency that never became ready.
We’ll use the companion GitHub repository, MeteorOps/crossplane-aws-provider-bootstrap, as the working reference for manifests and configuration. Use it as an executable example, not as a production platform template. The parts worth carrying forward are the separation between platform-owned infrastructure and app-owned workload configuration, the resource ownership boundaries, and the habit of debugging through status conditions instead of guessing. The resource names, AWS region, tagging model, IAM scope, namespace layout, and environment promotion process should all be adapted to your organization’s standards before you treat this pattern as a reusable internal offering.
Platform teams are usually caught between three pressures: developers want environments immediately, security wants tighter control over cloud access, and operations wants systems that behave the same way after every deploy, rollback, and incident. The Kubernetes workload, AWS dependencies, access controls, configuration, and runtime wiring all have to line up. Creating one S3 bucket, IAM role, database, or Deployment is not the hard part. The hard part is making the entire environment repeatable, reviewable, and recoverable without turning each release into a handoff chain between application teams, DevOps, security, and cloud administrators.
What this Part 2 walkthrough covers in practice
In this article, we’ll deploy a small application environment from Kubernetes and let the same control plane manage the AWS resources that application needs. This is the practical pressure point platform teams run into: developers want fast, self-service environments, while infrastructure definitions, workload manifests, credentials, policy, and incident ownership are often split across different tools and teams. Crossplane can close part of that gap by exposing cloud resources as Kubernetes APIs. It does not remove the hard decisions around ownership, least privilege, lifecycle rules, or production readiness. We’ll stay focused on the choices that matter in a real rollout: which resources Crossplane should own, what belongs in application manifests or Helm charts, how operators can see infrastructure dependencies, and how to debug the common failure cases when reconciliation stalls, AWS permissions are too narrow, or the workload starts before its dependencies are ready.
This is a step-by-step guide with an example and a Git repository, so by the end of it you should be able to deploy a sample env.
You can run the commands end to end and reach a working result, but don’t treat the walkthrough as a black box. The useful skill is knowing which component owns each resource, what “ready” means at the AWS, Crossplane, and Kubernetes layers, and which status field, event, or log to inspect when reconciliation stalls.
That said, spending an extra 5–10 minutes on the explanations will make the setup easier to adapt, debug, and extend later.
Hope you enjoy!
What should you expect from this walkthrough?
By the end of it you'll understand:
- How Crossplane can be used for full environment deployment
- How to deploy a sample app with AWS resources
What not to expect?
We’ll keep the deployment intentionally small. The point is not to build a full reference platform in one pass; it is to make the control-plane handoff obvious. The application declares a dependency, Crossplane reconciles the AWS resource, and Kubernetes runs the workload once the pieces are in place. After that path is boring and repeatable, you can add the production layers that usually matter next: policy enforcement, external secrets, promotion between environments, observability, and CI/CD automation. Starting small also makes ownership easier to see instead of burying it under a large demo stack.
It also does not attempt to be a full guide to Helm providers or chart lifecycle management in Crossplane. Instead, it covers the boundary decisions you need before combining Helm and Crossplane in the same delivery workflow.
Why Crossplane for the Full Environment Use-Case?
When you want to deploy a full environment, it usually involves 3 layers:
- Infrastructure: Resources the application needs to run well
- Application: The programs built by the company to serve users
- Data: The data the application uses
But you already know that.
For years, teams treated infrastructure and application delivery as two separate phases. Crossplane challenges that habit.
The traditional sequence was easy to reason about: provision the infrastructure first, then deploy the application on top.
Crossplane changes the boundary by letting a Kubernetes-facing resource request the cloud infrastructure it depends on as part of the same reconciliation loop.
Pull-Request Environments are also easier
By creating a namespace with all of the apps and the AWS resources required with Crossplane, the use-case of creating a full environment per Pull-Request as part of the CI becomes much easier.
That's a nice benefit of such setup for companies utilizing the feature-branch or Gitflow approaches.
A Traditional Full Env Example
In a traditional workflow, provisioning and deploying a full environment usually meant stitching together several separate steps and hoping the handoffs stayed consistent:
- Provision VPC+EKS+... using Terraform
- Use Terraform to bootstrap the cluster with a CD tool (e.g., ArgoCD)
- ArgoCD looks at a repo that deploys all apps from there
- An application needs a new S3 Bucket, so the developer writes Terraform code for it
- The application gets removed after a while (but the bucket stays)
- Someone needs to remember that bucket was owned by that app and remove it from Terraform
A Crossplane Full Env Example
To provision and deploy a full environment with Crossplane, the flow is similar: we still start with a Kubernetes cluster, then let Crossplane reconcile the external AWS resources needed by the application.
- Provision VPC+EKS+... using Terraform
- Deploy Crossplane's prerequisites to the cluster with Terraform
- Package app-specific Crossplane resources inside the application Helm chart, so an application release also requests the infrastructure it depends on.
- Use Crossplane to install the application Helm chart and provision the shared infrastructure required by that class of applications.
- When an application is removed, its AWS resources are gone with it
- When an entire environment is terminated, its AWS resources are gone with it
Choosing the Boundary: Crossplane Manages Helm, or Helm Ships Crossplane
Once Helm enters the workflow, the design question becomes concrete:
Do you package Crossplane resources inside the application chart, or do you let Crossplane install the chart after it provisions the supporting infrastructure?
The practical answer is: both, but at different points in the lifecycle.
Reasons for Crossplane in Helm:
- Create or modify app-specific resources when that app is deployed
- Delete app-specific resources when that app is deleted
Reasons for Helm in Crossplane:
- Manage dependencies between resources and applications using Crossplane
- Create shared resources that are not owned by a single application
The Step-by-Step Guide
Deploy the simple application alongside a S3 bucket using a Crossplane Composite Application.
Before proceeding
Before continuing, finish the setup from the first article. This section assumes the AWS provider is installed, the ProviderConfig is valid, and Crossplane can create managed resources in AWS. If that foundation is not working, the manifests below may still apply to the cluster, but the external AWS resources will never become ready—and the application symptoms will point you in the wrong direction.
Deploy the Crossplane Kubernetes Provider
Prepare the AWS Credentials for the Application to be able to use AWS
Run the following oneliner to create the Secret containing the AWS credentials in the right format as required by the Application (the application will simply run aws s3 ls to show the bucket):
kubectl create secret generic aws-creds \
--from-literal=aws_access_key_id=$(grep -i aws_access_key_id creds | awk -F' = ' '{print $2}') \
--from-literal=aws_secret_access_key=$(grep -i aws_secret_access_key creds | awk -F' = ' '{print $2}')
Make sure it was created as expected by fetching the secret:

Deploy the Crossplane Kubernetes Provider resources using the k8s-provider-bootstrap.yaml file
kubectl apply -f k8s-provider-conf.yaml
Before you create anything that depends on the provider, verify that it exists and is ready. Kubernetes may accept the custom resources before the provider is healthy, but Crossplane will not reconcile them correctly until the provider is installed, configured, and reporting a ready state.
kubectl get providers provider-kubernetes
When the provider is healthy, the status should look similar to this:

Deploy the Crossplane Kubernetes Provider Configuration using the k8s-provider-conf.yaml file:
kubectl apply -f k8s-provider-conf.yaml
This is done separately as it needs to happen after the Provider resources were created.
This is where we tell the Crossplane Kubernetes Provider in which Kubernetes cluster it should operate when it's creating resources.
Create a deployable unit for an application and its AWS resources with Crossplane
Here we do 3 things with 3 files:
- The
composite-app-xrdfile:
Contains the CompositeResourceDefinition (XRD) for the K8sApplication by using the Composition of a K8s Deployment and S3 Bucket (described below) - The
composite-app-compositionfile:
Contains the Composition definition which creates both the Kubernetes Deployment and the S3 Bucket - The
composite-app-examplefile:
Calls the CompositeResource defined bycomposite-app-xrdfile
Crossplane Resources Files Breakdown & Creation
composite-app-xrd.yaml
~ K8sApplication CompositeResourceDefinition
This defines a composite resource for a Kubernetes application, with bucketName and bucketRegion fields in the spec. Users can claim this resource as K8sApplication.
The K8sApplication CompositeResource (XRD) accepts the bucketName & bucketRegion fields and uses them to create a S3 Bucket, and to create a K8s Deployment of a mock "service" that simply runs aws s3 ls to see the bucket.
~ Deploy the CompositeResourceDefinition (XRD)
kubectl apply -f composite-app-xrd.yaml
composite-app-composition.yaml
Defines a Composition of resources that can be created by a CompositeResource.
This is where we define the Composition that creates a combo of a Kubernetes Deployment with the mock "service" that runs aws s3 ls as well as the S3 bucket - The CompositeResource simply calls this resource.
~ Deploy the Composition
kubectl apply -f composite-app-composition.yaml
composite-app-example.yaml
Deploys the actual K8sApplication CompositeResource, and passes the details of the region in which the bucket should be created, and the name of the bucket (both are also passed to the Kubernetes Deployment as environment variables that helps it access the same bucket).
As mentioned above, the CompositeResource calls the Composition which creates the resources using the Crossplane providers.
Deploy the app by running the following command:
kubectl apply -f composite-app-example.yaml
Look at your pretty Application
Fetch the K8sApplication resource you've just created by running the below command obsessively until it's marked as Healthy:
kubectl get K8sApplication
After reconciliation completes, the status should look roughly like this:

Print the logs of the application and see it fetching the AWS S3 Bucket:
kubectl logs -l app=awscli
# 2024-10-17 16:00:31 my-app-bucket-nqzhx-xzjcq
# 2024-10-17 16:00:50 my-app-bucket-nqzhx-xzjcq
Cleanup
kubectl delete -f composite-app-example.yaml
Recap
To briefly recap what you did here:
- Prepared Crossplane for deploying a mix of Kubernetes and AWS resources
- Defined the manifests required to deploy an app built of a Deployment and a S3 Bucket
- Sharpened your grasp on some Crossplane concepts
- Discussed some use-cases for which it's useful
If this walkthrough helped, or if there’s a related platform engineering or cloud-native topic you want covered next, send the idea to michael@meteorops.com.
Disclaimer: In actual environments or production, it’s essential to fine-tune the permissions in the different manifests. Instead of using access keys and secret keys directly, consider implementing IAM Roles for Service Accounts (IRSA) to manage permissions more securely.




