Skip to main content

Compositions

In addition to provisioning individual cloud resources, Crossplane offers a higher abstraction layer called Compositions. Compositions allow users to build opinionated templates for deploying cloud resources. For example, organizations may require certain tags to be present to all AWS resources or add specific encryption keys for all Amazon Simple Storage (S3) buckets. Platform teams can define these self-service API abstractions within Compositions and ensure that all the resources created through these Compositions meet the organization’s requirements.

A CompositeResourceDefinition (or XRD) defines the type and schema of your Composite Resource (XR). It lets Crossplane know that you want a particular kind of XR to exist, and what fields that XR should have. An XRD is a little like a CustomResourceDefinition (CRD), but slightly more opinionated. Writing an XRD is mostly a matter of specifying an OpenAPI "structural schema".

First, lets provide a definition that can be used to create a database by members of the application team in their corresponding namespace. In this example the user only needs to specify databaseName, storageGB and secret location

~/environment/eks-workshop/modules/automation/controlplanes/crossplane/compositions/definition.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xrelationaldatabases.awsblueprints.io
spec:
defaultCompositionRef:
name: rds-mysql.awsblueprints.io
group: awsblueprints.io
names:
kind: XRelationalDatabase
plural: xrelationaldatabases
claimNames:
kind: RelationalDatabase
plural: relationaldatabases
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
properties:
spec:
properties:
databaseName:
type: string
storageGB:
type: integer
secret:
type: string
resourceConfig:
description: ResourceConfig defines general properties of this AWS
resource.
properties:
providerConfigName:
type: string
type: object
required:
- secret
- databaseName
- storageGB
type: object

Create this composite definition:

~$kubectl apply -f ~/environment/eks-workshop/modules/automation/controlplanes/crossplane/compositions/definition.yaml
compositeresourcedefinition.apiextensions.crossplane.io/xrelationaldatabases.awsblueprints.io created

A Composition lets Crossplane know what to do when someone creates a Composite Resource. Each Composition creates a link between an XR and a set of one or more Managed Resources - when the XR is created, updated, or deleted the set of Managed Resources are created, updated or deleted accordingly.

The following Composition provisions the managed resources DBSubnetGroup, SecurityGroup and DBInstance

The DBInstance is configure to auto generate the DB password, and store it in a Kubernetes secret with the name specified in the claim spec.secret in the same namespace as the claim. The location of the secret is specified by masterUserPasswordSecretRef. The DB username and endpoint values are stored in the same secret specified by spec.writeConnectionSecretToRef:

~/environment/eks-workshop/modules/automation/controlplanes/crossplane/compositions/composition/composition.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: rds-mysql.awsblueprints.io
spec:
compositeTypeRef:
apiVersion: awsblueprints.io/v1alpha1
kind: XRelationalDatabase
patchSets:
- name: common-fields
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.resourceConfig.providerConfigName
toFieldPath: spec.providerConfigRef.name
resources:
- base:
apiVersion: database.aws.crossplane.io/v1beta1
kind: DBSubnetGroup
metadata:
labels:
app.kubernetes.io/created-by: eks-workshop
spec:
forProvider:
region: $(AWS_REGION)
description: "rds-mysql"
subnetIds:
- $(VPC_PRIVATE_SUBNET_ID_1)
- $(VPC_PRIVATE_SUBNET_ID_2)
- $(VPC_PRIVATE_SUBNET_ID_3)
tags:
- key: created-by
value: eks-workshop-v2
- key: env
value: $(EKS_CLUSTER_NAME)
- key: managed-by
value: crossplane
patches:
- type: PatchSet
patchSetName: common-fields
- fromFieldPath: metadata.name
toFieldPath: metadata.annotations[crossplane.io/external-name]
transforms:
- type: string
string:
fmt: $(EKS_CLUSTER_NAME)-%s
- base:
apiVersion: ec2.aws.crossplane.io/v1beta1
kind: SecurityGroup
metadata:
labels:
app.kubernetes.io/created-by: eks-workshop
spec:
forProvider:
region: $(AWS_REGION)
vpcId: $(VPC_ID)
description: "rds-mysq-sg"
ingress:
- ipProtocol: tcp
fromPort: 3306
toPort: 3306
ipRanges:
- cidrIp: "$(VPC_CIDR)"
tags:
- key: created-by
value: eks-workshop-v2
- key: env
value: $(EKS_CLUSTER_NAME)
- key: managed-by
value: crossplane
- key: Name
value: to-be-patch
patches:
- type: PatchSet
patchSetName: common-fields
- fromFieldPath: "metadata.name"
toFieldPath: "spec.forProvider.groupName"
transforms:
- type: string
string:
fmt: $(EKS_CLUSTER_NAME)-rds-mysql-sg-%s
- fromFieldPath: "metadata.name"
toFieldPath: "spec.forProvider.tags[3].value"
transforms:
- type: string
string:
fmt: $(EKS_CLUSTER_NAME)-rds-mysql-sg-%s
- base:
apiVersion: rds.aws.crossplane.io/v1alpha1
kind: DBInstance
metadata:
labels:
app.kubernetes.io/created-by: eks-workshop
spec:
forProvider:
region: $(AWS_REGION)
applyImmediately: true
autogeneratePassword: true
dbSubnetGroupNameSelector:
matchControllerRef: true
dbInstanceClass: db.t4g.micro
masterUsername: admin
engine: mysql
engineVersion: "8.0"
dbName: to-be-patched
allocatedStorage: 20
skipFinalSnapshot: true
publiclyAccessible: false
vpcSecurityGroupIDs: []
vpcSecurityGroupIDSelector:
matchControllerRef: true
masterUserPasswordSecretRef:
key: password
name: to-be-patched
namespace: to-be-patched
tags:
- key: created-by
value: eks-workshop-v2
- key: env
value: $(EKS_CLUSTER_NAME)
- key: managed-by
value: crossplane
patches:
- type: PatchSet
patchSetName: common-fields
- fromFieldPath: "spec.storageGB"
toFieldPath: "spec.forProvider.allocatedStorage"
- fromFieldPath: "spec.databaseName"
toFieldPath: "spec.forProvider.dbName"
- fromFieldPath: metadata.labels[crossplane.io/claim-namespace]
toFieldPath: spec.writeConnectionSecretToRef.namespace
- fromFieldPath: spec.secret
toFieldPath: spec.writeConnectionSecretToRef.name
- fromFieldPath: metadata.labels[crossplane.io/claim-namespace]
toFieldPath: spec.forProvider.masterUserPasswordSecretRef.namespace
- fromFieldPath: spec.secret
toFieldPath: spec.forProvider.masterUserPasswordSecretRef.name
transforms:
- type: string
string:
fmt: "%s-passwd"
- fromFieldPath: metadata.name
toFieldPath: metadata.annotations[crossplane.io/external-name]
transforms:
- type: string
string:
fmt: $(EKS_CLUSTER_NAME)-%s

Apply this to our EKS cluster:

~$kubectl apply -k ~/environment/eks-workshop/modules/automation/controlplanes/crossplane/compositions/composition
composition.apiextensions.crossplane.io/rds-mysql.awsblueprints.io created

Once we’ve configured Crossplane with the details of the new XR we can either create one directly or use a Claim. Typically only the team responsible for configuring Crossplane (often a platform or SRE team) have permission to create XRs directly. Everyone else manages XRs via a lightweight proxy resource called a Composite Resource Claim (or claim for short).

With this claim the developer only needs to specify a default database name, size, and location to store the credentials to connect to the database. This allows the platform or SRE team to standardize on aspects such as database engine, high-availability architecture and security configuration.

~/environment/eks-workshop/modules/automation/controlplanes/crossplane/compositions/claim/claim.yaml
apiVersion: awsblueprints.io/v1alpha1
kind: RelationalDatabase
metadata:
name: catalog-composition
namespace: catalog
spec:
databaseName: catalog
storageGB: 20
secret: catalog-db-composition
resourceConfig:
providerConfigName: default

Create the database by creating a Claim:

~$kubectl apply -f ~/environment/eks-workshop/modules/automation/controlplanes/crossplane/compositions/claim/claim.yaml
relationaldatabase.awsblueprints.io/rds-eks-workshop created

It takes some time to provision the AWS managed services, in the case of RDS up to 10 minutes. Crossplane will report the status of the reconciliation in the status field of the Kubernetes custom resources.

To verify that the provisioning is done you can check that the condition “Ready” is true using the Kubernetes CLI. Run the following commands and they will exit once the condition is met:

~$kubectl wait relationaldatabase.awsblueprints.io catalog-composition -n catalog --for=condition=Ready --timeout=20m
dbinstances.rds.services.k8s.aws/rds-eks-workshop condition met

Crossplane will have automatically created a Kubernetes secret object that contains the credentials to connect to the RDS instance:

~$kubectl get secret catalog-db-composition -n catalog -o yaml
apiVersion: v1
kind: Secret
metadata:
  name: catalog-db-composition
  namespace: catalog
type: connection.crossplane.io/v1alpha1
data:
  endpoint: cmRzLWVrcy13b3Jrc2hvcC5jamthdHFkMWNucnoudXMtd2VzdC0yLnJkcy5hbWF6b25hd3MuY29t
  password: eGRnS1NNN2RSQ3dlc2VvRmhrRUEwWDN3OXpp
  port: MzMwNg==
  username: YWRtaW4=

Update the application to use the RDS endpoint and credentials:

~$kubectl apply -k ~/environment/eks-workshop/modules/automation/controlplanes/crossplane/compositions/application
namespace/catalog unchanged
serviceaccount/catalog unchanged
configmap/catalog unchanged
secret/catalog-db unchanged
service/catalog unchanged
service/catalog-mysql unchanged
service/ui-nlb created
deployment.apps/catalog configured
statefulset.apps/catalog-mysql unchanged
~$kubectl rollout restart -n catalog deployment/catalog
~$kubectl rollout status -n catalog deployment/catalog --timeout=30s

We can now check the logs of the catalog service to verify its connecting to the RDS database provisioned by Crossplane:

~$kubectl -n catalog logs deployment/catalog
2023/06/02 21:16:18 Running database migration...
2023/06/02 21:16:18 Schema migration applied
2023/06/02 21:16:18 Connecting to eks-workshop-test-catalog-composition-68jlv-gd6q5.cjkatqd1cnrz.us-west-2.rds.amazonaws.com/catalog?timeout=5s
2023/06/02 21:16:18 Connected
2023/06/02 21:16:18 Connecting to eks-workshop-test-catalog-composition-68jlv-gd6q5.cjkatqd1cnrz.us-west-2.rds.amazonaws.com/catalog?timeout=5s
2023/06/02 21:16:18 Connected