So you’re convinced that Crossplane is the Real Deal but your organization has millions of lines of Terraform code? Worry not.
You don’t have to choose between Crossplane or Terraform. But you’re probably wondering how you can make use of both.
You’re in luck – because this is exactly what we’re going to do today!
By the way, I want to keep this post very hands-on, so please make sure you had a look at our introductory post to Crossplane first.
Crossplane + Terraform = the perfect couple?
Before we dive in, a quick refresher on Crossplane.
As you can remember, Crossplane creates provider-specific Managed Resources. When Crossplane project first came to be Providers typically were referring to cloud resources.
Luckily, over time more providers have emerged (for various services) – and the Terraform Provider was one of them.
In practice, this means that nowadays we can leverage existing Terraform workspaces and modules as the building blocks for our Crossplane-based self-service infrastructure platform.
Yay!
Getting started with Crossplane and Terraform Provider
By the way, before we start (last time, I promise):
I prepared for you a fully-working demo using a local kind
cluster – feel free to clone it and follow the steps ahead.
Once you have installed Crossplane in your cluster, it’s time to install the Terraform Provider
.
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-terraform
spec:
package: xpkg.upbound.io/upbound/provider-terraform:v0.7.0
The next step is configuring the provider. This is actually your regular Terraform config, embedded in a ProviderConfig
manifest.
Note: for the purpose of this post, I’m going to use Kubernetes as the Terraform backend. Feel free to use a different backend.
apiVersion: tf.upbound.io/v1beta1
kind: ProviderConfig
metadata:
name: terraform-default
spec:
configuration: |
terraform {
backend "kubernetes" {
secret_suffix = "providerconfig-default"
namespace = "crossplane-system"
in_cluster_config = true
}
}
Using Terraform with Crossplane
Now that our Terraform Provider has been configured it’s time to provision some resources using it. To keep things simple, let’s deploy nginx and see how it works.
Terraform module
To this end, we’ll be deploying a Terraform module, consisting of:
- a
kubernetes_namespace
, - a
kubernetes_deployment_v1
, - a
kubernetes_service_v1
, - and a
kubernetes_ingress_v1
.
Here’s the complete module:
# variables.tf
variable "environment" {
type = string
}
# main.tf
resource "kubernetes_namespace" "nginx_app" {
metadata {
name = "nginx-app-${var.environment}"
labels = {
app = "nginx"
env = var.environment
}
}
}
resource "kubernetes_deployment_v1" "nginx_app" {
metadata {
name = "nginx-app"
namespace = kubernetes_namespace.nginx_app.metadata.0.name
labels = {
app = "nginx"
env = var.environment
}
}
spec {
replicas = 1
selector {
match_labels = {
app = "nginx"
env = var.environment
}
}
template {
metadata {
labels = {
app = "nginx"
env = var.environment
}
}
spec {
container {
image = "nginx:1.25.0"
name = "server"
resources {
limits = {
cpu = "0.5"
memory = "512Mi"
}
requests = {
cpu = "250m"
memory = "50Mi"
}
}
}
}
}
}
}
resource "kubernetes_service_v1" "nginx" {
metadata {
name = "nginx-app"
namespace = kubernetes_namespace.nginx_app.metadata.0.name
labels = {
app = "nginx"
env = var.environment
}
}
spec {
selector = {
app = "nginx"
env = var.environment
}
port {
port = 80
target_port = 80
}
type = "ClusterIP"
}
}
resource "kubernetes_ingress_v1" "nginx" {
metadata {
name = "nginx-app"
namespace = kubernetes_namespace.nginx_app.metadata.0.name
labels = {
app = "nginx"
env = var.environment
}
}
spec {
ingress_class_name = "nginx"
rule {
http {
path {
path = "/"
backend {
service {
name = kubernetes_service_v1.nginx.metadata.0.name
port {
number = 80
}
}
}
}
}
}
}
}
As you can see, it’s pretty basic. All it does is define an externally-reachable nginx deployment.
Let’s focus on the most interesting part.
How to use the Terraform module in Crossplane
Now, let’s wire the module so that it can be used with Crossplane.
As you can remember from our intro, we need these 3 building blocks:
- the
CompositeResourceDefinition
(XRD), - the
Composition
(XRC), - and of course the implied
Managed Resources
– which in our case is just going to be a single Terraform workspace.
On top of that, we’ll need to define our Composite Resource Claims
, since we want to be able to provision our app in a specific namespace.
The API
Let’s start with the CRD, i.e. our API.
For this simple app, we just want to be able to supply the environment
parameter. We define the schema – which the Crossplane controller will enforce for us.
We also define the claim names to be used within a Kubernetes namespace.
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xnginxapps.examples.kubecharm.com
spec:
group: examples.kubecharm.com
names:
kind: XNginxApp
plural: xnginxapps
claimNames:
kind: NginxApp
plural: nginxapps
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
env:
type: string
The Terraform Workspace
Before we take a look at the Composition, it’s important that we understand the Managed Resource first – which in our case will be Terraform Provider’s Workspace
.
Let’s have a look at it.
apiVersion: tf.upbound.io/v1beta1
kind: Workspace
metadata:
name: nginx-app
annotations:
crossplane.io/external-name: default
spec:
forProvider:
source: Remote
module: git::https://github.com/kubecharm/crossplane-terraform-demo.git//terraform/nginx-app?ref=master
vars:
- key: environment
value: dev
providerConfigRef:
name: terraform-default
You can probably tell pretty intuitively what it’s doing.
Now, there’s a few interesting things happening here already.
First off, we have the crossplane.io/external-name
annotation. The meaning of this annotation is provider-specific, but in the Terraform Provider it defines the Terraform Workspace to use.
Next is the forProvider
provider definition block. Since our module is hosted on GitHub, we specify that its source is Remote
, along with the information where we can find it. The syntax in module
follows the options available in Terraform.
We also define the variables to be supplied to the module.
Last but not least, we have the providerConfigRef
. If we omit this block, by default, Crossplane will use the default
provider – and ours is named terraform-default
.
Plus, in real world scenarios we usually have more than one provider.
Now, before we proceed, I must admit to something.
I lied to you.
We don’t really need the XRDs and XRCs to use your Terraform modules with Crossplane. In fact, you could use just the above Workspace
manifest to successfully provision our app.
However, it lacks encapsulation, quickly becomes repetitive – and above all, the implementation is coupled tightly to Terraform.
And in a real world scenario we want our platform to be a black box. An API for the end user.
Which is why we’ll create a Composition
which leverages the Workspace
manifest, but uses the API we defined earlier with our XRD.
The Composition
Alright, let’s turn our Workspace into a Composition.
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: nginx-app
labels:
crossplane.io/xrd: xnginxapps.examples.kubecharm.com
spec:
compositeTypeRef:
apiVersion: examples.kubecharm.com/v1alpha1
kind: XNginxApp
resources:
- name: nginx-app
base:
kind: Workspace
apiVersion: tf.upbound.io/v1beta1
metadata:
annotations:
crossplane.io/external-name: default
spec:
providerConfigRef:
name: terraform-default
forProvider:
source: Remote
module: git::https://github.com/kubecharm/crossplane-terraform-demo.git//terraform/nginx-app?ref=master
vars:
- key: environment
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.env
toFieldPath: spec.forProvider.vars[0].value
Let’s analyze it step by step.
We’re creating the composition with the schema we defined earlier – and we’re doing so by referencing it in compositeTypeRef
.
Also, as you can see, the Workspace
has now been embedded under spec.resources[0].base
.
The other interesting thing are the patches. We defined a mapping that will supply parameters of our claims as Terraform variables inside our Workspace
managed resource.
See, it’s actually quite simple!
The Claim
To top it off, let’s finally provision something using a Composite Resource Claim.
apiVersion: examples.kubecharm.com/v1alpha1
kind: NginxApp
metadata:
name: nginx-app-staging
spec:
env: stag1
compositionRef:
name: nginx-app
No surprises here!
We’re using the type & schema we defined earlier in the XRD. We’re also referencing the Composition we want to use to fulfill this claim.
Once we apply this manifest, Crossplane will provision the Deployment, Service and Ingress, all of them in the nginx-app-stag1
namespace.
If you’re using the local setup I provided, we can now test that our app works and responds to requests:
$ curl localhost
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
And that’s it!
The bottom line
As you can see, even if your infrastructure consists mainly of pre-existing Terraform code, starting with Crossplane doesn’t have to be daunting.
By integrating your Terraform modules with Crossplane you can start reaping benefits of self-serve infrastructure without costly rewrites and empower your teams today.
When your teams can provision, modify, and scale their infrastructure independently you encourage them to take ownership, reduce infrastructure-related bottlenecks, and ultimately accelerate your development process.
This was of course a simplified example – but one that should give you an idea on how to integrate Crossplane with your existing Terraform code base.
Lastly, we also offer Crossplane implementation consulting to help you quickly roll out a platform that helps you move faster.
Looking for Crossplane implementation consultancy?
We're offering Crossplane consulting services. Let's build a platform tailored to your business needs.
Contact us for a quick quote.