หลังจากตอนที่แล้วเราก็ได้ทำความรู้จักกับศัพท์เทคนิคใน Kubenetes ไปแล้ว ตอนนี้เราจะมาลองดูกันครับว่าการติดตั้งโปรแกรมจริงๆ ใน Kubernetes จะทำยังไงบ้าง
สำหรับวันนี้เราจะลองติดตั้ง Sentry ซึ่งเป็นบริการจับ exception กันดูครับ
System requirements
เวลาติดตั้ง Sentry จะประกอบด้วย
- PostgreSQL เป็น Database server (Sentry 8 ไม่รองรับการติดตั้งใน MySQL แล้ว)
- Redis
- ตัว Sentry เองเป็นแอพ Django ซึ่งจะแบ่งเป็นส่วนย่อยๆ ดังนี้
- Web server
- Worker สำหรับประมวลผล event
- Cron
ติตตั้ง Postgres
สำหรับ Postgres นั้นจะติดตั้งจาก Docker Hub ได้เลย โดยจะติดตั้งด้วย Replication Controller ไม่ใช้ Deployment เนื่องจากว่ามันทำ Rolling deploy ไม่ได้อยู่ดี (ถ้าอยากลองทำเป็น Deployment ก็ทำได้ไม่ผิดเช่นกัน)
ปกติแล้วไฟล์ config Kubernetes ทั้งหมดของผมจะถูกเก็บไว้ใน Git Repo แยกแต่ละ cluster ไป เพื่อให้สะดวกในการ track changes สำหรับไฟล์นี้ก็ไว้ที่ kube/postgres/postgres.yaml
ซึ่งจริงๆ แล้วก็สามารถตั้งชื่อไฟล์ได้ตามสะดวก
apiVersion: v1
kind: ReplicationController
metadata:
name: postgres
spec:
replicas: 1
selector:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:9.6.4
ports:
- containerPort: 5432
resources:
requests:
memory: "256Mi"
cpu: "10m"
readinessProbe:
tcpSocket:
port: 5432
volumeMounts:
- mountPath: /var/lib/postgresql/data
name: postgres
env:
- name: POSTGRES_PASSWORD
value: postgres
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumes:
- name: postgres
gcePersistentDisk:
pdName: postgres
fsType: ext4
อย่าเพิ่งโหลดเข้าไปนะครับ จะเห็นว่าเรามีการกำหนด volume ด้วยซึ่งจะชี้ไปที่ disk ชื่อ postgres
ฉะนั้นเราจะต้องเข้าไปสร้างใน console เสียก่อน โดยกำหนดชื่อให้ตรงกับ pdName
ก็คือ postgres และที่สำคัญมากคือจะต้อง Zone ให้ตรงกับเครื่องเรา
เมื่อสร้างเสร็จแล้วก็สามารถโหลดเข้าไปได้เลยด้วยคำสั่ง kubectl apply -f kube/postgres/postgres.yaml
ถัดมาเราจะต้องสร้าง service ไว้ที่ kube/postgres/service.yaml
apiVersion: v1
kind: Service
metadata:
name: postgres
labels:
app: postgres
spec:
selector:
app: postgres
ports:
- port: 5432
ก็คือระบุว่า service postgres นั้น จะไปตามหา Pod ที่มี label app: postgres
แล้วให้เชื่อมเข้าหา port 5432
Note เพิ่มเติม:
- ปัจจุบัน Replication Controller ถูกแทนด้วย Replica Set แล้ว แต่ผมพบว่าการเขียน RC เขียนง่ายกว่า ก็เลยยังใช้อยู่
- Kubernetes สามารถสร้าง disk ให้เราอัตโนมัติได้ด้วยถ้าเราเขียน PersistentVolumeClaim แต่ผมพบว่ามันจะซับซ้อนกว่า ก็เลยยังไม่ใช้ และถ้าจะเซตให้ GKE สร้าง SSD Disk ได้จะต้องเซตเพิ่มอีกด้วย
- ใน production แล้วไม่แนะนำให้สร้างของ stateful ไว้ใน Kubernetes เช่น database server, cache ที่มีการ save ลง disk เนื่องจากว่ามันใช้ความสามารถในการ scale ของ Kubernetes ไม่ได้เลย (เพราะต้องมี disk) และเวลา Kubernetes สร้าง Pod ใหม่ก็จะต้องถอด Disk ต่อเข้าใหม่ (ถึงจะอยู่ในเครื่องเดิม) ซึ่งใช้เวลาพอสมควร
- แต่สำหรับบทความนี้แล้วเราจะไม่เอางบไปสร้างเครื่องเพิ่ม ฉะนั้นก็ใส่ไว้นี้นี่แหละ
ติดตั้ง Redis
สำหรับ Redis ก็จะติดตั้งคล้ายๆ กันเลย ก็คือมีไฟล์ kube/redis/redis.yaml
apiVersion: v1
kind: ReplicationController
metadata:
name: redis
spec:
replicas: 1
selector:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:32bit
ports:
- containerPort: 6379
resources:
requests:
memory: "64Mi"
cpu: "3m"
readinessProbe:
tcpSocket:
port: 6379
และ kube/redis/service.yaml
apiVersion: v1
kind: Service
metadata:
name: redis
labels:
app: redis
spec:
selector:
app: redis
ports:
- port: 6379
เสร็จแล้วก็ติดตั้งเข้าไปได้เลย
kubectl apply -f kube/redis/redis.yaml
kubectl apply -f kube/redis/service.yaml
ติดตั้ง Sentry
ส่วน Sentry นั้น ขั้นแรกจะต้องสร้าง Secret key ขึ้นมาก่อนด้วยคำสั่ง kubectl run --restart=Never -i --rm --image=sentry sentry config generate-secret-key
(เทียบเท่า docker run --rm sentry config generate-secret-key
)
$ kubectl run --restart=Never -i --rm --image=sentry sentry config generate-secret-key
suutz#6a439htslt53uhd=%!8z&uzy2-knywxt!m!*4v!7rdwu
$ echo -n 'suutz#6a439htslt53uhd=%!8z&uzy2-knywxt!m!*4v!7rdwu' | base64 -w0
c3V1dHojNmE0MzlodHNsdDUzdWhkPSUhOHomdXp5Mi1rbnl3eHQhbSEqNHYhN3Jkd3U=
Tip: ใส่ space 1 ตัวหน้า command เพื่อไม่ให้เก็บใน history
จากนั้นเราจะเอา secret อันนี้ไปเก็บไว้ในไฟล์ Secret ที่ kube/sentry/secrets.yaml
apiVersion: v1
kind: Secret
metadata:
name: sentry
type: Opaque
data:
secret: c3V1dHojNmE0MzlodHNsdDUzdWhkPSUhOHomdXp5Mi1rbnl3eHQhbSEqNHYhN3Jkd3U=
Secret จะเป็น key-value ซึ่งใน data นั้นเราสามารถระบุ key เป็นอะไรก็ได้ (แต่จะต้องถูกรูปแบบ DNS name ด้วย) และส่วนของ value นั้นจะต้องระบุเป็นแบบ Base64 เพราะเราสามารถเก็บ secret ที่เป็น binary ได้ด้วย
(Note: ปกติแล้วเราจะเก็บรหัส database ไว้ในนี้ด้วย แต่เนื่องจากเราไม่ได้สร้าง user ใหม่ให้ postgres ก็เลยไม่มีรหัสให้เก็บ จะลองทำดูก็ได้ครับ)
โหลด Secret เข้าไปด้วยคำสั่ง kubectl apply -f kube/sentry/secrets.yaml
เสร็จแล้วก็สร้าง Deployment ของ Sentry ขึ้นมาที่ kube/sentry/sentry.yaml
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: sentry
spec:
replicas: 1
template:
metadata:
labels:
app: sentry
spec:
containers:
- name: sentry
image: sentry:8.19.0
resources:
requests:
memory: "64Mi"
cpu: "1m"
ports:
- containerPort: 9000
readinessProbe: &probe
httpGet:
path: /_health/
port: 9000
livenessProbe: *probe
env:
- name: SENTRY_POSTGRES_HOST
value: postgres
- name: SENTRY_DB_NAME
value: postgres
- name: SENTRY_DB_USER
value: postgres
- name: SENTRY_DB_PASSWORD
value: postgres
- name: SENTRY_REDIS_HOST
value: redis
- name: SENTRY_SECRET_KEY
valueFrom:
secretKeyRef:
name: sentry
key: secret
volumeMounts:
- mountPath: /var/lib/sentry/files
name: sentry
volumes:
- name: sentry
gcePersistentDisk:
pdName: sentry
fsType: ext4
อย่าลืมสร้าง Disk ชื่อ sentry และโหลดเข้าไปด้วยคำสั่ง kubectl apply -f kube/sentry/sentry.yaml
สร้าง service ให้ Sentry ที่ kube/sentry/service.yaml
apiVersion: v1
kind: Service
metadata:
name: sentry
labels:
app: sentry
spec:
selector:
app: sentry
ports:
- port: 80
targetPort: 9000
ซึ่งจะกำหนดว่า port 80 ของ service IP ให้ map ไปที่ port 9000 ของ Sentry เสร็จแล้วก็โหลดด้วยคำสั่ง kubectl apply -f kube/sentry/service.yaml
สุดท้ายสร้าง cron และ worker ขึ้นมา ซึ่งจะใช้ config คล้ายๆ กัน แบบนี้
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: sentry-worker
spec:
replicas: 1
template:
metadata:
labels:
app: sentry-worker
spec:
containers:
- name: sentry-worker
image: sentry:8.19.0
args: [run, worker]
resources:
requests:
memory: "64Mi"
cpu: "1m"
env:
- name: SENTRY_POSTGRES_HOST
value: postgres
- name: SENTRY_DB_NAME
value: postgres
- name: SENTRY_DB_USER
value: postgres
- name: SENTRY_DB_PASSWORD
value: postgres
- name: SENTRY_REDIS_HOST
value: redis
- name: SENTRY_SECRET_KEY
valueFrom:
secretKeyRef:
name: sentry
key: secret
---
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: sentry-cron
spec:
replicas: 1
template:
metadata:
labels:
app: sentry-cron
spec:
containers:
- name: sentry-cron
image: sentry:8.19.0
args: [run, cron]
resources:
requests:
memory: "64Mi"
cpu: "1m"
env:
- name: SENTRY_POSTGRES_HOST
value: postgres
- name: SENTRY_DB_NAME
value: postgres
- name: SENTRY_DB_USER
value: postgres
- name: SENTRY_DB_PASSWORD
value: postgres
- name: SENTRY_REDIS_HOST
value: redis
- name: SENTRY_SECRET_KEY
valueFrom:
secretKeyRef:
name: sentry
key: secret
(สามารถใช้ ---
คั่นระหว่างเอกสารใน yaml แล้วโหลดทีเดียวพร้อมกันก็ได้)
Note: โดยปกติแล้วถ้ามี environment ที่แชร์กันเรามักจะเขียน ConfigMap
สร้าง Database
ในการติดตั้ง Sentry จะต้องโหลด database เข้าไป
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
postgres-5d9j4 1/1 Running 0 28m
redis-3r5wz 1/1 Running 0 26m
sentry-1066741037-pkwbb 1/1 Running 0 2m
sentry-cron-2987324329-l6tll 1/1 Running 0 2m
sentry-worker-415181135-gxvg3 1/1 Running 0 3s
$ kubectl exec -it exec -it sentry-1066741037-pkwbb -- bash
root@sentry-1066741037-pkwbb:/# sentry upgrade
Syncing...
Creating tables ...
Creating table django_admin_log
....
Created internal Sentry project (slug=internal, id=1)
Would you like to create a user account now? [Y/n]: Y
Email: ใส่อีเมล
Password: ใส่รหัส
Repeat for confirmation: ใส่รหัส
Should this user be a superuser? [y/N]: y
User created:
Added to organization: sentry
- Loading initial data for sentry.
...
root@sentry-1066741037-pkwbb:/# exit
เสร็จแล้วก็ลองเข้า Sentry ดูได้เลยจากคำสั่ง kubectl --namespace sentry port-forward sentry-1066741037-pkwbb 9000
ซึ่งจะทำให้ kubectl forward port 9000 บนเครื่องเราเข้าไปบน Pod

ลองล็อคอินได้ แต่อย่าเพิ่งติดตั้ง Sentry เพราะเราจะเอา Sentry ขึ้นเป็นเว็บสวยๆ ก่อน
Reverse proxy
ปกติแล้วเวลาเราจะเปิด port ภายนอกใน Kubernetes เราจะใช้ service ประเภท LoadBalancer หรือใช้ Ingress เพื่อให้ Kubernetes สร้าง Load balancer ให้ แต่เนื่องจาก Load balancer ราคาแพงมากเราเลยจะเปิด port ที่เครื่องโดยตรงให้วิ่งเข้าสู่ Reverse proxy ภายใน
Reverse proxy ที่เราจะใช้คือ Traefik ซึ่งเขียนขึ้นในภาษา Go มันจะเข้าไปอ่าน Ingress ของ Kubernetes ให้เรา ทำให้ไม่ต้องแก้ไข config เวลาใช้งาน และถึงจะเป็นของใหม่ Traefik ก็พิสูจน์จากการใช้งานบน production ทุกระบบของ Wongnai แล้ว
เราจะติดตั้ง Traefik ด้วย RC ดังนี้ครับ kube/traefik/traefik.yaml
apiVersion: v1
kind: ReplicationController
metadata:
name: traefik
namespace: sentry
spec:
replicas: 1
selector:
app: traefik
template:
metadata:
labels:
app: traefik
spec:
hostNetwork: true
containers:
- name: traefik
image: traefik
args:
- --web
- --kubernetes
ports:
- containerPort: 80
- containerPort: 443
- containerPort: 8080
resources:
requests:
memory: "64Mi"
cpu: "1m"
readinessProbe: &probe
httpGet:
path: /health
port: 8080
livenessProbe: *probe
การกำหนด hostNetwork: true
จะทำให้ kubernetes เปิดด้วยโหมด --net=host
ทำให้เราเปิด port ได้เลย
ถ้าต้องการให้ Traefik ออก Let’s Encrypt อัตโนมัติ ก็สามารทำได้โดยเพิ่ม args ดังต่อไปนี้
args:
- --web
- --kubernetes
- --acme
- --acme.acmelogging
- --acme.storage=/certs/traefik.json
- --acme.email=กรอกอีเมล
- --acme.entrypoint=https
- --acme.onhostrule
- --entryPoints=Name:https Address::443 TLS
- --entryPoints=Name:http Address::80 Redirect.EntryPoint:https
- --defaultentrypoints=http,https
และอย่าลืม mount /certs/ เข้าไปด้วย โดยการตั้งค่านี้จะทำให้เมื่อเราสร้าง host ใหม่แล้ว Traefik จะออก Let’s Encrypt ทันที และให้ redirect HTTP -> HTTPS อัตโนมัติ
(ในบทความนี้จะยังไม่ใช้ฟีเจอร์นี้นะครับ แต่ไปลองเล่นเองได้)
ถัดมา เพื่อความปลอดภัย (ของกระเป๋าตัง) เราจะต้องปิด Google Cloud Ingress Controller เพื่อป้องกันไม่ให้ Kubernetes สร้าง Load balancer อัตโนมัติ
$ gcloud container clusters update cluster-name --update-addons=HttpLoadBalancing=DISABLED
Updating cluster-name...done.
Updated [https://container.googleapis.com/v1/projects/project-name/zones/asia-southeast1-a/clusters/cluster-name].
ลองเช็คใน kubectl --namespace kube-system get pods
ว่าไม่มี l7-default-backend
สุดท้ายเราจะสร้าง Ingress เพื่อกำหนดว่าให้ forward URL ที่กำหนดเข้าไปที่ Sentry
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: regist
annotations:
kubernetes.io/ingress.class: "traefik"
spec:
rules:
- host: sentry.mysite.com
http:
paths:
- path: /
backend:
serviceName: sentry
servicePort: 80
เสร็จแล้วก็เข้าไป เปิด port 80 และลองเข้าเว็บดูก็จะสามารถเริ่มใช้งาน Sentry ได้เลย

Small scale cluster
ประเด็นสำคัญที่ทำให้อยากเขียนเรื่อง Small scale ก็คงเป็นว่าอยากแชร์วิธีการใช้ Kubernetes โดยไม่ใช้ Load balancer นี่ล่ะครับ เพราะถ้าเป็นบริษัทการจะซื้อ Load Balancer ไม่ใช่เรื่องแปลก โดยเฉพาะบน cloud แต่พอเราใช้เครื่องๆ เดียวแบบนี้แล้ว การซื้อ Load balancer กลายเป็นค่าใช้จ่ายที่อาจจะแพงกว่าเครื่องเสียอีก
แล้ว Kubernetes ก็พยายามผลักดันเหลือเกินให้เราซื้อให้ได้ ไม่ว่าจะเป็น service หรือ ingress ที่สั่งทีเดียวได้ load balancer มาเลย ตอนผมตัดสินใจว่าเว็บจะใช้ Kubernetes ก็เลยต้องมานั่งนึกข้อดี-ข้อเสียอยู่ ว่าเราจะใช้ Kubernetes ทำไมถ้าจะดื้อกับวิธีของมันแบบนี้ เราใช้ Docker Compose แบบเดิมไม่ได้หรอ
ประเด็นสำคัญที่ยังเลือก Kubernetes อยู่ก็คงเป็นเรื่องของ Rolling deploy ที่อยากทำนานแล้ว แต่ Compose ทำไม่ได้, เรื่องว่าจะเอาไว้ทดลองเล่น Kubernetes ด้วย และก็เป็นทางเลือกที่ future proof ดีว่าจะขยายระบบในอนาคตก็แค่แก้ตัวเลขไม่กี่ที่ก็ได้แล้ว
ถามว่ามีปัญหามั้ยกับการเปิด port ตรงๆ แบบนี้ ก็บอกเลยว่า downtime เป็นเรื่องหลีกเลี่ยงไม่ได้เลยครับ เพราะว่า
- เราไม่มีเครื่อง spare เลย เวลา node upgrade ก็คือต้อง schedule downtime อย่างเดียว
- และจะเปิด 2 เครื่องตอนอัพเกรดก็ไม่ได้เพราะว่าไม่มี load balancer ถ้า ip เครื่องหลักล่มไปก็บ๊ายบาย
- ไว้มีเวลาจะลองวิจัยตรงนี้ดูว่า minimize downtime ได้มั้ย พอมี solution ในหัวอยู่ที่อยากลอง automate ดู
- และเรา rolling deploy ตัวโปรแกรมที่อยู่ port 80 ไม่ได้เลย (ในตัวอย่างคือ traefik) เพราะมันไม่ได้ใช้ service ip ที่ยังมีการ route traffic ไป pod อื่นได้
- ของ TipMe ใช้ Caddy อยู่ ถ้าจะแก้ forwarding rule นี่คือ restart อย่างเดียวเลย
ซึ่งก็เป็น tradeoff ที่คิดว่ายอมรับได้ เพราะเว็บเราคงไม่จำเป็นจะต้อง 100% up จะมี scheduled downtime บ้าง ก็ยังพอรับได้
สำหรับใครที่จะทำเว็บเล็กๆ อยู่และอยู่บน Docker อยู่แล้ว Kubernetes ก็เป็นตัวเลือกนึงที่ยังพอเป็นไปได้ อาจจะเหนื่อยหน่อย แต่ก็จะทำให้ในอนาคตถ้ามีการขยับขยายก็สามารถทำได้ง่าย