Automating Linkerd installation on Kubernetes in Terraform

(Edited 17-Feb-2020 due to an updated Helm chart from Linkerd)

If you're looking at deploying your containerised services on Kubernetes, you'd do well to consider some kind of service mesh to go with it. And if you're looking for something that's easy to install, you can't go far wrong with Linkerd. Linkerd works by injecting tiny sidecar proxies next to your services and passing traffic through them, giving you visibility into what's coming in and out of your services, failure/success rate of calls, mutual TLS and more. Pretty neat.

Installation of Linkerd is also a snap. The CLI binary includes a linkerd install command which effectively spits out a Kubernetes deployment which can be piped directly into kubectl apply. If you're trying to automate your Kubernetnes cluster creation, however, this is probably not the approach you want. Moreover, Linkerd relies on an internal Certification Authority to generate and sign certificates that enable the mTLS, and by default these certificates are created with an expiry date of one year in the future - the intention being that these certificates will be renewed at some point. Rotating the Root and Issuer certificates is a task left up to you, and it is recommended that you generate your own certificates in the first place rather than using the ones generated by linkerd install. Generating your own certificates can be done with relative ease using tools such as step or openssl, but nonetheless you'd have to hook this task into your Kubernetes bootstrapping process.

Wouldn't it be nice to automate all this somehow? Turns out it's actually really easy!

For the Linkerd installation, you need to supply three things:

  • The "Trust Anchor" certificate (the root CA)
  • The Issuer Certificate
  • The Issuer private key

The Trust Anchor CA signs the certificate for the Issuer, and the Issuer is used to sign certificates for the various services that you deploy. The service certificates are valid for 24 hours and are renewed automatically by Linkerd. Clear as mud?

Terraform has a TLS provider that can generate self-signed and locally-signed certificates, which is precisely what we need here. So let's go ahead and do that.

resource "tls_private_key" "trustanchor_key" {
  algorithm   = "ECDSA"
  ecdsa_curve = "P256"
}

resource "tls_self_signed_cert" "trustanchor_cert" {
  key_algorithm         = tls_private_key.trustanchor_key.algorithm
  private_key_pem       = tls_private_key.trustanchor_key.private_key_pem
  validity_period_hours = 87600
  is_ca_certificate     = true

  subject {
    common_name = "identity.linkerd.cluster.local"
  }

  allowed_uses = [
    "crl_signing",
    "cert_signing",
    "server_auth",
    "client_auth"
  ]
}

The above code will generate the Trust Anchor private key and certificate with all the correct attributes. It's also set to be valid for ten years. Should be enough, right?

Next, the Issuer certificate:

resource "tls_private_key" "issuer_key" {
  algorithm   = "ECDSA"
  ecdsa_curve = "P256"
}

resource "tls_cert_request" "issuer_req" {
  key_algorithm   = tls_private_key.issuer_key.algorithm
  private_key_pem = tls_private_key.issuer_key.private_key_pem

  subject {
    common_name = "identity.linkerd.cluster.local"
  }
}

resource "tls_locally_signed_cert" "issuer_cert" {
  cert_request_pem      = tls_cert_request.issuer_req.cert_request_pem
  ca_key_algorithm      = tls_private_key.trustanchor_key.algorithm
  ca_private_key_pem    = tls_private_key.trustanchor_key.private_key_pem
  ca_cert_pem           = tls_self_signed_cert.trustanchor_cert.cert_pem
  validity_period_hours = 8760
  is_ca_certificate     = true

  allowed_uses = [
    "crl_signing",
    "cert_signing",
    "server_auth",
    "client_auth"
  ]
}

This one is slightly different. In this case, we generate a private key and a CSR, and then we use the CA that we generated in the first instance to sign that CSR. This gives us everything we need to satisfy Linkerd's installation requirements.

Now for the deployment. Linkerd has a Helm chart. Terraform has a Helm provider. I'm sure you know what's coming next.

First, let's add the Helm repository data source:

data "helm_repository" "linkerd" {
  name = "linkerd"
  url  = "https://helm.linkerd.io/stable"
}

Next, let's create a helm_release resource to invoke the deployment using the chart. We can use the outputs generated from the certificate resources as inputs for our Helm release:

resource "helm_release" "linkerd" {
  name       = "linkerd"
  repository = data.helm_repository.linkerd.metadata[0].name
  chart      = "linkerd/linkerd2"

  set {
    name  = "global.identityTrustAnchorsPEM"
    value = tls_self_signed_cert.trustanchor_cert.cert_pem
  }

  set {
    name  = "identity.issuer.crtExpiry"
    value = tls_locally_signed_cert.issuer_cert.validity_end_time
  }

  set {
    name  = "identity.issuer.tls.crtPEM"
    value = tls_locally_signed_cert.issuer_cert.cert_pem
  }

  set {
    name  = "identity.issuer.tls.keyPEM"
    value = tls_private_key.issuer_key.private_key_pem
  }
}

Provided you have your provisioners set up correctly, run a terraform plan and a terraform apply on that lot, and you should be ready to go.

Automating the injection of Linkerd's proxy via a namespace annotation is easy too, and that will be covered in a future post.