Setup Cert-Manager and Cloudflare
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