Introduction
This tutorial will guide you through the installation of Traefik on top of a fresh kubernetes cluster. With help of a simple nginx service different solutions will be shown on how to serve the page with a self-signed certificate, a let's encrypt staging and production certificate. All certificate stuff is done with cert-manager.
You should be familiar with basic kubernetes usage and have a working k8s cluster on hand. If you do not have a cluster yet, simply build your own in a few minutes following this tutorial.
Prerequisites
- A working kubernetes cluster (build your own with this tutorial)
- A floating IP pointing to your cluster or the public IP of one of your nodes (
<10.0.0.1>in this tutorial; the<>are not part of the IP) - Familiarity with linux and working on the shell
kubectlcommand line tool installed- An email address for usage with Let's Encrypt, this tutorial uses:
mail@example.com
Recommended
- Familiarity with the concepts of Kubernetes
- A valid domain or subdomain, with access to DNS setup.
*This tutorial was tested on Ubuntu 18.04 Hetzner Cloud server and Kubernetes version v1.15.7 and v1.16.4, Ubuntu 18.04 was used as local machine.
All steps can be executed as is on the machine where kubectl is installed. Most steps are using heredoc notation to prevent config file creation. The used kubernetes services will use different syntax, where possible. This shall show you different possibilities.
Step 1 - Deploy Traefik as ingress controller
The following setup is based on the official setup instructions.
Step 1.1 - Create the required RBAC roles
Traefik needs some kubernetes roles configured, before the setup of the service can be done.
kubectl apply -f https://raw.githubusercontent.com/containous/traefik/v1.7/examples/k8s/traefik-rbac.yamlStep 1.2 - Setup Traefik
As stated in the docs, traefik can be installed as Controller or a DaemonSet. In this tutorial we use the DaemonSet deployment. If you have a cluster consisting of multiple nodes (master or workers), exactly one traefik pod will be created on every node without additional configuration.
!!! If you just have a single node cluster, take care to taint the master node to accept pods: kubectl taint nodes --all node-role.kubernetes.io/master-
You can define the traefik configuration with cli flags or using a toml file. The toml file is easier to handle for complex setups. Due to simplicity, cli flags are used in this tutorial.
!!! The config uses the type LoadBalancer instead of NodePort. This should work for bare metall setups with metallb like in this tutorial. If you're using a cloud provider with an external load balancer, you might use NodePort instead.
!!! The used traefik configuration is not production ready! You should use http basic auth to secure the dashboard in combination with automatic https redirection. For a production environment it's also recommended to setup traefik into a seperate namespace.
cat << EOF | kubectl apply -f -
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: traefik-ingress-controller
namespace: kube-system
---
kind: DaemonSet
apiVersion: extensions/v1beta1
metadata:
name: traefik-ingress-controller
namespace: kube-system
labels:
k8s-app: traefik-ingress-lb
spec:
template:
metadata:
labels:
k8s-app: traefik-ingress-lb
name: traefik-ingress-lb
spec:
serviceAccountName: traefik-ingress-controller
terminationGracePeriodSeconds: 60
containers:
- image: traefik:v1.7
name: traefik-ingress-lb
ports:
- name: http
containerPort: 80
hostPort: 80
- name: https
containerPort: 443
hostPort: 443
- name: dashboard
containerPort: 8080
hostPort: 8080
securityContext:
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
args:
- --api
- --accesslog
- --logLevel=INFO
- --kubernetes
- --defaultentrypoints=http,https
- --entrypoints=Name:https Address::443 TLS
- --entrypoints=Name:http Address::80
---
kind: Service
apiVersion: v1
metadata:
name: traefik-ingress-service
namespace: kube-system
spec:
selector:
k8s-app: traefik-ingress-lb
ports:
- protocol: TCP
port: 80
name: http
- protocol: TCP
port: 443
name: https
- protocol: TCP
port: 8080
name: dashboard
type: LoadBalancer
EOFThe above config publishes the Traefik dashboard on port 8080 and publishes the default ports used for HTTP traffic: 80 and 443.
Step 1.3 - Validate setup
Check if all pods are Running.
kubectl -n kube-system get pod# Output:
NAME READY STATUS RESTARTS AGE
coredns-5d4dd4b4db-frkpb 1/1 Running 0 12m
coredns-5d4dd4b4db-t8z2s 1/1 Running 0 12m
etcd-k1 1/1 Running 1 11m
kube-apiserver-k1 1/1 Running 1 6m49s
kube-controller-manager-k1 1/1 Running 1 11m
kube-flannel-ds-amd64-hbk58 1/1 Running 0 5m47s
kube-proxy-dmng9 1/1 Running 1 12m
kube-scheduler-k1 1/1 Running 1 12m
traefik-ingress-controller-z8xdn 1/1 Running 0 36sGet the EXTERNAL-IP assigned to the traefik-ingress-service.
kubectl -n kube-system get service# Output:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP,9153/TCP 16m
traefik-ingress-service LoadBalancer 10.109.23.93 10.0.0.1 80:31681/TCP,443:31856/TCP,8080:32684/TCP 4m23sYou can also show the logs for the traefik pod using its given label.
-
logs for a specific pod
kubectl -n kube-system logs -f $(kubectl -n kube-system get pods -l k8s-app=traefik-ingress-lb -o jsonpath='{.items[0].metadata.name}') -
logs for all pods, useful if you have multiple nodes
kubectl -n kube-system logs -f -l k8s-app=traefik-ingress-lb
Step 1.4 - Open Traefik dashboard
The Service you created, should have got the EXTERNAL-IP of the server. This should be the same as our floating IP or the public IP of the server, depending on your setup. A LoadBalancer service binds to some random port(s) as you see in column PORT(S). Knowing the port and the IP is all you need to open the Dashboard (http://<10.0.0.1>:32684).
Step 2 - Host a simple nginx service
In the next step we will deploy a simple nginx service and will publish it on your domain.
Step 2.1 - Prepare the domain
If you have your own domain (in this tutorial: <example.com>), take care to create an A-Record in the DNS settings to your cluster's floating IP (<10.0.0.1>) or the server's public IP. Depending on your provider's TTL (Time to Live in seconds) it can take some time, that the DNS changes are propagated. Because your local system DNS will not update as frequently as the server settings, it's also a good idea to do the check on the server.
You can check if your domain points to the right IP with:
host <example.com># Outputs:
example.com has address 10.0.0.1Store your domain as environmental variable for further usage:
DOMAIN=<example.com>Step 2.2 - Deploy nginx service
We will now use the configured DOMAIN directly inside the config. As you can see, the service will be created in a seperate namespace. This is the recommended way.
cat << EOF | kubectl apply -f -
---
apiVersion: v1
kind: Namespace
metadata:
name: testing
spec:
finalizers:
- kubernetes
status:
phase: Active
---
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: nginx
namespace: testing
labels:
app: nginx
spec:
selector:
matchLabels:
app: nginx
replicas: 1
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- name: http
containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx
namespace: testing
spec:
selector:
app: nginx
ports:
- name: http
port: 80
targetPort: 80
type: ClusterIP
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: traefik
traefik.ingress.kubernetes.io/rule-type: "PathPrefixStrip"
name: nginx
namespace: testing
spec:
rules:
- host: ${DOMAIN}
http:
paths:
- path: /
backend:
serviceName: nginx
servicePort: 80
EOFCheck that the nginx pod is created and Running.
kubectl -n testing get podNAME READY STATUS RESTARTS AGE
nginx-74f6bc9c7c-bvjh5 1/1 Running 0 9sAfter applying the changes, open <example.com> in your browser. You should see a very basic html page now.
If you open the pods log and reload the domain in the browser, you'll see the requests in the nginx logs.
kubectl -n testing logs -f $(kubectl -n testing get pods -l app=nginx -o jsonpath='{.items[0].metadata.name}')Step 3 - Deploy HTTPS
For a production ready page, you should always use https (HTTP over TLS = encrypted http) for security reasons! In days of Let's Encrypt this is a very easy task. No complicated certificate handling anymore.
Traefik can use Let's Encrypt on it's own, but this is not the recommended way and might fail in some cases. A much better way (in the sense of do-one-thing-and-to-it-right) is the usage of cert-manager.
Step 3.1 - Setup cert-manager
- Official docs: https://cert-manager.io/docs/usage/ingress/
kubectl create namespace cert-manager
kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v0.12.0/cert-manager.yamlAfter a few seconds all pods should be in RUNNING state.
!!! The watch command is not a special kubernetes command. It's simply a standard unix command that executes the following command every second.
watch kubectl -n cert-manager get pods#Outputs:
NAME READY STATUS RESTARTS AGE
cert-manager-66c8bc8b67-5qcb4 1/1 Running 0 24s
cert-manager-cainjector-df4dc78cd-s56zv 1/1 Running 0 24s
cert-manager-webhook-5f78ff89bc-hr9ck 1/1 Running 0 24scert-manager is now ready to use. In the next steps we will tryout different ways to create testing and production ready certificates.
Step 3.2 - Create self-signed certificate
For some use cases, e.g. internal or test usage, you can easily create a self-signed certificate with cert-manager. To configure a certificate with cert-manager you always need an Issuer, ClusterIssuer and the Certificate ressource.
cat << EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1alpha2
kind: Issuer
metadata:
name: selfsigned
namespace: testing
spec:
selfSigned: {}
---
apiVersion: cert-manager.io/v1alpha2
kind: Certificate
metadata:
name: ${DOMAIN}-cert
namespace: testing
spec:
commonName: ${DOMAIN}
secretName: ${DOMAIN}-cert
issuerRef:
name: selfsigned
EOFYou can check the status of the Certificate very easily.
-
short status
kubectl -n testing get certificate# Outputs: 10.0.0.1.example.com-cert True 10.0.0.1.example.com-cert 47s -
full info and status
kubectl -n testing describe certificate <10.0.0.1>.example.com-cert# Outputs: ... Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal GeneratedKey 94s cert-manager Generated a new private key Normal Requested 94s cert-manager Created new CertificateRequest resource "10.0.0.1.example.com-cert-2241091300" Normal Issued 94s cert-manager Certificate issued successfully
Our nginx service doesn't know about the certificate right now. Reconfigure the Ingress to change this.
cat << EOF | kubectl apply -f -
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: traefik
traefik.ingress.kubernetes.io/rule-type: "PathPrefixStrip"
name: nginx
namespace: testing
spec:
rules:
- host: ${DOMAIN}
http:
paths:
- path: /
backend:
serviceName: nginx
servicePort: 80
tls:
- hosts:
- ${DOMAIN}
secretName: ${DOMAIN}-cert
EOFIf you open https://<example.com> in your browser, you have to accept the certificate first. That's because your certificate is not signed by an official, trusted Certificate Authority (CA). After adding the exception, you should see the default nginx page, but secured by https.
Step 3.3 - Create a staging certificate
"Let's Encrypt is a non-profit certificate authority run by Internet Security Research Group (ISRG) that provides X.509 certificates for Transport Layer Security (TLS) encryption at no charge. The certificate is valid for 90 days, during which renewal can take place at any time. The offer is accompanied by an automated process designed to overcome manual creation, validation, signing, installation, and renewal of certificates for secure websites." (Wikimedia)
The process behind Let`s Encrypt is very easy:
- check if the requested domain points to the same IP the request comes from
- check for some special files on the pointing server
- if one of the conditions is not met, refuse the request
A staging certificate is for testing purposes only and has to be accepted manually like a self-signed certificate. Take care, that your domain is configured properly! Please replace the email with a proper mail address!
Issuer
To get a certificate from Let's Encrypt, you need to setup an Issuer or ClusterIssuer. A Issuer is valid for the current namespace, a ClusterIssuer does not depend on the namespace.
# Please replace mail with your mail
YOUR_MAIL_ADDRESS=mail@example.com
cat << EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
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: ${YOUR_MAIL_ADDRESS}
server: https://acme-staging-v02.api.letsencrypt.org/directory
privateKeySecretRef:
# Secret resource used to store the account's private key.
name: staging-issuer-account-key
# Add a single challenge solver, HTTP01 using nginx
solvers:
- http01:
ingress:
class: traefik
EOFGet the status with describe or simple status with get.
# check status
kubectl describe clusterissuer letsencrypt-staging# Outputs:
Status:
Acme:
Last Registered Email: mail@example.com
Uri: https://acme-staging-v02.api.letsencrypt.org/acme/acct/12082973
Conditions:
Last Transition Time: 2020-01-13T15:40:13Z
Message: The ACME account was registered with the ACME server
Reason: ACMEAccountRegistered
Status: True
Type: Ready
Events: <none>Certificate
Take care to configure your domain properly like described in Step 2.
The certificates content is stored in a secret. To create a new certificate, we have to delete this secret. Also delete the certificate, otherwise cert-manager will create a new one instantly. You have to add a label to your nginx ingress cert-manager.io/cluster-issuer: letsencrypt-staging. Otherwise cert-manager does not know which issuer to use.
kubectl -n testing delete certificate "${DOMAIN}-cert"
kubectl -n testing delete secret "${DOMAIN}-cert"cat << EOF | kubectl apply -f -
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: traefik
traefik.ingress.kubernetes.io/rule-type: "PathPrefixStrip"
cert-manager.io/cluster-issuer: letsencrypt-staging
name: nginx
namespace: testing
spec:
rules:
- host: ${DOMAIN}
http:
paths:
- path: /
backend:
serviceName: nginx
servicePort: 80
tls:
- hosts:
- ${DOMAIN}
secretName: ${DOMAIN}-cert
---
apiVersion: cert-manager.io/v1alpha2
kind: Certificate
metadata:
name: ${DOMAIN}-cert
namespace: testing
spec:
commonName: ${DOMAIN}
secretName: ${DOMAIN}-cert
issuerRef:
name: letsencrypt-staging
EOFThe request takes about 30 seconds. You can check the status continuously with the bash command watch.
watch kubectl -n testing describe certificate "${DOMAIN}-cert"# Output
Status:
Conditions:
Last Transition Time: 2020-01-13T16:06:33Z
Message: Certificate is up to date and has not expired
Reason: Ready
Status: True
Type: Ready
Not After: 2020-04-12T15:06:33Z
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal GeneratedKey 2m26s cert-manager Generated a new private key
Normal Requested 2m26s cert-manager Created new CertificateRequest resource "10.0.0.1.xip.io-cert-4183194560"
Normal Issued 2m cert-manager Certificate issued successfullycert-manager outputs lots of stuff during this operation. If something goes wrong (e.g. no certificate after a minute or two), you can check the logs with:
-
static log
kubectl -n cert-manager logs $(kubectl -n cert-manager get pod -l app=cert-manager -o jsonpath='{.items[0].metadata.name}') -
live log
kubectl -n cert-manager logs -f --tail 20 $(kubectl -n cert-manager get pod -l app=cert-manager -o jsonpath='{.items[0].metadata.name}')
Open in browser
If you open or reload https://example.com or https://$DOMAIN in your browser, the certificate will not be trusted, comparable to the self-signed certificate. The name differs now is the name of the issuer Fake LE Intermediate X1 instead of cert-manager.
Step 3.4 - Create a production certificate
If everything worked well in the last steps, you can continue to obtain a production ready Let's Encrypt certificate. A production certificate is signed by an official CA and will be trusted out of the box by all mainstream browsers (e.g. Chrome/Chromium, Firefox, IE, Edge, Safari, Opera). The resulting certificate is valid for 90 days. cert-manager renews the certificates every 30 days on its own, doing so the certificate should never expire.
!!! To force a renewal, simply delete the secret containing the certificate: kubectl -n testing delete secret "${DOMAIN}-cert".
If you are using your own domain, the process should work.
Issuer
YOUR_MAIL_ADDRESS=mail@example.com
cat << EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1alpha2
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: ${YOUR_MAIL_ADDRESS}
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
# Secret resource used to store the account's private key.
name: letsencrypt-issuer-account-key
# Add a single challenge solver, HTTP01 using nginx
solvers:
- http01:
ingress:
class: traefik
EOFCheck status
kubectl describe clusterissuer letsencrypt# Outputs:
Status:
Acme:
Last Registered Email: mail@example.com
Uri: https://acme-v02.api.letsencrypt.org/acme/acct/75756861
Conditions:
Last Transition Time: 2020-01-13T17:08:07Z
Message: The ACME account was registered with the ACME server
Reason: ACMEAccountRegistered
Status: True
Type: Ready
Events: <none>Certificate
Again, drop the Certificate and the corresponding Secret.
kubectl -n testing delete certificate "${DOMAIN}-cert"
kubectl -n testing delete secret "${DOMAIN}-cert"
DOMAIN=example.com
cat << EOF | kubectl apply -f -
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: traefik
traefik.ingress.kubernetes.io/rule-type: "PathPrefixStrip"
cert-manager.io/cluster-issuer: letsencrypt
name: nginx
namespace: testing
spec:
rules:
- host: ${DOMAIN}
http:
paths:
- path: /
backend:
serviceName: nginx
servicePort: 80
tls:
- hosts:
- ${DOMAIN}
secretName: ${DOMAIN}-cert
---
apiVersion: cert-manager.io/v1alpha2
kind: Certificate
metadata:
name: ${DOMAIN}-cert
namespace: testing
spec:
commonName: ${DOMAIN}
secretName: ${DOMAIN}-cert
issuerRef:
name: letsencrypt
EOFAgain, check the status of the request and check the logs of cert-manager after a minute if the certificate doesn't show Status: True.
watch kubectl -n testing describe certificate "${DOMAIN}-cert"# Output
Status:
Conditions:
Last Transition Time: 2020-01-13T16:06:33Z
Message: Certificate is up to date and has not expired
Reason: Ready
Status: True
Type: Ready
Not After: 2020-04-12T15:06:33Z
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal GeneratedKey 2m26s cert-manager Generated a new private key
Normal Requested 2m26s cert-manager Created new CertificateRequest resource "example.com-cert-4183194560"
Normal Issued 2m cert-manager Certificate issued successfullykubectl -n cert-manager logs -f --tail 10 $(kubectl -n cert-manager get pod -l app=cert-manager -o jsonpath='{.items[0].metadata.name}')Conclusion
This tutorial has shown you how to setup traefik as ingress controller on a plain kubernetes cluster. Additionally you should be able to secure a deployed webservice with a self-signed TLS certificate or one from Let's Encrypt.
!!! The used traefik configuration is not production ready! You should use (at least) http basic auth to secure the dashboard in combination with automatic https redirection.