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
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:
compositeresourcedefinition.apiextensions.crossplane.io "xrelationaldatabases.awsblueprints.io" deleted
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:
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
        spec:
          forProvider:
            region: $(AWS_DEFAULT_REGION)
            description: "rds-mysql"
            subnetIds:
            - $(VPC_PRIVATE_SUBNET_ID_0)
            - $(VPC_PRIVATE_SUBNET_ID_1)
            - $(VPC_PRIVATE_SUBNET_ID_2)
            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
    - base:
        apiVersion: ec2.aws.crossplane.io/v1beta1
        kind: SecurityGroup
        spec:
          forProvider:
            region: $(AWS_DEFAULT_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
      patches:
        - type: PatchSet
          patchSetName: common-fields
        - fromFieldPath: "metadata.uid"
          toFieldPath: "spec.forProvider.groupName"
          transforms:
            - type: string
              string:
                fmt: "rds-mysql-sg-%s"
    - base:
        apiVersion: rds.aws.crossplane.io/v1alpha1
        kind: DBInstance
        spec:
          forProvider:
            region: $(AWS_DEFAULT_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
Apply this to our EKS cluster:
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.
apiVersion: awsblueprints.io/v1alpha1
kind: RelationalDatabase
metadata:
  name: $(EKS_CLUSTER_NAME)-catalog-composition
  namespace: catalog
spec:
  databaseName: catalog
  storageGB: 20
  secret: catalog-db-composition
  resourceConfig:
    providerConfigName: aws-provider-config
Create the database by creating a Claim:
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:
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:
apiVersion: v1
data:
endpoint: cmRzLWVrcy13b3Jrc2hvcC5jamthdHFkMWNucnoudXMtd2VzdC0yLnJkcy5hbWF6b25hd3MuY29t
password: eGRnS1NNN2RSQ3dlc2VvRmhrRUEwWDN3OXpp
port: MzMwNg==
username: YWRtaW4=
kind: Secret
metadata:
creationTimestamp: "2023-01-26T15:12:41Z"
name: catalog-db-composition
namespace: catalog
ownerReferences:
- apiVersion: rds.aws.crossplane.io/v1alpha1
controller: true
kind: DBInstance
name: rds-eks-workshop
uid: bff607d9-86f2-4710-aabd-e60985b56995
resourceVersion: "28395"
uid: 1407281b-d282-42d8-b898-733400d3d11a
type: connection.crossplane.io/v1alpha1
Update the application to use the RDS endpoint and credentials:
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
An NLB has been created to expose the sample application for testing:
k8s-ui-uinlb-a9797f0f61.elb.us-west-2.amazonaws.com
To wait until the load balancer has finished provisioning you can run this command:
Once the load balancer is provisioned you can access it by pasting the URL in your web browser. You will see the UI from the web store displayed and will be able to navigate around the site as a user.
