Skip to main content

Managed Resources

A managed resource represents any external resource provisioned and controlled by a Crossplane provider. External resources, such as those in cloud providers, are mapped to managed resources within Kubernetes, allowing their declarative management through native Kubernetes objects.

Crossplane offers two approaches for working with managed resources:

  • Individual resources: Create and manage individual managed resources directly.
  • Compositions: Define sets of managed resources that are provisioned and managed as a single logical unit.

The Crossplane team itself recommends starting by working with individual resources to better understand how the system works and, as you gain more experience, advance to using compositions.

Every managed resource has an associated deletion policy that defines the behavior when the managed resource is deleted:

  • deletionPolicy: Delete (Default - Delete the external resource when deleting the managed resource)
  • deletionPolicy: Orphan (Leave the external resource when deleting the managed resource)

forProvider

Looking at the yaml below that defines an ec2 in AWS.

apiVersion: ec2.aws.upbound.io/v1beta1
kind: Instance
metadata:
name: crossplane-test
spec:
# The spec.forProvider of a managed resource is mapped to the external resource parameters.
# forProvider is considered the source of truth. After the resource is created in the provider, if a user makes a change within a provider's web console, Crossplane will revert that change back to what is configured in forProvider.
forProvider:
region: us-east-1
ami: ami-02a89066c48741345
subnetId: subnet-0a056609984bfbb94
instanceType: t2.micro
providerConfigRef:
name: default

Many times a resource needs to reference another resource like the subnetId we defined above. This resource can be done manually or by inference.

Since we installed the provider aws upbound which uses upbound's apiversion, let's use their github https://github.com/upbound/provider-aws/tree/main for some examples

Let's create a VPC and then a subnet using this VPC.

apiVersion: ec2.aws.upbound.io/v1beta1
kind: VPC
metadata:
name: crossplane-vpc
spec:
forProvider:
region: us-east-1
cidrBlock: 10.0.0.0/16
enableDnsSupport: true
providerConfigRef:
name: default

Let's apply the manifest above and check


kubectl apply -f vpc.yaml
vpc.ec2.aws.upbound.io/crossplane-vpc created

kubectl get vpcs.ec2.aws.upbound.io
NAME READY SYNCED EXTERNAL-NAME AGE
crossplane-vpc True True vpc-0bbe67dde157bb143 15m

# Do a describe and see what we have
kubectl describe vpcs.ec2.aws.upbound.io crossplane-vpc

What mattered there was having a VPC to create a subnet referencing by its name which will be automatically replaced by its external name, observe.

apiVersion: ec2.aws.upbound.io/v1beta1
kind: Subnet
metadata:
name: crossplane-subnet1
spec:
forProvider:
region: us-east-1
availabilityZone: us-east-1a
# Here we use the reference by name, which will bring us the ID
vpcIdRef:
name: crossplane-vpc
cidrBlock: 10.0.10.0/24
providerConfigRef:
name: default
---
apiVersion: ec2.aws.upbound.io/v1beta1
kind: Subnet
metadata:
name: crossplane-subnet2
spec:
forProvider:
region: us-east-1
availabilityZone: us-east-1b
# Here we use the ID directly without making a reference
vpcId: vpc-0647cd3d73b2245b5
cidrBlock: 10.0.20.0/24
providerConfigRef:
name: default
---
apiVersion: ec2.aws.upbound.io/v1beta1
kind: Subnet
metadata:
name: crossplane-subnet3
spec:
forProvider:
region: us-east-1
availabilityZone: us-east-1b
# Here we select using labels
vpcIdSelector:
matchLabels:
name: crossplane-vpc
cidrBlock: 10.0.30.0/24
providerConfigRef:
name: default
---

And let's apply...

❯ kubectl get subnets.ec2.aws.upbound.io
NAME READY SYNCED EXTERNAL-NAME AGE
crossplane-subnet1 True True subnet-021e79c108d979bee 6m10s
crossplane-subnet2 True True subnet-06194392f1196f39e 86s

We understand the references, but did you notice that we always need to define the providerConfigRef block?

If we make a single composition creating the VPCs and subnets in a single manifest, we can have only one providerConfigRef.

What would happen if we tried to delete a VPC that has a subnet inside? Remember that in the AWS console itself, it wouldn't be possible to delete without first removing the subnets.

And if we changed the region of a subnet? We couldn't apply it, as this field is immutable.

Crossplane treats the managed resource as the source of truth by default; it expects to have all values, spec.forProvider including the optional ones. If not provided, Crossplane will fill empty fields with values assigned by the provider. For example, consider fields like region and availabilityZone. You can specify only the region and let the cloud provider choose the availability zone. In that case, if the provider assigns an availability zone, Crossplane will use that value to fill the spec.forProvider.availabilityZone field.

Annotations

Some annotations are created by crossplane itself on resources.

AnnotationDefinition
crossplane.io/external-nameThe name of the managed resource within the Provider. By default the given name is the same value as name in metadata, but it's possible to have a specific name. It's good practice to keep the same.
crossplane.io/external-create-pendingAutomatically created date/time when Crossplane started creating the managed resource.
crossplane.io/external-create-succeededAutomatically created date/time when the Provider successfully created the managed resource.
crossplane.io/external-create-failedAutomatically created date/time when the Provider failed to create the managed resource.
crossplane.io/pausedIndicates that Crossplane is not reconciling this resource. Read the pause annotation for more details.
crossplane.io/composition-resource-nameFor managed resources created by a Composition, this is the resources.name value from the Composition.

These annotations are created automatically if they happen.

Creating an RDS cluster for example in AWS, without instance, we can observe the use of annotation to expose the name we want. This example is in the resource folder to be applied.

apiVersion: rds.aws.upbound.io/v1beta1
kind: Cluster
metadata:
# This would be the name if we hadn't used the annotation below
name: clusterdb
annotations:
crossplane.io/external-name: cluster-db-custom-name
spec:
forProvider:
region: us-east-1
engine: aurora-postgresql
masterUsername: cpadmin
masterPasswordSecretRef:
name: clusterdb-password
namespace: crossplane-system
key: password
skipFinalSnapshot: true
writeConnectionSecretToRef:
name: rds-clusterdb-secret
namespace: crossplane-system
---
apiVersion: v1
kind: Secret
metadata:
name: clusterdb-password
namespace: crossplane-system
type: Opaque
stringData:
password: TestPass0!
kubectl apply -f rds.yaml

kubectl get cluster.rds
NAME READY SYNCED EXTERNAL-NAME AGE
clusterdb True True cluster-db-custom-name 4m7s

kubectl describe cluster.rds clusterdb
Name: clusterdb
Namespace:
Labels: <none>
## See the annotations. Since it didn't fail there's no failed or paused annotation.
Annotations: crossplane.io/external-create-pending: 2023-10-24T13:53:58Z
crossplane.io/external-create-succeeded: 2023-10-24T13:53:58Z
crossplane.io/external-name: cluster-db-custom-name
upjet.crossplane.io/provider-meta:
{"e2bfb730-ecaa-11e6-8f88-34363bc7c4c0":{"create":7200000000000,"delete":7200000000000,"update":7200000000000}}
...

rds

It's critical to first understand how managed resources work, but as I mentioned earlier, using compositions that we'll see later is a much better way to define and manage resources.

providerConfigRef

The providerConfigRef tells the Provider which ProviderConfig to use when creating the managed resource. We pass the configuration name.

Each managed resource can reference different ProviderConfigs. This allows different managed resources to be authenticated with different credentials in the same Provider.

Drift and Reconciliation

What happens if we delete a pod in Kubernetes that is controlled by a replicaset? It will recreate it. It guarantees the desired state right? It takes care that the pod is always up, in the correct quantity.

Crossplane is a controller right? So, if a resource is deleted from the cloud, but it's defined in Crossplane, it will recreate it. It will see that the resource it has to provision is no longer there and will apply it again. In Terraform, if this happens it will only be detected the difference the next time the project is applied.

Crossplane elevated the concept of reconciliation and drift far beyond pods, but for everything!

Actually when we apply several manifests at once Crossplane executes them all simultaneously, it's not like Terraform with depends_on. It will apply and as it becomes true, the references will be automatically corrected.

view managed resources

kubectl get managed
NAME READY SYNCED EXTERNAL-NAME AGE
kafkatopic.confluent.crossplane.io/emx-test-composition-zn79p-w6wxd True True lkc-7nx291/emx_test-composition 3d16h
kafkatopic.confluent.crossplane.io/topic-emx-ninjas-test True True lkc-7nx291/emx_test-crossplane-topic-emx 11d
kafkatopic.confluent.crossplane.io/topic-emx-ninjas-test2 True True lkc-7nx291/emx_test-crossplane-topic-emx2 11d

NAME READY SYNCED AGE PATH WITH NAMESPACE
project.projects.gitlab.crossplane.io/test-project-crossplane True True 6d14h latamairlines/oper/emx/platform-engineering/port/test