--- title: AWS Quickstart Part 2 weight: 120 tocHidden: true --- {{< hint "important" >}} This guide is part 2 of a series. Follow **[part 1]({{}})** to install Crossplane and connect your Kubernetes cluster to AWS. **[Part 3]({{}})** covers patching _composite resources_ and using Crossplane _packages_. {{< /hint >}} This section creates a _[Composition](#create-a-composition)_, _[Composite Resource Definition](#define-a-composite-resource)_ and a _[Claim](#create-a-claim)_ to create a custom Kubernetes API to create AWS resources. This custom API is a _Composite Resource_ (XR) API. ## Prerequisites * Complete [quickstart part 1]({{}}) connecting Kubernetes to AWS. * an AWS account with permissions to create an AWS S3 storage bucket and a DynamoDB instance {{}} 1. Add the Crossplane Helm repository and install Crossplane ```shell helm repo add \ crossplane-stable https://charts.crossplane.io/stable helm repo update && helm install crossplane \ crossplane-stable/crossplane \ --namespace crossplane-system \ --create-namespace ``` 2. When the Crossplane pods finish installing and are ready, apply the AWS Provider ```yaml {label="provider",copy-lines="all"} cat <}} ```ini {copy-lines="all"} [default] aws_access_key_id = $@$@ aws_secret_access_key = $@$@ ``` {{}} 4. Create a Kubernetes secret from the AWS keys ```shell {label="kube-create-secret",copy-lines="all"} kubectl create secret \ generic aws-secret \ -n crossplane-system \ --from-file=creds=./aws-credentials.txt ``` 5. Create a _ProviderConfig_ ```yaml {label="providerconfig",copy-lines="all"} cat <}} ## Create a composition [Part 1]({{}}) created a single _managed resource_. A _Composition_ is a template to create one or more _managed resource_ at the same time. This sample _composition_ creates an DynamoDB instance and associated S3 storage bucket. {{< hint "note" >}} This example comes from the AWS recommendation for [storing large DynamoDB attributes in S3](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-use-s3-too.html#bp-use-s3-too-large-values). {{< /hint >}} To create a _composition_, first define each individual managed resource. ### Create an S3 bucket object Define a `bucket` resource using the configuration from the previous section: {{< hint "note" >}} Don't apply this configuration. This YAML is part of a larger definition. {{< /hint >}} ```yaml apiVersion: s3.aws.upbound.io/v1beta1 kind: Bucket metadata: name: crossplane-quickstart-bucket spec: forProvider: region: "us-east-2" providerConfigRef: name: default ``` ### Create a DynamoDB table resource Next, define a DynamoDB `table` resource. {{< hint "tip" >}} The [Upbound Marketplace](https://marketplace.upbound.io/) provides [schema documentation](https://marketplace.upbound.io/providers/upbound/provider-aws/v0.27.0/resources/dynamodb.aws.upbound.io/Table/v1beta1) for a `Table` resource. {{< /hint >}} The _AWS Provider_ defines the {{}}apiVersion{{}} and {{}}kind{{}}. DynamoDB instances require a {{}}region{{}}, {{}}writeCapacity{{}} and {{}}readCapacity{{}} parameters. The {{}}attribute{{}} section creates the database "Partition key" and "Hash key." This example creates a single key named {{}}S3ID{{}} of type {{}}S{{}} for "string" ```yaml {label="dynamoMR"} apiVersion: dynamodb.aws.upbound.io/v1beta1 kind: Table metadata: name: crossplane-quickstart-database spec: forProvider: region: "us-east-2" writeCapacity: 1 readCapacity: 1 attribute: - name: S3ID type: S hashKey: S3ID ``` {{< hint "note" >}} DynamoDB specifics are beyond the scope of this guide. Read the [DynamoDB Developer Guide](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html) for more information. {{}} ### Create the composition object The _composition_ combines the two resource definitions. A {{}}Composition{{}} comes from the {{}}Crossplane{{}} API resources. Create any {{}}name{{}} for this _composition_. ```yaml {label="compName"} apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: name: dynamodb-with-bucket ``` Add the resources to the {{}}spec.resources{{}} section of the _composition_. Give each resource a {{}}name{{}} and put the resource definition under the {{}}base{{}} key. ```yaml {label="specResources"} apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: name: dynamodb-with-bucket spec: resources: - name: s3-bucket base: apiVersion: s3.aws.upbound.io/v1beta1 kind: Bucket spec: forProvider: region: "us-east-2" providerConfigRef: name: default - name: dynamodb base: apiVersion: dynamodb.aws.upbound.io/v1beta1 kind: Table spec: forProvider: region: "us-east-2" writeCapacity: 1 readCapacity: 1 attribute: - name: S3ID type: S hashKey: S3ID ``` _Compositions_ are a template for generating resources. A _composite resource_ actually creates the resources. A _composition_ defines what _composite resources_ can use this template. _Compositions_ do this with the {{}}spec.compositeTypeRef{{}} definition. {{< hint "tip" >}} Crossplane recommends prefacing the `kind` with an `X` to show it's a Composition. {{< /hint >}} ```yaml {label="compRef"} apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: name: dynamodb-with-bucket spec: compositeTypeRef: apiVersion: custom-api.example.org/v1alpha1 kind: XDatabase resources: # Removed for Brevity ``` A _composite resource_ is actually a custom Kubernetes API type you define. The platform team controls the kind, API endpoint and version. With this {{}}spec.compositeTypeRef{{}} Crossplane only allows _composite resources_ from the API group {{}}custom-api.example.org{{}} that are of {{}}kind: XDatabase{{}} to use this template to create resources. ### Apply the composition Apply the full _Composition_ to your Kubernetes cluster. ```yaml cat <}} _Composite resource definitions_ are also called `XRDs` for short. {{< /hint >}} Just like a _composition_ the {{}}composite resource definition{{}} is part of the {{}}Crossplane{{}} API group. The _XRD_ {{}}name{{}} is the new API endpoint. {{< hint "tip" >}} Crossplane recommends using a plural name for the _XRD_ {{}}name{{}}. {{< /hint >}} ```yaml {label="xrdName"} apiVersion: apiextensions.crossplane.io/v1 kind: CompositeResourceDefinition metadata: name: xdatabases.custom-api.example.org ``` The _XRD's_ {{}}spec{{}} defines the new custom API. ### Define the API endpoint and kind First, define the new API {{}}group{{}}. Next, create the API {{}}kind{{}} and {{}}plural{{}}. ```yaml {label="xrdGroup"} apiVersion: apiextensions.crossplane.io/v1 kind: CompositeResourceDefinition metadata: name: xdatabases.custom-api.example.org spec: group: custom-api.example.org names: kind: XDatabase plural: xdatabases ``` {{}} The _XRD_ {{}}group{{}} matches the _composition_ {{}}apiVersion{{}} and the _XRD_ {{}}kind{{}} matches the _composition_ {{}}compositeTypeRef.kind{{}}. ```yaml {label="noteComp"} kind: Composition # Removed for brevity spec: compositeTypeRef: apiVersion: custom-api.example.org/v1alpha1 kind: XDatabase ``` {{< /hint >}} ### Set the API version In Kubernetes, all API endpoints have a version to show the stability of the API and track revisions. Apply a version to the _XRD_ with a {{}}versions.name{{}}. This matches the {{}}compositeTypeRef.apiVersion{{}} _XRDs_ require both {{}}versions.served{{}} and {{}}versions.referenceable{{}}. ```yaml {label="xrdVersion"} apiVersion: apiextensions.crossplane.io/v1 kind: CompositeResourceDefinition metadata: name: xdatabases.custom-api.example.org spec: group: custom-api.example.org names: kind: XDatabase plural: xdatabases versions: - name: v1alpha1 served: true referenceable: true ``` {{}} For more information on defining versions in Kubernetes read the [API versioning](https://kubernetes.io/docs/reference/using-api/#api-versioning) section of the Kubernetes documentation. {{< /hint >}} ### Create the API schema With an API endpoint named, now define the API schema, or what's allowed inside the `spec` of the new Kubernetes object. {{< hint "note" >}} _XRDs_ follow the Kubernetes [_custom resource definition_ rules for schemas](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#specifying-a-structural-schema). {{}} Place the API {{< hover label="xrdSchema" line="8" >}}schema{{}} under the {{< hover label="xrdSchema" line="7" >}}version.name{{}} The _XRD_ type defines the next lines. They're always the same. {{< hover label="xrdSchema" line="9" >}}openAPIV3Schema{{}} specifies how the schema gets validated. Next, the entire API is an {{< hover label="xrdSchema" line="10" >}}object{{}} with a {{< hover label="xrdSchema" line="11" >}}property{{}} of {{< hover label="xrdSchema" line="12" >}}spec{{}}. The {{< hover label="xrdSchema" line="12" >}}spec{{}} is also an {{< hover label="xrdSchema" line="13" >}}object{{}} with {{< hover label="xrdSchema" line="14" >}}properties{{}}. ```yaml {label="xrdSchema"} apiVersion: apiextensions.crossplane.io/v1 kind: CompositeResourceDefinition # Removed for brevity spec: # Removed for brevity versions: - name: v1alpha1 schema: openAPIV3Schema: type: object properties: spec: type: object properties: ``` {{< hint "tip" >}} For more information on the values allowed in a _composite resource definition_ view its schema with `kubectl explain xrd` {{< /hint >}} Now, define the custom API. Your custom API continues under the last {{}}properties{{}} definition in the previous example. This custom API has only one setting: * {{}}region{{}} - where to deploy the resources, a choice of "EU" or "US" Users can't change any other settings of the S3 bucket or DynamoDB instance. The{{}}region{{}} is a {{}}string{{}} and can match the regular expression that's {{}}oneOf{{}} {{}}EU{{}} or {{}}US{{}}. This API requires the setting {{}}region{{}}. ```yaml {label="customAPI"} # Removed for brevity # schema.openAPIV3Schema.type.properties.spec properties: region: type: string oneOf: - pattern: '^EU$' - pattern: '^US$' required: - region ``` ### Enable claims to the API Tell this _XRD_ to offer a _claim_ by defining the _claim_ API endpoint under the _XRD_ {{}}spec{{< /hover >}}. {{< hint "tip" >}} Crossplane recommends a _Claim_ {{}}kind{{}} match the _Composite Resource_ (XR) {{}}kind{{}}, without the preceding `X`. {{< /hint >}} ```yaml {label="XRDclaim"} apiVersion: apiextensions.crossplane.io/v1 kind: CompositeResourceDefinition # Removed for brevity spec: # Removed for brevity names: kind: XDatabase plural: xdatabases claimNames: kind: Database plural: databases ``` {{}} The [Claims](#create-a-claim) section later in this guide discusses _claims_. {{< /hint >}} ### Apply the composite resource definition Apply the complete _XRD_ to your Kubernetes cluster. ```yaml cat <}}group{{}} becomes the _composite resource_ {{}}apiVersion{{}}. The _XRD_ {{}}kind{{}} is the _composite resource_ {{}}kind{{}} The _XRD_ API {{}}spec{{}} defines the _composite resource_ {{}}spec{{}}. The _XRD_ {{}}properties{{}} section defines the options for the _composite resource_ {{}}spec{{}}. The one option is {{}}region{{}} and it can be either {{}}EU{{}} or {{}}US{{}}. This _composite resource_ uses {{}}region: US{{}}. ### Apply the composite resource Apply the composite resource to the Kubernetes cluster. ```yaml {label="xr"} cat <}} Use `kubectl get ` to view a specific `kind` of _composite resource_. View all _composite resources_ with `kubectl get composite`. {{< /hint >}} ```shell {copy-lines="1"} kubectl get xdatabase NAME SYNCED READY COMPOSITION AGE my-composite-resource True True dynamo-with-bucket 31s ``` Both `SYNCED` and `READY` are `True` when Crossplane created the AWS resources. Now look at the S3 `bucket` and DynmoDB `table` _managed resources_ with `kubectl get bucket` and `kubectl get table`. {{< hint "important" >}} This guide uses Upbound AWS provider v0.27.0. AWS Provider v0.30.0 and later requires the full CRD name `bucket.s3.aws.upbound.io` instead of `buckets`. {{}} ```shell {copy-lines="1"} kubectl get bucket NAME READY SYNCED EXTERNAL-NAME AGE my-composite-resource-8b6tx True True my-composite-resource-8b6tx 56s ``` ```shell {copy-lines="1"} kubectl get table NAME READY SYNCED EXTERNAL-NAME AGE my-composite-resource-m6vk6 True True my-composite-resource-m6vk6 59s ``` The _composite resource_ automatically generated both _managed resources_. Using `kubectl describe` on a _managed resource_ shows the `Owner References` is the _composite resource_. ```yaml {copy-lines="1"} kubectl describe bucket | grep "Owner References" -A5 Owner References: API Version: custom-api.example.org/v1alpha1 Block Owner Deletion: true Controller: true Kind: XDatabase Name: my-composite-resource ``` Each _composite resource_ creates and owns a unique set of _managed resources_. If you create a second _composite resource_ Crossplane creates a new S3 `bucket` and DynamoDB `table`. ```yaml {label="xr"} cat <}} Delete a specific _composite resource_ with `kubectl delete ` or `kubectl delete composite`. {{< /hint >}} Delete the second composition ```shell kubectl delete xdatabase my-second-composite-resource ``` {{}} There may a delay in deleting the _managed resources_. Crossplane is making API calls to AWS and waits for AWS to confirm they deleted the resources before updating the state in Kubernetes. {{}} Now only one bucket and table exist. ```shell {copy-lines="1"} kubectl get bucket NAME READY SYNCED EXTERNAL-NAME AGE my-composite-resource-8b6tx True True my-composite-resource-8b6tx 7m34s ``` ```shell {copy-lines="1"} kubectl get table NAME READY SYNCED EXTERNAL-NAME AGE my-composite-resource-m6vk6 True True my-composite-resource-m6vk6 7m37s ``` Delete the other _composite resource_ to remove the last `bucket` and `table` _managed resources_. ```shell kubectl delete xdatabase my-composite-resource ``` _Composite resources_ are great for creating one or more related resources against a template, but all _composite resources_ exist at the Kubernetes "cluster level." There's no isolation between _composite resources_. Crossplane uses _claims_ to create resources with namespace isolation. ## Create a claim _Claims_, just like _composite resources_ use the custom API defined in the _XRD_. Unlike a _composite resource_, Crossplane can create _claims_ in a namespace. ### Create a new Kubernetes namespace Create a new namespace with `kubectl create namespace`. ```shell kubectl create namespace test ``` Look at the _XRD_ to see the parameters for the _claim_. A _claim_ uses the same {{}}group{{}} a _composite resource_ uses but a different {{}}kind{{}}. ```yaml {label="XRDclaim2"} apiVersion: apiextensions.crossplane.io/v1 kind: CompositeResourceDefinition # Removed for brevity spec: # Removed for brevity group: custom-api.example.org claimNames: kind: Database plural: databases ``` Like the _composite resource_, create a new object with the {{}}custom-api.example.org{{}} API endpoint. The _XRD_ {{}}claimNames.kind{{}} defines the {{}}kind{{}}. The {{}}spec{{}} uses the same API options as the _composite resource_. ### Apply the claim Apply the _claim_ to your Kubernetes cluster. ```yaml {label="claim"} cat <}} View claims with `kubectl get ` or use `kubectl get claim` to view all _claims_. {{}} ```shell {copy-lines="1"} kubectl get database -n test NAME SYNCED READY CONNECTION-SECRET AGE claimed-database True True 35s ``` When Crossplane creates a _claim_, a unique _composite resource_ is also created. View the new _composite resource_ with `kubectl get xdatabase`. ```shell {copy-lines="1"} kubectl get xdatabase NAME SYNCED READY COMPOSITION AGE claimed-database-6xsgq True True dynamo-with-bucket 46s ``` The _composite resource_ exists at the "cluster scope" while the _claim_ exists at the "namespace scope." Create a second namespace and a second claim. ```shell kubectl create namespace test2 cat <}}) of this guide covers _composition patches_ and making all this configuration portable in Crossplane _packages_. ## Next steps * **[Continue to part 3]({{< ref "provider-aws-part-3">}})** to create a learn about _patching_ resources and creating Crossplane _packages_. * Explore AWS resources that Crossplane can configure in the [Provider CRD reference](https://marketplace.upbound.io/providers/upbound/provider-family-aws/). * Join the [Crossplane Slack](https://slack.crossplane.io/) and connect with Crossplane users and contributors.