The challenge

Deploy a solution for configuring Kubernetes “from the inside”.

Install Crossplane, which is like Terraform but you manage the infra from inside Kubernetes, not from outside. Crossplane is an open source Kubernetes add-on that enables platform teams to assemble infrastructure from multiple vendors, and expose higher level self-service APIs for application teams to consume, without having to write any code.

Initial thoughts

I have used Terraform for more than a year now and to provision infrastructures using plain Kubernetes manifests is a very refreshing idea to me. I feel that Crossplane definitely benefits organizations that are already heavily invested in Kubernetes. Having a unified language to manage both infrastructures and applications makes a lot of sense especially if developers in your team are comfortable working with Kubernetes. This reduces the overhead of learning something completely new like Terraform. That said, Crossplane is still a relatively young project at the time of writing this. There aren’t as many providers and the managed resources under each of them are way lesser as compared to Terraform providers.

Deploy Crossplane in DigitalOcean Kubernetes cluster

Ironically for this project, I have used Terraform to provision the DigitalOcean and Crossplane resources. All Terraform code shown can be found in this repo. Note that nothing shown here is production-ready by any means e.g. I could have used a remote backend to store my Terraform state file but this was merely for a quick proof-of-concept.

Deploy DigitalOcean resources

First, we will need to configure the DigitalOcean Terraform provider.

provider "digitalocean" {
  token = var.do_token
}

I have used a variable to set the DigitalOcean API token (which can be created from the DigitalOcean portal). Create a terraform.tfvars file with do_token=<your do token>, in the same directory with the other .tf files will allow you to apply DigitalOcean resources successfully.

Next, let’s start by defining a VPC. Local variables can be found in the repo aforementioned.

resource "digitalocean_vpc" "vpc" {
  name        = format("%s-vpc", local.project_name)
  region      = local.region
  description = format("VPC for %s", local.project_name)
  ip_range    = local.ip_range
}

Last, we can now define a Kubernetes cluster in the VPC.

data "digitalocean_kubernetes_versions" "versions" {
  version_prefix = "1.21."
}

resource "digitalocean_kubernetes_cluster" "cluster" {
  name          = format("%s-cluster", local.project_name)
  region        = local.region
  version       = data.digitalocean_kubernetes_versions.versions.latest_version
  vpc_uuid      = digitalocean_vpc.vpc.id
  auto_upgrade  = false
  surge_upgrade = false
  tags          = local.tags

  node_pool {
    name       = format("%s-default-worker-pool", local.project_name)
    size       = local.size
    node_count = 2
    auto_scale = false
    tags       = local.tags
  }
}

Here I have used the latest version of Kubernetes supported in DigitalOcean. Note that Crossplane version 1.5 (the one that I will be installing) requires a Kubernetes cluster to be v1.16.0 and above. Check here for Crossplane and Kubernetes compatible versions.

In the repo, I have also added an output for the cluster ID for convenience.

output "cluster_id" {
  description = "DO Kubernetes cluster ID"
  value       = digitalocean_kubernetes_cluster.cluster.id
}

Deploy Crossplane

First, we will need to configure the Helm Terraform provider.

provider "helm" {
  kubernetes {
    host                   = digitalocean_kubernetes_cluster.cluster.kube_config[0].host
    token                  = digitalocean_kubernetes_cluster.cluster.kube_config[0].token
    cluster_ca_certificate = base64decode(digitalocean_kubernetes_cluster.cluster.kube_config[0].cluster_ca_certificate)
  }
}

Install the Crossplane Helm chart.

resource "helm_release" "crossplane" {
  name             = "crossplane"
  chart            = "crossplane"
  repository       = "https://charts.crossplane.io/stable"
  version          = "1.5.1"
  namespace        = "crossplane-system"
  create_namespace = true
  values           = []
}

Verify Crossplane is up and running

Now that the resources are deployed, let’s check to see if they are working as intended.

Add the DigitalOcean cluster configuration to kubeconfig

DigitalOcean CLI tool doctl can be used to update your kubeconfig file easily.

doctl kubernetes cluster kubeconfig save <paste the cluster_id output value here>

Now we can use kubectl to interact with the API server of the DigitalOcean Kubernetes cluster.

Check Crossplane pods

The Crossplane pods are deployed in the crossplane-system namespace.

kubectl get pods -n crossplane-system
NAME                                      READY   STATUS    RESTARTS   AGE
crossplane-7cfcfb84c9-zj99x               1/1     Running   0          92s
crossplane-rbac-manager-cdcc7f487-hld26   1/1     Running   0          92s

Install and configure Azure Provider

For this challenge, I have decided to go with the Azure Provider since I have some free credits lying around.

Install Azure Provider

There are multiple ways to install the provider controllers and CRDs. Below uses the raw manifests way to install the Azure Provider.

apiVersion: pkg.crossplane.io/v1alpha1
kind: ControllerConfig
metadata:
  name: debug-config
spec:
  args:
    - --debug
---
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
  name: provider-azure
spec:
  package: "crossplane/provider-azure:v0.18.0"
  controllerConfigRef:
    name: debug-config

Note that the ControllerConfig is not necessary. I added it to check out the Azure controller’s debug logs.

The file above is located in the azure/ folder of the aforementioned repo. Simply apply the manifests to the cluster and the core Crossplane controller will install the provider along with their CRDs.

Configure Azure Provider

Each provider has its own ways to set up the credentials for Crossplane to utilize when deploying resources on your behalf. The options also vary depending on which managed Kubernetes service Crossplane is deployed into e.g. for AWS EKS, you can opt for using IAM roles for service account instead of IAM user credentials.

For the Azure provider, a service principal has to first be created with the right role and permissions granted. Follow the instructions here to prepare the base64 encoded secret value. Apply the following manifests to create the Azure provider configuration.

apiVersion: v1
kind: Secret
metadata:
  name: azure-account-creds
  namespace: crossplane-system
type: Opaque
data:
  credentials: <paste your base64 encoded Azure credentials here>
---
apiVersion: azure.crossplane.io/v1beta1
kind: ProviderConfig
metadata:
  name: default
spec:
  credentials:
    source: Secret
    secretRef:
      namespace: crossplane-system
      name: azure-account-creds
      key: credentials

Note that this will be the default Azure provider which will be used by the Azure Crossplane controller for creating any Azure managed resource without providerConfigRef.

Create Azure resources

Two Azure resources will be created - a resource group and a PostgreSQL server. Apply the following manifests and the Azure Crossplane controller will create the resources.

Resource group

apiVersion: azure.crossplane.io/v1alpha3
kind: ResourceGroup
metadata:
  name: sqlserverpostgresql-rg
spec:
  location: East US

PostgreSQLServer

apiVersion: database.azure.crossplane.io/v1beta1
kind: PostgreSQLServer
metadata:
  name: sqlserverpostgresql41241
spec:
  forProvider:
    administratorLogin: myadmin
    resourceGroupNameRef:
      name: sqlserverpostgresql-rg
    location: East US
    sslEnforcement: Disabled
    version: "9.6"
    sku:
      tier: GeneralPurpose
      capacity: 2
      family: Gen5
    storageProfile:
      storageMB: 20480
  writeConnectionSecretToRef:
    namespace: crossplane-system
    name: sqlserverpostgresql-conn

Note that the metadata.name has to be unique.

Ending thoughts

This post covers the very basics of using and deploying Crossplane. There are more that I am planning to explore in the near future, which includes:

  • Using GitOps approach to manage the managed and composite resources
  • Create composite resources for production-ready EKS/AKS clusters with bare minimum add-ons
  • Experiment with AWS Provider’s managed resources

Thanks for reading this post till the end!