Crossplane: Delivering cloud resources in a self-service model

Daniel Rosa
6 min readApr 11, 2021

--

I this article, I will demonstrate how to allow your developers to create their application requirements such as a database instance, a queue service, cache service, etc with no dependency on your Ops team using crossplane.

First, I assume you already have crossplane installed in your Kubernetes cluster, if not, please follow the instructions here

Crossplane compositions allow you to abstract all the infrastructure requirements need to create some cloud resources in a self-service model, hiding the infrastructure complexity. With compositions, developers can create their applications dependency by themselves using Kubernetes as a control plane.

In this scenario, Platform Builders can define previously all the infrastructure requirements to provide some cloud resources such as a database instance. As you may know, to create a database instance on your cloud provider you need to provide certain components such as virtual private networks, subnets, security groups, and so on, and your developers may not be confident or they don´t be familiar with this infrastructure base, so how can you abstract it for then? using compositions!

Now, Platform consumers can take advantage of this approach and consume the resources in a self-service model. In other words, developers can create a database instance or another component just by applying one single declarative manifest configuring only the settings that you defined to be customized such as the storage size, instance type, and the database name as well.

One of the most benefits of this approach is to allow your developers to create their application dependencies following all the best practices your organization requires such as security patterns.
Crossplane Providers are a set of Kubernetes CRDs and controllers that reconcile them!

https://npm.io/package/crossplane-cdk

Well, let´s explain how to do it using an RDS Instance on AWS but not limited to because you can use the same approach to prepare the infrastructure requirements to provisioning cloud resources on others cloud providers supported too.

RDS Instance composition:
composition.yaml

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: compositepostgresqlinstances.aws.database.example.org
labels:
provider: aws-provider
guide: quickstart
vpc: vpc-xxxxxxxxxxxxxx
spec:
writeConnectionSecretsToNamespace: crossplane-system
compositeTypeRef:
apiVersion: database.example.org/v1alpha1
kind: CompositePostgreSQLInstance
resources:
- name: securitygroup
base:
apiVersion: ec2.aws.crossplane.io/v1beta1
kind: SecurityGroup
spec:
providerConfigRef:
name: aws-provider
forProvider:
region: us-east-1
vpcIdRef:
name: my-vpc-name
groupName: default
description: default
ingress:
- fromPort: 5432
toPort: 5432
ipProtocol: tcp
ipRanges:
- cidrIp: x.x.x.x/x
patches:
- fromFieldPath: "metadata.name"
toFieldPath: "spec.forProvider.groupName"
- name: rdsinstance
base:
apiVersion: database.aws.crossplane.io/v1beta1
kind: RDSInstance
spec:
providerConfigRef:
name: aws-provider
forProvider:
dbSubnetGroupName: "db-subnet-group"
region: us-east-1
vpcSecurityGroupIDSelector:
matchControllerRef: true
allocatedStorage: 20
autoMinorVersionUpgrade: true
backupRetentionPeriod: 0
caCertificateIdentifier: rds-ca-2019
copyTagsToSnapshot: false
dbInstanceClass: db.t3.micro
deletionProtection: false
enableIAMDatabaseAuthentication: false
enablePerformanceInsights: false
engine: postgres
engineVersion: "13"
finalDBSnapshotIdentifier: sample
masterUsername: demo
multiAZ: false
port: 5432
preferredBackupWindow: 06:15-06:45
preferredMaintenanceWindow: sat:09:21-sat:09:51
publiclyAccessible: false
storageEncrypted: false
storageType: gp2
writeConnectionSecretToRef:
namespace: crossplane-system
name: default
patches:
- fromFieldPath: "metadata.uid"
toFieldPath: "spec.writeConnectionSecretToRef.name"
transforms:
- type: string
string:
fmt: "%s-postgresql"
- fromFieldPath: "spec.parameters.storageGB"
toFieldPath: "spec.forProvider.allocatedStorage"
- fromFieldPath: "spec.parameters.finalDBSnapshot"
toFieldPath: "spec.forProvider.finalDBSnapshotIdentifier"
- fromFieldPath: "spec.parameters.dbInstanceClass"
toFieldPath: "spec.forProvider.dbInstanceClass"
- fromFieldPath: "metadata.namespace"
toFieldPath: "spec.writeConnectionSecretToRef.namespace"
- fromFieldPath: "metadata.name"
toFieldPath: "spec.writeConnectionSecretToRef.name"
connectionDetails:
- fromConnectionSecretKey: username
- fromConnectionSecretKey: password
- fromConnectionSecretKey: endpoint
- fromConnectionSecretKey: port

Note that you abstract most of all the infrastructure needs to create RDS Instance, you defined the right VPC to use, defined the create a right security group rules, the subnet group, backup window, upgrade policy, and many others configurations regarding your organization best practices.

XRD Example
xrd.yaml

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: compositepostgresqlinstances.database.example.org
spec:
group: database.example.org
names:
kind: CompositePostgreSQLInstance
plural: compositepostgresqlinstances
claimNames:
kind: RDSPostgresInstance
plural: rdspostgresqlinstancess
connectionSecretKeys:
- username
- password
- endpoint
- port
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
parameters:
type: object
properties:
storageGB:
type: integer
finalDBSnapshot:
type: string
dbInstanceClass:
type: string
required:
- storageGB
- finalDBSnapshot
- dbInstanceClass
required:
- parameters

Note that you defined the required fields to be informed as a required field, some fields will be patched as soon as the Resource Claim is applied.

DB Subnet Group
db-subnet-group.yaml

apiVersion: database.aws.crossplane.io/v1beta1
kind: DBSubnetGroup
metadata:
name: db-subnet-group
spec:
forProvider:
region: us-east-1
description: "DB Subnet Group"
subnetIds:
- subnet-xxxxxx
- subnet-xxxxxx
- subnet-xxxxxx
providerConfigRef:
name: aws-provider

VPC
vpc.yaml

In case you already have a VPC provisioned, you need to import the VPC reference to crossplane make a reference when you mention the VPC.
If this is your case, you can use the example below.

apiVersion: ec2.aws.crossplane.io/v1beta1 
kind: VPC
metadata:
name: my-vpc-name
annotations:
crossplane.io/external-name: vpc-0880f7bca04b71622
spec:
forProvider:
region: us-east-1
cidrBlock: x.x.x.x/X
enableDnsHostNames: true
enableDnsSupport: true
instanceTenancy: default
tags:
- key: Name
value: my-vpc-name
providerConfigRef:
name: aws-provider

So, after you apply all the compositions above to let your infrastructure ready to receive the claims, the only thing your developer can do is to apply this Claim manifest to create RDS Instance with the name my-db-instance, using 10 GB of storage and using the instance type db.t3.medium

Claim
claim.yaml

apiVersion: v1
kind: Namespace
metadata:
name: demo
---
apiVersion: database.example.org/v1alpha1
kind: RDSPostgresInstance
metadata:
name: my-db-instance
namespace: demo
spec:
parameters:
storageGB: 10
finalDBSnapshot: my-db-instance
dbInstanceClass: db.t3.medium
compositionSelector:
matchLabels:
provider: aws-provider
writeConnectionSecretToRef:
name: my-db-instance

Apply all the compositions:

kubectl apply -f composition.yaml
kubectl apply -f xrd.yaml
kubectl apply -f db-subnet-group.yaml
kubectl apply -f vpc.yaml
kubectl apply -f claim.yam

So, if another developer wants to create another database but using db.t3.large instance type and 50GB of storage, he just needs to apply as below :

apiVersion: database.example.org/v1alpha1
kind: RDSPostgresInstance
metadata:
name: my-db-instance-2
namespace: demo
spec:
parameters:
storageGB: 50
finalDBSnapshot: my-db-instance-2
dbInstanceClass: db.t3.large
compositionSelector:
matchLabels:
provider: aws-provider
writeConnectionSecretToRef:
name: my-db-instance-2

Remember: If you would like to work with more than one cloud provider allowing your developers to provide their needs, you as a Platform Builder just need to create another provider and apply the right compositions related to your cloud provider. After that, you just need to provide a template of claim to your developers, instructed them to reference another cloud provider on their claims. Example:

compositionSelector:
matchLabels:
provider: azure-provider
compositionSelector:
matchLabels:
provider: gcp-provider

In this way, you are abstracting all the infrastructure complexities, allowing your developers to create their infrastructure requirements by just applying a Kubernetes Kind.

Note: You can use the git-ops process to automatize this using a CI/CD pipeline to provisioning this resource along with the application manifest as well.

Thanks for reading!

Further Readings

https://crossplane.io
https://alibaba-cloud.medium.com/oam-and-crossplane-the-next-stage-for-building-modern-application-6a0e8642dbda
https://blog.crossplane.io/crossplane-vs-terraform/
https://next.redhat.com/2021/03/31/crossplane-as-an-abstraction-platform-to-manage-and-deploy-service-operators/
https://danielmangum.com/posts/crossplane-infrastructure-llvm/
https://blog.crossplane.io/

Videos
https://www.cncf.io/webinars/this-week-in-cloud-native-crossplane-gitops-based-infrastructure-as-code-through-kubernetes-api/
https://www.youtube.com/watch?v=yrj4lmScKHQ&t=1s
https://www.youtube.com/watch?v=RaoKcJGchKM&t=1874s

--

--

Daniel Rosa

Cloud infrastructure specialist with more than 19 years of experience with critical production systems. https://www.linkedin.com/in/danielmartinsrosa/