Setup Cert-Manager and Cloudflare

4 minute read

In this guide, I’m going to describe how to setup Cert-Manager on EKS with Terraform and DNS validation on Cloudflare. I will also provide an example of how to request a certificate on your ingress manifest. All the referenced Terraform code can be obtained here.

These are the providers that we’ll be using in the environment. You may need to adjust how the helm and kubectl providers are getting the cluster name and token for your environment.

Providers/Versions

providers.tf

 1locals {
 2  env    = "sandbox"
 3  region = "us-east-1"
 4}
 5
 6provider "aws" {
 7  region = local.region
 8  default_tags {
 9    tags = {
10      env       = local.env
11      terraform = true
12    }
13  }
14}
15
16provider "helm" {
17  kubernetes {
18    host                   = module.eks-cluster.endpoint
19    cluster_ca_certificate = base64decode(module.eks-cluster.certificate)
20    exec {
21      api_version = "client.authentication.k8s.io/v1beta1"
22      # This requires the awscli to be installed locally where Terraform is executed
23      args        = ["eks", "get-token", "--cluster-name", module.eks-cluster.name]
24      command     = "aws"
25    }
26  }
27}

versions.tf

 1terraform {
 2  required_providers {
 3    aws = {
 4      source  = "hashicorp/aws"
 5      version = "~> 5.0"
 6    }
 7    helm = {
 8      source  = "hashicorp/helm"
 9      version = "~> 2.11.0"
10    }
11  }
12  required_version = "~> 1.5.7"
13}

Module

Initialize the module where needed.

1module "cert_manager" {
2  source               = "../../aws/eks-addons/cert_manager"
3  env                  = var.env
4  cert_manager_version = "v1.13.3"
5}

Module files

Here we’re installing cert-manager through Helm and setting a nodeaffinity to my core managed group, so they don’t slotted into any nodes created by Karpenter. It’s recommended to install the CRD’s used by cert-manager separately for production workloads to avoid certificate resources being removed if the Helm release is removed.

In this example, my DNS provider is CloudFlare and creating a secret with the token stored in SecretsManager. When a certificate is requested, you can show ownership through DNS or HTTP and in this example, its using the CloudFlare provider to prove ownership. The plus side to the DNS method is the ability to get wildcard certifcates.

Next we’re creating two cluster issuers; one for testing (staging) and one for non-testing (prod). The staging endpoint will give non-official certificates without an API limit for testing.

main.tf

  1resource "helm_release" "cert_manager" {
  2  namespace        = "cert-manager"
  3  create_namespace = true
  4  name             = "cert-manager"
  5  repository       = "https://charts.jetstack.io"
  6  chart            = "cert-manager"
  7  version          = var.cert_manager_version
  8  values = [
  9    <<-EOT
 10    installCRDs: false
 11    affinity:
 12      nodeAffinity:
 13        requiredDuringSchedulingIgnoredDuringExecution:
 14          nodeSelectorTerms:
 15          - matchExpressions:
 16            - key: role
 17              operator: In
 18              values:
 19              - core
 20    EOT
 21  ]
 22  depends_on = [
 23    kubectl_manifest.crds
 24  ]
 25}
 26
 27data "http" "crd_manifest" {
 28  url = "https://github.com/cert-manager/cert-manager/releases/download/${var.cert_manager_version}/cert-manager.crds.yaml"
 29}
 30
 31locals {
 32  crd_manifests = split("---", data.http.crd_manifest.response_body)
 33}
 34
 35resource "kubectl_manifest" "crds" {
 36  count     = length(local.crd_manifests)
 37  yaml_body = element(local.crd_manifests, count.index)
 38}
 39
 40resource "kubernetes_secret" "cloudflare_token" {
 41  metadata {
 42    name      = "cloudflare-api-token-secret"
 43    namespace = "cert-manager"
 44  }
 45  data = {
 46    api-token = jsondecode(data.aws_secretsmanager_secret_version.cloudflare_api_token.secret_string)["cert-manager"]
 47  }
 48  depends_on = [
 49    helm_release.cert_manager
 50  ]
 51}
 52
 53resource "kubectl_manifest" "cluster_issuer_test" {
 54  yaml_body = <<-YAML
 55  apiVersion: cert-manager.io/v1
 56  kind: ClusterIssuer
 57  metadata:
 58    name: letsencrypt-test
 59    namespace: cert-manager
 60  spec:
 61    acme:
 62      email: your@email
 63      server: https://acme-staging-v02.api.letsencrypt.org/directory
 64      privateKeySecretRef:
 65        name: letsencrypt-staging
 66      solvers:
 67      - dns01:
 68          cloudflare:
 69            apiTokenSecretRef:
 70              name: cloudflare-api-token-secret
 71              key: api-token
 72  YAML
 73
 74  depends_on = [
 75    kubernetes_secret.cloudflare_token,
 76    helm_release.cert_manager
 77  ]
 78}
 79
 80resource "kubectl_manifest" "cluster_issuer" {
 81  yaml_body = <<-YAML
 82  apiVersion: cert-manager.io/v1
 83  kind: ClusterIssuer
 84  metadata:
 85    name: letsencrypt
 86    namespace: cert-manager
 87  spec:
 88    acme:
 89      email: your@email
 90      server: https://acme-v02.api.letsencrypt.org/directory
 91      privateKeySecretRef:
 92        name: letsencrypt-prod
 93      solvers:
 94      - dns01:
 95          cloudflare:
 96            apiTokenSecretRef:
 97              name: cloudflare-api-token-secret
 98              key: api-token
 99  YAML
100
101  depends_on = [
102    kubernetes_secret.cloudflare_token,
103    helm_release.cert_manager
104  ]
105}

data.tf

1data "aws_secretsmanager_secret" "cloudflare_api_token" {
2  name = "cloudflare-api-token"
3}
4
5data "aws_secretsmanager_secret_version" "cloudflare_api_token" {
6  secret_id = data.aws_secretsmanager_secret.cloudflare_api_token.id
7}

variables.tf

1variable "env" {
2  type = string
3}
4variable "cert_manager_version" {
5  type = string
6}

Demo

Here we’re creating an ingress using the test cluster issuer. By setting the cert-manager annotation, it will discover this and automatically create a certificate resource and store the cert keypair in the secret specified in the TLS section.

Once the certificate has a “true” issued status, which can take a minute sometimes, it will create the secret to be used by whichever method you use for TLS.

When testing is done, changing to the prod cluster issuer will give an official letsencrypt cert.

ingress.yaml

 1apiVersion: networking.k8s.io/v1
 2kind: Ingress
 3metadata:
 4  annotations:
 5    cert-manager.io/cluster-issuer: letsencrypt-test
 6  name: demo-app
 7  namespace: sandbox
 8spec:
 9  rules:
10  - host: demo.sandbox.example.com
11    http:
12      paths:
13      - pathType: Prefix
14        path: /
15        backend:
16          service:
17            name: demo-app
18            port:
19              number: 8000
20  tls:
21  - hosts:
22    - demo.sandbox.example.com
23    secretName: demo-sandbox