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
kubectl
command 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.yaml
Step 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
EOF
The 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 36s
Get 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 4m23s
You 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.1
Store 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
EOF
Check that the nginx pod is created and Running
.
kubectl -n testing get pod
NAME READY STATUS RESTARTS AGE
nginx-74f6bc9c7c-bvjh5 1/1 Running 0 9s
After 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.yaml
After 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 24s
cert-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
EOF
You 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
EOF
If 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
EOF
Get 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
EOF
The 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 successfully
cert-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
EOF
Check 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
EOF
Again, 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 successfully
kubectl -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.