sumarsono.com
Take it with a grain of salt


Cara Mudah Upgrade Ingress Controller Traefik V1 Ke V2 Tanpa Downtime

Posted on

Traefik adalah reverse-proxy yang dibuat dalam konteks artikel ini yaitu reverse-proxy untuk service dalam kubernetes cluster. Dalam konteks artikel ini traefik berperan sebagai ingress controller. Tugasnya adalah untuk meneruskan request dari luar kubernetes cluster menuju service yang ada di dalam kubernetes cluster. Gambar berikut ini akan menjelaskan apa fungsi Traefik

Traefik

Traefik sebagai reverse proxy tentu saja belum sematang HAProxy atau Nginx. Lalu kenapa pakai traefik? karena Traefik sudah cukup stabil untuk habdle request, built-in let's encrypt integration, dan tentunya karena dukungan traefik sebagai kubernetes cluster ingress controller. Informasi lebih lanjut tentang traefik dapat di baca di halaman dokumentasi traefik.

Upgrade Traefik

Traefik yang baru lahir ini, tentu memiliki kekurangan. Salah satu kekurangan yang paling terasa adalah adanya major breaking changes ketika upgrade dari traefik v1.x ke traefik v2.x. Ingress object traefik v1 tidak bisa langsung dipakai oleh traefik v2, begitu juga dengan acme.json (Sertifikat let's encrypt) yang tidak kompatibel dengan traefik v2. Untuk acme.json, untungnya ada konverter yang disediakan, sehingga praktis memudahkan. Konverter acme.json traefik v1 ke traefik v2 bisa di cek di github traefik-migration-tool. Tinggal gunakan tool tersebut untuk konversi acme.json kemudian mount ke pvc yang akan digunakan traefik v2.

Untuk ingress object, ini yang masih PR. Harus manual konversinya. Masalah selanjutnya adalah bagaimana caranya split trafik antara traefik v1 dengan traefik v2. Split trafik ini untuk membagi trafik, trafik ingress object yang sudah dikonversi akan diarahkan ke traefik v2, sedangkan yang belum di konversi akan diarahkan ke traefik v1. Ya, migrasinya pelan-pelan, sambil melihat perkembangan hasil migrasi.

"Anything that can go wrong will go wrong" -- Murphy's law

Tulisan ini berisi sedikit effort yang aku tempuh untuk upgrade traefik v1 ke v2 tanpa downtime.

Topologi

Untungnya, ingress controller diciptakan dengan asumsi adanya load balancer di depan. Oleh sebab itu, sedari awal aku memasang HAproxy di depan kubernetes cluster. Dengan adanya HAproxy didepan kubernetes cluster, maka split trafik antara traefik v1 dengan trafik v2 menjadi mudah. Secara sederhana ada di gambar berikut ini:

Topologi

Deploy Traefik v2

Selanjutnya, Deploy Traefik v2 ke dalam kubernetes cluster. Berkas yaml ini aku ambil dari dokumentasi traefik. Aku hanya melakukan beberapa penyesuaian sesuai kebutuhan, terutama untuk menerima proxy-protocol.

custom-resource.yaml traefik v2.x yang kupakai:

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

---
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: traefikv2

deployment.yaml traefik v2.x yang kupakai:

apiVersion: v1
kind: ServiceAccount
metadata:
  namespace: traefikv2
  name: traefik-ingress-controller

---
kind: Deployment
apiVersion: apps/v1
metadata:
  namespace: traefikv2
  name: traefik
  labels:
    app: traefik

spec:
  replicas: 1
  selector:
    matchLabels:
      app: traefik
  template:
    metadata:
      labels:
        app: traefik
    spec:
      serviceAccountName: traefik-ingress-controller
      nodeSelector:
          kubernetes.io/hostname: kubemaster
      containers:
        - name: traefik
          image: traefik:v2.2.11
          args:
            #- --api.insecure
            - --api=false
            - --accesslog=true
            - --accesslog.filepath=/mytraefik/access.log
            - --accesslog.bufferingsize=100
            - --log.level=INFO
            - --entrypoints.web.Address=:8000
            - --entryPoints.web.proxyProtocol.trustedIPs=192.168.140.66,192.168.140.67,192.168.140.68
            #- --entryPoints.web.proxyProtocol.insecure
            - --entrypoints.websecure.Address=:4443
            - --entryPoints.websecure.proxyProtocol.trustedIPs=192.168.140.66,192.168.140.67,192.168.140.68
            #- --entryPoints.websecure.proxyProtocol.insecure
            - --providers.kubernetescrd
            - --providers.kubernetescrd.ingressclass=traefik-dua
            - --certificatesresolvers.leprod.acme.tlschallenge
            - --certificatesresolvers.leprod.acme.email=sumarsono@mindosolutions.com
            - --certificatesresolvers.leprod.acme.storage=/mytraefik/acme-prod.json
            - --certificatesresolvers.lestaging.acme.tlschallenge
            - --certificatesresolvers.lestaging.acme.email=sumarsono@mindosolutions.com
            - --certificatesresolvers.lestaging.acme.storage=/mytraefik/acme-staging.json
            # Please note that this is the staging Let's Encrypt server.
            # Once you get things working, you should remove that whole line altogether.
            - --certificatesresolvers.lestaging.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
          ports:
            - name: web
              containerPort: 8000
            - name: websecure
              containerPort: 4443
            #- name: admin
            #  containerPort: 8080
          volumeMounts:
          - name: traefikv2-persistent-storage
            mountPath: /mytraefik
      volumes:
      - name: traefikv2-persistent-storage
        persistentVolumeClaim:
          claimName: traefikv2-pvc

service.yaml traefik v2.x yang kupakai:

apiVersion: v1
kind: Service
metadata:
  name: traefik
  namespace: traefikv2
spec:
  clusterIP: 10.99.235.116
  ports:
    - protocol: TCP
      name: web
      port: 8000
    #- protocol: TCP
    #  name: admin
    #  port: 8080
    - protocol: TCP
      name: websecure
      port: 4443
  selector:
    app: traefik

middleware security-header traefik v2 yang kupakai:

apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
  name: security-header
spec:
  headers:
    frameDeny: true
    sslRedirect: true
    browserXssFilter: true
    contentTypeNosniff: true
    #HSTS
    stsIncludeSubdomains: true
    stsPreload: true
    stsSeconds: 31536000

tls.options yang kupakai:

apiVersion: traefik.containo.us/v1alpha1
kind: TLSOption
metadata:
  name: mytlsoption
  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

Hasil deploymentnya adalah sebagai berikut:

NAME                           READY   STATUS    RESTARTS   AGE
pod/traefik-855559f97d-vm5vm   1/1     Running   0          10h

NAME              TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)             AGE
service/traefik   ClusterIP   10.99.235.116    <none>        8000/TCP,4443/TCP   51d

NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/traefik   1/1     1            1           50d

Konfigurasi HAproxy

HAproxy harus dikonfigurasi dalam mode full passtrough, ssl di terminate di Traefik karena SSL manajemen akan dipegang oleh traefik.

Rule HAproxy secara singkat adalah sebagai berikut:

Berikut ini adalah konfigurasi haproxy yang kupakai

bash global log /dev/log local0 log /dev/log local1 notice chroot /var/lib/haproxy stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners stats timeout 30s user haproxy group haproxy daemon

    # Default SSL material locations
    ca-base /etc/ssl/certs
    crt-base /etc/ssl/private

    # Default ciphers to use on SSL-enabled listening sockets.
    # For more information, see ciphers(1SSL). This list is from:
    #  https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
    # An alternative list with additional directives can be obtained from
    #  https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=haproxy
    ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS
    ssl-default-bind-options no-sslv3

    nbproc  4
            cpu-map  1 1
            cpu-map  2 2
            cpu-map  3 3
            cpu-map  4 4

defaults log global mode http option httplog option dontlognull

    errorfile 400 /etc/haproxy/errors/400.http
    errorfile 403 /etc/haproxy/errors/403.http
    errorfile 408 /etc/haproxy/errors/408.http
    errorfile 500 /etc/haproxy/errors/500.http
    errorfile 502 /etc/haproxy/errors/502.http
    errorfile 503 /etc/haproxy/errors/503.http
    errorfile 504 /etc/haproxy/errors/504.http


    option redispatch 
    retries 3 
    timeout http-request 60s
    timeout queue 2m 
    timeout connect 10s 
    timeout client 2m 
    timeout server 2m 
    timeout http-keep-alive 60s
    timeout check 10s 
    maxconn 3000

frontend http_front bind *:80

    mode http
    http-request redirect scheme https code 301 if !{ ssl_fc }

frontend https_front bind *:443 mode tcp option tcplog

    tcp-request inspect-delay 5s
    tcp-request content capture req.ssl_sni len 25
    
    acl sumarsono_com req_ssl_sni -i sumarsono.com || req_ssl_sni -i www.sumarsono.com

    use_backend traefikv2_443 if sumarsono_com

    default_backend ingress_443

backend ingress_80 mode tcp server traefik 127.0.0.1:30815 send-proxy maxconn 1000

backend ingress_443 mode tcp server traefik 127.0.0.1:30814 send-proxy maxconn 1000

backend traefikv2_443 mode tcp server traefikv2 10.99.235.116:4443 send-proxy-v2 maxconn 1000




## Migrasi Ingress Object Traefik v1 ke IngressRoutes Traefik v2

Contoh ingress object traefik v1

```yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  namespace: production
  name: sumarsono.com
  annotations:
    kubernetes.io/ingress.class: traefik
    ingress.kubernetes.io/browser-xss-filter: 'true'
    ingress.kubernetes.io/content-type-nosniff: 'true'
    traefik.ingress.kubernetes.io/redirect-permanent: "true"
    traefik.ingress.kubernetes.io/redirect-regex: ^https?://(www.)?(.*)
    traefik.ingress.kubernetes.io/redirect-replacement: https://${2}
    ingress.kubernetes.io/force-hsts: "true"
    ingress.kubernetes.io/hsts-max-age: "2592000"
    ingress.kubernetes.io/hsts-include-subdomains: "true"
    ingress.kubernetes.io/hsts-preload: "false"
    ingress.kubernetes.io/frame-deny: "true"
    ingress.kubernetes.io/custom-frame-options-value: "SAMEORIGIN"
    ingress.kubernetes.io/content-security-policy: upgrade-insecure-requests
spec:
  rules:
  - host: sumarsono.com
    http:
      paths:
      - path: /
        backend:
          serviceName: apache
          servicePort: 80
  - host: www.sumarsono.com
    http:
      paths:
      - path: /
        backend:
          serviceName: apache
          servicePort: 80

Ingress object tersebut aku ubah menjadi IngressRoutes object traefik v2:

apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: sumarsono.com
  namespace: production
  annotations:
    kubernetes.io/ingress.class: "traefik-dua"
spec:
  entryPoints:
    - web
  routes:
  - match: "Host(`sumarsono.com`) || Host(`www.sumarsono.com`)"
    kind: Rule
    services:
    - name: apache
      port: 80
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: sumarsono.com-tls
  namespace: production
  annotations:
    kubernetes.io/ingress.class: "traefik-dua"
spec:
  entryPoints:
    - websecure
  routes:
  - match: "Host(`sumarsono.com`) || Host(`www.sumarsono.com`)"
    kind: Rule
    services:
    - name: production
      port: 80
    middlewares:
      - name: security-header
        namespace: default
  tls:
    certResolver: leprod
    options:
      name: mytlsoption
      namespace: default

Itulah ringkasan engga jelas dari effortku untuk upgrade traefik v1 ingress controller ke traefik v2 ingress controller tanpa downtime. Take with a grain of salt!