Provision Cert-Manager and Let's Encrypt on Kubernetes using OpenTofu

TL;DR:

  • Ask your AI coding agent to scaffold a generated Cert-Manager platform feature module
  • Provision Cert-Manager from its upstream Helm chart on Kubernetes
  • Configure Cert-Manager to use Let's Encrypt to issue certificates

Introduction

No matter if web site, web app or API, any service exposed to the internet must have transport layer security (TLS) enabled. And this requires a certificate signed by a certificate authority that browsers and other HTTP clients trust.

Following this tutorial, you will provision Cert-Manager and configure it to issue Let's Encrypt certificates via Kubernetes custom resources.

Let's Encrypt is a nonprofit certificate authority providing TLS certificates for free and fully automated. Cert-Manager is a Kubernetes operator, that can provision certificates from certificate authorities like Let's Encrypt automatically.

First step is to install Cert-Manager on the Kubernetes cluster. We will use a generated platform feature module for that. Your AI coding agent scaffolds a small OpenTofu module in your repository that deploys the upstream Cert-Manager Helm chart, rendered to YAML using helm template. The rendered manifests and the module live in your repository, so you own them like the rest of your platform, and updates come directly from upstream releases.

The module uses the Kubestack maintained Kustomization provider to fully integrate the Cert-Manager Kubernetes resources into the OpenTofu lifecycle. It also allows to customize the configuration without modifications to the upstream YAML. This has the benefit that the custom configuration is significantly less likely to break on future updates.

Before we can provision Cert-Manager, we need a Kubestack repository. If you do not have a Kubestack repository yet, follow the Kubestack tutorial first. This tutorial assumes you have a Kubestack framework repository.

Cert-Manager Installation

To install Cert-Manager, ask your agent that has learned the Kubestack skill to add it as a platform feature.

# add cert-manager feature to every cluster
Add the cert-manager platform feature to all clusters
# to only add to a single cluster
Add the cert-manager platform feature to the eks_gc0_eu-west-1 cluster

Your agent scaffolds the feature module modules/cert_manager/ and one binding file per cluster. Cert-Manager publishes its chart as an OCI artifact, so your agent asks you to run the following command to render the chart into modules/cert_manager/manifests/upstream.yaml:

helm template cert-manager oci://quay.io/jetstack/cert-manager \
--version <chart-version> \
--values modules/cert_manager/values.yaml \
--namespace cert-manager \
--create-namespace \
--include-crds \
> modules/cert_manager/manifests/upstream.yaml

--include-crds and --create-namespace ensure the namespace, CRDs and all resources end up in the single upstream.yaml. The kustomization provider handles creation order automatically. The exact command and the chart version are recorded in modules/cert_manager/README.md, so future updates are a single re-run away.

Module Configuration

Now you have one <cluster_name>_feature_cert_manager.tf binding file per cluster. The files are almost identical between AKS, EKS or GKE. Main difference is the aliased kustomization provider, that controls what cluster Cert-Manager will be installed on.

The configuration map in modules/cert_manager/main.tf is where we will now configure Let's Encrypt. The upstream manifests are deployed through the module's resources attribute, and additional manifests can be added to the list. We will use this to include our Let's Encrypt ClusterIssuer below.

Let's Encrypt ClusterIssuer

To configure Cert-Manager to issue certificates using Let's Encrypt we apply a ClusterIssuer custom resource. For this we first create a YAML file in modules/cert_manager/manifests/cluster-issuer.yaml with the configuration for Let's Encrypt.

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt
spec:
acme:
# You must replace this email address with your own.
# Let's Encrypt will use this to contact you about expiring
# certificates, and issues related to your account.
email: user@example.com
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
# Secret resource that will be used to store the account's private key.
name: letsencrypt-account-key
# Add a single challenge solver, HTTP01 using nginx
solvers:
- http01:
ingress:
class: nginx

Apply ClusterIssuer alongside upstream YAML

Then we instruct the feature module to apply our ClusterIssuer alongside the upstream YAML on the respective Kubernetes cluster. For this we add the manifest to resources in the external environment (usually apps or apps-prod) in the module's configuration in modules/cert_manager/main.tf.

configuration = {
apps = {
resources = [
"${path.module}/manifests/upstream.yaml",
+ "${path.module}/manifests/cluster-issuer.yaml",
]
}
ops = {}
}

Patch ClusterIssuer to use Let's Encrypt staging

Let's Encrypt has strict API rate limits. Since the Kubestack ops environment does not run any application workloads, we don't need certificates that are trusted by browsers here. This is also a great opportunity to show how to patch upstream YAML using the Kubestack platform feature modules and how to overwrite the inherited configuration from apps or apps-prod in ops.

configuration = {
apps = {
resources = [
"${path.module}/manifests/upstream.yaml",
"${path.module}/manifests/cluster-issuer.yaml",
]
}
ops = {
+ patches = [
+ {
+ patch = <<-EOF
+ - op: replace
+ path: /spec/acme/server
+ value: https://acme-staging-v02.api.letsencrypt.org/directory
+ EOF
+
+ target = {
+ group = "cert-manager.io"
+ version = "v1"
+ kind = "ClusterIssuer"
+ name = "letsencrypt"
+ }
+ }
+ ]
}
}

Apply Changes

As with every change, we now follow the GitOps process. First, commit and push to start the peer review, then merge when the plan looks good. After the changes have been validated in the internal environment, promote the changes to the external environment.

The full workflow is documented on the GitOps process page.

But here's a short summary for convenience:

# create a new feature branch
git checkout -b add-cert-manager
# add the changes and commit them
git add .
git commit -m "Install cert-manager and let's encrypt issuer"
# push the changes to trigger the pipeline
git push origin add-cert-manager

Then follow the link in the output, to create a new pull request. Review the pipeline run. And merge the pull request, when everything is green.

Last but not least, promote the changes once you validated them in ops by setting a tag.

# make sure you're on the merge commit
git checkout main
git pull
# then tag the commit
git tag apps-deploy-$(git rev-parse --short HEAD)
# finally push the tag, to trigger the pipeline to promote
git push origin apps-deploy-$(git rev-parse --short HEAD)