Crossplane ‐ Providers - olga-mir/playground GitHub Wiki

Note: This project uses Crossplane v2.0-preview version, with GA expected in Aug 2025.

Introduction

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

Providers

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.

Provider Installation

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.

Package, Image, OCI Artifact

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).

Understanding the Package/Image Structure

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.

Crossplane Content inside Package/Image

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

Running Provider in a Container

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.
...

Provider Resources and RBAC

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.

CRDs

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.

Crossplane roles

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.

Audit Logs for Illustration

ClusterRoles and Bindings created by RBAC manager:

CRDs created by Crossplane itself:

This also shows role aggregation in action.

Upjet Framework

State management

provider-template

Other resources

https://github.com/crossplane/crossplane/blob/b98b81cd96886df1038508800dc574f0ad5e821f/design/one-pager-package-format-v2.md

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.

⚠️ **GitHub.com Fallback** ⚠️