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