Migrating to Crossplane with provider-terraform

2023_01_Blog_OG_Migrating-to-XP-Terraform_1200x630
date icon

January 5, 2023

author icon

Yury Tsarev

read time icon

Reading time: 5 min read

Share:

LinkedIn icon
Twitter icon
Facebook icon

With provider-terraform we have a new Crossplane provider that runs Terraform configurations and enables platform teams to include custom Terraform code within a Crossplane powered architecture. This provider enables many integration scenarios and allows teams who have invested in Terraform to leverage a powerful control plane architecture. Please read the announcement for more information. Furthermore, if you haven’t consider watching the webinar: Kubernetes Called and it Wants Your IaC Back: Using Control Planes to Modernize Your IaC Tech Stack.

In this blog post, we would like to explore a phased approach on how to include existing Terraform code and start migrating towards a Crossplane native solution.

Example: Let developers create their own subnet

Let’s say we are a platform team and are tasked to create AWS Subnets for developers. We have already spent a significant amount of time into a Terraform module, and we want to re-use that code, so developers can self-serve subnets for their application. Let’s see on how can use Crossplane to do so and port our custom code to Crossplane and possibly migrate fully to Crossplane if we'd like to.

Overview

We will do this in four steps:

  1. Create a common interface for developers to use
  2. Run the full module in provider-terraform
  3. Extract one resource (subnet) into the native AWS provider
  4. Move the second resource into the native AWS provider

The beauty of this approach is that we have a running platform after step one and can test our platform and do the subsequent steps 3 and 4 when the time is right.

The following steps will contain short abbreviated code examples. See the upbound/provider-terraform repository for the complete example.

1) Create a common interface for developers to use

With Crossplane, a platform team offers their users a way to create a subnet by simply creating a specific Kubernetes resource. e.g. something like:

apiVersion: aws.platformref.upbound.io/v1alpha1
kind: XSubnet
metadata:
  name: my-subnet
spec:
  vpcName: my-vpc-name

In order for this to materialize, we first need to create a Composite Resource Definition (or short XRD). This is Kubernetes resources that Crossplane understands. See Composite Resources · Docs for more information. The XRD contains a spec where we can describe the interface for the developer. In this case, we want to make it easy for the developer and make him only specify the vpcName.

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
spec:
  group: aws.platformref.upbound.io
  names:
    kind: XSubnet
  versions:
  - name: v1alpha1
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              vpcName:
                type: string

(Abbreviated. See definition.yaml for the complete version.)

2) Run the full module in provider-terraform

After we have created the interface, we need to create an implementation, which is called a Composition in Crossplane. In this step, we want to run the full Terraform module in this composition by using provider-terraform. As you might guess, we are creating another Kubernetes resource with the type Composition. We start by just specifying one Crossplane resource by the kind Workspace which includes the Terraform code and a patches to convert the necessary information to Kubernetes. In this example, we have in-lined all the Terraform code, but we can also specify remote resources (see examples/workspace-remote.yaml for an example.)

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
spec:
  compositeTypeRef:
    apiVersion: aws.platformref.upbound.io/v1alpha1
    kind: XSubnet
  resources:
    - name: tf-vpc-and-subnet
      base:
        apiVersion: tf.upbound.io/v1beta1
        kind: Workspace
        metadata:
          name: observe-only-vpc
        spec:
          forProvider:
            source: Inline
            module: |
              resource "aws_vpc" "main" {
                cidr_block       = "10.0.0.0/16"
              }
              resource "aws_subnet" "main" {
                vpc_id     = aws_vpc.main.id
                cidr_block = "10.0.1.0/24"
              }
              output "vpc_id" {
                value       = aws_vpc.main.id
              }
              variable "vpcName" {
                description = "VPC name"
                type        = string
              }
            vars:
              - key: vpcName
      patches:
        - fromFieldPath: spec.vpcName
          toFieldPath: spec.forProvider.vars[0].value
        - type: ToCompositeFieldPath
          fromFieldPath: status.atProvider.outputs.vpc_id
          toFieldPath: status.share.vpcId

(Abbreviated. See 01-composition-tf-only/composition.yaml for the complete version.)

🎉 Success! We now have a fully running control plane with our custom Terraform logic. You can start testing it with your team.

2) Extract one resource into the native provider

To fully transition to Crossplane, we now want to refactor the Subnet resource to use provider-aws. For this, we adapt our composition to now have two resources of kind Workspace and Subnet and use patching to combine these two.

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
spec:
  resources:
    - name: vpc
      base:
        apiVersion: tf.upbound.io/v1beta1
        kind: Workspace
        spec:
          forProvider:
            source: Inline
            module: |
              resource "aws_vpc" "main" {
              }
              … // No "aws_subnet" resource anymore!
      patches:
        - fromFieldPath: spec.vpcName
          toFieldPath: spec.forProvider.vars[0].value
        - type: ToCompositeFieldPath
          fromFieldPath: status.atProvider.outputs.vpc_id
          toFieldPath: status.share.vpcId
    - name: subnet
      base:
        apiVersion: ec2.aws.upbound.io/v1beta1
        kind: Subnet
        spec:
          forProvider:
            region: eu-central-1
            cidrBlock: 10.0.1.0/24
      patches:
        - fromFieldPath: status.share.vpcId
          toFieldPath: spec.forProvider.vpcId

(Abbreviated. See 02-tf-and-native/composition.yaml for the complete version.)

You can see on how this enables us to transition to Crossplane native providers gradually. And depending on the complexity of the resources you have been developing, you can repeat this step.

3) Move the second resource into the native provider

In our last step, we want to remove provider-terraform from our composition and fully use the native aws provider. The outcome is for folks familiar with Crossplane straightforward. One composition that contains the two resource VPC and Subnet:

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
spec:
  resources:
    - name: vpc
      base:
        apiVersion: ec2.aws.upbound.io/v1beta1
        kind: VPC
        spec:
          forProvider:...
      patches:
        - fromFieldPath: spec.vpcName
          toFieldPath: spec.forProvider.tags.Name
    - name: subnet
      base:
        apiVersion: ec2.aws.upbound.io/v1beta1
        kind: Subnet
        spec:
          forProvider: …

(Abbreviated. See 03-native-only/composition.yaml for the complete version.)

Summary

Crossplane is a great way to build a developer platform powered by Kubernetes. Terraform has a great ecosystem and existing investments from platform teams. With provider-terraform we can combine both ecosystems and enable integration scenarios. This blogpost showed a quick get started guide on how to take a step-by-step guide behind a stable developer facing API. If you want to learn more and see the above in action, please check out the migration guides.

Subscribe to the Upbound Newsletter