บันทึกการย้าย Service ของ cscms.me

สวัสดีครับ (ปกติไม่เคยสวัสดีคนอ่านเลย) ผมเลยขอเขียนบันทึกนี้เอาไว้ เพื่อระลึกถึงความยากลำบากที่ได้ประสบพบเจอมา จากการย้าย service ต่างๆ ขึ้นไปรันอยู่บน Kubernetes Cluster (I'm just a noob ;-;)

Ready Set Go!

OnlineJudge ของ cscms.me

Service ของ cscms.me ที่ให้บริการอยู่มีดังนี้

  • cscms.me ซึ่งเป็น Online Grader สำหรับทำโจทย์ programming
  • aka.cscms.me บริการย่อ URL
  • tmp.cscms.me บริการรับฝากไฟล์
  • timetable.cscms.me เว็บดูตารางเรียนของ CS@SIT
  • scroll.cscms.me เว็บไซต์สำหรับคนที่อยาก scroll mouse ยาวๆ

นอกจากนี้ยังมี blog.sethanantp.com ที่เป็น Ghost CMS

โดยก่อนหน้านี้ service ทุกตัวรันอยู่บน Virtual Machine บน Microsoft Azure ซึ่งมี Nginx ทำหน้าที่ serve static file กับทำ reverse proxy โดยแน่นอนว่าการ deploy service ต่างๆ ก็ต้องทำด้วยการ SSH เข้าไปทุกครั้งที่มีเวอร์ชั่นใหม่ออกมา ซึ่งมันเสียเวลาโดยเปล่าประโยชน์ อันที่จริงเราสามารถเขียน script การ deploy version ใหม่ๆ ได้และทำการเรียกผ่าน GitHub Action แต่นั้นก็เป็นทางเลือกที่ไม่ได้ยั่งยืนนัก ผมเลยเลือกที่จะย้าย service ทุกตัวไปอยู่บน Kubernetes Cluster เพื่อการจัดการที่ง่ายขึ้น และสามารถรองรับ service ในอนาคตได้อีกด้วย และเนื่องจากว่า service แต่ล่ะตัวก็ถูกเขียนขึ้นมาด้วยภาษาที่ต่างกัน ดังนั้นการใช้ Docker Container ก็เป็นทางเลือกที่ผมเลือกใช้

Dockerlized everything

ขั้นแรกเริ่มจากการเขียน Dockerfile ให้แต่ล่ะ service ซึ่งบางอันก็ง่าย บางอันก็ยาก บางอันต้องแก้ code เดิม แต่บางอันก็สามารถที่จะ Copy Paste ได้เลย โดยอันที่ต้องแก้เยอะหน่อยก็เป็น tmp.cscms.me ที่เขียน API ด้วย Golang ซึ่งตอนที่เขียนนั้นตัวผมก็ไม่ได้เก่ง Golang มาก ทำให้ code ที่ออกมาค่อนข้างจะยุ่งมากๆ รวมไปถึงการที่จะต้องใช้ไฟล์ Service Account Key ของ GCP เพื่อเข้าถึง Google Cloud Storage ที่ผมไม่เคยแก้ปัญหาว่าถ้าเรา Build binary ออกมาแล้วจะเอาไฟล์นี้ไปใช้กับไฟล์ Service Account อย่างไร ต้องสารภาพบาปว่าต้องรันก่อนหน้านี้ก็ใช้วิธีว่ารัน go run main.go ค้างไว้ เพื่อ serve ใน production แตครั้งนี้ก็หาทางแก้ด้วยการใช้ Full path กับ environment variable แล้วทำการ mount ไฟล์ service account เข้าไปทีหลังผ่าน secret ของ Kubernetes

Docker Image Size ของ Temp Storage ที่เขียนด้วย Golang ขนาดแค่ 12MB
Docker Image Size ของ URL Shortener ที่เขียนด้วย Express บน NodeJS กินพื้นที่ไป 120MB

ท้ายที่สุดแล้วแล้วผมชอบการ Deploy ด้วย Golang มากๆ เพราะว่าการที่เราสามารถ build binary ออกมาจาก Golang ได้เนี่ย มันทำให้ขนาดของ Docker image เล็กลงมากๆ ผมเลือกใช้ base image เป็น alpine ที่ขนาดเล็กแค่ 5MB พอเพิ่ม binary ที่ได้จาก build มาก็กลายเป็น 25MB เท่านั้นเอง ส่วน NodeJS นะหรอ ไม่ต้องพูดถึงเลย (1xx MB++ ทั้งนั้น)

Cluster with 1 Node

ต่อมาเราก็ต้องมี Kubernetes Cluster ซึ่งผมเลือกใช้ Azure Kubernetes Service หรือ AKS เนื่องจากมี Credit $150/เดือน จากโครงการ Microsoft Learn Student Ambassadors โดยผมเพิ่งจะรู้ว่า AKS กำหนดขั้นต่ำของ Node ไว้อยู่ที่ 2 vCPU กับ 8 GB RAM ทำให้ node size ต่ำสุดที่ผมเลือกได้คือ Standard_B2s ซึ่งราคาประมาณ $40 และรวมเข้ากับ VM ที่รัน service อยู่ก่อนหน้านี้ที่เป็นสเปคเดียวกันแล้ว ทำให้ผมไม่มีเงินพอที่จะเพิ่ม node ให้กับ Kubernetes Cluster ดังนั้นก็ต้องใช้ให้พอใน Node เดียว (สุดท้ายก็พอ) โดยการจัดการ Infrastructor บน Azure ทำผ่านการใช้ Terraform กับ Terraform Cloud ที่เพิ่งได้ร่ำเรียนมา โดย Resource ก็ไม่มีอะไรมาก เพราะมีแค่ AKS เพียง Cluster เดียว ทำให้การจัดการสร้าง Resource เป็นไปอย่างราบรื่น

It's K8S YAML TIME!

หลังจากเราได้ Docker image อยู่บน Docker Hub แล้ว ขั้นต่อไปก็คือการเขียน Kubernetes Config ซึ่งแน่นอว่าขั้นตอนนี้ไม่ได้ยากอะไรมาก ผมเริ่มจากการเขียน Config ของ application ที่ไม่ต้องการ dependency อื่นๆ ก่อนซึ่งก็คือ aka.cscms.me ที่ใช้ Database เป็น Azure Cosmos DB ทำให้เราสามารถ deploy ได้เลย ต้องการเพียงแค่ environment variable กับ secret เท่านั้น

Service อีกตัวที่ต้องใช้เป็นหลักก็คือ Ingress Nginx ซึ่งแน่นอนว่าการ deploy ก็ทำตาม docs ได้อย่างง่ายดายมากๆ

เนื่องจากมี service มากกว่า 1 ตัวต้องการใช้ Redis ผมเลยสร้าง Common Redis Deployment ขึ้นมา เพื่อทำเป็น Redis ส่วนกลาง จะได้ไม่ต้องไปสร้าง Redis แยกให้รก

หลังจากเขียน yaml เสร็จก็ทำการ apply และแน่นอนว่าทุกอย่างก็ใช้ได้ด้วยดี หลังจากนั้นจึงเริ่มเขียน config ของ application ตัวอื่นๆ ตามไปเรื่อย โดย services ที่ผม deploy ขึ้นไปเป็นกลุ่มแรกก็คือ aka.cscms.me, tmp.cscms.me, และ timetable.cscms.me เนื่องจากว่าพวกนี้เป็น service ที่ไม่ต้องทำการ migrate ข้อมูลเดิมที่อยู่บน VM ออกมา

GitHub Action in a weird ways

หลังจากเราสามารถที่จะเอา services ต่างๆ ขึ้นไปอยู่บน Kubernetes Cluster ได้แล้ว สิ่งต่อไปก็คือการทำ CI/CD ซึ่งแน่นอนว่า code ที่เคยเขียนไว้นั้น ไม่ได้มีการเขียนเทสไว้เลย ผมเลยต้องจัดการเขียนเทสให้กับ service ที่คนใช้มากที่สุดนั้นก็คือ aka.cscms.me โดยเมื่อเรียบร้อยแล้ว ก็ทำการเขียน GitHub Action เพื่อให้รันเทสและสร้าง Docker Image และ push ไปเก็บที่ Docker Hub และแน่นอนว่าถึงจุดๆ นี้ มันยังมีไม่มีอะไรยากเลย

แต่ปัญหาต่อมาที่เจอก็คือ ผมทำการสร้าง Repository ใหม่สำหรับเก็บไฟล์ Kubernetes config  ของทุกๆ service ให้อยู่ในที่เดียวกัน การที่จะให้ GitHub Action trigger ข้าม Git repo เป็นสิ่งที่ผมไม่เคยลองทำมาก่อน โดยปกติแล้วผมจะใช้ Kustomize เพื่อแก้ image tag ที่เป็น SHA256 ของ Docker image แล้วทำการสั่ง kubectl apply -f เพื่อให้ Kubernetes ไปเอา image ใหม่มาใช้ แต่เนื่องจากที่ไฟล์ config กับไฟล์ code มันอยู่คนละที่กัน ทำให้มันไม่สามารถส่ง SHA256 ข้ามไปยังอีก repo เพื่อแก้ไข Kube config ด้วย Kustomize ได้

on:
  repository_dispatch:
    types: ['aka_cscms']

ทางแก้ที่ผมคิดออกก็คือ การใช้ REST API ของ GitHub ในการ trigger การรันของ GitHub Action โดยผมใช้ repository_dispatch ด้วยชื่อ event ที่แตกต่างกันตามแต่ล่ะ service โดยเมื่อ repo ที่เก็บ code ได้รับ commit ใหม่ GitHub Action ก็จะทำการ Test และ Build, Push Docker image โดยเมื่อทำงานเสร็จแล้วก็จะยิง HTTP Request ไปที่ https://api.github.com/repos/thetkpark/cscms-services-deployment/dispatches
พร้อมกับ event_name ที่เป็นชื่อ service ของตัวเอง โดย HTTP Request นี้จะเป็นตัว trigger ให้ GitHub Action ใน repo ที่เก็บไฟล์ config นั้นทำงาน โดยจะทำการ setup kubectl  และสั่ง kubectl rollout restart deployment ... เพื่อทำการเช็คเวอร์ชั่นใหม่ของ Docker image นั้นและทำการ update ให้กับ deployment ที่รันอยู่

โดยสรุปแล้ว มันเป็นท่าที่ยุ่งยากนิดหน่อย ผมมองว่ามันไม่ได้เป็น Best Practice สักทีเดียว แต่ที่ผมต้องการเก็บไฟล์ config ไว้ใน repo เดียวกันก็เพราะ เวลาเราใช้ GitHub Action ในการ connect ไปที่ Cluster ของเรา เราต้องใช้ credential ของ Azure ซึ่งผมไม่อยากที่จะเอา credential ไปใส่ไว้กับทุก ๆ repo ที่เก็บ code เพราะการ maintain มันน่าจะยุ่งยากกว่าเดิมไปอีก ดังนั้นก็เลยต้องจบที่ท่าการทำ GitHub Action แบบแปลกๆ แบบนี้

This is why I hate managing data


หลังจากที่ได้เอา service ง่ายๆขึ้นไปบน cluster หมดแล้ว ก็ถึงเวลาสำหรับ service ที่ต้องการ storage ซึ่งก็คือ blog.sethanantp.com โดยสิ่งแรกที่ผมทำก็คือการ compress data ทั้งหมดและพอสร้าง Ghost CMS ตัวใหม่บน cluster เรียบร้อยแล้วก็ทำการใช้คำสั่ง exec เพื่อทำการ sh เข้าไปยัง pod แล้วโหลดข้อมูลที่ compress ไว้มา ซึ่งผลที่เกิดขึ้นก็คือ มันใช้งานได้ครับ แต่ผมก็เพิ่งจะคิดได้หลังทำเสร็จแล้วว่า Ghost CMS deployment ที่ผมเขียนเนี่ย มันไปเรียกใช้ PVC หรือ Persistent Volume Claim ที่มี Retain Policy ใน Storage class เป็น delete ผลก็คือ ถ้าผมทำการลบ deployment นั้นไป ข้อมูลทุกอย่างที่ผมเคยเก็บไว้ ก็จะหายไปเพราะมันถูกลบไปทั้ง disk เพราะ deployment นั้นไม่ต้องการ PV อีกแล้ว อีกหนึ่งปัญหาก็คือชื่อของ Disk ใน Azure มันจะถูกตั้งมั่วๆ เพราะ Kubernetes Cluster เป็นคนสร้างขึ้นมาเอง และมันอยู่คนละ resource group กับที่ผมใช้งานด้วย

AKS ทำงานโดยการสร้าง Resource Group ขึ้นมาใหม่ เพื่อใช้จัดการ Resource ที่จำเป็นในการรัน Kubenetes Cluster อย่างเช่น VM, Network, Disk, Storage Account, ...

ทางแก้ที่ผมใช้ก็คือการสร้าง Azure Managed Disk เปล่าๆ ขึ้นมาก่อน เพราะทำการ load data ใส่เข้าไป (หาทางนานมาก สรุปคือก็ใช้ VM เครื่องเก่านั้นแหละ mount disk เข้าไป แล้วก็ copy ได้เลย) และ speicfic ใน Ghost CMS deployment ไปเลยว่าให้ใช้ disk URI นี้ในการทำงาน

ปัญหาที่แก้อยู่นานมาก็คือ รูปที่เคยใช้มันไม่โหลด ทั้งที่ใน disk ก็มี สรุปเป็นเพราะว่าไม่ได้ใส่ env url ;-;
อีกอย่างก็คือ Disk ที่สร้างขึ้นมาจะต้องอยู่ใน Resource Group ที่ AKS สร้างขึ้นมา เพราะตัว Cluster มันไม่ได้มี permission เข้าถึง disk ข้างนอก

Here come the hardest part

มาถึง service ที่ใหญ่ที่สุด ซึ่งก็คือตัว OnlineJudge ของ cscms.me โดยตัว OnlineJudge ที่ใช้อยู่นั้น เป็น open-source   /OnlineJudge ซึ่งมันเป็น Docker Container อยู่แล้ว โดยปกติจะ deploy ผ่าน Docker Compose (วิธี Official เลยนะ) ดังนั้นการจะเอามาเขียนใหม่ให้เป็น Kubernetes Configuration ก็ทำได้ไม่ยาก โดยมันจะประกอบไปด้วย service ทั้งหมด 4 ตัว ก็คือ

  1. Backend เป็นทั้ง Backend และ Web Server
  2. Judger เป็น service ในการรัน code เพื่อตรวจคำตอบ
  3. Postgres เป็น Database
  4. Redis เป็น in-memory cache

โชคดีที่ผมเลยมีความคิดนี้เมื่อเกือบ 2 ปีที่แล้ว และเคยทำไฟล์ yaml สำหรับตัว OnlineJudge ไว้เรียบร้อยแล้ว (ตอนนั้นนั่นปวดหัวอยู่หลายวันเหมือนกัน) ดังนั้นผมจึงดึงไฟล์นั้นมาใช้ และทำการแก้ไขนิดหน่อยก็สามารถใช้ได้เลย

แน่นอนว่า OnlineJudge นั้นต้องการ storage โดยทุก container (4 อันข้างบนนั้น) ต้องสามารถอ่านและเขียนลงไปได้และไฟล์บางอย่างจำเป็นต้องแชร์กันด้วย ดังนั้น storage แบบเดียวที่ใช้ได้ก็คือ Azure Managed Disk กับ access mode แบบ ReadWriteOnce ผมก็เลยจัดการใช้ท่าเดิมก็คือการสร้าง Disk เปล่าแล้วทำการ copy data ทั้งหมดลงไป ซึ่งตามหลักการแล้วมันก็ควรจะใช้ได้....แล้วมันก็ใช้ได้ เย้

ช่วงหาทำ

หลังจากที่ทุกอย่างสามารถใช้งานได้ปกติแล้ว สิ่งต่อไปที่ผม(หาทำ) ก็คือการอัพเดทเวอร์ชั่นของ OnlineJudge ให้เป็นแบบล่าสุด เนื่องจากเวอร์ชั่นใหม่นั้นรองรับการตรวจ code ในภาษา Golang ดังนั้นจึงตัดสินใจที่จะอัพเดทมัน ซึ่งตอนเวอร์ชั่นนี้มันออกมาใหม่ๆ เนี่ย ผมเคยลอง pull Docker Image ใหม่มาแล้ว และเมื่อลอง restart ดู แน่นอนว่าภาษา Golang ก็ไม่ขึ้นมาให้เลือก แต่ตอนนั้นก็เลยปล่อยไป เพราะไม่ได้สนใจอะไรมันมากนัก ผลกรรมเลยมาตกที่ครั้งนี้แทน

ผมเริ่มจากการลอง deploy OnlineJudge โดยเริ่มจาก 0 ก็คือเอา data เข้าไปใส่ใดๆ ทั้งสิ้น ผลก็คือมี Golang ให้เลือก...อ่าว แล้วทำไมอันที่มัน migrate มามันถึงไม่มีล่ะ เลขเวอร์ชั่นก็เลขเดียวกัน ผมจึงลอง เอา data disk ไปใส่ให้แล้วลอง restart ใหม่ดู...ผลที่ได้ก็คือ Golang หายไป...

ต่อมาผมเลยเริ่มต้นใหม่ จาก OnlineJudge เปล่าๆ และทำการ copy data เข้าไปใน service ทีล่ะตัว โดยสิ่งที่ผมเจอก็คือปัญหามันอยู่ใน Postgres ที่ก่อนหน้านี้ทำการยัด data เข้าไปผ่าน pgdump แล้ว Golang ก็หายไปเลย...ผมจึงลองทำการ import table ผ่านการรัน SQL กับการ import CSV ด้วยตัวเองเฉพาะใน table ที่เป็นข้อมูลผู้ใช้ โจทย์และการส่งคำตอบเท่านั้น โดยไม่ไปยุ่งกับ table ที่เก็บข้อมูลของตัวระบบ ผลที่ได้ก็คือ Golang ยังอยู่...

หลังจากนั้น ผมเลยทำการเปิด Database เทียบกัน ระหว่างอันที่ผ่านการ pgdump กับอันที่ผม import ด้วยตัวเอง สิ่งที่เจอก็คือใน table options_sysoptions ที่ทำการเก็บ key-value pairs ของ config ไว้นั้น มีการเพิ่ม config ที่ใช้ในการตรวจ code Golang เข้ามาด้วย ซึ่งเป็นผลมาจากการรัน db migration จากตัว backend version ใหม่ ซึ่งการทำงานก็คือถ้ามันมีข้อมูลอยู่แล้ว ตัว migration จะไม่ทำงาน ผมจึงเลยไม่ได้ Golang มาใช้ในครั้งก่อนที่ update version ไป แต่ถ้าเราให้มันรัน migration ให้เรียบร้อยและทำการ import data เข้าไปโดยไม่ dump ลงไปทั้งก้อน มันก็จะทำให้ Config Golang ตัวนี้ยังอยู่เช่นกัน หรืออีกวิธีหนึ่งก็คือการเพิ่มเข้าไปเองผ่านทาง DB ก็ทำได้เช่นกัน

อีกปัญหาที่เจอแล้วงงมากก็คือตัว OnlineJudge Backend มันพยายามจะรัน db migration ก่อนทุกครั้งที่ up ขึ้นมา และมันจะทำการสร้าง user root ทุกครั้ง แม้ว่าจะมี user root อยู่แล้วก็ตาม ผลก็คือโดน primary key violation error ผมต้องมานั่งลบ root ออกจาก db ทุกครั้ง ก่อนที่จะสั่ง up ตัว backend ขึ้นมา

First time to do monitoring

หลังจากที่ service ทุกอย่างสามารถใช้งานได้เรียบร้อยแล้ว สิ่งหนึ่งที่ผมต้องการทำเพิ่มก็คือการทำ monitoring โดยก่อนหน้านี้ที่ยังใช้อยู่บน VM tools ที่ใช้ monitor ก็มีแค่ Netdata ซึ่งมันก็ไม่ได้มี insight อะไรมากนัก ครั้งนี้พอเรามี Kubernetes Cluster แล้ว tools ที่ผมลองใช้ก็หนีไม่พ้น Prometheus และ Grafana และแน่นอนว่าผมไม่เคยใช้มันมาก่อน...

เริ่มต้นจากการหาว่าเราต้อง deploy มันยังไง โชคดีที่มี Helm Chart ของ kube-prometheus-stack ที่รวมทั้ง Prometheus และ Grafana ไว้เรียบร้อยแล้ว ดังนั้นก็แค่ใช้คำสั่ง helm install ได้เลย

Note: Helm เป็น Package manager สำหรับ Kubernetes โดยเราสามารถสร้าง Helm Chart ที่รวบรวม configuration file ของ Kubernetes ไว้ เพื่อให้คนอื่นหรือตัวเราเองนำไป deploy ลงใน Kubernetes Cluster ได้อย่างง่ายดาย

ซึ่งก็สามารถใช้ได้ปกติ โดยผมสามารถดู Resource ต่างๆ รวมไปถึงการใช้ resource ของ pods ต่างๆได้อีกด้วย แต่อีกสิ่งหนึ่งที่ผมอยากได้ก็คือการ Monitor ตัว Ingress Nginx ว่ามี connection เข้ามาขนาดไหนและมีการใช้ Network อย่างไรบ้าง และตัวนี่แหละ ที่เป็นปัญหา

การที่จะใช้ Nginx Ingress ให้ส่งข้อมูลไปให้ Prometheus นั้นจะต้องเช็ตค่าดังนี้

controller.metrics.enabled=true
controller.podAnnotations."prometheus.io/scrape"="true"
controller.podAnnotations."prometheus.io/port"="10254"

ซึ่งใน official docs บอกไว้ว่า The easiest way to configure the controller for metrics is via helm upgrade และขั้นตอนหลังจากนั้นก็เป็นการใช้ Helm command ล้วนๆ เลย ปัญหาก็คือตอนที่ผม deploy ไป ผมไม่ได้ใช้ Helm...แล้วผมจะแก้ config มันยังไงล่ะ...

หลังจากที่นั่งหามานานว่า เราต้องแก้ config นี้ตรงไหน สรุปก็คือเหนื่อยมากเพราะหาไม่เจอ ดังนั้นผมเลยลบ Ingress Nginx อันเดิมทิ้งไปและลงใหม่ผ่านการใช้ Helm และทำตาม docs ไป

หลังจากทำการแก้ config เรียบร้อยแล้ว ผมก็ลองเปิดกลับไปที่ Grafana เพื่อดูว่ามีข้อมูลอะไรเข้ามาผ่าน Ingress Nginx หรือเปล่า สรุปก็คือ ไม่มี...Data ทุกอย่างยังคงเป็น N/A อยู่

หลังจากนั้นก็ทำการหาไปเรื่อยๆ ว่ามันเกิดจากอะไร จนเลื่อนลงมาอ่าน docs ของ Ingress Nginx ในหัวข้อ Prometheus and Grafana installation ว่าทาง official ให้ใช้ Kubenetes Config ที่ทาง Ingress Nginx เขาเขียนมาเพื่อ deploy ตัว Prometheus ซึ่งมันคงมีอะไรสักอย่างที่มันทำให้ Ingress Nginx ทำงานกับ Prometheus ได้ แต่ Prometheus และ Grafana ที่ผมลงไปผ่าน Helm Chart มันไม่ได้ถูก Config ไว้...

เนื่องจากผมไม่อยากที่จะลบ Helm Release ที่ใช้อยู่ก็เลยพยายามหาวิธีไปเรื่อยๆ จนไปเจอ Question หนึ่งบน DigitalOcean ซึ่งเขาก็เจอปัญหาเหมือนผมเลย และก็มีคำตอบว่าให้ทำตามนี้ และมันก็ใช้ได้!! กราบบบ

# upgrade ingress to enable metrics
helm upgrade nginx-ingress ingress-nginx/ingress-nginx --namespace ingress-nginx --set controller.metrics.serviceMonitor.enabled=true --set controller.metrics.enabled=true

# upgrade prometheus operator to look in other namespaces
helm upgrade prometheus-operator stable/prometheus-operator --namespace prometheus-operator --set prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValues=false

ดังนั้นผมก็สามารถที่จะใช้ Grafana เพื่อดู Traffic ของทั้ง Ingress Nginx และการใช้ resource ของ Deployment ต่างๆ อีกด้วย :)

Conlusion

สรุปแล้วการย้ายครั้งนี้กินเวลาไปเกือบ 1 อาทิตย์เต็มๆ ทั้งการ Dockerized ทุกอย่าง จนไปถึงการ Load Testing โดยการจัดแข่งขัน Programming บน cscms.me ที่ให้นักศึกษาใหม่ปี 2564 มาได้ลองทำโจทย์ชิงเงินรางวัลรวม 1000 บาท โดยสรุปแล้วก็ทำให้ผมได้เรียนรู้อะไรใหม่ๆ เยอะมาก และได้แก้ปัญหาที่เข้ามาตลอดเวลา เนื่องจากผมก็เป็น noob เรื่อง Kubernetes คนนึง และในครั้งหน้าก็อยากจะลองอะไรใหม่ๆ อย่างเช่น Kong API Gateway ด้วย

สุดท้ายนี้ผมได้สร้าง Helm Chart (แบบง่อยๆ) ของ OnlineJudge ที่ผมใข้ เนื่องจากยังไม่เห็นว่ามีใครทำเลย ก็เลยทำไว้ให้คนอื่นได้ใช้งานกันที่ https://artifacthub.io/packages/helm/onlinejudge/onlinejudge (ไม่รู้ทำไมมันไม่อัพเดท README ผมก็ทำไม่เป็นสักเท่าไหร ;-;)
สุดท้ายแล้วผมก็เปลี่ยน spec เครื่อง VM เป็น Standard_D2a_v4 เพราะ credit มีเหลือๆ ต้องขอขอบคุณ Microsoft Learn Student Ambassador เป็นอย่างสูงงง