Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

การทำ Container ให้กับแอปพลิเคชัน Rails: คู่มือปฏิบัติจริง

การทำ Container (Containerizing) ของซอฟต์แวร์คือการบรรจุซอฟต์แวร์เป็นหน่วยมาตรฐานเพื่อความสะดวกในการพัฒนาและปรับใช้งาน (Deployment) โดย Container จะรวมโค้ดของแอปพลิเคชันร่วมกับทุก dependency ที่จำเป็นไว้ด้วยกัน Container สามารถทำงานได้แบบสแตนด์อโลน (Standalone) โดยประกอบด้วยแพ็กเกจของซอฟต์แวร์ รันไทม์ (Runtime environment) และไลบรารีระบบ (System libraries) Container ช่วยให้นักพัฒนาและทีม Operations มั่นใจได้ว่าซอฟต์แวร์จะทำงานเหมือนกันไม่ว่าจะอยู่ในสภาพแวดล้อมใดๆ ด้วยการแยกโค้ดออกจากโครงสร้างพื้นฐาน แอปที่ "ทำ Container แล้ว" จะรันได้เหมือนกันทั้งใน Local, Test และ Production

Docker คืออะไร?

Docker เป็นหนึ่งในแพลตฟอร์มยอดนิยมที่สุดสำหรับการพัฒนาและปรับใช้ซอฟต์แวร์ Docker บรรจุซอฟต์แวร์เป็น "Image" ซึ่งจะถูกเปลี่ยนเป็น Container ในขณะรันไทม์เมื่อ execute บน Docker Image การแยก Isolation นี้ช่วยให้นักพัฒนาสามารถรัน Container หลายตัวบน Host เดียวพร้อมกันได้

ความท้าทายสำหรับนักพัฒนา Rails

นักพัฒนา Rails เผชิญกับชุดความท้าทายที่เฉพาะตัวเมื่อทำ Container ให้กับแอปพลิเคชันที่มีอยู่แล้ว บทความนี้จะให้ Walkthrough การทำ Container ให้กับแอป Rails ที่ทำงานได้จริง และอธิบาย Concepts และ Pitfalls สำคัญตลอดทาง บทความนี้ไม่ใช่คำอธิบายพื้นฐานเกี่ยวกับ Container หรือ Docker แต่เป็นการอธิบายปัญหาที่นักพัฒนาเจอเมื่อทำ Container ให้ Production Applications

Prerequisites (ข้อกำหนดเบื้องต้น)

หากคุณต้องการติดตามทดลอง คุณจะต้องมีแอปพลิเคชัน Rails ที่ยังไม่ได้ทำ Docker 化 (Dockerized) ผมจะใช้ RailsWork โปรเจกต์ Side project ที่เพิ่ง Launch มา เป็น Job Board เขียนด้วย Rails และ Deploy บน Heroku แต่ยังไม่ได้ทำ Container

นอกจากนี้ คุณต้องมี Docker ติดตั้งอยู่แล้ว วิธียอดนิยมคือใช้ Docker Desktop ซึ่งดาวน์โหลดได้จากเว็บไซต์อย่างเป็นทางการ

การทำ Container ให้กับแอปพลิเคชัน Rails: คู่มือปฏิบัติจริง

หลังดาวน์โหลดเสร็จ ให้รัน Installer จะมี Prompt ให้ลากแอปพลิเคชันไปที่โฟลเดอร์ Applications แล้ว Launch จากโฟลเดอร์ Applications และ Grant สิทธิ์ Privileged ตามที่ขอ ตรวจสอบอีกครั้งว่า Docker ติดตั้งถูกต้องด้วยคำสั่ง:

docker ps

ถ้า Docker ติดตั้งแล้ว (และยังไม่มี Container รันอยู่) จะได้รายการว่างเปล่าพร้อม Header แบบนี้:

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES

The Dockerfile (ไฟล์ Dockerfile)

สำคัญมากที่จะเริ่มต้นด้วย Terminology ที่ชัดเจนก่อนจะลงลึก

  • Container: หลังจากแอป Rails ถูก "Dockerized" แล้ว จะรันใน Container ซึ่ง Standalone, Replaceable และมัก Rebuild บ่อย
  • Image: Container ถูกสร้างจาก Image ซึ่งเป็น Virtual Snapshot ของ File System คู่กับ Metadata
  • Dockerfile: เป็น Source Code ที่อธิบายว่าควรสร้าง Image อย่างไร Dockerfile มักจะรวมอยู่ใน Repo ของแอปที่ Dockerized และ Track ใน Version Control คู่กับแอป

การสร้าง Dockerfile ไมยากขนาดที่คิด Docker ให้ Syntax พิเศษที่ Abstract ความยากของการทำ Container ไป ก่อนอื่น ให้ไปที่ Root Directory ของแอปที่ต้องการทำ Container หากใช้ Git แนะนำสร้าง Branch ใหม่ เช่น dockerize-this-app:

git checkout -b dockerize-this-app

สร้าง Dockerfile และสั่ง Build Image จาก Ruby Application:

echo "FROM ruby:3.0.0" > Dockerfile

ที่นี่ระบุ Ruby Container Image (โปรเจกต์ผมใช้ Ruby 3.0.0) หากใช้ Version อื่น ไม่เป็นไร Docker มีรายการ Image ที่รองรับทั้งหมด

สั่ง Docker สร้าง Image:

docker build -t rails_work .

แทนที่ rails_work ด้วยชื่อ Image ที่ต้องการ และอย่าลืมจุด (.) ท้ายคำสั่ง

ตรวจสอบ Image ที่สร้าง:

docker image list

Image นี้ยังว่างเปล่า ไม่ได้มีโค้ดแอปของเรา ให้เพิ่มใน Dockerfile เพื่อ Copy โค้ดและ Install Dependencies:

ADD . /rails_work
WORKDIR /rails_work
RUN bundle install

(แทนที่ rails_work ด้วยชื่อแอปของคุณ)

รันสร้าง Image อีกครั้ง:

docker image list

ปัญหาที่อาจเกิดขึ้นกับ Bundle Install

มีโอกาสเกิด Issue โดยเฉพาะ Production App ที่มีอยู่แล้ว Bundler อาจบ่นว่า Version ที่ Image พยายามใช้ ต่างจาก Version ที่สร้าง Gemfile.lock มี 2 ทางเลือก:

  1. เปลี่ยน Version ที่ Image ใช้
  2. ลบ Gemfile.lock ทิ้ง แต่ต้อง Pin Version ของ Gems ที่จำเป็นไว้ก่อน เพราะ Lockfile จะถูกสร้างใหม่ทั้งหมด

ถ้า Bundle Install ยังล้มเหลว อาจต้องติดตั้งเพิ่มใน Dockerfile:

RUN apt-get update && apt-get install -y shared-mime-info

ถ้ายังมีปัญหา อาจเลือก Ruby Image ผิด** ควรตรวจสอบจากตรงนี้

ตั้งค่า Environment Variables

ENV RAILS_ENV production
ENV RAILS_SERVE_STATIC_FILES true

Expose Port 3000

EXPOSE 3000

คำสั่งเริ่มต้น Container

CMD ["bash"]

รวมกัน Dockerfile จะประมาณนี้ (แทนที่ชื่อ rails_work):

FROM ruby:3.0.0
ADD . /rails_work
WORKDIR /rails_work
RUN bundle install
ENV RAILS_ENV production
ENV RAILS_SERVE_STATIC_FILES true
EXPOSE 3000
CMD ["bash"]

คำอธิบายคำสั่ง Dockerfile

  • FROM: กำหนด Base Image
  • RUN: Execute คำสั่ง ภายใน Container
  • ENV: กำหนด Environment Variables
  • WORKDIR: เปลี่ยน Working Directory ของ Container
  • CMD: ระบุโปรแกรมที่รันตอน Container เริ่มต้น

Docker Compose

ตามเอกสาร Docker "Compose" เป็น Tool สำหรับสร้างและเริ่ม Application ที่มีหลาย Docker Containers ทุกอย่างที่ต้องการ Spin up Containers จะระบุใน YAML รัน docker-compose up ก็จะสร้าง Containers ได้ Docker-compose ช่วยให้เราอธิบาย Container Configuration แบบ Declarative

ก่อนสร้าง Docker Compose file ให้บอก Docker ว่าไฟล์ไหน Exclude จาก Image สร้างไฟล์ .dockerignore (มีจุดนำหน้า) ใส่:

.git
.dockerignore
.env

ถ้า Gemfile Maintained โดย Build Process ให้เพิ่ม Gemfile.lock ใน Ignore ด้วย

สร้าง docker-compose.yml:

version: '3.8'
services:
  db:
    image: postgres
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    volumes:
      - postgres:/var/lib/postgresql/data
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/Rails-Docker
    ports:
      - "3000:3000"
    depends_on:
      - db
volumes:
  postgres:

File นี้สร้าง 2 Services: db และ web db ใช้ Postgres Image สลับค่า POSTGRES_USER และ POSTGRES_PASSWORD ให้เหมาะสม ห้ามวาง Production Secrets ในไฟล์นี้** - ดูส่วน "Managing Secrets" ด้านล่าง

Web Container Build จาก Dockerfile ของเรา แล้ว Start Rails Server Port 3000 ที่ IP 0.0.0.0 Map Internal Port 3000 ไป Port 3000 จริง และมี Postgres Volume Persist Data

Managing Secrets (การจัดการ Secrets)

Authentication ตอน Build-time อาจเป็นปัญหาสำหรับ Production App เช่น App ดึง Gems จาก Private Repo หรือต้องเก็บ Database Credentials

ข้อมูลที่อยู่ใน Dockerfile โดยตรงจะถูก Bake เข้า Image และนี่คือ Security Pitfall ที่พบบ่อย

ถ้าใช้ Rails Credentials Manager ให้ Docker เข้าถึงได้ง่าย ใน Docker Compose ให้ให้ RAILS_MASTER_KEY Environment Variable:

version: '3.8'
services:
  db:
    image: postgres
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    volumes:
      - postgres:/var/lib/postgresql/data
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/Rails-Docker
    ports:
      - "3000:3000"
    depends_on:
      - db
    environment:
      - RAILS_MASTER_KEY=this_would_be_the_key
volumes:
  postgres:

แต่ตอนนี้เจอ Crossroads: อยาก Commit ไฟล์นี้ แต่ ห้ามให้ Master Key หรือ Database Password ถูก Track โดย Source Control เพราะเป็น Security Issue อีกอย่าง วิธีดีที่สุดคือใช้ dotenv Gem เก็บ Credentials ในไฟล์แยกที่ไม่ Track

Running the Dockerized Application (การรันแอปที่ Dockerized แล้ว)

รันแอปด้วยคำสั่ง:

docker compose up

เพียงเท่านี้! Docker-compose ทำให้ Spin up Container ง่ายมาก

ดู Container ที่รันอยู่:

docker ps

ถ้า Rails Container ชื่อ web สั่ง Exec เข้าไปได้:

docker exec -it web rails console

หรือเข้า Bash Shell:

docker exec -it web bash

Pitfalls เพิ่มเติม

Logs: ใน Production ไม่ควรเก็บ Logs ใน Container นาน Docker แนะนำ Redirect ไป STDOUT ตั้งค่าใน config/application.rb

Mailers: ถ้าใช้ Mailers ต้อง Define Connection Settings ให้ชัดเจน SMTP ใช้ได้ดีแต่อย่าลืมตั้ง Server Location ให้ตรง Container Config

Background Jobs (Sidekiq): ถ้ามี Workers ต้องรันใน Container แยก

Conclusion (สรุป)

การทำ Container ให้ Production Rails App มาพร้อม Challenges เยอะ แอปโตขึ้นมักมี Dependencies เยอะ ทำให้ Migration ยาก แต่มี Patterns จัดการ Pitfalls ส่วนใหญ่แล้ว งานตั้งต้นเสร็จแล้ว ความสะดวกในการ Deploy และเปลี่ยนแปลงภายหลังจะคุ้มค่า