Traefik 2 como ingress de Kubernetes

Kubernetes 21 de abr. de 2020

En este tutorial os voy a explicar como instalar Traefik v2 como ingress de vuestro cluster de Kubernetes.

Además de instalar Traefik v2, lo configuraremos para que nos redireccione todo el tráfico http a https y nos genere los certificados automáticamente con letsencrypt.

El tutorial estará dividido en 2 partes, una en la que explico como instalar y configurar Traefik y otra en la que explico como apuntar traefik a servicios que tenemos tanto en el cluster como fuera del cluster.

Antes de empezar

Primero de todo, quiero aclarar que no soy experto y quizás haya algo que se pueda hacer de otra manera y mejor, pero yo me ciño a todo lo que he ido aprendiendo de la documentación oficial y otros tutoriales.

Kubernetes and Let’s Encrypt - Traefik
Traefik Documentation

Aseguraos de que no tenéis otro ingress instalado en vuestro cluster, si es así tenéis dos opciones: eliminarlo o usar un puerto distinto al 80 y 443.


Instalación de Traefik v2

Para el tutorial usaré el namespace default para instalarlo todo, vosotros podéis usar el que queráis.

Primero de todo vamos a crear una carpeta donde vamos a tener los yaml de todos los componentes para Traefik. Tenemos que crear y guardar los siguientes archivos:

RBAC.yaml

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: traefik-ingress-controller

rules:
  - apiGroups:
      - ""
    resources:
      - services
      - endpoints
      - secrets
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - extensions
    resources:
      - ingresses
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - extensions
    resources:
      - ingresses/status
    verbs:
      - update
  - apiGroups:
      - traefik.containo.us
    resources:
      - middlewares
      - ingressroutes
      - traefikservices
      - ingressroutetcps
      - ingressrouteudps
      - tlsoptions
      - tlsstores
    verbs:
      - get
      - list
      - watch

---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: traefik-ingress-controller

roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: traefik-ingress-controller
subjects:
  - kind: ServiceAccount
    name: traefik-ingress-controller
    namespace: default
Esto es la asignación de permisos para traefik para que pueda administrar ciertas cosas de nuestro cluster.

CRD.yaml

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: ingressroutes.traefik.containo.us

spec:
  group: traefik.containo.us
  version: v1alpha1
  names:
    kind: IngressRoute
    plural: ingressroutes
    singular: ingressroute
  scope: Namespaced

---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: middlewares.traefik.containo.us

spec:
  group: traefik.containo.us
  version: v1alpha1
  names:
    kind: Middleware
    plural: middlewares
    singular: middleware
  scope: Namespaced

---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: ingressroutetcps.traefik.containo.us

spec:
  group: traefik.containo.us
  version: v1alpha1
  names:
    kind: IngressRouteTCP
    plural: ingressroutetcps
    singular: ingressroutetcp
  scope: Namespaced

---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: ingressrouteudps.traefik.containo.us

spec:
  group: traefik.containo.us
  version: v1alpha1
  names:
    kind: IngressRouteUDP
    plural: ingressrouteudps
    singular: ingressrouteudp
  scope: Namespaced

---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: tlsoptions.traefik.containo.us

spec:
  group: traefik.containo.us
  version: v1alpha1
  names:
    kind: TLSOption
    plural: tlsoptions
    singular: tlsoption
  scope: Namespaced

---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: tlsstores.traefik.containo.us

spec:
  group: traefik.containo.us
  version: v1alpha1
  names:
    kind: TLSStore
    plural: tlsstores
    singular: tlsstore
  scope: Namespaced

---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: traefikservices.traefik.containo.us

spec:
  group: traefik.containo.us
  version: v1alpha1
  names:
    kind: TraefikService
    plural: traefikservices
    singular: traefikservice
  scope: Namespaced
Esto creará los recursos que necesita Traefik para funcionar.

Deployment.yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-traefik
spec:
  capacity:
    storage: 2Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  storageClassName: storage-nfs
  mountOptions:
    - nfsvers=4.1
  nfs:
    path: /mnt/SSD/kubernetes/traefik-prod
    server: 10.0.4.20
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  finalizers:
  - kubernetes.io/pvc-protection
  labels:
    app: traefik-ingress-lb
  name: pvc-traefik
spec:
  accessModes:
  - ReadWriteMany
  resources:
    requests:
      storage: 2Gi
  storageClassName: storage-nfs
  volumeName: pv-traefik
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: traefik-ingress-controller
  labels:
    k8s-app: traefik-ingress-lb
    kubernetes.io/cluster-service: "true"
spec:
  selector:
    matchLabels:
      k8s-app: traefik-ingress-lb
  template:
    metadata:
      labels:
        k8s-app: traefik-ingress-lb
        name: traefik-ingress-lb
    spec:
      hostNetwork: true
      dnsPolicy: ClusterFirstWithHostNet
      serviceAccountName: traefik-ingress-controller
      terminationGracePeriodSeconds: 60
      tolerations:
      - key: node-role.kubernetes.io/master
        effect: NoSchedule
      containers:
      - image: traefik:v2.2
        name: traefik-ingress-lb
        imagePullPolicy: Always
        volumeMounts:
          - mountPath: "/cert/"
            name: cert
        resources:
          requests:
            cpu: 120m
            memory: 80Mi
        args:
          #- --log.level=DEBUG
          - --api.insecure
          - --serversTransport.insecureSkipVerify=true
          - --accesslog
          - --providers.kubernetescrd
          - --entrypoints.web.address=:80
          - --entrypoints.websecure.address=:443
          - --entrypoints.udpep.address=:9000/udp
          - --entrypoints.tcpep.address=:8000
          - [email protected]
          - --certificatesresolvers.default.acme.storage=/cert/acme.json
          - --certificatesresolvers.default.acme.tlschallenge
          # Please note that this is the staging Let's Encrypt server.
          # Once you get things working, you should remove that whole line altogether.
          # - --certificatesresolvers.default.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
          # Production Let's Encrypt server.
          - --certificatesresolvers.default.acme.caserver=https://acme-v02.api.letsencrypt.org/directory
      volumes:
      - name: cert
        persistentVolumeClaim:
          claimName: pvc-traefik
---

apiVersion: v1
kind: Service
metadata:
  name: traefik
spec:
  type: LoadBalancer
  externalTrafficPolicy: Local
  selector:
    app: traefik
  ports:
    - protocol: TCP
      port: 80
      name: web
      targetPort: 80
    - protocol: TCP
      port: 443
      name: websecure
      targetPort: 443
    - protocol: TCP
      port: 8080
      name: admin
      targetPort: 8080
    - protocol: TCP
      port: 8000
      name: tcpep
      targetPort: 8000

---
apiVersion: v1
kind: Service
metadata:
  name: traefikudp
spec:
  type: LoadBalancer
  externalTrafficPolicy: Local
  selector:
    app: traefik
  ports:
    - protocol: UDP
      port: 9000
      name: udpep
      targetPort: 9000
Esto es la instalación de Traefik en si con su dashboard

Como podéis ver, en Deployment.yaml, al principio creo un PV y un PVC NFS para guardar todos los certificados. No sería posible usar Longhorn dado que habrá un ingress en cada nodo, y recordemos que Longhorn no permite RWM en sus volúmenes (Read Write Many).

A parte de eso solo tendréis que modificar vuestro email en la linea 81. Y los puertos 80 y 443 en caso de que tengáis otro ingress en el cluster y no lo hayáis eliminado.

Por último añadir estos 2 archivos más que son para la redirección HTTPS y parámetros de seguridad para el SSL:

HTTPSredirect.yaml

apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
  namespace: default
  name: https-redirect
spec:
  redirectScheme:
    scheme: https

TLSOptions.yaml

apiVersion: traefik.containo.us/v1alpha1
kind: TLSOption
metadata:
  name: secure-tls-option
  namespace: default
spec:
  minVersion: VersionTLS12
  cipherSuites:
    - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384   # TLS 1.2
    - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305    # TLS 1.2
    - TLS_AES_256_GCM_SHA384                  # TLS 1.3
    - TLS_CHACHA20_POLY1305_SHA256            # TLS 1.3
  curvePreferences:
    - CurveP521
    - CurveP384
  sniStrict: true
---
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
  name: security
spec:
  headers:
    frameDeny: true
    sslRedirect: true
    browserXssFilter: true
    contentTypeNosniff: true
    #HSTS
    stsIncludeSubdomains: true
    stsPreload: true
    stsSeconds: 31536000
    useXForwardedFor: true
    customFrameOptionsValue: "SAMEORIGIN"
    referrerPolicy: "same-origin"

Una vez lo tengamos todo listo, entramos a la carpeta y ejecutamos:

kubectl apply -f .

Y con esto ya tendremos Traefik funcionando. Si vamos a http://IP_de_un_worker:8080/ veremos la interfaz de Traefik con los servicios que tengamos creados (por ahora ninguno).


Servicios de Kubernetes con Traefik como ingress

Para esto os pondré una plantilla y ya vosotros la adaptáis como queráis a vuestro caso concreto, en mi caso os pondré de ejemplo el ingress y el deployment de Ghost.

Ghost.yaml

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: ghost
  namespace: ghost
spec:
  storageClassName: longhorn
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ghost
  namespace: ghost
  labels:
    app: ghost
    tier: frontend
    host: byted
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ghost
      tier: frontend
      host: byted
  template:
    metadata:
      namespace: ghost
      labels:
        app: ghost
        tier: frontend
        host: byted
    spec:
      containers:
      - name: ghost
        image: ghost
        imagePullPolicy: Always
        ports:
        - containerPort: 2368
        volumeMounts:
        - name: content
          mountPath: /var/lib/ghost/content
        env:
        - name: url
          value: "https://byted.xyz"
      volumes:
      - name: content
        persistentVolumeClaim:
          claimName: ghost

Ghost-Service-Proxy.yaml

apiVersion: v1
kind: Service
metadata:
  name: ghost
  namespace: ghost
spec:
  ports:
    - protocol: TCP
      name: web
      port: 2368
  selector:
    app: ghost
    tier: frontend
    host: byted
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  namespace: default
  name: ghost-ingress
spec:
  entryPoints:
  - web
  routes:
  - kind: Rule
    match: HostRegexp(`{host:.+}`)
    services:
    - name: ghost
      namespace: ghost
      port: 2368
    middlewares:
    - name: https-redirect
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  namespace: default
  name: ghost-ingress-tls
spec:
  entryPoints:
  - websecure
  routes:
  - kind: Rule
    match: Host(`byted.xyz`) || Host(`www.byted.xyz`)
    services:
    - name: ghost
      namespace: ghost
      port: 2368
  tls:
    certResolver: default
    options:
      name: secure-tls-option
      namespace: default
El primer bloque corresponde al servicio de ghost en si, el segundo bloque corresponde a la redirección HTTP a HTTPS y el tercer bloque corresponde al ingress HTTPS.

Y como podréis ver de la linea 51 a 53 añadimos las opciones de seguridad para el certificado.

Una vez tengamos los dos archivos guardados, entramos en la carpeta donde los hayamos guardado y hacemos:

kubectl apply -f Ghost.yaml
kubectl apply -f Ghost-Service-Proxy.yaml

Antes de aplicar las opciones de seguridad:

Después de aplicar las opciones de seguridad:

Con esto nos ya tendremos nuestra app lista y funcionando con un certificado SSL con puntuación A en el test de SSL Labs (en un futuro explicaré como reforzarlo todavía más para conseguir A+, pero eso ya depende del DNS de cada uno, no de Traefik).

SSL Server Test (Powered by Qualys SSL Labs)
A comprehensive free SSL test for your public web servers.

Servicios externos con Traefik como ingress

En caso de tener un servicio ajeno a Kubernetes, también podemos usar Traefik para exponerlo y generar un certificado SSL.

Si el servicio que tenemos corre bajo HTTP se haría de la siguiente manera:

External-Service.yaml

apiVersion: v1
kind: Service
metadata:
  name: external
  namespace: default
spec:
  ports:
  - name: http
    port: 80
  type: ExternalName
  externalName: 10.0.2.24
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  namespace: default
  name: external-ingress
spec:
  entryPoints:
  - web
  routes:
  - kind: Rule
    match: HostRegexp(`{host:.+}`)
    services:
    - name: external
      port: 80
    middlewares:
    - name: https-redirect
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  namespace: default
  name: external-ingress-tls
spec:
  entryPoints:
  - websecure
  routes:
  - kind: Rule
    match: Host(`external.byted.xyz`)
    services:
    - name: external
      port: 80
    middlewares:
      - name: security
  tls:
    certResolver: default
    options:
      name: secure-tls-option
      namespace: default
El primer bloque corresponde al servicio de ghost en si, el segundo bloque corresponde a la redirección HTTP a HTTPS y el tercer bloque corresponde al ingress HTTPS.

Donde 10.0.2.24 es la IP del host que tiene nuestro servicio y el puerto 80 en todo momento es el puerto donde está el servicio.


Por otro lado si tenemos un servicio que corre bajo HTTPS, se haría de una manera un poco distinta para indicarle a Traefik que es un servicio HTTPS y no HTTP.

External-Service.yaml

apiVersion: v1
kind: Service
metadata:
  name: external
  namespace: default
spec:
  ports:
  - name: https
    port: 8443
  type: ExternalName
  externalName: 10.0.2.24
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  namespace: default
  name: external-ingress
spec:
  entryPoints:
  - web
  routes:
  - kind: Rule
    match: HostRegexp(`{host:.+}`)
    services:
    - name: external
      port: 8443
      scheme: https
    middlewares:
    - name: https-redirect
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  namespace: default
  name: external-ingress-tls
spec:
  entryPoints:
  - websecure
  routes:
  - kind: Rule
    match: Host(`external.byted.xyz`)
    services:
    - name: external
      port: 8443
      scheme: https
  tls:
    certResolver: default
    options:
      name: secure-tls-option
      namespace: default
El primer bloque corresponde al servicio de ghost en si, el segundo bloque corresponde a la redirección HTTP a HTTPS y el tercer bloque corresponde al ingress HTTPS.

Donde 10.0.2.24 es la IP del host que tiene nuestro servicio y el puerto 8443 en todo momento es el puerto donde está el servicio.


Una vez tengamos el archivo guardado, ya sea el HTTP o el HTTPS, entramos en la carpeta donde los hayamos guardado y hacemos:

kubectl apply -f External-Service.yaml

Y con esto doy concluido el tutorial. Cualquier pregunta o duda estoy a vuestra disposición.

Etiquetas

Carlos Mora Clavero

Soy un estudiante al que le gusta aprender cosas nuevas en sus ratos libres sobre sistemas, hardware, programación, etc.