How to Deploy Your App to Kubernetes with Free and Automatic SSL

By the end of this article you’ll have the tools at your disposal to deploy however many apps and services your heart desires, completely for free with automatic SSL to your shiny k8s cluster.
You still won’t be able to talk to your loved ones at the dinner table about what you do, but at least you’ll be able to rest at night knowing you’ll never need to buy and renew SSL certificates ever again.
Got a new app you want to deploy? Just ensure the DNS record hosting is on one of these supported providers: https://cert-manager.io/docs/configuration/acme/dns01/#supported-dns01-providers (we will be using Cloudflare in this tutorial, with the cluster hosted by Digital Ocean), and you have an X in your cluster, then all you need to do is mark your ingress resource for your new deployment with some annotations and you’re done!
Let’s begin by defining terms. If you’re experienced with Kubernetes skip ahead, but I’m a first principles kinda guy, and it’s never harmful to ensure that we’re all talking about the same thing.
Think of a K8s (Kubernetes) resource as a class - it’s a type of entity. For example:
- Pod
- Deployment
- Secret
An object is an instance of a resource define by the Kubernetes API (above). For example, later on we will be creating a “cert-manager” deployment in the “cert-manager” namespace (a namespace is how objects are isolated / grouped in a cluster).
A deployment is a Kubernetes object that manages a set of replica Pods based on a template. It ensures a defined number of Pods are running and can handle rolling updates.
A pod is a single running container
A service is a resource that uses selectors to route traffic to the right pods even if the pods restart / their IPs change. This is known as stable networking.
An Ingress is a Kubernetes API object that defines rules for exposing HTTP/S services to external users. It allows defining URL-based routing, host-based routing, and SSL termination using certificates. Ingress resources rely on an Ingress Controller to function.
A Secret is a Kubernetes object used to store sensitive data, such as API keys.
A Certificate is a Kubernetes resource (managed by Cert Manager) that requests and renews TLS certificates from a Cluster Issuer or Issuer. The generated TLS certificate is stored in a Secret, which can be referenced by Ingress or other services. In other words, the K8s Ingress resource does not “understand” what a certificate is, only secrets (which will contain TLS certificates), which in turn will be managed by Cert Manager.
An Ingress Controller is a Kubernetes component (i.e. a deployment with usually 1 running pod) responsible for directing HTTP/S traffic to the appropriate services (as identified by their ingress resource - see below) within the cluster.
It acts as a reverse proxy, managing routing, SSL termination, and load balancing for incoming requests.
It’s important to note that this is not the same as your cluster’s external load balancer. In order for your cluster to be accessible from the web, you still need to have a load balancer outside the cluster. For instance, with Digital Ocean (DO), if you create a service of type “LoadBalancer” DO will provision a cloud managed load balancer that will receive a public IP and forward requests to your cluster’s worker nodes.
Your ingress controller can also be behind a “NodePort” service, but we will not be covering that today.
The Automated Certificate Management Environment protocol (ACME) defines a standard for managing (issuing and renewing) TLS certificates.
- The ACME client (we will be using and deploying Cert Manager) requests a certificate from an ACME server (Let’s Encrypt). The Cluster Issuer (below) determines what CA (ACME server) will be used.
- The ACME server challenges the client to prove control over the domain (e.g., via DNS-01 or HTTP-01 challenge). This challenge type and other required info (e.g. API key) is provided by the Cluster Issuer.
- If successful, the ACME server issues a TLS certificate.
- The ACME client installs the certificate and handles automatic renewal before expiration.
A Cluster Issuer is a Kubernetes Cert Manager resource that defines a certificate authority (CA) at the cluster level. It allows issuing TLS certificates to workloads across multiple namespaces. A Cluster Issuer can have multiple “solvers” each with selectors. A selector will define for what domains will this solver be used to issue certificate requests. A solver can be one of two challenge types:
- HTTP-01
- DNS-01
This assumes you are already connected to your cluster (i.e. have kubectl installed on your system and are in the context of your shiny k8s cluster defined in your Kubeconfig yaml).
Please note that you may want to install more recent versions of all the below deployments that we will be installing, and thus you will need to ensure compatibility between them.
I will be using Cloudflare in this tutorial to host the DNS records (i.e. the DNS provider). Your Domain Registrar (where you bought your domain) may not have an API which we can use for DNS-01 resolution. Have a look here: https://cert-manager.io/docs/configuration/acme/dns01/#supported-dns01-providers. It may take a few hours for your domain records to propagate from your existing provider to Cloudflare, so get this process under way before starting the next steps. This process involves setting (changing) your nameservers in the dashboard of your registrar, and then adding a site (within Cloudflare) and choosing to import DNS records from another provider.
Ensure you have an external load balancer and ingress controller. If you are using Digital Ocean you can run:
javascriptkubectl apply -f
This will install ingress-nginx-controller in the ingress-nginx namespace.
You can verify that ingress controller is behind a LoadBalancer service with a public IP (under External IP) by running this command.
javascriptkubectl get svc -n ingress-nginx
Now you should be able to see a 404 Not Found from Nginx when accessing your public IP.
Now you will need to install Cert Manager. This is incredibly complicated.
javascriptkubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.12.0/cert-manager.yaml
Just kidding. Now you’re done.
Our Cluster Issuer will need a secret for the api key. Install it using the below in your terminal.
javascriptcat <<EOF | kubectl apply -f -apiVersion: v1kind: Secretmetadata:name: cloudflare-api-token-secretnamespace: cert-managertype: OpaquestringData:api-token: TOKENGOESHEREEOF
Use the below command to install the Cluster Issuer referencing the above certificate. Note that you can leave out the “selector” yaml segment if you want this dns01 solver to be used for all domains in your cluster (in which case, if you ensure all your records are hosted by Cloudflare this will be the easiest solution).
javascriptcat <<EOF | kubectl apply -f -apiVersion: cert-manager.io/v1kind: ClusterIssuermetadata:name: letsencrypt-prodnamespace: cert-managerspec:acme:email: SOMEONE@EXAMPLE.COMserver: https://acme-v02.api.letsencrypt.org/directoryprivateKeySecretRef:name: letsencrypt-issuer-account-keysolvers:- dns01:cloudflare:email: USER@EXAMPLE.COMapiTokenSecretRef:name: cloudflare-api-token-secretkey: api-tokenselector:dnsZones:- 'example.com'EOF
This is very specific to your application. Here is an example of a deployment we have (which will variable replacement performed on the yaml by Octopus Deploy).
javascriptapiVersion: apps/v1kind: Deploymentmetadata:name: example-appannotations:application: example-appversion: ''namespace: '#{Library.Environment.Alias}'spec:selector:matchLabels:octopusexport: OctopusExportreplicas: 1strategy: {}template:metadata:labels:octopusexport: OctopusExportannotations:application: example-appversion: ''spec:volumes:- name: example-app-configconfigMap:name: configmapnameitems:- key: app-envpath: .env- name: ca-cert-volumeemptyDir: {}containers:- name: example-appimage: registry.digitalocean.com/your-container-registry/example-appports:- name: http3000containerPort: 3000protocol: TCPvolumeMounts:- name: example-app-configmountPath: /app/configsubPath: ''- name: ca-cert-volumemountPath: /certssubPath: ''initContainers:- name: download-ca-certimage: index.docker.io/curlimages/curlcommand:- /bin/sh- '-c'- >echo "Downloading DigitalOcean CA certificate..."curl -sSL -o /certs/ca.crthttps://www.digicert.com/CACerts/DigiCertGlobalRootCA.crt.pemvolumeMounts:- name: ca-cert-volumemountPath: /certssubPath: ''
javascriptapiVersion: v1kind: Servicemetadata:name: example-appnamespace: '#{Library.Environment.Alias}'spec:type: ClusterIPports:- name: http-example-appport: 80targetPort: 3000protocol: TCPselector:octopusexport: OctopusExport
The important annotation here is:
“cert-manager.io/cluster-issuer: letsencrypt-prod”
This notifies Cert Manager that “Hey, for hosts specified in this ingress, we need to manage the certificates and dump them in e.g. example-app-tls”.
Check here for the latest supported annotations: https://cert-manager.io/docs/usage/ingress/#supported-annotations
You should not need to ever create a certificate resource if you have configured this correctly.
javascriptapiVersion: networking.k8s.io/v1kind: Ingressmetadata:name: example-app-ingressannotations:meta.helm.sh/release-name: example-appkubernetes.io/ingress.class: nginxcert-manager.io/cluster-issuer: letsencrypt-prodingress.kubernetes.io/ssl-redirect: 'true'ingress.kubernetes.io/proxy-body-size: 7mnginx.ingress.kubernetes.io/proxy-body-size: 7mnginx.ingress.kubernetes.io/ssl-redirect: 'true'nginx.ingress.kubernetes.io/proxy-buffering: 'off'nginx.ingress.kubernetes.io/proxy-body-size: 7mnginx.org/client-max-body-size: '9'nginx.ingress.kubernetes.io/ssl-redirect: 'true'namespace: '#{Library.Environment.Alias}'spec:ingressClassName: nginxrules:- host: 'example.com'http:paths:- path: /pathType: Prefixbackend:service:name: example-appport:number: 3000tls:- hosts:- 'example.com'secretName: example-app-tls
Replace #{Library.Environment.Alias} with the namespace of your deployment.
javascriptkubectl get certificate -n #{Library.Environment.Alias}
You should see something like:
javascriptNAME READY SECRET AGEexample-app-tls True example-app-tls 5m
That’s it! You’re done!
For subsequent deployments (assuming you have the DNS records with Cloudflare), all you need to do is mark your ingress with “cert-manager.io/cluster-issuer: letsencrypt-prod” and Cert Manager will dump the TLS cert in the secret you specify.