DEV Community

Cover image for KCL + Crossplane: A Declarative Language for Deploying Complex Infrastructure on AWS with Kubernetes.
Javier Sepúlveda
Javier Sepúlveda

Posted on

KCL + Crossplane: A Declarative Language for Deploying Complex Infrastructure on AWS with Kubernetes.

In recent blogs, the exploration of how Crossplane works and its potential to create self-service models by enabling custom APIs has been discussed. The previous example, however, was quite basic.

This time, the focus shifts to a more advanced concept in Crossplane: functions.

One limitation of Crossplane is its inability to use loops or conditionals when creating resources. A previous experiment highlighted this challenge, requiring resources to be defined individually. This approach often resulted in manifests that were difficult to manage and prone to complexity, resembling spaghetti code.

For context, here is an example using only providers: My first steps with Crossplane.

To address these issues, functions provide a solution that improves code reusability, adheres to the DRY (Don't Repeat Yourself) principle, and reduces the likelihood of human error. Functions enable the use of complex logic within compositions, making them more robust and easier to maintain.

function-kcl

By incorporating functions like these, Crossplane compositions can overcome many limitations, becoming more flexible and capable of handling intricate scenarios.

Deploying A network Layer with crossplane and KCL.

Requirements

  • Kubernetes cluster (Minikube)
  • Helm version v3.13.1 or later
  • Crossplane
  • programmatic access AWS

Step 1.

This blog assumes that you have all requirements completed and assume that you have installed the providers and the ProviderConfig used in the last blog.
If you have any doubt, please check the this post.

Step 2.

Install kcl function.

kubectl apply -f functions.yaml
Enter fullscreen mode Exit fullscreen mode

crossplane kcl functions

Step 3.

Defined the API using the CompositeResourceDefinition.yaml or XRD.

In this case, the API only receives: vpccidrBlock, region, and projectName through claim.yaml.

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
 name: multiazwordpressinfra.segoja7.example
spec:
 group: segoja7.example
 names:
    kind: MultiAzWordpressInfra
    plural: multiazwordpressinfra
 versions:
   - name: v1alpha1
     served: true
     referenceable: true
     schema:
       openAPIV3Schema:
         type: object
         properties:
           spec:
             type: object
             properties:
               parameters:
                 type: object
                 properties:
                   projectName:
                     type: string
                   vpccidrBlock:
                     type: string
                     default: "172.16.0.0/16"
                   region:
                     type: string
                     default: "us-east-1"
                 required:
                   - region
Enter fullscreen mode Exit fullscreen mode

Step 4.

Composition.

Crossplane has two modes of composition: pipeline and resouces(Deprecated) check the documentation.

Following best practices, the compositions have the pipeline mode, and through it, a sequence of steps is defined.

This is the most important lines for the compositions using kcl function.

  pipeline:
  - step: normal
    functionRef:
      name: function-kcl
    input:
      apiVersion: krm.kcl.dev/v1alpha1
      kind: KCLRun
      metadata:
        name: basic
      spec:
        source: |
# Removed for Brevity
  - step: ready
    functionRef:
      name: function-auto-ready
Enter fullscreen mode Exit fullscreen mode

How do KCL functions help users?

As previously mentioned, when needing to deploy complex infrastructure, Crossplane doesn't directly allow this. Functions become necessary, and with KCL functions, users can solve and improve the code for deploying that infrastructure.

For example, this is subnets resource with out crossplane. have a declaration for create 4 subnets type private.

---
apiVersion: ec2.aws.upbound.io/v1beta1
kind: Subnet
metadata:
  name: database-private-subnet-az1
  labels:
    dbsubnet1: private1
spec:
  forProvider:
    availabilityZone: us-east-1a
    cidrBlock: 172.16.1.0/24
    mapPublicIpOnLaunch: false
    region: us-east-1
    tags:
      Name: database-private-subnet-az1
      ManagedBy: crossplane
    vpcIdSelector:
      matchLabels:
        vpc: wordpress
  providerConfigRef:
    name: segoja7
---
apiVersion: ec2.aws.upbound.io/v1beta1
kind: Subnet
metadata:
  name: database-private-subnet-az2
  labels:
    dbsubnet2: private2
spec:
  forProvider:
    availabilityZone: us-east-1b
    cidrBlock: 172.16.2.0/24
    mapPublicIpOnLaunch: false
    region: us-east-1
    tags:
      Name: database-private-subnet-az2
      ManagedBy: crossplane
    vpcIdSelector:
      matchLabels:
        vpc: wordpress
  providerConfigRef:
    name: segoja7
---
apiVersion: ec2.aws.upbound.io/v1beta1
kind: Subnet
metadata:
  name: app-private-subnet-az1
  labels:
    appsubnet1: private1
spec:
  forProvider:
    availabilityZone: us-east-1a
    cidrBlock: 172.16.3.0/24
    mapPublicIpOnLaunch: false
    region: us-east-1
    tags:
      Name: app-private-subnet-az1
      ManagedBy: crossplane
    vpcIdSelector:
      matchLabels:
        vpc: wordpress
  providerConfigRef:
    name: segoja7
---
apiVersion: ec2.aws.upbound.io/v1beta1
kind: Subnet
metadata:
  name: app-private-subnet-az2
  labels:
    appsubnet2: private2
spec:
  forProvider:
    availabilityZone: us-east-1b
    cidrBlock: 172.16.4.0/24
    mapPublicIpOnLaunch: false
    region: us-east-1
    tags:
      Name: app-private-subnet-az2
      ManagedBy: crossplane
    vpcIdSelector:
      matchLabels:
        vpc: wordpress
  providerConfigRef:
    name: segoja7
Enter fullscreen mode Exit fullscreen mode

And this is an example using kcl-function.

  • Defining an map of objects for iterate about them.
          database_subnet_configs = [
              {"name": "data-private-subnet-az1", "cidr": "172.16.1.0/24", "zone": "us-east-1a", "type": "private"},
              {"name": "data-private-subnet-az2", "cidr": "172.16.2.0/24", "zone": "us-east-1b", "type": "private"},
              {"name": "app-private-subnet-az1", "cidr": "172.16.3.0/24", "zone": "us-east-1a", "type": "private"},
              {"name": "app-private-subnet-az2", "cidr": "172.16.4.0/24", "zone": "us-east-1b", "type": "private"},
              {"name": "public-subnet-az1", "cidr": "172.16.5.0/24", "zone": "us-east-1a", "type": "public"},
              {"name": "public-subnet-az2", "cidr": "172.16.6.0/24", "zone": "us-east-1b", "type": "public"}
          ]
Enter fullscreen mode Exit fullscreen mode
  • creating subnets using an for.
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: multiazwordpressinfra-composition
spec:
  compositeTypeRef:
    apiVersion: segoja7.example/v1alpha1
    kind: MultiAzWordpressInfra
  mode: Pipeline
  pipeline:
  - step: normal
    functionRef:
      name: function-kcl
    input:
      apiVersion: krm.kcl.dev/v1alpha1
      kind: KCLRun
      metadata:
        name: basic
      spec:
        source: |
          # Removed for Brevity

          _resources += [
          {
              apiVersion = "ec2.aws.upbound.io/v1beta1"
              kind = "Subnet"
              metadata.name = subnet_config.name
              metadata.labels = {
                    type = subnet_config.type    
              }
              spec.forProvider = {
                  region = region
                  vpcIdSelector.matchControllerRef = True
                  availabilityZone = subnet_config.zone
                  cidrBlock = subnet_config.cidr
                  mapPublicIpOnLaunch = True if subnet_config.type == "public" else False
                  tags = {
                    "app" = "wordpress"
                    "Name" = metadata.name + "-" + projectName
                    "Type" = subnet_config.type
                  }
              }
              spec.providerConfigRef.name = providerConfigName         
          }for subnet_config in database_subnet_configs 
          ]
# Removed for Brevity                                 
          items = _resources
  - step: ready
    functionRef:
      name: function-auto-ready                  
Enter fullscreen mode Exit fullscreen mode

For this blog the compositions only allow the resources creation of network layer in AWS.
Check all code in this repo.

GitHub logo segoja7 / crossplane_compositions_kcl

This is an example using compositions with kcl

Step 5.

The claim is the trigger to deploy the resources defined in the compositions.

apiVersion: segoja7.example/v1alpha1
kind: MultiAzWordpressInfra
metadata:
 name: wordpress-claim
 namespace: default
spec:
  parameters:
    projectName: wordpress-team1s
Enter fullscreen mode Exit fullscreen mode

In this claim, the only parameter for the end user is the project name, which allows the abstraction to be on the platform team side and not the product team side.

Step 6.

Verify the Resources

crossplane managed resources

kubectl get managed -A

Please check the following help links, which offer valuable information to improve your understanding of the tool and make the most of its features.

Useful links:

Conclusion: Crossplane, along with KCL, provides a powerful solution for managing cloud resources more flexibly and efficiently. By enabling the creation of reusable compositions and integrating complex logic, it improves maintainability and reduces errors. Additionally, by abstracting technical details, it eases the work of platform teams, optimizing the experience for end users.

Thanks to the Crossplane and KCL-Functions community for the support, they helped me with doubts and questions to make this post.

Thanks for reading this post, let me know if you have any question or comment.

Top comments (0)