How to create Composite Resources with Crossplane

crossplane kubernetes aws

5 min read | by Jordi Prats

With Crossplane we define Composite resources as the combination of other resources. Let's take a look on how to do this we are going to take some terraform code, tranform it into Crossplane objects and the create a Composition based on them

We are going to use the following terraform code that creates a SecurityGroup on AWS with an outgoing rule (SecurityGroupRule):

resource "aws_security_group" "pod_sg" {  name = "xplane-xplanesg"  description = "xplane SG test"  vpc_id = "vpc-1234abcd" } resource "aws_security_group_rule" "egress_any_sg_pod" {  description = "allow all outbound traffic"  security_group_id = aws_security_group.pod_sg.id  type = "egress"  from_port = 0  to_port = 0  protocol = "-1"  cidr_blocks = ["0.0.0.0/0"] } 

The process to translate this terraform code to a Crossplane object is specially easy if we are using the AWS jet provider: We just need to convert the resources's and it's properties names to camelcase. For example, from_port would be forPort. For the resource's names we will have to also exclude the aws_ bit: aws_security_group would be SecurityGroup.

So, the resulting Crossplane objects would be:

apiVersion: ec2.aws.jet.crossplane.io/v1alpha2 kind: SecurityGroup metadata:  name: 'xplanesg'  labels:  sgname: 'xplanesg' spec:  forProvider:  name: 'xplane-xplanesg'  region: 'eu-west-1'  description: 'xplane SG test'  vpcId: 'vpc-1234abcd'  providerConfigRef:  name: 'aws-jetprovider-config' --- apiVersion: ec2.aws.jet.crossplane.io/v1alpha2 kind: SecurityGroupRule metadata:  name: 'xplanesg-egress' spec:  forProvider:  securityGroupIdSelector:  matchLabels:  sgname: 'xplanesg'  fromPort: 0  toPort: 0  protocol: "-1"  type: 'egress'  region: 'eu-west-1'  cidrBlocks:  - '0.0.0.0/0'  providerConfigRef:  name: 'aws-jetprovider-config' 

Please notice how it's not completely boilerplate-free:

  • We need to specify what ProviderConfig we want to use (we can skip this bit by naming it default, but explicit is always better than implicit)
  • There are several ways to select the SecurityGroup we want to add the rule to, but for simplicity's sake we are going to select it using a label

By applying these objects we will be able to see how they get created on AWS:

$ kubectl get securitygroup NAME READY SYNCED EXTERNAL-NAME AGE xplanesg True False sg-11c0052b1ad079839 1m30s $ kubectl get securitygrouprule NAME READY SYNCED EXTERNAL-NAME AGE xplanesg-egress True True sgrule-3584302207 1m30s 

To create a Composition with these two resources we will need to create two objects:

  • First, a CompositeResourceDefinition with the details of the object we want to use as a composition
  • Then the actual Composition that will define what objects are going to be created

To create the CompositeResourceDefinition we need to, basically, define the OpenAPI structural schema. In order to simplify it's definition we are going to use just one variable: It's region. The actual definition would look like follows:

apiVersion: apiextensions.crossplane.io/v1 kind: CompositeResourceDefinition metadata:  name: sgsallowoutgoing.pet2cattle.com spec:  group: pet2cattle.com  names:  kind: SGAllowOutgoing  plural: sgsallowoutgoing  versions:  - name: v1alpha1  served: true  referenceable: true  schema:  openAPIV3Schema:  type: object  properties:  spec:  type: object  properties:  parameters:  type: object  properties:  region:  type: string  required:  - region  required:  - parameters 

Once we have the CompositeResourceDefinition in place we can now create a Composition for it. To do so we can take the objects we want it to create and us the patch definitions in order to create them with the proper values:

apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata:  name: sgsallowoutgoing  labels:  crossplane.io/xrd: sgsallowoutgoing.pet2cattle.com spec:  writeConnectionSecretsToNamespace: crossplane-system  compositeTypeRef:  apiVersion: pet2cattle.com/v1alpha1  kind: SGAllowOutgoing  resources:  - name: sg  base:  apiVersion: ec2.aws.jet.crossplane.io/v1alpha2  kind: SecurityGroup  spec:  forProvider:  description: 'xplane SG test'  vpcId: 'vpc-1234abcd'  providerConfigRef:  name: 'aws-jetprovider-config'  patches:  - type: FromCompositeFieldPath  fromFieldPath: metadata.name  toFieldPath: spec.forProvider.name  - type: FromCompositeFieldPath  fromFieldPath: metadata.name  toFieldPath: metadata.annotations.crossplane.io/external-name  - type: FromCompositeFieldPath  fromFieldPath: metadata.name  toFieldPath: metadata.labels.sgname  - type: FromCompositeFieldPath  fromFieldPath: spec.parameters.region  toFieldPath: spec.forProvider.region  - name: sg-egress  base:  apiVersion: ec2.aws.jet.crossplane.io/v1alpha2  kind: SecurityGroupRule  spec:  forProvider:  fromPort: 0  toPort: 0  protocol: "-1"  type: 'egress'  cidrBlocks:  - '0.0.0.0/0'  providerConfigRef:  name: 'aws-jetprovider-config'  patches:  - type: FromCompositeFieldPath  fromFieldPath: spec.parameters.region  toFieldPath: spec.forProvider.region  - type: FromCompositeFieldPath  fromFieldPath: metadata.name  toFieldPath: spec.forProvider.securityGroupIdSelector.matchLabels.sgname 

This is an over simplification to make it easier to understand with plenty of hardcoded values so you can make sense out of it.

Once both resources are available we can now proceed creating an instance of this new resource:

apiVersion: pet2cattle.com/v1alpha1 kind: SGAllowOutgoing metadata:  name: sgone spec:  parameters:  region: eu-west-1 

If we apply this object it will create, in turn, create the objects we have defined:

$ kubectl get sgallowoutgoing NAME READY COMPOSITION AGE sgone True sgsallowoutgoing 4m13s $ kubectl get securitygroup NAME READY SYNCED EXTERNAL-NAME AGE xplanesg True False sg-11c0052b1ad079839 5h33m sgone-jr6lv True False sg-ad6a7ad890ca61632 4m25s $ kubectl get securitygrouprule NAME READY SYNCED EXTERNAL-NAME AGE xplanesg-egress True True sgrule-3584302207 5h26m sgone-mdvmc True True sgrule-3652368947 4m30s 

Using kubectl describe on the composition instance we'll be able to see the objects it's using under the hood:

$ kubectl describe sgallowoutgoing.pet2cattle.com/sgone Name: sgone Namespace:  Labels: crossplane.io/composite=sgone Annotations: <none> API Version: pet2cattle.com/v1alpha1 Kind: SGAllowOutgoing Metadata: (...) Spec:  Composition Ref:  Name: sgsallowoutgoing  Composition Update Policy: Automatic  Parameters:  Region: eu-west-1  Resource Refs:  API Version: ec2.aws.jet.crossplane.io/v1alpha2  Kind: SecurityGroup  Name: sgone-jr6lv  API Version: ec2.aws.jet.crossplane.io/v1alpha2  Kind: SecurityGroupRule  Name: sgone-mdvmc  Write Connection Secret To Ref:  Name: e5ca5def-ab90-4fe9-a2e1-fd588105586a  Namespace: crossplane-system Status:  Conditions:  Last Transition Time: 2022-03-14T22:42:21Z  Reason: Available  Status: True  Type: Ready  Connection Details:  Last Published Time: 2022-03-14T22:41:51Z Events:  Type Reason Age From Message  ---- ------ ---- ---- -------  Normal PublishConnectionSecret 3m15s defined/compositeresourcedefinition.apiextensions.crossplane.io Successfully published connection details  Normal SelectComposition 44s (x9 over 3m16s) defined/compositeresourcedefinition.apiextensions.crossplane.io Successfully selected composition  Normal ComposeResources 43s (x9 over 3m15s) defined/compositeresourcedefinition.apiextensions.crossplane.io Successfully composed resources 

For your convenience, I have also uploaded all these objects to the GitHub's repo: pet2cattle/crossplane-composition-example


Posted on 16/03/2022