To show how to add an Ingress to Kubernetes so that you can redirect traffic to multiple applications to fully utilise a Kubernetes cluster.
And to then show how to easily add a TLS certificate to secure your sites traffic, using Let's Encrypt.
All this will be done using Helm, the package manager for Kubernetes.
This howto follows on from my Kubernetes 101: Launch your first application with Kubernetes. There is no need to have followed each step in that howto as we will mostly build from scratch in this howto, and refer to the previous howto where applicable to avoid duplication, but it may help to have read the whole of previous one.
You do need some basic understanding of Kubernetes. Please read my Kubernetes basics to get up to scratch. It's brief but gets you going.
You do need a Kubernetes cluster up and running. Please follow my create a Kubernetes cluster instructions. A fresh new cluster is preferable to avoid any confusion and mistakes, but it should work with existing clusters.
You do need to have kubectl installed. Please follow my install kubectl instructions.
And you do need to make sure you have downloaded the cluster configuration and authenticated kubectl with it. Again, refer to my kubectl connect section of the introduction howto.
Helm is the package manager for Kubernetes. Think the apt, homebrew, npm, rubygem, maven, etc but for k8s.
Helm allows one command to install complicated applications. Often includes RBAC, Namespaces, multiple services, several deployments and other dependencies.
Helm uses charts to define what to install. This library includes most of the applications you might use with Kubernetes. You can also create your own charts.
Helm consists of a local part, the Helm client, and a server part, the Tiller service.
To install Helm locally you can use Homebrew, Snap, or there are binary downloads.
brew install kubernetes-helm
sudo snap install helm --classic
Or download e.g. for Linux 64 bit:
wget https://storage.googleapis.com/kubernetes-helm/helm-v2.13.0-linux-arm64.tar.gz
tar xzf helm-v2.13.0-linux-arm64.tar.gz
sudo mv helm tiller /usr/bin/
To install Tiller we first will create a service account for it
kubectl -n kube-system create serviceaccount tiller
kubectl create clusterrolebinding tiller --clusterrole cluster-admin --serviceaccount=kube-system:tiller
Then actually install Tiller by initializing Helm.
helm init --service-account tiller
You can confirm installation by listing any packages installed (none at this time).
helm list
Let's setup a simple application deployment and service, similar to the previous howto. But this time we will set up 2 applications.
First lets create our first echo deployment. (Make sure you version control these files)
If you are building from the previous howto this deployment may already exist.
vi echo1-deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: echo1
spec:
selector:
matchLabels:
app: echo1
replicas: 2
template:
metadata:
labels:
app: echo1
spec:
containers:
- name: echo1
image: hashicorp/http-echo
args:
- "-text=echoNumberOne"
ports:
- containerPort: 5678
kubectl apply -f echo1-deployment.yml
And lets quickly create a second echo deployment which we did not have in the previous howto.
vi echo2-deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: echo2
spec:
selector:
matchLabels:
app: echo2
replicas: 2
template:
metadata:
labels:
app: echo2
spec:
containers:
- name: echo1
image: hashicorp/http-echo
args:
- "-text=echoNumberTwo"
ports:
- containerPort: 5678
kubectl apply -f echo2-deployment.yml
This should give us two deployments:
kubectl get deployments
The output should be something like:
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
echo1
2
2
2
0
21s
echo2
2
2
2
0
2s
If you have come from the previous howto we need to delete the load balanced service. If fresh cluster this is not needed.
kubectl delete service echo1
Lets (re)add a service in front of each of echo1.
vi echo1-service.yml
apiVersion: v1
kind: Service
metadata:
name: echo1
spec:
ports:
- port: 80
targetPort: 5678
selector:
app: echo1
kubectl apply -f echo1-service.yml
And the same for the second echo:
vi echo2-service.yml
apiVersion: v1
kind: Service
metadata:
name: echo2
spec:
ports:
- port: 80
targetPort: 5678
selector:
app: echo2
kubectl apply -f echo2-service.yml
This should create two services listed like this:
kubectl get services
NAME
TYPE
CLUSTER-IP
EXTERNAL-IP
PORT(S)
AGE
echo1
ClusterIP
10.24.40.234
<none>
80/TCP
15s
echo2
ClusterIP
10.24.65.74
<none>
80/TCP
48s
kubernetes
ClusterIP
10.24.0.1
<none>
443/TCP
2d
Without any external IP addresses.
So now we have two applications exposed as services internally. To expose these we need an Ingress.
Before we set up a custom Ingress we need an Ingress controller. For this we will install it with Helm as it otherwise is a complicated list of RBAC, namespaces etc that needs to be configured. An ingress controller is basically a type of load balancer.
A common Ingress controller is Nginx. and there are many alternative to the one we use below including an Nginx based one made by Nginx Inc themselves. Another popular traffic manager is Istio.
This Nginx chart uses ConfigMap to configure Nginx. We don't need to configure anything in our use case.
helm install --name nginx-ingress stable/nginx-ingress
As you can see from the output it install a lot of things, that we now don't need to worry about.
We now have a few more services:
kubectl get services
NAME
TYPE
CLUSTER-IP
EXTERNAL-IP
PORT(S)
AGE
echo1
ClusterIP
10.24.40.234
<none>
80/TCP
1d
echo2
ClusterIP
10.24.65.74
<none>
80/TCP
1d
kubernetes
ClusterIP
10.24.0.1
<none>
443/TCP
2d
nginx-ingress-controller
LoadBalancer
10.24.22.205
1.2.3.4
80:30617/TCP,443:32262/TCP
2m
nginx-ingress-default-backend
ClusterIP
10.24.10.74
<none>
80/TCP
2m
As you can see we now have two more services: nginx-ingress-controller and nginx-ingress-default-backend. If you lookup that external IP you will see the default response from nginx-ingress-default-backend, basically a 404. The default backend is what respond when no Ingress rules are matched.
curl 1.2.3.4
default backend - 404
Let's add an Ingress to direct request traffic to our echo services.
vi echo-ingress.yml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: echo-ingress
spec:
rules:
- host: echo1.ex.com
http:
paths:
- backend:
serviceName: echo1
servicePort: 80
- host: echo2.ex.com
http:
paths:
- backend:
serviceName: echo2
servicePort: 80
Note this redirects on echo1.ex.com and echo2.ex.com (abbreviated from example.com for display purposes). This will only work if you add these to your /etc/hosts file as the external IP 1.2.3.4 from the ingress controller service. However you may prefer to use real DNS names for others to also use the service, and to later add SSL etc.
kubectl create -f echo-ingress.yml
kubectl get ingress
NAME
HOSTS
ADDRESS
PORTS
AGE
echo-ingress
echo1.ex.com,echo2.ex.com
5.4.3.2
80
3s
You now have an Ingress routing traffic to either echo service depending on hostname in the request.
curl 1.2.3.4
default backend - 404
curl echo1.ex.com
echoNumberOne
curl echo2.ex.com
echoNumberTwo
These days there is no excuse for all web traffic not to use https. To do that we need to add a TLS certificate to our echo sites.
SSL & TLS certificates used to be a convoluted and expensive ordeal. But not any more since Let's encrypt was launched.
With Kubernetes there is a Cert Manager to act as a Cluster Issuer for generating and managing certificates with Let's Encrypt, which makes this very easy to configure and automate.
With Helm installing a Cert Manager there are a few steps (compared to a lot of steps in a manifest) to do first:
kubectl apply -f https://raw.githubusercontent.com/jetstack/cert-manager/release-0.6/deploy/manifests/00-crds.yaml
Output should list various apiextensions.k8s.io created.
kubectl label namespace kube-system certmanager.k8s.io/disable-validation="true"
Then you can install it with:
helm install \
--name cert-manager \
--namespace kube-system \
jetstack/cert-manager
I sometimes have to specify which version, e.g. "--version v0.5.2". But at the time of writing the default works fine. The output of that should towards the end say
cert-manager has been deployed successfully!
Whilst testing let's create an issuer but use Let's Encrypt's staging server to avoid flooding the production one with bad data.
vi staging-issuer.yml
apiVersion: certmanager.k8s.io/v1alpha1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef:
name: letsencrypt-staging-secret
http01: {}
kube apply -f staging-issuer.yml
kube get clusterissuer
NAME AGE
letsencrypt-staging 16s
Lets now modify the ingress for echo to include the cluster issuer and hosts to create a certificate for.
vi echo-issuer.yml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: echo-ingress
annotations:
kubernetes.io/ingress.class: nginx
certmanager.k8s.io/cluster-issuer: letsencrypt-staging
spec:
tls:
- hosts:
- echo1.ex.com
- echo2.ex.com
secretName: letsencrypt-staging-secret
rules:
- host: echo1.ex.com
http:
paths:
- backend:
serviceName: echo1
servicePort: 80
- host: echo2.ex.com
http:
paths:
- backend:
serviceName: echo2
servicePort: 80
kube apply -f echo-issuer.yml
kubectl get ingress
NAME
HOSTS
ADDRESS
PORTS
AGE
echo-ingress
echo1.ex.com,echo2.ex.com
3.5.8.13
80, 443
3s
You should now also have port 433 redirected to this ingress. Note, the IP listed in the output is not exposed so ignore it.
You can inspect the certificate generated:
kube describe certificate letsencrypt-staging
It should list your domains under spec/Acme/Config/Domains. You can also use the describe keyword on the certificate and ingress to check any recent events, e.g. certificate creation etc.
Lets inspect the certificate in a https call with curl and wget.
curl -I echo1.ex.com
HTTP/1.1 308 Permanent Redirect
Server: nginx/1.15.8
...
Location: https://echo1.ex.com/
wget --save-headers -O- echo1.ex.com
...
Connecting to echo1.ex.com (echo1.ex.com)|1.2.3.4|:443... connected.
ERROR: cannot verify echo1.ex.com's certificate, issued by ‘CN=Fake LE Intermediate X1’:
Unable to locally verify the issuer's authority.
To connect to echo1.ex.com insecurely, use `--no-check-certificate'.
So a normal http request issues a redirect to the https url. And the https call works and has a certificate. And as expected as it is using the Let's Encrypt staging API so it is not verified. Lets fix that.
Now the staging provider works lets switch to the real production one.
vi production-issuer.yml
apiVersion: certmanager.k8s.io/v1alpha1
kind: ClusterIssuer
metadata:
name: letsencrypt-production
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef:
name: letsencrypt-production-secret
http01: {}
kube apply -f production-issuer.yml
kube get clusterissuer
NAME
AGE
letsencrypt-production 6s
letsencrypt-staging 15m
And then update the cluster issuer and secret in the ingress configuration.
vi echo-issuer.yml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: echo-ingress
annotations:
kubernetes.io/ingress.class: nginx
certmanager.k8s.io/cluster-issuer: letsencrypt-production
spec:
tls:
- hosts:
- echo1.ex.com
- echo2.ex.com
secretName: letsencrypt-production-secret
rules:
- host: echo1.ex.com
http:
paths:
- backend:
serviceName: echo1
servicePort: 80
- host: echo2.ex.com
http:
paths:
- backend:
serviceName: echo2
servicePort: 80
kube apply -f echo-issuer.yml
kubectl get certificate
NAME
AGE
letsencrypt-production-secret
7s
letsencrypt-staging-secret
1d
kubectl describe certificate letsencrypt-production
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Generated 3m cert-manager Generated new private key
Normal OrderCreated 3m cert-manager Created Order resource "letsencrypt-prod-secret-9876"
Normal OrderComplete 2m cert-manager Order "letsencrypt-prod-secret-9876" completed successfully
Normal CertIssued 2m cert-manager Certificate issued successfully
When the events say the certificate has been created successfully:
wget --save-headers -O- echo1.ex.com
...
Connecting to echo1.ex.com (echo1.ex.com)|1.2.3.4|:443... connected.
...
HTTP/1.1 200 OK
...
There should be no certificate errors and wget will simply download the echo1 response of echoNumberOne.
So now you have a load balanced service, routing traffic via an ingress and over secure TLS traffic.
If you want to try out your own Docker images, read how to use 3rd party Docker registry in my previous howto.
This document was influenced by Digital Ocean's excellent kubernetes documentation. I do run some of my clusters with Digital Ocean (referral link) and they have been very good so far.
Please fork and send a pull request for to correct any typos, or useful additions.
Buy a t-shirt if you found this guide useful. Hire me for short term advice or long term consultancy.
Otherwise contact me. Especially for things factually incorrect. Apologies for procrastinated replies.