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 DynamoDB table by members of the application team in their corresponding namespace. In this example the user only needs to specify name, key attributes and index name fields.

~/environment/eks-workshop/modules/automation/controlplanes/crossplane/compositions/composition/definition.yaml
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xdynamodbtables.awsblueprints.io
spec:
group: awsblueprints.io
names:
kind: XDynamoDBTable
plural: xdynamodbtables
claimNames:
kind: DynamoDBTable
plural: dynamodbtables
connectionSecretKeys:
- tableName
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
description: Table is the Schema for the tables API
properties:
spec:
type: object
properties:
resourceConfig:
properties:
deletionPolicy:
description: Defaults to Delete
enum:
- Delete
- Orphan
type: string
name:
type: string
providerConfigName:
type: string
default: aws-provider-config
region:
type: string
default: ""
tags:
additionalProperties:
type: string
description: Key-value map of resource tags.
type: object
required:
- region
type: object
dynamoConfig:
properties:
attribute: #required for hashKey and/or rangeKey
items:
properties:
name: #name of the hashKey and/or rangeKey
type: string
type:
enum:
- B #binary
- N #number
- S #string
type: string
required:
- name
- type
type: object
type: array
hashKey:
type: string
rangeKey:
type: string
billingMode:
type: string
default: PAY_PER_REQUEST
readCapacity:
type: number
writeCapacity:
type: number
globalSecondaryIndex:
items:
properties:
hashKey:
type: string
name:
type: string
rangeKey:
type: string
readCapacity:
type: number
writeCapacity:
type: number
projectionType:
type: string
default: ALL
nonKeyAttributes: #required for gsi
items:
type: string
type: array
type: object
required:
- name
type: array
localSecondaryIndex:
items:
properties:
name:
type: string
rangeKey:
type: string
projectionType:
type: string
nonKeyAttributes: #required for lsi
items:
type: string
type: array
type: object
required:
- name
- rangeKey
- projectionType
- nonKeyAttributes
type: array
required:
- attribute
type: object
required:
- dynamoConfig
status:
type: object
description: TableStatus defines the observed state of Table
properties:
tableArn:
description: Indicates this table's ARN
type: string
tableName:
description: Indicates this table's Name
type: string
required:
- spec

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 Table

~/environment/eks-workshop/modules/automation/controlplanes/crossplane/compositions/composition/table.yaml
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: table.dynamodb.awsblueprints.io
labels:
awsblueprints.io/provider: aws
awsblueprints.io/environment: dev
spec:
writeConnectionSecretsToNamespace: crossplane-system
compositeTypeRef:
apiVersion: awsblueprints.io/v1alpha1
kind: XDynamoDBTable
patchSets:
- name: common-fields
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.resourceConfig.providerConfigName
toFieldPath: spec.providerConfigRef.name
- type: FromCompositeFieldPath
fromFieldPath: spec.name
toFieldPath: metadata.annotations[crossplane.io/external-name]
- type: FromCompositeFieldPath
fromFieldPath: metadata.name
toFieldPath: metadata.annotations[crossplane.io/external-name]
transforms:
- type: string
string:
type: Regexp
regexp:
match: ^(.*?)-crossplane
resources:
- name: table
connectionDetails:
- type: FromFieldPath
name: tableName
fromFieldPath: status.atProvider.id
base:
apiVersion: dynamodb.aws.upbound.io/v1beta1
kind: Table
spec:
forProvider:
writeConnectionSecretToRef:
name: cartsdynamo
namespace: crossplane-system
region: ""
providerConfigRef:
name: aws-provider-config
patches:
- type: PatchSet
patchSetName: common-fields
- type: FromCompositeFieldPath
fromFieldPath: spec.dynamoConfig.attribute
toFieldPath: spec.forProvider.attribute
policy:
mergeOptions:
appendSlice: true
keepMapValues: true
- type: FromCompositeFieldPath
fromFieldPath: spec.resourceConfig.tags
toFieldPath: spec.forProvider.tags
policy:
mergeOptions:
keepMapValues: true
- type: FromCompositeFieldPath
fromFieldPath: spec.dynamoConfig.attribute[0].name
toFieldPath: spec.forProvider.hashKey
- type: FromCompositeFieldPath
fromFieldPath: spec.dynamoConfig.billingMode
toFieldPath: spec.forProvider.billingMode
- type: FromCompositeFieldPath
fromFieldPath: spec.dynamoConfig.rangeKey
toFieldPath: spec.forProvider.rangeKey
- type: FromCompositeFieldPath
fromFieldPath: spec.dynamoConfig.readCapacity
toFieldPath: spec.forProvider.readCapacity
- type: FromCompositeFieldPath
fromFieldPath: spec.dynamoConfig.writeCapacity
toFieldPath: spec.forProvider.writeCapacity
- type: FromCompositeFieldPath
fromFieldPath: spec.dynamoConfig.globalSecondaryIndex[0].name
toFieldPath: spec.forProvider.globalSecondaryIndex[0].name
- type: FromCompositeFieldPath
fromFieldPath: spec.dynamoConfig.attribute[1].name
toFieldPath: spec.forProvider.globalSecondaryIndex[0].hashKey
- type: FromCompositeFieldPath
fromFieldPath: spec.dynamoConfig.globalSecondaryIndex[0].projectionType
toFieldPath: spec.forProvider.globalSecondaryIndex[0].projectionType
policy:
mergeOptions:
keepMapValues: true
- type: FromCompositeFieldPath
fromFieldPath: spec.dynamoConfig.localSecondaryIndex
toFieldPath: spec.forProvider.localSecondaryIndex
policy:
mergeOptions:
keepMapValues: true
- type: ToCompositeFieldPath
fromFieldPath: status.atProvider.id
toFieldPath: status.tableName
- type: ToCompositeFieldPath
fromFieldPath: status.atProvider.arn
toFieldPath: status.tableArn

Apply this to our EKS cluster:

~$kubectl apply -k ~/environment/eks-workshop/modules/automation/controlplanes/crossplane/compositions/composition
compositeresourcedefinition.apiextensions.crossplane.io/xdynamodbtables.awsblueprints.io created
composition.apiextensions.crossplane.io/table.dynamodb.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 DynamoDB table name, hash keys, global index name to create the table. This allows the platform or SRE team to standardize on aspects such as billing mode, default read/write capacity, projection type, cost and infrastructure related tags.

~/environment/eks-workshop/modules/automation/controlplanes/crossplane/compositions/claim/claim.yaml
apiVersion: awsblueprints.io/v1alpha1
kind: DynamoDBTable
metadata:
name: "${EKS_CLUSTER_NAME}-carts-crossplane"
spec:
dynamoConfig:
attribute:
- name: id
type: S
- name: customerId
type: S
globalSecondaryIndex:
- name: idx_global_customerId

Cleanup the Dynamodb table created from the previous Managed Resource section.

~$kubectl delete tables.dynamodb.aws.upbound.io --all --ignore-not-found=true
~$kubectl wait --for=delete tables.dynamodb.aws.upbound.io --all --timeout=5m

Create the table by creating a Claim:

~$cat ~/environment/eks-workshop/modules/automation/controlplanes/crossplane/compositions/claim/claim.yaml \
| envsubst | kubectl -n carts apply -f -
dynamodbtable.awsblueprints.io/eks-workshop-carts-crossplane created
~$kubectl wait dynamodbtables.awsblueprints.io ${EKS_CLUSTER_NAME}-carts-crossplane -n carts \
--for=condition=Ready --timeout=5m

It takes some time to provision the AWS managed services, in the case of DynamoDB up to 2 minutes. Crossplane will report the status of the reconciliation in the SYNCED field of the Kubernetes Composite and Managed resource.

~$kubectl get table
NAME                                        READY   SYNCED   EXTERNAL-NAME                   AGE
eks-workshop-carts-crossplane-bt28w-lnb4r   True   True      eks-workshop-carts-crossplane   6s

Now, lets try to understand how the DynamoDB table is deployed using this claim:

Crossplane reconciler concept

On querying the claim DynamoDBTable deployed in the carts namespace, we can observe that it points to and creates a Composite Resource (XR) XDynamoDBTable

~$kubectl get DynamoDBTable -n carts -o yaml | grep "resourceRef:" -A 3
    resourceRef:
      apiVersion: awsblueprints.io/v1alpha1
      kind: XDynamoDBTable
      name: eks-workshop-carts-crossplane-bt28w

The Composition table.dynamodb.awsblueprints.io shows Composite Resource Kind (XR-KIND) as XDynamoDBTable. This Composition lets Crossplane know what to do when we created the XDynamoDBTable XR. Each Composition creates a link between an XR and a set of one or more Managed Resources.

~$kubectl get composition
NAME                              XR-KIND          XR-APIVERSION               AGE
table.dynamodb.awsblueprints.io   XDynamoDBTable   awsblueprints.io/v1alpha1   143m

On querying the XDynamoDBTable XR which is not confined to any namespace, we can observe that it creates DynamoDB Managed Resource Table.

~$kubectl get XDynamoDBTable -o yaml | grep "resourceRefs:" -A 3
    resourceRefs:
    - apiVersion: dynamodb.aws.upbound.io/v1beta1
      kind: Table
      name: eks-workshop-carts-crossplane-bt28w-lnb4r

When new resources are created or updated, application configurations also need to be updated to use these new resources. We've already configured the workload to use the correct table name in the previous section so lets just restart the pods:

~$kubectl rollout restart -n carts deployment/carts
~$kubectl rollout status -n carts deployment/carts --timeout=2m
deployment "carts" successfully rolled out

Now, how do we know that the application is working with the new DynamoDB table?

An NLB has been created to expose the sample application for testing, allowing us to directly interact with the application through the browser:

~$kubectl get service -n ui ui-nlb -o jsonpath="{.status.loadBalancer.ingress[*].hostname}{'\n'}"
k8s-ui-uinlb-a9797f0f61.elb.us-west-2.amazonaws.com
info

Please note that the actual endpoint will be different when you run this command as a new Network Load Balancer endpoint will be provisioned.

To wait until the load balancer has finished provisioning you can run this command:

~$wait-for-lb $(kubectl get service -n ui ui-nlb -o jsonpath="{.status.loadBalancer.ingress[*].hostname}{'\n'}")

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.

http://k8s-ui-uinlb-a9797f0f61.elb.us-west-2.amazonaws.com

To verify that the Carts module is in fact using the DynamoDB table we just provisioned, try adding a few items to the cart.

Cart screenshot

And to check if items are in the DynamoDB table as well, run

~$aws dynamodb scan --table-name "${EKS_CLUSTER_NAME}-carts-crossplane" --query 'Items[].{itemId:itemId,Price:unitPrice}' --output text
PRICE   795
ITEMID  510a0d7e-8e83-4193-b483-e27e09ddc34d
PRICE   385
ITEMID  6d62d909-f957-430e-8689-b5129c0bb75e
PRICE   50
ITEMID  a0a4f044-b040-410d-8ead-4de0446aec7e

Congratulations! You've successfully created AWS Resources without leaving the confines of the Kubernetes API!