Small Scale Kubernetes Part III: ทดลองติดตั้ง Sentry

หลังจากตอนที่แล้วเราก็ได้ทำความรู้จักกับศัพท์เทคนิคใน 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 ก็เป็นตัวเลือกนึงที่ยังพอเป็นไปได้ อาจจะเหนื่อยหน่อย แต่ก็จะทำให้ในอนาคตถ้ามีการขยับขยายก็สามารถทำได้ง่าย

Small Scale Kubernetes Part II: ทำความเข้าใจ Kubernetes

ช่วงนี้งานเยอะมากครับ ตอนแรกว่าจะ publish ตอน 2 กลางสัปดาห์ แต่ก็ไม่มีเวลาเขียนเลย

สำหรับตอนนี้จะเป็นส่วนของทฤษฎีที่เกี่ยวกับ Kubernetes ครับ ซึ่งจะใช้คำศัพท์เฉพาะตัวเยอะพอสมควร ซึ่งอยากแนะนำให้ดูภาพประกอบจาก The Children’s Illustrated Guide to Kubernetes ด้วยครับ คิดว่าเป็นบทความที่เขียนดีมากๆ ผมก็เคยยืมไปนำเสนอในบริษัทมาแล้วหนหนึ่ง

Kubernetes

หน้าที่ของ Kubernetes มีอยู่ 4 อย่างด้วยกัน

  1. ทำให้จัดการเครื่องคอมพิวเตอร์หลายๆ เครื่อง ได้จากที่เดียว
  2. วาง container ไว้ในเครื่องคอมพิวเตอร์เหล่านั้น ไม่ให้แออัดเกินไป
  3. จับตาดู container ที่วางไว้ไม่ให้มันงอแง
  4. ให้บริการ service และ resource ที่ container จำเป็นต้องใช้ในการทำงาน

ในบทความนี้เราจะลองมาดูกันว่า Kubernetes ทำหน้าที่เหล่านี้อย่างไรบ้าง

Cluster Design

By Khtan66 (Own work) [CC BY-SA 4.0 (http://creativecommons.org/licenses/by-sa/4.0)], via Wikimedia Commons

ในระบบ Kubernetes แบ่งเครื่องคอมพิวเตอร์เป็น 2 แบบ คือ

  1. Master ทำหน้าที่ให้บริการ Kubernetes API (เป็น REST)
  2. Node ทำหน้าที่ run container

ซึ่งเราสามารถมีเครื่องในประเภทหนึ่งมากกว่า 1 ตัวก็ได้

Master

Master จะให้บริการ REST API ซึ่งโปรแกรม kubectl ใช้อยู่ในเบื้องหลัง

เวลาเราสั่งให้มันสร้าง object สักอย่าง (เช่น Pod) Master ก็จะเก็บ data ที่เราส่งเข้ามาไว้ใน etcd แล้วก็จัดหา Node ที่เหมาะสมให้ติดตั้ง Pod ตัวนั้น

ฉะนั้นแล้วถ้า Master ตาย ก็จะไม่สามารถสร้าง Pod หรือใช้ kubectl ได้ แต่ Container ที่รันอยู่ทั้งหมดยังสามารถใช้งานปกติ

Node

ในฝั่งของ Node นั้นก็จะมีหน้าที่รัน Container ต่างๆ ซึ่งเราสามารถดูสถานะของ node ได้ด้วยคำสั่งดังนี้

$ kubectl get nodes
NAME                         STATUS    AGE       VERSION
gke-tipme-n1-a7ee1a90-mwzz   Ready     6d        v1.7.2
$ kubectl describe no gke-tipme-n1-a7ee1a90-mwzz
[...]
Labels:         beta.kubernetes.io/instance-type=n1-standard-1
            failure-domain.beta.kubernetes.io/region=asia-southeast1
            failure-domain.beta.kubernetes.io/zone=asia-southeast1-a
[...]
Capacity:
 cpu:       1
 memory:    3794520Ki
 pods:      110
[...]
Non-terminated Pods:
  Namespace      Name                       CPU Requests    CPU Limits  Memory Requests Memory Limits
  ---------      ----                       ------------    ----------  --------------- -------------
  kube-system    event-exporter-v0.1.4-4272745813-86m8f     0 (0%)      0 (0%)      0 (0%)      0 (0%)
  kube-system    fluentd-gcp-v2.0-z02z6             100m (10%)  0 (0%)      200Mi (5%)  300Mi (8%)
  kube-system    heapster-v1.4.0-807765746-p8pd0            138m (13%)  138m (13%)  301656Ki (7%)   301656Ki (7%)
  kube-system    kube-dns-1413379277-mb7gl          260m (26%)  0 (0%)      110Mi (2%)  170Mi (4%)
  kube-system    kube-dns-autoscaler-3880103346-sfxdw       20m (2%)    0 (0%)      10Mi (0%)   0 (0%)
  kube-system    kube-proxy-gke-tipme-n1-a7ee1a90-mwzz      100m (10%)  0 (0%)      0 (0%)      0 (0%)
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  CPU Requests  CPU Limits  Memory Requests Memory Limits
  ------------  ----------  --------------- -------------
  905m (90%)    138m (13%)  2087512Ki (55%) 2617944Ki (68%)

(ProTip: สามารถย่อ nodes เหลือ no ได้ เช่น kubectl get no)

จะเห็นว่า Kubernetes นั้นจะมีการบันทึกไว้ว่าเครื่องนี้มี resource เท่าไร ทั้ง CPU Core และ Memory นอกจากนี้ยังมี Label กำกับไว้ด้วย โดย Kubernetes จะแปะมาให้เราอัตโนมัติ เช่นใน GKE ก็จะมีการแปะ instance type, node pool, zone, region ให้ แต่ถ้าจะแปะเพิ่มเองก็ทำได้เช่นกัน

Pod

หน่วยเล็กที่สุดใน Kubernetes เรียกว่า Pod ซึ่งมันคือ กลุ่มของ container ที่จะอยู่ด้วยกัน ซึ่งแปลว่า

  • 1 Pod จะอยู่บน 1 Node เท่านั้น ไม่ว่า pod นั้นจะมี container กี่ตัวก็ตาม
  • ภายใน Pod จะ share network card กัน แปลว่าถ้ามี container นึงเปิด port 3000 อีก container หนึ่งสามารถเข้า localhost:3000 เพื่อคุยกันได้เลย (แต่จะเปิด port 3000 พร้อมกันไม่ได้)
  • ฉะนั้น 1 Pod จะมี 1 IP เท่านั้น ไม่ได้มีตามจำนวน container

ใน use case 90% 1 Pod จะมีเพียงแค่ 1 Container เท่านั้น สำหรับตัวอย่าง use case ที่ 1 Pod มีหลาย container ก็จะเป็นขั้น advance หน่อย เช่น

  • Kubernetes DNS เคยใช้ 3 container แบบนี้ (ปัจจุบันเปลี่ยน design ไปแล้ว)
    • Container ตัวนึงจะรัน SkyDNS เป็น DNS Server
    • SkyDNS ไม่รู้จัก Kubernetes ก็เลยจะต้องมีโปรแกรมตัวนึงที่อ่าน Kubernetes API มาแล้วเขียนลง database ให้ SkyDNS อ่าน
    • สำหรับ Database ที่ใช้ก็คือ etcd ฉะนั้นเลยต้องมี etcd ส่วนตัวด้วย
  • ผมเคยรัน Django กับ static server ใน Pod เดียวกัน เพราะจะแชร์ volume กัน ให้ Django collectstatic ไปใส่ volume ที่ใช้ร่วมกัน แล้วให้ web server serve

ข้อสำคัญของ Pod คือ Pod เป็น Immutable เช่นเดียวกับ Container เมื่อสร้าง Pod แล้วจะไม่สามารถแก้ไขได้ และ Pod ยัง #yolo ด้วย เมื่อตายแล้วจะไม่นำกลับมา start ใหม่อีกครั้ง

สำหรับหน้าตาของ Pod เราได้เห็นไปในตอนที่แล้วแล้ว คราวนี้จะลองมาเพิ่มของเล่นดูบ้าง

apiVersion: v1
kind: Pod
metadata:
  name: postgres
  labels:
    app: postgres
spec:
  containers:
  - name: postgres
    image: postgres
    env:
    - name: POSTGRES_PASSWORD
      value: postgres
    - name: PGDATA
      value: /var/lib/postgresql/data/pgdata
    resources:
      requests:
        memory: "256Mi"
        cpu: "30m"
      limits:
        memory: "512Mi"
        cpu: "1"
    livenessProbe:
      tcpSocket:
        port: 5432
    readinessProbe:
      tcpSocket:
        port: 5432
    volumeMounts:
    - mountPath: /var/lib/postgresql/data
      name: postgres
  volumes:
  - name: postgres
    gcePersistentDisk:
      pdName: postgres
      fsType: ext4

จะสังเกตว่าจาก Pod definition นั้น จะเพิ่มความสามารถมากกว่า Docker ดังนี้

  • Pod สามารถกำหนด resource request ได้ ซึ่ง Kubernetes จะไม่ยัด Pod ลงในเครื่องเดียวจนเกิน resource ที่มีอยู่ของเครื่องนั้น เช่น ถ้าเครื่องแรม 1GB ก็สามารถรัน Pod ที่ request แรม 256MB ได้ 4 ตัวเท่านั้น
    • สำหรับ CPU Request นั้น Kubernetes กำหนดให้ 1 core (vCPU) = 1 แต่ปกติแล้วโปรแกรมไม่น่าจะใช้ CPU 100% ตลอดเวลา เราก็จะซอย CPU เล็กลงไป เช่น 0.5 ก็คือครึ่ง CPU ปกติจะนิยมเขียนกันในหน่วย millicpu เช่นในตัวอย่าง 30m ก็เท่ากับ 0.03 CPU Core (3% ของ core)
    • ถ้า resource เต็มทุกเครื่อง Pod จะติดสถานะ Pending ไม่ได้รัน ฉะนั้นการกำหนด resource ให้เหมาะสมเป็นเรื่องสำคัญ
  • สำหรับ resource limit นั้นจะเหมือนกับของ Docker นั่นคือถ้าใช้แรมเกิน 512MB ปุ๊บ pod จะโดนยิงทิ้งทันที ส่วน CPU Limit นั้นจะถูก throttle แทน
    • การบังคับใช้ CPU Limit จะต้องใช้ Container-optimized OS เท่านั้นถึงจะมีผล
  • เราสามารถกำหนด livenessProbe/readinessProbe ได้ โดย Kubernetes จะ poll TCP เราเรื่อยๆ หรือจะให้ยิง web request ก็ได้ เพื่อตรวจสอบว่า Pod พร้อมทำงานอยู่ ถ้าหากไม่สามารถเชื่อมต่อได้ หรือ web request return error ก็จะทำดังนี้
    • ถ้าเป็น readinessProbe จะเอาออกจาก Load balancer
    • ถ้าเป็น livenessProbe pod จะถูกปิด

นอกจากนี้ Pod ยังสามารถ mount disk ได้ด้วย ซึ่งในตัวอย่างจะใช้ disk ชื่อ postgres ใน GCE (เราจะต้องสร้างเตรียมไว้ก่อน) เวลา Pod ไปอยู่เครื่องไหน Kubernetes ก็จะบอก Compute Engine ให้นำ disk ไปต่อกับเครื่องนั้น และในเครื่องนั้นก็จะเอาไปต่อเข้าใน container อีกที และถ้าลบ Pod ก็จะปลด disk ออกจากเครื่องนั้นด้วย

ReplicationController

ปัญหาของ Pod คือมันมีชีวิตอยู่ได้แค่รอบเดียว เจ้า ReplicationController จะทำหน้าที่สร้าง Pod อีกทีหนึ่งให้รักษาจำนวนที่กำหนดไว้ตลอด อาจจะเทียบกับ Autoscale Group ใน AWS ก็ได้

หน้าตา ReplicationController จะเป็นแบบนี้

apiVersion: v1
kind: ReplicationController
metadata:
  name: postgres
spec:
  replicas: 1
  selector:
    app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres
        env:
        - name: POSTGRES_PASSWORD
          value: postgres
        - name: PGDATA
          value: /var/lib/postgresql/data/pgdata
        resources:
          requests:
            memory: "256Mi"
            cpu: "30m"
          limits:
            memory: "512Mi"
            cpu: "1"
        livenessProbe:
          tcpSocket:
            port: 5432
        readinessProbe:
          tcpSocket:
            port: 5432
        volumeMounts:
        - mountPath: /var/lib/postgresql/data
          name: postgres
      volumes:
      - name: postgres
        gcePersistentDisk:
          pdName: postgres
          fsType: ext4

จะสังเกตว่าข้างใน spec.template นี่มันก๊อปมาจาก spec ของ Pod ทั้งดุ้น และจะมีการกำหนด replicas: 1 แปลว่าเอา 1 Pod ถ้าจะเอาเยอะๆ ก็แก้เลขได้เลย

(ReplicationController ไม่ใช่ immutable สามารถแก้ไขเลขได้ตลอดเวลาด้วยคำสั่ง kubectl edit rc postgres หรือ kubectl scale rc postgres --replicas=10 ก็ได้)

พอสร้างตามนี้แล้วถ้าเรา kubectl get pod ออกมาดู จะเห็นว่าชื่อ Pod ที่สร้างขึ้นมีข้อความสุ่มต่อท้าย และถ้าเราลองลบ Pod นั้นดู ก็จะเห็นว่าจะมี Pod ใหม่ถูกสร้างมาทดแทนตลอดเวลา

(อ่านเบื้องหลังเกมนี้ได้ blog Google Cloud)

ถามว่าทำแบบนี้ดีกว่าไป stop/start container ปกติยังไง?
คำตอบคือ Pod ที่ถูกสร้างใหม่ไม่จำเป็นต้องอยู่ที่เดิม ซึ่งจะมีประโยชน์มากๆ เวลาเราจะ maintenance เครื่องเราสามารถใช้ท่านี้ได้

$ kubectl cordon gke-tipme-n1-a7ee1a90-mwzz
$ kubectl drain gke-tipme-n1-a7ee1a90-mwzz

โดยคำสั่ง cordon จะทำให้เครื่องที่ระบุไม่มีการนำ pod ใหม่นำมาติดตั้ง พอเราสั่ง drain จะทำให้ pod ทุกตัวในเครื่องนั้นจะถูกลบ ReplicationController ก็จะรีบสร้าง pod มาชดเชยในเครื่องอื่นๆ ทันที

(จริงๆ drain จะรวม cordon อยู่แล้ว ถ้า drain เล่นไปแล้วจะยกเลิกให้ uncordon node นั้นๆ แล้วอาจจะต้องไปลบ pod เพื่อให้มันกลับมาสร้างใหม่ที่เดิม)

ถ้า Pod เปิดวนได้หลายครั้ง ก็จะทำให้ย้ายเครื่องได้ไม่สะดวกเท่าลบสร้างใหม่เรื่อยๆ

Service

ปัญหาถัดมาคือถ้าเรามี pod หลายตัว แล้วเราจะติดต่อหา pod ยังไงดี? แถม pod IP ไม่ fix ด้วยนะ ถ้าโดนลบ pod ใหม่ก็จะเปลี่ยน IP ใหม่

วิธีการใน Kubernetes ก็คือเราจะต้องประกาศ service หน้าตาแบบนี้

apiVersion: v1
kind: Service
metadata:
  name: postgres
spec:
  selector:
    app: postgres
  ports:
    - port: 5432

จะสังเกตว่าใน spec จะมีการกำหนด selector ซึ่งจะไปตรงกับ labels ใน Pod อีกทีนึง ตรง labels นี้ไม่จำเป็นจะต้องใช้ app: postgres เสมอไป แต่จะใช้เป็น key-value อะไรก็ได้ เช่น tier: backend ก็ไม่ได้ห้ามอะไร (แต่ต้องตรงกับใน Pod นะ)

สำหรับ service ตัวอย่างก็จะประกาศว่า service postgres จะเปิด port 5432 ต่อเข้าหา pod ที่มี label postgres

เมื่อ add เข้าไปแล้วก็สามารถใช้คำสั่ง kubectl get services หรือย่อ kubectl get svc เพื่อดูสถานะได้

NAME           CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
postgres       10.27.254.4     <none>        5432/TCP   19d

ก็จะเห็นว่า service จะมี IP เฉพาะตัวของมันเลย โดยด้านหลังสิ่งที่ Kubernetes ทำก็คือ เซต iptables DNAT ให้เวลาส่งข้อมูลมาที่ IP นี้ ถูก rewrite หา IP ของ Pod อีกทีหนึ่ง โดยจะมีการ load balance ให้ด้วย

เวลาจะต่อเข้าหา service นั้นก็สามารถใช้ชื่อ DNS เรียกว่า postgres ได้เลย ไม่จำเป็นจะต้องจำ IP ตรงๆ หรืออ่านจาก environment variable POSTGRES_SERVICE_HOST ก็จะได้ IP ออกมาเหมือนกัน

นี่คือบริการ service discovery ที่ใช้ใน Kubernetes แบบนึง (อีกแบบนึงก็คือไป query จาก Kubernetes API ตรงๆ ก็ได้เหมือนกัน)

Deployment

เมื่อกี้เราพบว่า ReplicationController สร้าง Pod แล้ว

Deployment คือผู้สร้าง ReplicationController อีกทีนึง (จริงๆ มันใช้ ReplicaSet ที่เป็น API ใหม่กว่า RC) และเป็นจุดขายของ Kubernetes เลยทีเดียว

โดย Deployment จะมีการทำงานแบบนี้

  1. เวลาสร้าง Deployment ครั้งแรก มันจะไปสร้าง ReplicaSet แล้ว ReplicaSet จะไปสร้าง Pod
  2. เราสามารถแก้ไข deployment ได้ ด้วยคำสั่ง kubectl apply -f deployment.yaml (ใส่พาธไปที่ไฟล์ที่เก็บ deployment ที่แก้แล้ว) หรือจะใช้ kubectl edit deployment name ก็ได้
  3. เมื่อแก้ไขแล้ว Deployment จะสร้าง ReplicaSet ขึ้นมาใหม่อีกอันหนึ่ง (เท่ากับว่ามี 2 ตัว)
  4. ReplicaSet ใหม่จะเปิด Pod ขึ้นมา 1 ตัว
  5. พอ Pod ตัวนี้สร้างเสร็จแล้ว (หรือถ้า config readinessProbe ไว้ก็จะรอจน Ready แล้ว) Deployment จะลดขนาด ReplicaSet เดิมลง 1 และเพิ่มขนาด ReplicaSet ใหม่อีก 1
  6. ทำซ้ำไปเรื่อยๆ จนกระทั่ง ReplicaSet ใหม่ขนาดเท่าเดิม และอันเดิมขนาดเหลือ 0

พูดง่ายๆ ก็คือ Deployment จะแทนที่ Pod เก่าด้วย Pod ใหม่ทีละ Pod ทำให้เวลา deploy แล้วระบบเราจะไม่มี downtime เกิดขึ้นเลย

นอกจากนี้ Deployment ยังจะเก็บ ReplicaSet ของเก่าไว้ด้วย นั่นแปลว่าถ้า deploy แล้วพังก็สามารถสั่ง kubectl rollout undo เพื่อถอยกลับได้อีกด้วย

หน้าตาของ Deployment จะเป็นแบบนี้

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: tm-static
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: tmstreamlabs
        service: static
    spec:
      containers:
      - name: static
        image: registry/tmstreamlabs:version
        command: [/caddy, -conf, /app/Caddyfile]
        resources:
          requests:
            memory: "64Mi"
            cpu: "1m"
        readinessProbe: &probe
          httpGet:
            path: /js/runtime.js
            port: 3030
        livenessProbe: *probe

(ตรง readinessProbe/livenessProbe ผมให้มันก๊อปกันมาโดยใช้ YAML Anchor ที่ต้องทั้งใส่ 2 แบบเพราะต้องการให้เช็คตอน deploy ว่า ready แล้วหรือยัง และเวลารันอยู่ถ้าค้างก็ให้ kill ไปเลย ไม่ใช่แค่เอาออกจาก service)

อ้อ ถ้าใครชอบไม่ติด tag Docker image ใช้แต่ latest จะใช้ Deployment ไม่ได้ เพราะ apply ไปแล้วมันจะไม่เจอว่าแก้อะไร

Namespace

สุดท้ายถ้าหากเราใช้ cluster กันหลายๆ project สามารถแยกของไม่ให้ปนกันได้ด้วยการใช้ Namespace แบบนี้

apiVersion: v1
kind: Namespace
metadata:
  name: app2

เวลาเราจะทำอะไรก็เรียก kubectl --namespace app2 ... ก็จะเห็นเฉพาะของที่อยู่ใน namespace นั้นๆ และเวลาเรียก service ก็จะพิมพ์ชื่อได้เฉพาะใน namespace เดียวกันเท่านั้น

(แต่ไม่ได้บล็อคนะ แค่ต้องพิมพ์ชื่อเต็มๆ ของ service คือ postgres.default)

โดยปกติแล้ว Kubernetes จะมี namespace สำคัญๆ 2 อัน ก็คือ default ที่เราใช้ได้เวลาไม่ระบุชื่อ namespace และ kube-system ที่เก็บ service ของ Kubernetes

สรุป

จากตอนนี้เราจะเห็นว่า Kubernetes มีการออกแบบตามสไตล์ Design for failure มากๆ ถ้าอะไรสักอันพังไปใน cluster ก็จะไม่ส่งผลกระทบต่อการทำงาน และเผลอๆ จะสามารถกู้ได้เองอีกด้วย

  • ถ้า Master ตาย ระบบยังสามารถทำงานได้ปกติ แค่ไม่สามารถ schedule pod ใหม่ได้
  • ถ้า Node ตาย Pod ก็จะถูก ReplicationController สร้างใหม่ที่เครื่องอื่นๆ
  • ถ้า Pod ค้าง ก็จะตรวจพบจาก livenessProbe แล้ว kill ทิ้งไปสร้างใหม่

บางจุดอาจจะเห็นว่าใช้งานซับซ้อนพอสมควร แต่ก็เป็นวิธีที่ดีถ้าหากเราต้องการระบบที่เสถียรจริงๆ

ในตอนหน้าจะลองมาพูดถึงของเล่นที่ advance ไปกว่าตอนนี้กันบ้างครับ