Crossplane ‐ Providers - olga-mir/playground GitHub Wiki
Note: This project uses Crossplane v2.0-preview
version, with GA expected in Aug 2025.
Crossplane section of playground's Wiki is aimed at Platform Engineers with limited to no prior Crossplane experience, but with good understanding of Kubernetes, GitOps and cloud.
My goal is to provide deeper insights into Crossplane concepts, demonstrate deployment patterns beyond basic examples from official documentation.
This page focuses on Providers. Find other sections in the sidebar
Documentation explains the concepts but does not give enough implementation details. When it comes to deploying providers these details may become crucial to understand.
First off, make sure you are familiar with Providers and Packages. The closest source in user facing docs is Configuration Packages, but Configuration
bit only confusing already confusing matters at this stage and is not necessary right now.
What we know from the docs is that provider is installed from a package and a package is an OCI compliant artefact. We also know that Crossplane "pulls" the "package" but why and how is not for the official page to explain, at least not in the Concepts sections.
To install provider all what is needed is to apply a provider manifest. We'll take a basic one from this Playground project: crossplane-contrib-provider-gcp-cloudrun:
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: crossplane-contrib-provider-gcp-cloudrun
spec:
package: xpkg.crossplane.io/crossplane-contrib/provider-gcp-cloudrun:v1.14.0
This is a custom resource watched by the core crossplane pod (apiVersion: pkg.crossplane.io/v1
). Crossplane reads this manifest and pulls specified package. This is a small detail but it is important one. For example it will require crossplane to be able to pull from OCI registry, which is not a typical permission for k8s workloads. And what does it need it for? what information is available there?
% k get providers crossplane-contrib-provider-gcp-cloudrun
NAME INSTALLED HEALTHY PACKAGE AGE
crossplane-contrib-provider-gcp-cloudrun True True xpkg.crossplane.io/crossplane-contrib/provider-gcp-cloudrun:v1.14.0 32m
And we have the pod running too
% k get po
NAME READY STATUS RESTARTS AGE
crossplane-949867c5b-lnnmt 1/1 Running 0 29m
crossplane-contrib-provider-gcp-cloudrun-e81f3141feb4-7b59nzpkv 1/1 Running 0 28m
There is more resources but will get there later.
If you describe provider pod, the image that it runs is exactly the same "thing" as what we supplied as a "package" in Provider custom resource:
% k get po crossplane-contrib-provider-gcp-cloudrun-e81f3141feb4-7b59nzpkv -o "custom-columns=CONTAINER:.spec.containers[0].name,IMAGE:.spec.containers[0].image"
CONTAINER IMAGE
package-runtime xpkg.crossplane.io/crossplane-contrib/provider-gcp-cloudrun:v1.14.0
How can the same "thing" be at the same crossplane package and a docker image that runs inside the pod?
Crossplane package (xpkg
) and docker images are both types of OCI artefacts. OCI artefact is nothing more than a collection of "layers" with metadata. You can push a movie video file into a layer, package it as a docker image and push it to docker registry. And people were doing all sorts of weird things - if anything can be used as a database (or as a storage for this matter), it will be. (including kubernetes itself that some enterprising folks who were using managed k8s as a production-grade free database, but I digress).
So let's look what's inside this packages. Pull this locally and inspect:
% docker manifest inspect xpkg.crossplane.io/crossplane-contrib/provider-gcp-cloudrun:v1.14.0
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
"manifests": [
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 1171,
"digest": "sha256:6ed08df713f395bf0ce37f165dfa6cb3f3114d0db7e172a0b0c1fcc4e55899c7",
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "... similar content for arm....",
}
]
}
This is a multi-platform build so we'll need to work a bit harder.
% docker manifest inspect xpkg.crossplane.io/crossplane-contrib/provider-gcp-cloudrun@sha256:6ed08df713f395bf0ce37f165dfa6cb3f3114d0db7e172a0b0c1fcc4e55899c7 | jq '{mediaType, config: {mediaType: .config.mediaType}, layers: [.layers[] | {mediaType, annotations}]}'
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"annotations": {
"io.crossplane.xpkg": "base"
}
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"annotations": {
"io.crossplane.xpkg": "upbound"
}
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
}
]
}
aha, so now we see it is a docker image (vnd.docker.image
), and a crossplane package as seen from the annotations.
- layer1 - base image
- layer2 - crossplane base package
- layer3 - something upbound
- layer4 - largest layer (61MB) which is the provider's magic.
tip: you can also use docker image inspect <imageid>
but it doesn't have image types.
But we are really curious, aren't we? Let's run the image and see what's inside:
% docker run -it --entrypoint /bin/sh af70f72afc56
~ $ ls
bin dev etc home lib media
mnt opt package.yaml proc root run
sbin srv sys tmp usr var
~ $ head -3 package.yaml
---
apiVersion: meta.pkg.crossplane.io/v1
kind: Provider
This is the package.yaml
that is supplied by package authors. meta.pkg.crossplane.io
is not a type available in the cluster and provider.yaml
is not a deployable manifest. But managing schema for internal and client-side only configuration is a common pattern making the same tools for schema management available to these type of manifests.
For this provider package.yaml
is a 20K line file, what's in there?
$ grep -E "^kind: CustomResourceDefinition|^ name:" package.yaml
name: provider-gcp-cloudrun
kind: CustomResourceDefinition
name: domainmappings.cloudrun.gcp.upbound.io
kind: CustomResourceDefinition
name: serviceiammembers.cloudrun.gcp.upbound.io
kind: CustomResourceDefinition
name: services.cloudrun.gcp.upbound.io
kind: CustomResourceDefinition
name: v2jobs.cloudrun.gcp.upbound.io
kind: CustomResourceDefinition
name: v2services.cloudrun.gcp.upbound.io
All the CRDs that this provider implements, with full payloads.
Read more on Crossplane Package Design
On the provider deployment there is no args
or cmd
, in the default installation. The information what to execute when image is launched is available in the image itself. In docker image inspect
you can find entrypoint
(and other config such as path, env, and many more). This is by the way explains why you need --entrypoint
param when running docker image locally docker run -it --entrypoint /bin/sh af70f72afc56
, try without it and see what happens.
% docker image inspect af70f72afc56 | jq '.[]|.Config'
{
"Hostname": "",
"Domainname": "",
"User": "65532",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"USER_ID=65532",
"GOOGLE_TERRAFORM_USERAGENT_EXTENSION=upbound-provider-gcp/v1.14.0"
],
"Cmd": null,
"Image": "",
"Volumes": null,
"WorkingDir": "/",
"Entrypoint": [
"provider"
],
"OnBuild": null,
"Labels": {
"io.crossplane.xpkg:sha256:70d892b90bb25aa0330aec62975d1cda3dd2bd47a3513371906084e8ffa6a74b": "base",
"io.crossplane.xpkg:sha256:dd844e9dc563c2261f04310eb2370c15e9629a85057283e8e25461b83bffd455": "upbound"
}
}
And inside the same container from previous section, you can see how k8s runs this "crossplane package" which is also a "docker image":
~ $ which provider
/usr/local/bin/provider
~ $ /usr/local/bin/provider --help
usage: provider [<flags>]
Terraform based Crossplane provider for GCP
Flags:
--help Show context-sensitive help (also try --help-long and --help-man).
-d, --debug Run with debug logging.
-s, --sync=1h Sync interval controls how often all resources will be double checked for drift.
--poll=10m Poll interval controls how often an individual resource should be checked for drift.
--poll-state-metric=5s State metric recording interval
-l, --leader-election Use leader election for the controller manager.
--max-reconcile-rate=100 The global maximum rate per second at which resources may checked for drift from the desired state.
--namespace="crossplane-system"
Namespace used to set as default scope in default secret store config.
...
That was a deep-dive to the depth of the package/image. Let's get to the surface and explore what happens on k8s level. The knowledge covered in the above sections will come in handy.
When provider is installed we get all CRDs that this provider implements.
Each provider will also define its own providerConfig
and providerConfigUsages
. There is no way to get all providerConfigs
with straight-forward kubectl across all providers.
providerconfigs gcp.upbound.io/v1beta1 false ProviderConfig
providerconfigusages gcp.upbound.io/v1beta1 false ProviderConfigUsage
providerconfigs github.upbound.io/v1beta1 false ProviderConfig
providerconfigusages github.upbound.io/v1beta1 false ProviderConfigUsage
providerconfigs kubernetes.crossplane.io/v1alpha1 false ProviderConfig
providerconfigusages kubernetes.crossplane.io/v1alpha1 false ProviderConfigUsage
And back to our Cloud Run provider, these are the resources that we can now use either directly or in compositions:
% k api-resources | grep cloudrun
domainmappings cloudrun.gcp.upbound.io/v1beta2 false DomainMapping
serviceiammembers cloudrun.gcp.upbound.io/v1beta2 false ServiceIAMMember
services cloudrun.gcp.upbound.io/v1beta2 false Service
v2jobs cloudrun.gcp.upbound.io/v1beta2 false V2Job
v2services cloudrun.gcp.upbound.io/v1beta2 false V2Service
These types that didn't exist before the provider installation. crossplane-rbac-manager
can automate roles for these new types as described here: https://docs.crossplane.io/latest/concepts/pods/#rbac-manager-pod
When Crossplane installs a provider it also creates a resource providerrevision
for that provider revision, as the name suggests. CRDs are attached to a providerRevision because they can change from one revision to the next.
% k get providerrevision crossplane-contrib-provider-gcp-cloudrun-e81f3141feb4 -o yaml | yq .status.objectRefs
- apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
name: v2services.cloudrun.gcp.upbound.io
uid: b744cc1b-9185-4b1b-baa3-f15ae24cf783
- apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
name: v2jobs.cloudrun.gcp.upbound.io
uid: c2421cd8-8fba-4e4e-afac-fd220c54f699
....
And we already saw how crossplane pod has this information before the provider pod is even started.
When the provider is being born, new RBAC resources are created for it automatically (if not disabled).
% k get clusterrole | grep crossplane
...
crossplane-admin 2025-07-23T08:07:18Z
crossplane-browse 2025-07-23T08:07:18Z
crossplane-edit 2025-07-23T08:07:18Z
crossplane-rbac-manager 2025-07-23T08:07:18Z
crossplane-view 2025-07-23T08:07:18Z
crossplane:aggregate-to-admin 2025-07-23T08:07:18Z
crossplane:aggregate-to-browse 2025-07-23T08:07:18Z
crossplane:aggregate-to-edit 2025-07-23T08:07:18Z
crossplane:aggregate-to-view 2025-07-23T08:07:18Z
...
These are base roles, and Crossplane then uses k8s aggregated roles feature. When a new provider revision is created a set of new clusterroles is defined:
crossplane:provider:crossplane-contrib-provider-gcp-cloudrun-e81f3141feb4:aggregate-to-edit 2025-07-23T08:08:27Z
crossplane:provider:crossplane-contrib-provider-gcp-cloudrun-e81f3141feb4:aggregate-to-view 2025-07-23T08:08:27Z
crossplane:provider:crossplane-contrib-provider-gcp-cloudrun-e81f3141feb4:system 2025-07-23T08:08:27Z
and corresponding bindings are created too. Provider pod of this revision will be using crossplane:provider:crossplane-contrib-provider-gcp-cloudrun-e81f3141feb4:system
role bound to the SA which also carries revision information in its name.
ClusterRoles and Bindings created by RBAC manager:

CRDs created by Crossplane itself:
This also shows role aggregation in action.

https://github.com/crossplane/crossplane/blob/main/contributing/guide-provider-development.md - building a provider. However using something like template-provider should provide this OOTB.