Crossplane doesn’t actually run your workloads; it orchestrates the infrastructure your workloads run on.
Let’s say you’ve got a team that needs to provision PostgreSQL databases and Redis caches on AWS. They’re using Crossplane, and they’ve got a CompositeResourceDefinition (CRD) for a "ManagedDatabase" that maps to AWS RDS. You also have another team that needs similar resources on Azure.
Here’s a look at how a ManagedDatabase might be defined:
apiVersion: database.example.com/v1alpha1
kind: ManagedDatabase
metadata:
name: my-app-db
spec:
compositionSelector:
matchLabels:
type: rds
parameters:
storageGB: 100
instanceClass: db.t3.medium
And here’s how Crossplane might compose that into an actual RDS instance:
apiVersion: rds.aws.upbound.io/v1beta1
kind: Instance
metadata:
name: my-app-db
spec:
forProvider:
region: us-east-1
allocatedStorage: 100
dbInstanceClass: db.t3.medium
engine: postgres
engineVersion: "13.7"
skipFinalSnapshot: true
providerConfigRef:
name: default-provider-config
This works fine when everyone is sharing the same Crossplane instance and the same set of Provider resources. But what if you want to give a team autonomy over which cloud providers they can use, or even which specific credentials they use for those providers, without impacting other teams?
That’s where namespace-scoped providers come in. Instead of having a single, cluster-wide Provider resource that any CompositeResourceDefinition can reference, you can restrict Provider resources to a specific Kubernetes namespace.
Imagine this scenario: Team A needs to manage RDS instances in team-a-ns using AWS credentials stored in team-a-ns. Team B needs to manage Azure SQL databases in team-b-ns using Azure credentials stored in team-b-ns.
Here’s how you’d set up a namespace-scoped provider for Team A:
First, the ProviderConfig in the team’s namespace:
apiVersion: aws.upbound.io/v1beta1
kind: ProviderConfig
metadata:
name: aws-provider-config
namespace: team-a-ns
spec:
credentials:
source: Secret
secretRef:
namespace: team-a-ns
name: aws-credentials
And the corresponding AWS credentials secret in team-a-ns:
apiVersion: v1
kind: Secret
metadata:
name: aws-credentials
namespace: team-a-ns
type: Opaque
data:
# ... base64 encoded AWS access key ID and secret access key ...
Then, the Provider resource itself, also in team-a-ns:
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-aws.upbound.io
namespace: team-a-ns
spec:
package: xpkg.upbound.io/upbound/provider-aws:v0.45.0
controllerConfigRef:
name: aws-controller-config
Notice the namespace field on both ProviderConfig and Provider. This is the key. This Provider resource is now only visible and usable by other Kubernetes resources within the team-a-ns namespace.
Now, when Team A defines their ManagedDatabase CRD and references a ProviderConfig, they’ll point to the one in their namespace:
apiVersion: database.example.com/v1alpha1
kind: ManagedDatabase
metadata:
name: my-app-db
namespace: team-a-ns # The CRD instance also lives in the team's namespace
spec:
compositionSelector:
matchLabels:
type: rds
parameters:
storageGB: 100
instanceClass: db.t3.medium
providerConfigRef: # This is the new field
name: aws-provider-config
Crossplane’s controllers, when processing the ManagedDatabase in team-a-ns, will see the providerConfigRef pointing to aws-provider-config. Because aws-provider-config is in the same namespace (team-a-ns), Crossplane knows it can use the Provider resource also in team-a-ns to fulfill this request. The Provider resource in team-a-ns is configured with the AWS credentials from the aws-credentials secret in team-a-ns.
The magic is that a Provider resource in team-b-ns (say, provider-azure.upbound.io) would not be discoverable or usable by a ManagedDatabase instance in team-a-ns, even if the cluster-wide Crossplane installation knows about both. This provides strict isolation.
This allows for a multi-tenant setup where each team gets its own set of cloud provider credentials and configurations, managed entirely within their own Kubernetes namespace. They can provision AWS resources, Azure resources, GCP resources, or any combination, without interfering with or even seeing the credentials or configurations of other teams.
The most surprising part is that the Provider and ProviderConfig resources themselves are just Kubernetes Custom Resources. When you scope them to a namespace, you’re leveraging the fundamental RBAC and namespacing capabilities of Kubernetes to achieve Crossplane-level isolation. The Crossplane control plane, when reconciling a ManagedResource (which is what your CompositeResource eventually becomes), looks for a ProviderConfig in the same namespace as the ManagedResource. If found, it then looks for a Provider resource in that same namespace that is associated with that ProviderConfig’s cloud provider type.
The next logical step is to explore how to manage Crossplane’s own lifecycle and packages across multiple tenants.