--- title: Azure 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 Azure. **[Part 3]({{}})** covers patching _CompositeResources_ and using Crossplane _Packages_. {{< /hint >}} This section creates a _[Composition](#create-a-composition)_, _[CompositeResourceDefinition](#define-a-composite-resource)_ and a _[Claim](#create-a-claim)_ to create a custom Kubernetes API to create Azure resources. This custom API is a _composite resource_ (XR) API. ## Prerequisites * Complete [quickstart part 1]({{}}) connecting Kubernetes to Azure. * an Azure account with permissions to create an Azure Virtual Machine and Virtual Networking {{}} 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 Azure Provider ```yaml {label="provider",copy-lines="all"} cat <}} ```console az ad sp create-for-rbac \ --sdk-auth \ --role Owner \ --scopes /subscriptions/$$$$ ``` {{}} 4. Create a Kubernetes secret from the Azure JSON file. ```shell {label="kube-create-secret",copy-lines="all"} kubectl create secret \ generic azure-secret \ -n crossplane-system \ --from-file=creds=./azure-credentials.json ``` 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 Linux Virtual Machine and the required networking components. Compositions have multiple components: * The individual managed resources. * The Composition kind and version. * A Composite type reference. The following steps describe each of these components before [applying the final Composition](#apply-the-composition). ### Define a virtual network Define a `virtualnetwork` 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 {copy-lines="none"} apiVersion: network.azure.upbound.io/v1beta1 kind: VirtualNetwork metadata: name: crossplane-quickstart-network spec: forProvider: addressSpace: - 10.0.0.0/16 location: "Central US" resourceGroupName: ``` ### Define a subnet resource Next, define a `Subnet` resource. {{< hint "note" >}} Don't apply this configuration. This YAML is part of a larger definition. {{< /hint >}} ```yaml {label="subnet",copy-lines="none"} apiVersion: network.azure.upbound.io/v1beta1 kind: Subnet metadata: name: crossplane-quickstart-subnet spec: forProvider: addressPrefixes: - 10.0.1.0/24 resourceGroupName: ``` ### Define a network interface Define a network interface to attach to the virtual machine. {{< hint "note" >}} Don't apply this configuration. This YAML is part of a larger definition. {{< /hint >}} ```yaml {label="nic",copy-lines="none"} apiVersion: network.azure.upbound.io/v1beta1 kind: NetworkInterface metadata: name: crossplane-quickstart-nic spec: forProvider: ipConfiguration: - name: crossplane-quickstart-configuration privateIpAddressAllocation: Dynamic location: "Central US" resourceGroupName: ``` ### Define a virtual machine Define the `LinuxVirtualMachine` with its settings. {{< hint "note" >}} Don't apply this configuration. This YAML is part of a larger definition. {{< /hint >}} ```yaml {label="vm",copy-lines="none"} apiVersion: compute.azure.upbound.io/v1beta1 kind: LinuxVirtualMachine metadata: name: crossplane-quickstart-vm spec: forProvider: adminUsername: adminuser adminSshKey: - publicKey: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+wWK73dCr+jgQOAxNsHAnNNNMEMWOHYEccp6wJm2gotpr9katuF/ZAdou5AaW1C61slRkHRkpRRX9FA9CYBiitZgvCCz+3nWNN7l/Up54Zps/pHWGZLHNJZRYyAB6j5yVLMVHIHriY49d/GZTZVNB8GoJv9Gakwc/fuEZYYl4YDFiGMBP///TzlI4jhiJzjKnEvqPFki5p2ZRJqcbCiF4pJrxUQR/RXqVFQdbRLZgYfJ8xGB878RENq3yQ39d8dVOkq4edbkzwcUmwwwkYVPIoDGsYLaRHnG+To7FvMeyO7xDVQkMKzopTQV8AuKpyvpqu0a9pWOMaiCyDytO7GGN example@docs.crossplane.io username: adminuser location: "Central US" osDisk: - caching: ReadWrite storageAccountType: Standard_LRS size: Standard_B1ms sourceImageReference: - offer: debian-11 publisher: Debian sku: 11-backports-gen2 version: latest resourceGroupName: ``` ### Create the composition object The _Composition_ combines all the managed resources into a single object. A {{}}Composition{{}} comes from the {{}}Crossplane{{}} API resources. Create any {{}}name{{}} for this _Composition_. ```yaml {label="compName",copy-lines="none"} apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: name: crossplane-quickstart-vm-with-network ``` Add all the defined resources to the {{}}spec.resources{{}} section of the _Composition_. Give each resource a {{}}name{{}} and put the resource definition under the {{}}base{{}} key. Add your {{}}resourceGroupName{{< /hover >}} for each resource in the Composition. {{}} The contents of the {{}}base{{}} key doesn't include the `metadata` field from the managed resources. {{< /hint >}} ```yaml {label="specResources",copy-lines="none"} apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: name: crossplane-quickstart-vm-with-network spec: resources: - name: quickstart-subnet base: apiVersion: network.azure.upbound.io/v1beta1 kind: Subnet spec: forProvider: addressPrefixes: - 10.0.1.0/24 virtualNetworkNameSelector: matchControllerRef: true resourceGroupName: # Removed for brevity ``` {{}} Crossplane provides the {{}}matchControllerRef{{}} value to automatically link resources created by the same _Composition_. {{}} _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",copy-lines="none"} apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: name: crossplane-quickstart-vm-with-network spec: compositeTypeRef: apiVersion: custom-api.example.org/v1alpha1 kind: XVirtualMachine 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: XVirtualMachine{{}} to use this template to create resources. ### Apply the composition Apply the full _Composition_ to your Kubernetes cluster. {{}} Add your {{}}resourceGroupName{{}} to each resource. {{< /hint >}} {{< editCode >}} ```yaml {label="fullComp"} cat <$$ size: Standard_B1ms sourceImageReference: - offer: debian-11 publisher: Debian sku: 11-backports-gen2 version: latest networkInterfaceIdsSelector: matchControllerRef: true - name: quickstart-nic base: apiVersion: network.azure.upbound.io/v1beta1 kind: NetworkInterface spec: forProvider: ipConfiguration: - name: crossplane-quickstart-configuration privateIpAddressAllocation: Dynamic subnetIdSelector: matchControllerRef: true location: "Central US" resourceGroupName: $$$$ - name: quickstart-subnet base: apiVersion: network.azure.upbound.io/v1beta1 kind: Subnet spec: forProvider: addressPrefixes: - 10.0.1.0/24 virtualNetworkNameSelector: matchControllerRef: true resourceGroupName: $$$$ - name: quickstart-network base: apiVersion: network.azure.upbound.io/v1beta1 kind: VirtualNetwork spec: forProvider: addressSpace: - 10.0.0.0/16 location: "Central US" resourceGroupName: $$$$ EOF ``` {{< /editCode >}} Confirm the _Composition_ exists with `kubectl get composition` ```shell {copy-lines="1"} kubectl get composition NAME XR-KIND XR-APIVERSION AGE crossplane-quickstart-vm-with-network XVirtualMachine custom-api.example.org/v1alpha1 5s ``` Again, the _Composition_ is only a template. At this point, Crossplane hasn't created any resources inside of Azure. ## Define a composite resource The _Composition_ that was just created limited which _composite resources_ can use that template. A _composite resource_ is a custom API defined by the platform teams. A _CompositeResourceDefinition_ defines the schema for a _composite resource_. A _CompositeResourceDefinition_ installs the custom API type into Kubernetes and defines what `spec` keys and values are valid when calling this new custom API. Before creating a _composite resource_ Crossplane requires a _CompositeResourceDefinition_. {{< hint "tip" >}} _CompositeResourceDefinitions_ are also called `XRDs` for short. {{< /hint >}} Just like a _Composition_ the {{}}CompositeResourceDefinition{{}} 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",copy-lines="none"} apiVersion: apiextensions.crossplane.io/v1 kind: CompositeResourceDefinition metadata: name: xvirtualmachines.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",copy-lines="none"} apiVersion: apiextensions.crossplane.io/v1 kind: CompositeResourceDefinition metadata: name: xvirtualmachines.custom-api.example.org spec: group: custom-api.example.org names: kind: XVirtualMachine plural: xvirtualmachines ``` {{}} The _XRD_ {{}}group{{}} matches the _composition_ {{}}apiVersion{{}} and the _XRD_ {{}}kind{{}} matches the _composition_ {{}}compositeTypeRef.kind{{}}. ```yaml {label="noteComp",copy-lines="none"} kind: Composition # Removed for brevity spec: compositeTypeRef: apiVersion: custom-api.example.org/v1alpha1 kind: XVirtualMachine ``` {{< /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",copy-lines="none"} apiVersion: apiextensions.crossplane.io/v1 kind: CompositeResourceDefinition metadata: name: xvirtualmachines.custom-api.example.org spec: group: custom-api.example.org names: kind: XVirtualMachine plural: xvirtualmachines 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 [_CustomResourceDefinition_ 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",copy-lines="none"} 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 _CompositeResourceDefinition_ 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 VM or its network. 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",copy-lines="none"} # 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",copy-lines="none"} apiVersion: apiextensions.crossplane.io/v1 kind: CompositeResourceDefinition # Removed for brevity spec: # Removed for brevity names: kind: XVirtualMachine plural: xvirtualmachines claimNames: kind: VirtualMachine plural: virtualmachines ``` {{}} The [Claims](#create-a-claim) section later in this guide discusses _claims_. {{< /hint >}} ### Apply the CompositeResourceDefinition 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 <}} It may take up to 10 minutes for Azure to create the Virtual Machine resources. {{< /hint >}} ```shell {copy-lines="1"} kubectl get xvirtualmachine NAME SYNCED READY COMPOSITION AGE my-composite-resource True True crossplane-quickstart-vm-with-network 5m2s ``` {{}} Use `kubectl get ` to view a specific `kind` of _composite resource_. View all _composite resources_ with `kubectl get composite`. {{< /hint >}} Both `SYNCED` and `READY` are `True` when Crossplane created the Azure resources. Now look at the `linuxvirtualmachine` and `networkinterface` _managed resources_ with `kubectl get linuxvirtualmachine` and `kubectl get networkinterface`. ```shell {copy-lines="1"} kubectl get linuxvirtualmachine NAME READY SYNCED EXTERNAL-NAME AGE my-composite-resource-w564c True True my-composite-resource-w564c 8m33s ``` ```shell {copy-lines="1"} kubectl get networkinterface NAME READY SYNCED EXTERNAL-NAME AGE my-composite-resource-72ft8 True True my-composite-resource-72ft8 8m54s ``` The _composite resource_ automatically generated the _managed resources_. Using `kubectl describe` on a _managed resource_ shows the `Owner References` is the _composite resource_. ```yaml {copy-lines="1"} kubectl describe linuxvirtualmachine | grep "Owner References" -A5 Owner References: API Version: custom-api.example.org/v1alpha1 Block Owner Deletion: true Controller: true Kind: XVirtualMachine 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 `LinuxVirtualMachine` and new networking resources. ```yaml {label="xr"} cat <}} Delete a specific _composite resource_ with `kubectl delete ` or `kubectl delete composite`. {{< /hint >}} Delete the second composition ```shell kubectl delete xvirtualmachine my-second-composite-resource ``` {{}} It may take up to five minutes before Crossplane finishes deleting resources. {{}} Now only one virtual machine and network interface exist. ```shell {copy-lines="1"} kubectl get linuxvirtualmachines NAME READY SYNCED EXTERNAL-NAME AGE my-composite-resource-w564c True True my-composite-resource-w564c 28m ``` ```shell {copy-lines="1"} kubectl get networkinterface NAME READY SYNCED EXTERNAL-NAME AGE my-composite-resource-72ft8 True True my-composite-resource-72ft8 29m ``` Delete the other _composite resource_ to remove the last `linuxvirtualmachines` and `networkinterface` _managed resources_. ```shell kubectl delete xvirtualmachine 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",copy-lines="none"} apiVersion: apiextensions.crossplane.io/v1 kind: CompositeResourceDefinition # Removed for brevity spec: # Removed for brevity group: custom-api.example.org claimNames: kind: VirtualMachine plural: virtualmachines ``` 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 virtualmachine -n test NAME SYNCED READY CONNECTION-SECRET AGE claimed-virtualmachine True True 3m40s ``` When Crossplane creates a _Claim_, a unique _composite resource_ is also created. View the new _composite resource_ with `kubectl get xvirtualmachine`. ```shell {copy-lines="1"} kubectl get xvirtualmachine NAME SYNCED READY COMPOSITION AGE claimed-virtualmachine-cw6cv True True crossplane-quickstart-vm-with-network 3m57s ``` The _composite resource_ exists at the "cluster scope" while the _Claim_ exists at the "namespace scope." Create a second namespace and a second claim. ```yaml 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-azure-part-3">}})** to learn about _patching_ resources and creating Crossplane _Packages_. * Explore Azure resources that Crossplane can configure in the [Provider CRD reference](https://marketplace.upbound.io/providers/upbound/provider-family-azure/). * Join the [Crossplane Slack](https://slack.crossplane.io/) and connect with Crossplane users and contributors.