CoursesCrossplaneInstalling Crossplane & a provider

Installing Crossplane & a provider

The core + a cloud provider package.

Intermediate12 min · lesson 2 of 12

A brand-new smartphone can do almost nothing on its own. It ships with an operating system and an app store, so it knows how to find, install, and run apps, but until you install one, it cannot hail a cab or send a photo. Installing Crossplane has the same shape, and it takes two steps. First you install the core: a small set of controllers (little programs that watch the cluster and act on what they see) that turn an ordinary Kubernetes cluster into a control plane and teach it to handle packages. A control plane behaves like a thermostat. You set the target once, and it keeps nudging reality toward that target on its own. Here the target is the infrastructure you declared, and the control plane keeps driving real cloud resources toward it. The core is the operating system and the store client rolled into one. It can install and run cloud 'apps', but it ships with none of them, so it cannot create a single bucket or database yet.

The second step is installing a provider: one package that pulls a controller plus a stack of new API types. An API (Application Programming Interface) is the set of object kinds and requests your cluster understands, so adding types means the cluster learns to speak a whole new cloud service, like S3 (Amazon's Simple Storage Service), RDS (Relational Database Service), or VPC (Virtual Private Cloud). Core first. Providers on top.

Install the core with Helm

Helm is the package manager for Kubernetes, the apt or Homebrew of the cluster. The core ships as a Helm chart from Crossplane's stable chart repository, and it wants its own namespace, crossplane-system. A namespace is just a named partition inside the cluster that keeps related objects together, and this one holds the core's controllers, the package manager, and its permissions. Nothing here touches a cloud. You are installing the engine that will manage packages later, not anything that can call AWS (Amazon Web Services). So add the repository, look at which versions exist, and pin the one you install. Pinning matters for the same reason you would not let a production fleet auto-update every app overnight. You want the version you chose and tested, not whatever happens to be newest the second your pipeline runs.

terminal
# add Crossplane's stable Helm chart repo and refresh the local index
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update
# see which chart versions exist instead of blindly taking the newest
helm search repo crossplane-stable/crossplane --versions | head -4
output
"crossplane-stable" has been added to your repositories
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "crossplane-stable" chart repository
Update Complete. ⎈Happy Helming!⎈
NAME CHART VERSION APP VERSION DESCRIPTION
crossplane-stable/crossplane 2.3.0 2.3.0 Crossplane is an open source Kubernetes add-on ...
crossplane-stable/crossplane 2.2.0 2.2.0 Crossplane is an open source Kubernetes add-on ...
crossplane-stable/crossplane 2.1.0 2.1.0 Crossplane is an open source Kubernetes add-on ...

Now install the chart into its namespace at a pinned version. The --wait flag tells Helm to block until the deployments are truly running, not merely accepted by the API server, so a successful return actually means the pods came up. If anything fails to become ready, --wait returns a non-zero exit code, which is exactly what you want in a pipeline. Add --atomic if you also want a failed install to roll itself back instead of leaving half a release behind.

terminal
# install the core control-plane components, pinned, into their own namespace
helm install crossplane crossplane-stable/crossplane \
--namespace crossplane-system --create-namespace \
--version 2.3.0 \
--wait
output
NAME: crossplane
LAST DEPLOYED: Tue Jul 21 09:14:02 2026
NAMESPACE: crossplane-system
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Release: crossplane
Chart Name: crossplane
Chart Description: Crossplane is an open source Kubernetes add-on that enables platform teams to assemble infrastructure from multiple vendors.
Chart Version: 2.3.0
Chart Application Version: 2.3.0
Kube Version: v1.30.2
terminal
# --wait already blocked until they were ready; confirm the two core pods
kubectl get pods -n crossplane-system
output
NAME READY STATUS RESTARTS AGE
crossplane-6d67f8cd9d-g2gjw 1/1 Running 0 63s
crossplane-rbac-manager-86d9b5cf9f-2vc4s 1/1 Running 0 63s

Two pods, and that is the entire core. (A pod is the smallest thing Kubernetes runs, one or more containers scheduled together.) The first pod, crossplane, holds the package manager and the reconcilers that install and run everything you add later. A reconciler is the loop that keeps comparing what you asked for against what actually exists and closing the gap. When the core started it also registered a handful of its own API types, the packaging machinery: Provider, Configuration, Function, and the revision and Lock objects that track them.

The second pod, crossplane-rbac-manager, has one focused job. Think of RBAC as the building's keycard system, deciding who may open which doors. RBAC stands for Role-Based Access Control, Kubernetes' permission system. As new API types arrive from the providers you install, the rbac-manager writes the access rules so the core and each provider get exactly the permissions their new types need, and no more. Hold that picture as a defender. The instant this core is healthy, your cluster is a control plane that can grow the power to create and destroy cloud infrastructure. Anyone who can reach it, or install packages into it, is one short step from your cloud account.

Add a provider package

A provider is installed the Kubernetes way: declaratively. You describe the end state you want in a small file and let the cluster make it real, instead of running a sequence of commands yourself. You write a Provider object, apply it, and the core does the rest. It pulls the package from an OCI registry (OCI, the Open Container Initiative, is the standard format for container images) at xpkg.upbound.io, starts the provider's controller as a Deployment (a Kubernetes object that keeps a set of identical pods running and restarts them if they die), and registers one new API type per cloud resource the provider manages. Picture xpkg.upbound.io as the app store and the Provider object as tapping install on one specific version. Pin that version. A control plane should never roll to :latest on its own, because :latest hands the choice of which code runs, with your cluster's permissions, to the registry and to anyone who can push to it. For anything you care about, pin the immutable digest (the sha256 fingerprint of the exact image bytes), not only the tag.

provider-aws-s3.yaml
# provider-aws-s3.yaml: one member of the AWS provider family
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-aws-s3
spec:
# a tag is readable; a digest is immutable. Prefer the digest in production.
package: xpkg.upbound.io/upbound/provider-aws-s3:v2.6.1
# e.g. xpkg.upbound.io/upbound/provider-aws-s3@sha256:9f2b1c... pins exact bytes
terminal
kubectl apply -f provider-aws-s3.yaml
# right after applying, HEALTHY reads Unknown while the image pulls
kubectl get providers
output
provider.pkg.crossplane.io/provider-aws-s3 created
NAME INSTALLED HEALTHY PACKAGE AGE
provider-aws-s3 True Unknown xpkg.upbound.io/upbound/provider-aws-s3:v2.6.1 9s

INSTALLED flips to True the moment the package is downloaded. HEALTHY stays Unknown until the controller image is pulled and its API types are established, which on a fresh cluster takes a minute or two. Wait that minute and look again. Now a provider you never applied appears: upbound-provider-family-aws. That is not a mistake. provider-aws-s3 declares a dependency on the shared AWS family package, and Crossplane resolved it for you, the way installing one app can pull in a shared framework it needs. Crossplane records that resolution in a cluster-scoped Lock object (run kubectl get lock to see it), so the whole dependency graph stays inspectable. The family provider is the one that owns the ProviderConfig type every AWS resource uses to sign in, which is the whole of the next lesson, xp-providerconfig. For now, both rows reading True is your green light.

terminal
# give it a minute, then look again
kubectl get providers
output
NAME INSTALLED HEALTHY PACKAGE AGE
provider-aws-s3 True True xpkg.upbound.io/upbound/provider-aws-s3:v2.6.1 2m11s
upbound-provider-family-aws True True xpkg.upbound.io/upbound/provider-family-aws:v2.6.1 96s

Each provider adds new API types to your cluster, and those types are CRDs. A CRD (Custom Resource Definition) is the hook Kubernetes gives you to add your own kinds of object to its API, the same way installing an app can teach your phone to open a file type it never knew before. A moment ago kubectl knew about Pods and Services. Now it knows about S3 buckets and their many settings. The S3 provider alone registers around two dozen of them, one per S3 resource type, from the Bucket itself down to its encryption, versioning, and public-access-block configs.

terminal
# every new type the S3 provider taught your cluster to speak (first 10 shown)
kubectl get crds | grep s3.aws.upbound.io | head
output
bucketaccelerateconfigurations.s3.aws.upbound.io 2026-07-21T09:16:44Z
bucketacls.s3.aws.upbound.io 2026-07-21T09:16:44Z
bucketanalyticsconfigurations.s3.aws.upbound.io 2026-07-21T09:16:44Z
bucketcorsconfigurations.s3.aws.upbound.io 2026-07-21T09:16:44Z
bucketintelligenttieringconfigurations.s3.aws.upbound.io 2026-07-21T09:16:44Z
bucketinventories.s3.aws.upbound.io 2026-07-21T09:16:44Z
bucketlifecycleconfigurations.s3.aws.upbound.io 2026-07-21T09:16:44Z
bucketloggings.s3.aws.upbound.io 2026-07-21T09:16:44Z
bucketmetrics.s3.aws.upbound.io 2026-07-21T09:16:44Z
bucketnotifications.s3.aws.upbound.io 2026-07-21T09:16:44Z

Here is the rule that saves you real pain: install the family member you need, never the old monolith. The AWS, Azure, and GCP providers were split into families for a reason. The original monolithic provider-aws registers close to a thousand CRDs in one shot, which bloats the API server, slows discovery for every client that lists types, and on smaller clusters can push etcd (the key-value database where all Kubernetes state lives) toward its storage limits, sometimes leaving the provider stuck at HEALTHY: False while CRDs churn. Install only the family member you actually use, provider-aws-s3, provider-aws-rds, and so on. Each one pulls the shared family controller automatically, so you get exactly the types you need and nothing else. If you already applied the monolith, delete that Provider and let Crossplane garbage-collect its CRDs before you switch, because running the monolith and a family member for the same service will fight over ownership of the same CRDs.

Verify it, and watch it like an attacker would

A healthy provider means a new Deployment is now running inside crossplane-system, built from an image you pulled off the internet, holding a service account (the identity a pod runs as) wired to manage a class of cloud resources. Look at that directly. It confirms your install, and it is exactly what an intruder inspects after landing on the cluster. If a provider ever sits at HEALTHY: False, the same instinct applies: run kubectl get providerrevisions and kubectl describe provider provider-aws-s3, where the events spell out why the package would not install.

terminal
kubectl get deploy -n crossplane-system
output
NAME READY UP-TO-DATE AVAILABLE AGE
crossplane 1/1 1 1 6m
crossplane-rbac-manager 1/1 1 1 6m
provider-aws-s3-9c4e2f1a8b3d 1/1 1 1 2m
upbound-provider-family-aws-1f7a2c9d0e5b 1/1 1 1 96s

Two of those Deployments did not exist ten minutes ago, and each runs vendor code under an identity that can change your infrastructure. On any cluster running Crossplane, the control plane is the prize, because reaching it is a shortcut to the cloud account behind it. The permission to create Provider objects (create on providers.pkg.crossplane.io) sits close to full administrator over your infrastructure. Whoever holds it can install a package of their choosing and get a controller running with the core's blessing.

Two habits pay off. Watch the audit log (the cluster's running record of every API request) and your Git history for new or changed Provider objects, because in a healthy GitOps setup (where every infrastructure change flows through a reviewed commit) each provider install is a commit you can point to, and a surprise Provider is a signal worth an alert. And keep the right to create packages with the small group that runs the platform.

One more thing to know before you run anything destructive: removing a Provider deletes its CRDs, and deleting a resource's CRD tears down the objects under it. Because most managed resources default to a Delete deletion policy, uninstalling a provider on a cluster that owns live infrastructure can cascade into deleting real buckets and databases. Treat kubectl delete provider on a production control plane with the same care as a delete in the cloud console. There is a security edge here too: an attacker who can delete packages can wipe your infrastructure without ever signing in to the cloud provider directly.

A Provider runs vendor code in your cluster
Installing a Provider is nothing like adding a library you will read and compile later. Crossplane pulls the package from a registry and runs it as a controller with permission to touch cloud resources, right away. A mutable tag like :latest, or even a fixed tag if the registry is compromised, lets someone swap the bytes under you. Pin the immutable digest (package: xpkg.upbound.io/upbound/provider-aws-s3@sha256:...), mirror the packages you depend on into a registry you control, and verify signatures. Crossplane's own ImageConfig object (pkg.crossplane.io/v1beta1) can inject registry pull secrets and, with signature verification turned on, reject any package whose cosign signature does not check out before it ever runs. Cosign is the standard tool for signing and verifying container images. Treat a provider image with the suspicion you would give any container that runs with real privileges in production.
From an empty cluster to a cloud API
1helm install core
crossplane + rbac-manager land in crossplane-system
2apply a Provider
package pulled from xpkg.upbound.io; pin the digest
3dependency resolves
family provider auto-installed, recorded in Lock
4INSTALLED + HEALTHY = True
controller Deployment up, CRDs established
5cloud CRDs live
cluster now speaks s3.aws.upbound.io
The core only manages packages. Every provider you add runs new vendor code and widens the cluster's API surface, so each install is a supply-chain decision, not a formality.
Quick check
01You apply one Provider for provider-aws-s3. A minute later, kubectl get providers lists two providers: provider-aws-s3 and upbound-provider-family-aws, which you never applied. What happened?
Correct — Family members declare a dependency on the family package, and the package manager resolves it and records it in the Lock object.
Incorrect — No. The second provider comes from your install alone, as a resolved dependency, not from another person's action.
Incorrect — No. The family provider is a live dependency of provider-aws-s3; it would appear even on a brand-new cluster.
Incorrect — No. There is no pairing or failover; the extra provider carries shared types, it is not a replica.
02Right after you kubectl apply a Provider object, kubectl get providers shows provider-aws-s3 with INSTALLED=True but HEALTHY=Unknown, and nothing is obviously broken. What do these two columns mean here?
Correct — on a fresh cluster the health check stays Unknown for a minute or two while the controller image pulls and the CRDs come up.
Incorrect — INSTALLED=True shows the package downloaded fine; Unknown health is the normal transient state just after applying.
Incorrect — health reflects whether the controller and its types are up, not whether a ProviderConfig or credentials exist yet.
Incorrect — an unresolved dependency surfaces differently; here the provider is simply still starting.
03On a production control plane that manages several live databases, a colleague runs kubectl delete provider provider-aws-rds to tidy up a package that looks unused. The managed resources use default settings. What is the danger?
Incorrect — managed resources cannot reconcile without their controller, and removing the Provider goes further by deleting their CRDs.
Incorrect — this is a delete, not a pause; the CRDs and the objects under them are removed.
Incorrect — removing a Provider deletes its CRDs, and deleting a CRD tears down every object of that type.
Correct — uninstalling a provider on a cluster that owns live infrastructure can cascade into deleting real cloud resources, so it must be treated like a cloud destroy.

Try this

Run helm repo add crossplane-stable https://charts.crossplane.io/stable on a scratch host or disposable cluster and read the output against what this lesson described. Then change one input so it fails, and re-run: the error you get is the one you will meet in production.

Takeaway

The trap worth remembering here: a Provider runs vendor code in your cluster. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related