Skip to main content

Rewrite HCL for Crossplane or Upbound

Terraform code into a basic Crossplane configuration. The provider is a great way to get started on your Crossplane journey. With Crossplane, you can go even further by converting your Terraform HashiCorp Configuration Language (HCL) into Kubernetes-like manifests for more Crossplane benefits.

Why go all in on Crossplane?

Moving away from HCL to Crossplane configurations can simplify your deployment workflow. You can manage your applications and infrastructure with the same workflow and leverage the continuous reconciliation processes of Crossplane in Kubernetes.

Prerequisites

For this guide, you will convert the HCL configuration from the previous guide to use Crossplane native resources. Make sure you have:

  • Crossplane installed
  • A Kubernetes cluster
  • An AWS account

Create a Crossplane managed resource

The managed resource configuration in the previous guide created a virtual machine with the provider-terraform:

apiVersion: tf.upbound.io/v1beta1
kind: Workspace
metadata:
name: tf-vm
spec:
forProvider:
source: Inline
module: |
resource "aws_instance" "my_vm" {
ami = "ami-065deacbcaac64cf2"
instance_type = "t2.micro"
tags = {
Name = var.vmName
}
}

variable "vmName" {
description = "VM name"
type = string
}

vars:
- key: vmName
value: crossplanevm

A Crossplane native managed resource relies on the provider-aws instead:

apiVersion: ec2.aws.crossplane.io/v1alpha1
kind: Instance
metadata:
name: crossplane-vm
spec:
forProvider:
region: us-west-2
imageId: ami-065deacbcaac64cf2
instanceType: t2.micro
providerConfigRef:
name: awsconfig

This example uses the same fields as the Terraform configuration, with key differences. First, the apiVersion field references the API group for the AWS Crossplane provider. This provider focuses explicitly on the EC2 service of AWS.

The kind field identifies the schema type for the configuration. In this case, you'll use the Instance kind.

The metadata is a required field that contains information about the resource, like the name or other identifying values.

The spec field defines the parameters of the instance.

The forProvider sub-field defines the information you need for the instance configuration. Instead of relying on the Terraform configuration to define how you want to configure the instance, you'll use the Kubernetes manifest configuration language.

Create a composition

Your infrastructure needs supporting resources. Crossplane uses compositions to create and manage multiple resources.

Compositions let you compose all necessary resources into a file with every attribute your organization needs. These compositions are the explicit resources your teams need and the infrastructure consumers (developers and applications teams) aren't exposed to these files. In the next steps, you'll create a definition and a claim. The definition file defines what inputs you need to create the resources in the composition. The claim is the file you can expose to infrastructure consumers and lets them define the variables required from the definition.

In this section, you'll create a composition with an instance and all the supporting resources for it to be useful.

Create a new file called complete-instance-composition.yaml.

Expand the Composition below and copy and paste it into your file.

Instance composition
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: xinstance.aws.example.corp
spec:
writeConnectionSecretsToNamespace: upbound-system
compositeTypeRef:
apiVersion: aws.example.corp/v1alpha1
kind: XInstance
resources:
- name: test-env
base:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: VPC
spec:
forProvider:
cidrBlock: 10.0.0.0/16
enableDnsHostnames: true
enableDnsSupport: true
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.region

- name: subnet
base:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: Subnet
spec:
forProvider:
cidrBlock: 10.0.0.0/24
vpcIdSelector:
matchControllerRef: true
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.region
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.availabilityZone
transforms:
- type: string
string:
fmt: "%sa"
type: Format

- name: ingress-all-test
base:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: SecurityGroup
spec:
forProvider:
name: allow-all-sg
vpcIdSelector:
matchControllerRef: true
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.region

- name: ingress-all-test-ingress
base:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: SecurityGroupIngressRule
spec:
forProvider:
cidrIpv4: 0.0.0.0/0
fromPort: 22
toPort: 22
ipProtocol: tcp
securityGroupIdSelector:
matchControllerRef: true
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.region

- name: ingress-all-test-egress
base:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: SecurityGroupEgressRule
spec:
forProvider:
cidrIpv4: 0.0.0.0/0
fromPort: 0
toPort: 0
ipProtocol: "-1"
securityGroupIdSelector:
matchControllerRef: true
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.region

- name: test-env-gw
base:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: InternetGateway
spec:
forProvider:
vpcIdSelector:
matchControllerRef: true
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.region

- name: route-table-test-env
base:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: RouteTable
spec:
forProvider:
vpcIdSelector:
matchControllerRef: true
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.region

- name: route-table-test-env-route
base:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: Route
spec:
forProvider:
destinationCidrBlock: 0.0.0.0/0
gatewayIdSelector:
matchControllerRef: true
routeTableIdSelector:
matchControllerRef: true
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.region

- name: subnet-association
base:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: RouteTableAssociation
spec:
forProvider:
routeTableIdSelector:
matchControllerRef: true
subnetIdSelector:
matchControllerRef: true
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.region

- name: keypair
base:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: KeyPair
metadata:
labels:
testing.upbound.io/example-name: keypair
name: keypair
spec:
forProvider:
publicKey:
matchControllerRef: true
region:
matchControllerRef: true
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.region
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.publicKey
toFieldPath: spec.forProvider.publicKey

- name: ec2-instance
base:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: Instance
metadata:
labels:
testing.upbound.io/example-name: test
name: test
spec:
forProvider:
instanceType: t3.micro
amiId: ami-0005e0cfe09cc9050
vpcSecurityGroupIdSelector:
matchControllerRef: true
subnetIdSelector:
matchControllerRef: true
keyPairName:
matchControllerRef: true
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.region
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.keyPairName
toFieldPath: spec.forProvider.keyName

- name: ip-test-env
base:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: EIP
metadata:
labels:
testing.upbound.io/example-name: lb
name: lb
spec:
forProvider:
instanceSelector:
matchControllerRef: true
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.region

Create a Crossplane custom resource definition

A definition is a Crossplane spec that defines the allowed and required resources you want to deploy.

In this step, create a definition for the composition in the preceding step. Think of this as a custom API endpoint. You created the composition with all the necessary supporting resources and now your definition highlights all the parameters you need to pass when you deploy. Notice the only properties required in this definition are the region and the publicKey string. That's because you explicitly set the other attributes like the amiId in the composition. Your applications teams and developers don't need to know what AMI to use because you set one that meets your organizations needs.

Create a new file called complete-instance-definition.yaml.

Copy and paste the definition below:

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xinstances.aws.example.corp
spec:
group: aws.example.corp
names:
kind: XInstance
plural: xinstances
claimNames:
kind: Instance
plural: instances
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
parameters:
type: object
description: Instance configuration parameters.
properties:
publicKey:
type: string
region:
type: string
required:
- region
- publicKey
required:
- parameters

Create a claim

Now that you have a custom resource definition and a composition, you can create a claim and provision the resources.

A claim deploys a set of resources within a namespace. Creating claims is comparable to different Terraform workspaces. Resources in one namespace don't impact resources in another namespace.

apiVersion: aws.example.corp/v1alpha1
kind: Instance
metadata:
name: example
namespace: default
spec:
parameters:
region: us-east-1
publicKey: |
ssh-rsa AAAAB3NzaC1...
note

Copy and paste the contents of your public key (typically ~/.ssh/id_rsa.pub) into the publicKey field.

Claims are the highly opinionated entry points your infrastructure consumers to use. Your claim eliminates the need for them to think about underlying resources and attributes.

Authenticate with your cloud provider

The Crossplane AWS provider configuration handles authentication. You must create a Kubernetes secret file to authenticate with your AWS account.

The provider supports AWS authentication with:

note

For more information on cloud provider authentication, read the

Create a new Kubernetes secret.

kubectl -n upbound-system create secret generic aws-creds --from-file=credentials=aws-credentials

Verify your secret with kubectl describe secret.

kubectl describe secret aws-creds -n upbound-system

Name: aws-creds
Namespace: upbound-system
Labels: <none>
Annotations: <none>

Type: Opaque

Data
====
creds: 114 bytes

Install the provider

apiVersion: aws.crossplane.io/v1beta1
kind: ProviderConfig
metadata:
name: aws-provider-ec2
spec:
credentials:
source: Secret
secretRef:
namespace: upbound-system
name: aws-creds
key: credentials
kubectl apply -f aws-provider-config.yaml

Deploy your claim

Next, apply your Crossplane Composition.

kubectl apply -f claim.yaml

Verify your deployment

You can verify your deployment with kubectl get claim.

kubectl get claim

When you move from Terraform to Crossplane, the provider-terraform approach eases the transition. When you get more comfortable with Crossplane, the native cloud provider configuration enables you to remove your original HCL resource definitions.

The Upbound Marketplace can help you recreate your Terraform configurations with Crossplane providers. In the EC2 example, the Marketplace has a dedicated section for all the managed resources of the [EC2 API group](https://marketplace.upbound.io/providers/upbound/provider-aws-ec2/. The original Terraform resource requires two attributes to be valid, ami and instance_type.

The Upbound Marketplace represents these attributes in the API schema. The ami and instance_type keys are properties within the forProvider object. The attributes required by Terraform are the same values required by Crossplane.

Next steps

You just created an EC2 instance with an SSH key with Crossplane. You created a Composite Resource Definition and a Composition from your original Terraform configuration.