Scaling Software Agency with Rails

ในบทความนี้เราอยากมาแชร์ practice และเทคนิคการ develop และ operate software ที่เขียนด้วย Rails ให้ efficient และใช้แรงน้อยที่สุดในการ maintain ครับ

Scaling Software Agency with Rails
Photo by Austin Distel / Unsplash

สองสามปีที่ผ่านมาทีม MeCode ได้ deploy และ maintain

  • web application
  • API for mobile application
  • websites with CMS
  • enterprise grade micro-service cluster

ให้ลูกค้ากว่า 60 เจ้า (100+ service) ด้วย Ruby on Rails ซึ่งเราได้บทเรียนมากมายในการ scale ระบบด้วยทีมที่มี software engineer เพียง 7 คน

ในบทความนี้เราอยากมาแชร์ practice และเทคนิคการ develop และ operate software ที่เขียนด้วย Rails ให้ efficient และใช้แรงน้อยที่สุดในการ maintain ครับ

มาเริ่มต้นจาก tech stack ของเรากันก่อน

The MeCode's Stack

เป้าหมายของทีมเราชัดเจนคือ

ทำยังไงให้เราสร้าง quality software ให้ลูกค้าได้โดยใช้ effort ในการพัฒนาและดูแลน้อยที่สุด

เป้าหมายนี้เป็นโจทย์ที่ทีมเราให้ความสำคัญมากที่สุด ผมเชื่อว่าในยุคนี้ software agency มี tools มากมายที่ทำให้การ scale service ไม่ได้จำเป็นต้องเพิ่มคนเป็นอย่างแรกอีกต่อไป สิ่งที่ทีมควรให้ความสำคัญเป็นอยากแรก ๆ คือ

  1. Development Efficiency
  2. Maintenance Efficiency

Development Efficiency

การเลือก tech stack เพื่อใช้เพิ่ม development efficiency เป็นเรื่องใหญ่มาก สิ่งที่เราให้ความสำคัญไม่ใช่ความใหม่ ความว้าว หรือ ความเร็วของ code แต่เป็นความเร็วในการ ship ของขึ้น production ซึ่งสิ่งที่ทีมเราใช้เป็น core ของทุก ๆ อย่างคือ Ruby on Rails

Compress the complexity of modern web apps.
Learn just what you need to get started, then keep leveling up as you go. Ruby on Rails scales from HELLO WORLD to IPO.

จุดขายของ Ruby คือความ "ง่าย" ในการทำสิ่งที่มัน common ยกตัวอย่าง เช่น

User.where(updated_at: Time.current.in_time_zone('Asia/Bangkok').all_day)

การ query ข้อมูล User ที่ถูกอัพเดตวันนี้ใน time zone กรุงเทพ

ใน Rails ผมเขียนบรรทัดเดียว แต่ใน Node ผมต้องทำสิ่งเหล่านี้ก่อนที่จะเริ่มเขียนโค้ดได้ด้วยซ้ำ

  • ต้องเลือก library มาเล่นกับ timezone (moment.js / day.js)
  • ต้องเลือกว่าจะใช้ Drizzle, TypeORM หรือ Prisma
  • ต้องหาวิธีต่อเข้า database แล้วรัน query โดย context ของ application ผม ซึ่งหลาย ๆ ครั้งทำไม่ได้ด้วยซ้ำ อาจต้องต่อ database ด้วย CLI หรือ GUI tool แล้วรัน SQL ซึ่งก็ไม่ได้ replicate สิ่งที่ application ทำ 100%

สิ่งนี้ทำให้ทีมผมไม่ต้องมีการคุยเรื่อง tech stack เกี่ยวกับโค้ดมากนัก คำถามที่เกิดขึ้นมีแค่ "เราจะใช้ API mode หรือ รัน fullstack ดี" เท่านั้น

ไม่ใช่แค่ตัว ORM หรือ Time แต่ Rails ยังมีของมาให้ครบไม่ว่าจะเป็น

  • ActionMailer สำหรับส่งเมล integrate กับ Email Provider เจ้าดัง หรือ แม้กระทั้ง SMTP server ตรง ๆ ได้โดย config ไม่กี่บรรทัด
  • ActionText - Rich text support ด้วย Trix Edtior
  • ActionCable - สำหรับทำ websocket เพื่อ real-time action กับฝั่ง client
  • ActiveJob - สำหรับการ manage async job และ cronjob ที่แชร์โค้ดกับตัว application หลัก ทำให้เวลา deploy ใช้ image เดียวกับ main app ได้เลยไม่จำเป็นต้องทำ application แยก

และอื่น ๆ อีกมากที่ทีมเราไม่จำเป็นต้องเปลืองแรงตัดสินใจ ซึ่งทีมเราใช้ Rails คู่กับ Vite และ Inertia.js เพื่อ support ลูกค้าที่ต้องการ interactive application ที่ frontend จะต้องเขียนด้วย React หรือถ้าลูกค้าต้องการจริง ๆ เราสามารถทำ client / server แยกกันโดยใช้ Next.JS คู่กับ Rails API mode ได้ด้วยซึ่งจะเป็นทางเลือกสุดท้ายที่ไม่ค่อยได้ทำเท่าไหร่ เพราะ Inertia ตอบโจทย์ลูกค้าและ budget friendly มากกว่า

นอกจาก web application stack แล้วเรายังมี stack สำหรับทำ website + CMS คือ

Strapi on Rails
หลักการง่าย ๆ คือ ใช้ Strapi เป็น CMS จัดการฐานข้อมูล ส่วน Rails application ใช้ db:schema:dump และเขียน Model ง่าย ๆ map เข้ากับฐานข้อมูลที่ Strapi สร้างไว้เลย

ข้อดีของการทำท่านี้คือเราไม่ต้องเขียน API client เลย เพราะ เราอ่านจากฐานข้อมูลโดยตรง และ ActiveRecord ก็มอง table เป็น dynamic อยู่แล้วทำให้แทบไม่ต้องเขียนโค้ดเลยสำหรับ data layer ทำให้ engineer ในทีมโฟกัสกับการทำ UI ได้ 100%

นอกจากนี้ทีมเราให้ความสำคัญกับการเทสอย่างมาก เพราะ automation ทำให้เรามั่นใจทุกครั้งว่าเราไม่ได้ทำอะไรของเก่าพัง ทำให้ทีมเราสามารถ deploy ขึ้น production ได้วันละหลายรอบโดยไม่ต้องรอเป็น release cycle ซึ่ง Rails มาพร้อมกับ testing framework ที่ครบทั้ง unit, integration และ system test (UI clicking bot)

ที่เราสามารถ ship ได้บ่อยเพราะ feature flag management อย่าง Flipper ที่เราเอาไว้ control ว่า user จะเห็นอะไรบ้างได้

การโฟกัสที่ development efficiency ทำให้ทีม engineer ของเราโฟกัสที่การแก้ปัญหาให้ลูกค้ามากกว่าใช้เวลาไปกับ technical decision ที่อาจไม่ได้มี user value มากนัก

แล้ว database ล่ะ?

ทีมเรา default ทุกอย่างไปที่ managed PostgresQL ที่ใช้กันแพร่หลายและไว้ใจได้ ส่วนถ้าระบบต้องการ cache, queue, message broker เราใช้ Redis เป็นหลัก ซึ่ง RedisStream เป็นอะไรที่ทีมเราใช้บ่อยมาก ๆ แทนที่ Kafka ด้วยความง่ายของการ manage และ consumer based consumption (หลักการเหมือน partition offset ที่ consumer เลือกจุดที่ต้องการอ่านเองได้)

TDLR;
MeCode's Stack = Rails, Vite (React), Strapi, Postgres, Redis

Maintenance Efficiency

Majestic Monolith
ทีมเรา embrace การทำ monolithic application ซึ่งถ้าลูกค้าไม่ได้มี scale ที่ใหญ่มากจริง ๆ (เช่นระบบ vote ของ LINE TV AWARDS 2019-2022 ที่เราเคยทำ) เราจะ default ไปที่ monolith architecture เพราะความง่ายในการ maintain, deploy และ debug

โดยทุก application ที่ขึ้น production จะต้องมี log aggregation, real-time metrics และ tracing โดย stack ที่เราใช้คือ

  • DOKS (Managed Kubernetes from DigitalOcean) - เราต้องการ optimize resource usage สำหรับลูกค้าโดยใช้การ autoscale และ pool resource ระหว่างกันสำหรับลูกค้าที่ใช้ shared host และสร้าง dedicate nodepool เพื่อลูกค้าที่ต้องการ dedicate compute โดยเราไม่อยากจะ maintain K8s cluster ด้วยตัวเอง ซึ่ง DOSK ตอบโจทย์ทั้งเรื่องราคาและความง่ายในการ maintain ข้อดีอีกอย่างของ K8s คือ cronjob ต่าง ๆ ก็ถูก manage บนนี้ทำให้สะดวกในการทำหลายอย่าง ไม่ว่าจะเป็น SQL query based alerting, backup jobs หรือ heartbeat job ต่าง ๆ
  • Grafana, Loki, Tempo, Prometheus - เราใช้ tooling พวกนี้ในการทำ log aggregation และ metric & alerting system ที่แจ้งทั้ง internal และลูกค้าเวลามีเรื่องสำคัญ ไม่ว่าจะเป็น memory ใกล้เต็ม หรือ business related issue อย่าง order ไม่เข้ามาเกิน 12 ชั่วโมงแล้ว
  • Terraform - เราใช้ Terraform ในการ manage infrastructure นอก cluster เช่น Managed Postgres, Redis, หรือ VM สำหรับลูกค้าบางเจ้า
  • GitHub และ GitHub Actions - เรา version control ทุกอย่างทั้ง codebase ลูกค้าและ infrastructure tooling เช่น Helm Config บน GitHub และ automate การ release ทั้งหมดเป็นแบบ trunk base development ทำให้ทุกคนสามารถเอาของขึ้น production ได้และ revert กลับได้หากมีปัญหา
  • Dependabot - เราใช้ dependabot ในการ auto update dependency ต่าง ๆ ทำให้ codebase ของลูกค้าทุกเจ้า up-to-date ตลอดเวลา

Sensible Micro-Service
สำหรับลูกค้า enterprise ที่ support load จากผู้ใช้งานทั้งในและนอกประเทศ หลาย ๆ ครั้งเราหนีไม่พ้นที่จะออกแบบระบบที่สามารถ scale ได้ตามโจทย์

กรณีนี้ tech stack เราก็ยังคงเหมือนเดิม คือ Rails & Rails API Mode ด้วยความที่ตัว framework มี job management และ API mode มาในตัวทำให้การทำ micro service based system เราสามารถทำได้ไวและปัญหาน้อยเหมือนกัน

รวมถึงการทำ data pipeline สำหรับ reporting หรือ BI ที่ต้องการ message broker ในการ decouple producer schema กับ consumer logic ด้วย ซึ่งโจทย์นี้ถูกแก้ด้วย RedisStream ที่เราสามารถ scale producer และ consumer แยกกันได้

ซึ่งเราเขียน ETL pipeline ด้วย Ruby และ utilize โค้ดข้าม codebase โดยการทำ library แชร์กัน ซึ่งทำให้ทีมเราไม่ต้องทำงานซ้ำซ้อนหลายรอบ

กรณีที่ลูกค้ามีการใช้งาน compute ที่หนักจริง ๆ อย่างเช่นการประมวลผล video หรือ machine learning inference เราถึงเลือกใช้ภาษาอย่าง Golang ในการทำงาน ซึ่งมีเป็นส่วนน้อยมาก ๆ

TLDR;
MeCode Infrastructure = DOKS, Terraform, Helm, GitHub Actions, Rails

สรุป

ทีมเรามี goal ที่ชัดเจนที่ทำให้เราเลือก tech stack มาในรูปแบบนี้ซึ่งตอบโจทย์ลูกค้าของเราได้ทั้งหมด

ทั้งนี้ทั้งนั้นขึ้นอยู่กับความถนัดของคนในทีมด้วย หวังว่าบทความนี้จะเป็นประโยชน์กับทีมหรือ freelancer ที่กำลังพยายาม optimize resource และ scale ปริมาณงานที่ output ได้กันไม่มากก็น้อยครับ

ใครที่มีข้อสงสัยหรืออยากปรึกษาเรื่อง software development หรือสนใจจะจ้างงานสามารถติดต่อมาที่ [email protected] ได้เลยครับผม

และถ้าใครสนใจ Digital Ocean อยู่ทีมผมเป็น partner สามารถใช้ referal link นี้ได้ credit $200 ฟรี ครับ https://m.do.co/c/71a088fffcd6