Tech Stack ของ Indy Dish

Mobile/Web App <-> Service + Serverless on AWS

Pongsakorn Teeraparpwong
Hato Hub
4 min readSep 24, 2018

--

Indy Dish ตอนนี้ยังเป็น startup ที่ค่อนข้างเล็ก โดยมี dev แค่ 2–3 คน แต่เรามี app ที่ครอบคลุม channel ครบถ้วนทั้ง mobile app, web, และ Line chatbot (แต่ยังไม่ได้ release ทั้งหมด) เรายังมีระบบหลังบ้านที่ค่อนข้างซับซ้อนเนื่องจากมีทำอาหารที่ครัวของเราเองและต้องจัดการ logistics เพื่อส่งอาหารอยู่ทุกๆวัน การวาง tech stack เพื่อพัฒนาระบบเหล่านี้ด้วย resource ที่จำกัดจึงเป็นเรื่องสำคัญ

บทความนี้จะเน้นไปที่ stack ของ mobile & web App และ backend service ที่อยู่บน AWS ถึงแม้ว่าจะดูเป็น stack ธรรมดาแต่เราได้ใช้ tech น่าสนใจหลายอย่าง startup เราเปิดโอกาสให้ทดลองใช้ tech ใหม่ๆอยู่เสมอถ้ามันดูทำให้ชีวิต dev ดีขึ้น

ยังมีเรื่องสนุกๆที่อยากแชร์อีกหลายอย่างที่ต้องเก็บไว้ครั้งหน้า เช่น Line chatbot (All Servlerless โดยใช้ Go) การทำ DevOps ที่ได้ inspiration มาจากตอนที่ทำงานที่ Amazon มาเต็มๆ และที่ขาดไม่ได้สำหรับ startup คือเรื่อง analytics

ก่อนเข้าเรื่อง ถ้าใครอยากมา explore tech สนุกๆด้วยกันใน startup environment ส่ง resume มาได้เลยที่ jobs@indydish.com 😄

Architecture Overview

Frontend + Backend Overview

Overview ง่ายๆคือมี frontend 3 ระบบ และ backend ซึ่งใช้ services ต่างๆที่อยู่บน AWS โดย service หลักคือ Rails ที่อยู่บน AWS Beanstalk และใช้ RDS PostgresQL เป็น database นอกจากนี้เรายังใช้ Lambda function กับ S3 ควบคู่ไปด้วยกับ service หลักเพื่อ run async job หลายๆอย่าง

เนื่องจากทีมเราเล็กจึงพยายามจะใช้ managed services บน AWS ให้มากที่สุดเพื่อ reliability และลด operational cost (เวลาของ dev มีค่าที่สุดสำหรับทีมเล็กๆอย่างเรา!!)

Mobile & Web Apps

Frontend Architecture

Frontend ของเราใช้ React + Redux ทั้งหมดซึ่ง run อยู่บน platform คนละแบบ โดยส่วนตัวแล้วไม่ชอบ Javascript ตรงที่มันไม่มี static type check ช่วงหลังๆเลยพยายามเอา Typescript มาใช้เพื่อแก้ปัญหาตรงนี้ซึ่งก็ได้ผลค่อนข้างดี

อีก lib นึงซึ่งเพิ่งเริ่มใช้คือ GraphQL Apollo Client โดย Apollo Client นั้น ทำให้การ bind data จาก GraphQL Backend เข้ากับ React component ง่ายขึ้นมาก และยังช่วยจัดการ cache และ state ให้ด้วย ทำให้ทิ้ง Redux ไปได้เลย และยัง support Typescript ด้วย ซึ่งก็เหมาะกับ GraphQL เพราะ Schema มี type อยู่แล้ว

เราใช้ Firebase สำหรับ User authentication เพราะ integrate กับ frontend ง่ายมาก สามารถเชื่อมกับ Facebook Login ได้ง่ายๆ และยังทำให้ใช้ feature ดีๆหลายอย่างของ Firebase ได้เช่น Remote Config, A/B testing, Predictions, etc. ข้อเสียรุนแรงของ Firebase คือ latency สูงมากๆเพราะ server ยังอยู่ที่ US

Indy Dish Mobile App

Mobile App เขียนด้วย React Native + Redux ทั้งหมด ซึ่งออกมาลื่นไหลได้ดีเกือบเท่า Native เลยทีเดียว สำหรับ component ที่ซับซ้อนก็ใช้ Storybook เพื่อช่วย design และ test ได้ ข้อดีมากๆของ React Native คือสามารถใช้ dev คนเดียวเขียนได้ทั้ง iOS, Android และ Web แต่ถ้า app มีความซับซ้อนมากการใช้ React Native อาจจะไม่ค่อย efficient เท่าไหร่ทั้งด้าน performance และ memory management ในระยะยาวแล้วเมื่อทุกอย่างนิ่งแล้วก็ยังคิดว่าควรเขียน Native ดีกว่า

Indy Dish Website

ณ วันที่เขียนบทความนี้ Indy Dish Website ยังเป็น Wordpress + WooCommerce โง่ๆอยู่ แต่เรากำลังพัฒนาเวบใหม่โดยใช้ React ทั้งหมด เนื่องจากต้องคำนึงถึง SEO ทำให้ต้องใช้ Next.js เพื่อทำ server-side rendering ด้วย โดยตัว Next.js นี้จะ run อยู่บน Lambda function + API Gateway ไว้บทความหน้าจะเจาะลึกเกี่ยวกับ Serverless มากขึ้นโดยเฉพาะการทำ DevOps

Admin Website

เวบระบบหลังบ้านเราก็เขียนด้วย React เช่นเดียวกันและมีความซักซ้อนมากพอสมควรโดยเฉพาะระบบในครัวและการจัด logictics แต่เนื่องจากไม่ต้องมี server-side rendering เลยสามารถ serve จาก S3 เป็น static website ได้เลย เราอยากใช้ domain ของเราเองเป็น HTTPs จึงต้องใช้ S3 คู่กับ AWS Cloudfront (CDN) อีกทีเพราะ Cloudfront จะให้เรา bind SSL Certificate ของ domain เราได้ ซึ่ง Certificate นี้สร้างได้ฟรีใน AWS เอง

Backend Service

Backend Architecture

Backend Service หลักของเราเป็น Ruby on Rails ที่ต่อกับ RDS PostgresQL ตอนแรกๆ Rails ก็ run อยู่บน EC2 เครื่องเดียวแต่ว่ามัน scale ไม่ได้ เลยจับใส่ Docker และไป deploy ลง AWS Elastic Beanstalk ทำให้สามารถเพิ่มลดจำนวนและขนาดเครื่องได้ตามใจชอบ โดย Beanstalk มีข้อดีอีกหลายอย่างคือสามารถ manage ทุกอย่างได้ในตัวมันเอง มี Load balancer, Monitoring, Autoscaling มาให้ครบหมด รวมถึง integrate กับ AWS CodePipeline (CICD) ได้ง่ายๆ ไม่จำเป็นต้องไปใช้ Kubernetes ให้ยุ่งยากเพราะมี Docker แค่ตัวเดียว

ตัว Ruby on Rails มี gem ซึ่งใช้ Memcached เป็น cache เพื่อเพิ่มความเร็วอยู่ (ไม่งั้นมันจะช้ามากๆ) และก็มี gem ที่ใช้ Redis เป็นตัวเก็บพวก async job queue โดยทั้ง Memcached และ Redis ก็ run อยู่บน AWS ElastiCache

ด้วยขนาดทีมในตอนนี้การใช้ Monolithic service น่าเป็นวิธีที่ดีที่สุด ซึ่งก็เหมาะกับ Ruby on Rails อยู่เพราะทำให้พัฒนาได้เร็ว แต่ตัว Rails ก็มีข้อจำกัดหลายๆอย่างทำให้เราใช้ AWS Lambda ซึ่งเป็น Serverless Function มาช่วยในเกี่ยวกับ async job และพวก generic function ทั้งหลาย ดังตัวอย่างที่จะกล่าวถึงในด้านล่างนี้

ในที่สุดเมื่อระบบใหญ่ขึ้นก็จะต้องค่อยๆแตก service นี้ออกเป็น microservice ย่อยๆ แต่การใช้ Rails นั้นทำได้ค่อนข้างยาก และโดยส่วนตัวไม่ชอบ Rails เพราะช้าและ maintain ยากมาก (ไม่มี static type) ตอนนี้เราจึงเลยค่อยๆ migrate มาเป็น Serverless Golang โดยเริ่มจากตัว Chatbot ก่อน ซึ่งไว้ค่อยพูดถึงในคราวหน้า

On-the-fly Image Resizing

ทุกครั้งที่ upload image ใหม่เช่นรูปอาหาร Rails จะ resize รูปออกเป็น 3–4 ขนาดตามที่ต้ังไว้และใส่ไว้ใน S3 แต่เมื่อใดที่เราต้องการ size ใหม่ที่ยังไม่มีมาก่อนก็ต้องไป resize รูปที่มีใหม่ทั้งหมดซึ่ง painful มากๆ วิธีที่ดีกว่าคือการใช้ Lambda function ร่วมกับ S3 เพื่อช่วยในการ resize image on-the-fly ในเวลาที่ access รูป โดยสามารถใส่ size ที่ต้องการไว้ใน URL เลย โดยทาง AWS มีตัวอย่างโค้ดใน Github โดยเราสามารถเอาไป deploy ได้ง่ายๆโดยใช้ CloudFormation วิธีนี้ทำให้สามารถใช้ image size ไหนก็ได้โดยไม่ต้อง resize ไว้ล่วงหน้า ถ้าไม่มี size นั้น Lambda Function ก็จะ resize และ save ลง S3 เพื่อ access ในครั้งต่อๆไป เนื่องจากเป็น Serverless เราจึงไม่ต้องกังวลเรื่อง Scale ด้วย โดย trick จริงๆของการทำงานอันนี้คือการ redirect ไปๆมาๆระหว่าง S3 และ Lambda function นั่นเอง

จริงๆที่ Amazon มี Media Server ที่อลังการกว่านี้มาก สามารถ transform รูปได้หลายรูปแบบโดยการใส่เป็น parameter ต่อท้ายชื่อ file แล้วก็กระจายไปตาม CDN ทั่วโลก แต่สำหรับ Indy Dish คงยังไม่ต้องทำถึงขนาดนั้น

Bank Slip Verification

อันนี้เป็นตัวอย่างการใช้ Lambda function (Golang 😛) เพื่อ process async job ที่ถูก trigger ด้วย event จาก S3 เอาไว้ใช้เวลาลูกค้าส่ง bank slip เข้ามาเพื่อยืนยันว่าโอนเงินแล้ว โดย slip image นี้จะถูก upload เข้าไปใน S3 โดยตรงจาก mobile app (อย่าเปิด S3 public นะ มันมีวิธีทำอยู่) และจะไป trigger Lambda function ตัวนี้

เราใช้ Google Cloud Vision API เพื่อ parse text ที่อยู่ในรูป (คล้ายๆ OCR) เพื่อนำมาวิเคราะห์อีกที พอเสร็จก็จะส่งผลกลับไปที่ Rails Service เพื่อดูว่า Slip ถูกต้องรึเปล่า จริงๆ AWS มี Amazon Rekoginition ที่ทำอะไรคล้ายๆกับ Google Vision API แต่ว่าของ Google ดีกว่าเยอะมากแถมอ่านภาษาไทยได้ด้วย พวกเรื่อง AI นี่ต้องยกให้ Google จริงๆ

PDF generation

ตัวอย่างสุดท้ายนี้เป็น Lambda function อีกอันเพื่อเอาไว้แปลง HTML เป็น PDF (โค้ดมาได้จาก github นี้) จริงๆใน Rails ก็มี Gem เพื่อทำสิ่งนี้อยู่แต่พบว่ามันใช้ memory เยอะมากและไม่ยอมคืนทำให้ mem เต็มบ่อยๆ ในที่สุดจึงตัดสินใจแยกมันออกมาไว้ใน Lambda ทำให้ไม่เกิดปัญหานี้อีก จริงๆทางที่ดีควร save PDF ไว้ใน S3 เพื่อให้ client โหลดโดยตรงจะได้ไม่ต้องส่ง file ผ่าน Rails ให้เปลือง memory และ bandwidth

Key Takeaways

  • React + GraphQL Apollo Client + Typescript เป็น combination ที่ดีทีเดียว
  • ใช้ Lambda มาช่วยแบ่งภาระของ Monolith ได้โดยเฉพาะพวก async job หรือ event trigger ต่างๆ โดยเฉพาะ generic function ที่สามารถ Reuse ใช้ได้โดย service อื่นๆด้วย เช่น image resize หรือ pdf generation
  • พยายามใช้ Managed service ให้เยอะที่สุด (Firebase Auth, Beanstalk, Lambda, RDS, ElastiCache, API Gateway, Vision API, etc.) เพื่อลด operational cost และความเสี่ยงด้านอื่นๆ

--

--

Pongsakorn Teeraparpwong
Hato Hub

CTO & Co-founder @ Indie Dish • AWS Certified • TEDx Volunteer || Ex-SDE @ Amazon.com • Chula Intania 87 • BCC 150