Nginx ingress Kubernetes Cert manager

Nginx-Ingress with cert-manager and Let’s Encrypt — declarative Kubernetes templating

Nginx-Ingress with cert-manager and Let’s Encrypt — declarative Kubernetes templating

cover photo: Noorderdijkweg by Jarno Koenen, summer 2019.

I was happy with Helm when a far-more-experienced-Kubernetes-guy told me I should not use Helm because Tiller is unsafe and some other reasons. Now I follow the method of Tobias Bradtke, with the advantage of declarative application management; while I keep one cluster-definition in one Git repository.

When you want to route HTTP and HTTPS traffic from outside your Kubernetes cluster to services inside your cluster, this can be done with Nginx-Ingress. Let's Encrypt is a non-profit certificate authority run by Internet Security Research Group to provide free TLS certificates and cert-manager is a Kubernetes addon to automate the management and issuance of TLS certificates from various issuing sources.

First we will fetch all the resources into a Git repository; then we will render the manifests and apply them to our cluster. This Github repository contains an exampe of the files you'll end up with.

Before you continue, make sure you have:

  • access to a Kubernetes cluster (v1.6+) with kubectl;
  • a domain or subdomain with DNS records ready to point to an IP;
  • initialized Helm locally (not on the cluster) with helm init --client-only. Do not forget to update Helm (helm update repo).

Fetching cert-manager and nginx-ingress

Make the following nginx-ingress/apply.sh file:

#!/bin/bash
SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )"
mkdir -p $SCRIPTPATH/values

# FETCH
echo 'FETCH CHARTS'

## CERT-MANAGER
curl https://raw.githubusercontent.com/jetstack/cert-manager/release-0.10/deploy/manifests/00-crds.yaml -s --create-dirs -o $SCRIPTPATH/00-crds.yaml # cert-manager CRDs
helm fetch \
  --repo https://charts.jetstack.io \
--version v0.10.0 \
  --untar \
  --untardir $SCRIPTPATH/charts \
  cert-manager

FILE=$SCRIPTPATH/values/cert-manager.yaml
if ! test -f "$FILE"; then
    cp -p $SCRIPTPATH/charts/cert-manager/values.yaml $FILE
fi

## NGINX-INGRESS
helm fetch \
  --version 1.19.1 \
  --untar \
  --untardir $SCRIPTPATH/charts \
  stable/nginx-ingress

FILE=$SCRIPTPATH/values/nginx-ingress.yaml
if ! test -f "$FILE"; then
    cp -p $SCRIPTPATH/charts/nginx-ingress/values.yaml $FILE
fi

The file fetches the cert-manager and the Nginx-ingress Helm charts, makes copies of the value files and gets the required cert-manager CRDs yaml file. Note that Let's Encrypt announced to block all traffic from cert-manager versions less than 0.8.0 as of November 1, 2019.

Now fetch the resources:

./nginx-ingress/fetch.sh

The value files are copied to the nginx-ingress/values folder, but they don't need any adjustments for now.

Create a namespace definition file

The following nginx-ingress/nginx-ingress-ns.yaml simply defines the namespace:

apiVersion: v1
kind: Namespace
metadata:
  name: nginx-ingress

Create a ClusterIssuer

Make a nginx-ingress/cluster-issuers folder and include the following definition of a nginx-ingress/cluster-issuers/letsencrypt-prod.yaml file:

apiVersion: certmanager.k8s.io/v1alpha1
kind: ClusterIssuer
metadata:
labels:
  name: letsencrypt-prod
name: letsencrypt-prod
spec:
acme:
  email: EMAIL-ADDRESS
  http01: {}
  privateKeySecretRef:
    name: letsencrypt-prod
  server: https://acme-v02.api.letsencrypt.org/directory

Don't forget to replace EMAIL-ADRESS with your email address. You can read more about setting up ClusterIssuers here.

Render and apply the manifests to the cluster

Create the following nginx-ingress/apply.sh file:

#!/bin/bash
SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )"
rm -rf $SCRIPTPATH/manifests
mkdir -p $SCRIPTPATH/manifests

# RENDER
echo 'RENDER MANIFESTS'

## CERT-MANAGER
cp -r $SCRIPTPATH/charts/00-crds.yaml $SCRIPTPATH/manifests/ # cert-manager CRDs

helm template \
  --name cert-manager \
  --namespace nginx-ingress \
  --values $SCRIPTPATH/values/cert-manager.yaml \
  --output-dir $SCRIPTPATH/manifests \
    $SCRIPTPATH/charts/cert-manager

## NGINX-INGRESS
helm template \
  --name nginx-ingress \
  --namespace nginx-ingress \
  --values $SCRIPTPATH/values/nginx-ingress.yaml \
  --output-dir $SCRIPTPATH/manifests \
    $SCRIPTPATH/charts/nginx-ingress

# APPLY
echo 'APPLY MANIFESTS'

## NAMESPACE
kubectl apply -f $SCRIPTPATH/nginx-ingress-ns.yaml

## CERT-MANAGER
kubectl label namespace nginx-ingress --overwrite certmanager.k8s.io/disable-validation=true
kubectl apply -f $SCRIPTPATH/00-crds.yaml # cert-manager CRDs
kubectl apply -R -f $SCRIPTPATH/manifests/cert-manager

## CLUSTER ISSUERS
kubectl apply -n nginx-ingress -R -f $SCRIPTPATH/cluster-issuers

## NGINX-INGRESS
kubectl apply -n nginx-ingress -R -f $SCRIPTPATH/manifests/nginx-ingress

echo ''
echo ''
echo 'Now watch the status and the external IP by running:'
echo '    kubectl --namespace nginx-ingress get services -o wide -w nginx-ingress-controller'

The file will first render all the manifests, then applies the namespace and both the manifests to the cluster:

./nginx-ingress/apply.sh

Wait for all the pods to spin up; you can check this with:

$ kubectl get po -n nginx-ingress
NAME                                             READY   STATUS    RESTARTS   AGE
cert-manager-5cb87887d8-sbtgk                    1/1     Running   0          47s
cert-manager-cainjector-5d448d76df-6wm6f         1/1     Running   0          52s
cert-manager-webhook-6f75cfcbc8-9h9zf            1/1     Running   1          45s
nginx-ingress-controller-94bd8fd99-mdhqc         1/1     Running   0          43s
nginx-ingress-default-backend-576b86996d-5lc8j   1/1     Running   0          41s

When you received an Internal error occured: failed calling webhook… you need to wait for the cert-manager-webhook to spin-up before re-applying the ClusterIssuers: kubectl apply -n nginx-ingress -R -f nginx-ingress/cluster-issuers

All the objects are created; including a LoadBalancer (depending on your Kubernetes provider). Wait for the Load Balance to be created and then set your DNS records at this external IP.

$ kubectl --namespace nginx-ingress get services -o wide -w nginx-ingress-controller
NAME                       TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)                      AGE   SELECTOR
nginx-ingress-controller   LoadBalancer   10.245.89.89   <pending>     80:32104/TCP,443:31353/TCP   12s   app=nginx-ingress,component=controller,release=nginx-ingress
nginx-ingress-controller   LoadBalancer   10.245.89.89   206.189.241.190   80:32104/TCP,443:31353/TCP   2m29s   app=nginx-ingress,component=controller,release=nginx-ingress

Example deployment

We are done setting up cert-manager and nginx-ingress! We can test the setup with the following example-deployment.yaml:

apiVersion: v1
kind: Namespace
metadata:
  name: dev
---
apiVersion: apps/v1beta2
kind: Deployment
metadata:
  name: example
  namespace: dev
spec:
  selector:
    matchLabels:
      app: example
  replicas: 2
  template:
    metadata:
      labels:
        app: example
    spec:
      containers:
        - name: nginx
          image: stenote/nginx-hostname:latest
---
apiVersion: v1
kind: Service
metadata:
  name: example
  namespace: dev
spec:
  selector:
    app: example
  ports:
    - protocol: TCP
      port: 80
      name: http
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: example-prod-ingress
  namespace: dev
  annotations:
    kubernetes.io/ingress.class: nginx
    certmanager.k8s.io/cluster-issuer: letsencrypt-prod
spec:
  tls:
    - secretName: letsencrypt-prod
      hosts:
        - test.example.com
  rules:
    - host: test.example.com
      http:
        paths:
          - path: /
            backend:
              serviceName: example
              servicePort: 80

Replace the example.com domain with your own; where the DNS is set to the ExternalIp of your LoadBalancer; and apply the deployment file to your cluster:

$ kubectl apply -f example-deployment.yaml
namespace/dev created
deployment.apps/example created
service/example created
ingress.extensions/example-prod-ingress created

You can check the status of the certificate:

$ kubectl get certificates -n dev
NAMESPACE   NAME               READY   SECRET             AGE
dev         letsencrypt-prod   True    letsencrypt-prod   65s

Navigate to test.YOURDOMAIN.COM and you should have successfully configured HTTPS using a Let's Encrypt certificate for your Nginx Ingress.