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 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:
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:
- Semua request HTTP redirect ke HTTPS
- Semua request diarahkan ke Traefik v1 kecuali yang ada dalam ACL HAproxy
- HAproxy send-proxy ke Traefik v1 karena traefik v1 tidak send-proxy-v2
- HAproxy send-proxy-v2 ke Traefik v2
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!