18 คะแนน โดย xguru 2024-11-17 | 6 ความคิดเห็น | แชร์ทาง WhatsApp
  • เมื่อสร้าง Docker container image หาก Dockerfile ไม่ได้ใช้โครงสร้างแบบ Multi-Stage ก็มีโอกาสสูงที่จะมีไฟล์ที่ไม่จำเป็นถูกรวมเข้าไปด้วย
  • สิ่งนี้นำไปสู่การเพิ่มขึ้นของขนาดอิมเมจและความเสี่ยงด้านความปลอดภัยที่มากขึ้น
  • บทความนี้วิเคราะห์สาเหตุหลักของ “ไฟล์ที่ไม่จำเป็น” ที่อาจเกิดขึ้นใน container image และอธิบายวิธีแก้ไขด้วย Multi-Stage Build

สาเหตุที่ทำให้อิมเมจมีขนาดใหญ่ขึ้น

  • แอปพลิเคชันมี dependency ทั้งในช่วง build time และ runtime
  • dependency ในช่วง build time มีมากกว่าช่วง runtime และมีช่องโหว่ด้านความปลอดภัย (CVEs) มากกว่า
  • หากใช้อิมเมจเดียวกันทั้งสำหรับการ build และการรัน จะทำให้มี dependency ของ build time ที่ไม่จำเป็น (เช่น compiler, linter) รวมอยู่ด้วย
  • ควรแยก build image และ runtime image ออกจากกัน แต่หลายกรณีก็มักมองข้ามจุดนี้

ตัวอย่างโครงสร้าง Dockerfile ที่ไม่ถูกต้อง

ตัวอย่างที่ไม่ถูกต้องสำหรับแอปพลิเคชัน Go

FROM golang:1.23  
WORKDIR /app  
COPY . .  
RUN go build -o binary  
CMD ["/app/binary"]  
  • อิมเมจ golang:1.23 มีไว้สำหรับการคอมไพล์ แต่หากนำไปใช้ใน production ตรง ๆ ก็จะรวมทั้ง Go compiler และ dependency ทั้งหมดเข้าไปด้วย
  • ขนาดอิมเมจ: มากกว่า 800MB และมีช่องโหว่ด้านความปลอดภัยมากกว่า 800 รายการ

ตัวอย่างที่ไม่ถูกต้องสำหรับแอปพลิเคชัน Node.js

FROM node:lts-slim  
WORKDIR /app  
COPY . .  
RUN npm ci  
RUN npm run build  
ENV NODE_ENV=production  
EXPOSE 3000  
CMD ["node", "/app/.output/index.mjs"]  
  • โฟลเดอร์ node_modules จะรวม dependency สำหรับการพัฒนาที่ไม่จำเป็นต่อ runtime เข้าไปด้วย
  • ไม่สามารถแก้ได้ด้วย npm ci --omit=dev เพราะอาจทำให้ dependency สำหรับการ build ถูกลบออกไป

วิธีสร้าง Lean image ก่อนมี Multi-Stage Build

Builder pattern

  1. build แอปพลิเคชันใน Dockerfile.build:
FROM node:lts-slim  
WORKDIR /app  
COPY . .  
RUN npm ci  
RUN npm run build  
  1. คัดลอก artifact ที่ build เสร็จแล้วออกมาที่โฮสต์:
docker cp $(docker create build:v1):/app/.output .  
  1. สร้าง runtime image ใน Dockerfile.run:
FROM node:lts-slim  
WORKDIR /app  
COPY .output .  
CMD ["node", "/app/.output/index.mjs"]  
•	ปัญหา: ต้องเขียน Dockerfile หลายไฟล์ ต้องจัดการลำดับการ build และต้องมีสคริปต์เพิ่มเติม  

ทำความเข้าใจ Multi-Stage Build

  • Multi-Stage Build คือความสามารถที่นำ Builder pattern มาใช้งานภายใน Docker
    • สามารถกำหนดทั้ง build stage และ runtime stage ได้ใน Dockerfile เดียว ด้วยการใช้คำสั่ง FROM หลายครั้ง
    • ใช้คำสั่ง COPY --from=<stage> เพื่อนำไฟล์ที่ build จาก stage ก่อนหน้ามาใช้

ตัวอย่าง Multi-Stage Dockerfile (Node.js)

# Build stage  
FROM node:lts-slim AS build  
WORKDIR /app  
COPY . .  
RUN npm ci  
RUN npm run build  
  
# Runtime stage  
FROM node:lts-slim AS runtime  
WORKDIR /app  
COPY --from=build /app/.output .  
ENV NODE_ENV=production  
CMD ["node", "/app/.output/index.mjs"]  
  • การใช้ COPY --from=build เพื่อคัดลอก artifact ที่ build แล้วโดยตรง ทำให้ย้ายไฟล์ได้โดยไม่ต้องผ่านโฮสต์

ตัวอย่างการใช้งาน Multi-Stage Build จริง

React แอปพลิเคชัน

# Build stage  
FROM node:lts-slim AS build  
WORKDIR /app  
COPY . .  
RUN npm ci  
RUN npm run build  
  
# Runtime stage  
FROM nginx:alpine  
COPY --from=build /app/build /usr/share/nginx/html  
ENTRYPOINT ["nginx", "-g", "daemon off;"]  
  • หลังจาก build แล้ว React แอปพลิเคชันจะกลายเป็นไฟล์แบบ static และสามารถเสิร์ฟผ่าน Nginx ได้

Go แอปพลิเคชัน

# Build stage  
FROM golang:1.23 AS build  
WORKDIR /app  
COPY . .  
RUN go build -o binary  
  
# Runtime stage  
FROM gcr.io/distroless/static-debian12:nonroot  
COPY --from=build /app/binary /app/binary  
ENTRYPOINT ["/app/binary"]  
  • ใช้อิมเมจ distroless เพื่อให้ได้ runtime environment ที่เล็กที่สุดเท่าที่จำเป็น

Java แอปพลิเคชัน

# Build stage  
FROM eclipse-temurin:21-jdk-jammy AS build  
WORKDIR /build  
COPY . .  
RUN ./mvnw package -DskipTests  
  
# Runtime stage  
FROM eclipse-temurin:21-jre-jammy  
COPY --from=build /build/target/app.jar /app.jar  
CMD ["java", "-jar", "/app.jar"]  
  • ใช้ JDK ในขั้นตอน build และใช้ JRE ที่เบากว่าในขั้นตอน runtime

บทสรุป

  • Multi-Stage Build ช่วยแยกสภาพแวดล้อมสำหรับการ build และ runtime ออกจากกัน เพื่อลดปัญหาขนาดอิมเมจที่เพิ่มขึ้นจาก dependency สำหรับการพัฒนาที่ไม่จำเป็น
  • ด้วยวิธีนี้สามารถลดขนาดอิมเมจ เพิ่มความปลอดภัย และทำให้กระบวนการ build ง่ายขึ้นได้
  • Multi-Stage Build เป็นแนวทางมาตรฐานในการสร้าง container image ที่มีประสิทธิภาพ และยังรองรับฟีเจอร์ขั้นสูงด้วย (เช่น เงื่อนไขการแตกแขนง, unit test ระหว่างการ build)

6 ความคิดเห็น

 
savvykang 2024-11-18

ในกรณีของ Java แม้ว่า jlink จะถูกนำมาใช้ตั้งแต่เวอร์ชัน 9 แต่การใช้งานยังไม่ค่อยสะดวกนัก เช่น ต้องหาโมดูลที่พึ่งพาด้วย jdeps แล้วระบุเอง พอเห็นว่าคนจำนวนมากไม่รู้วิธีแบบนั้นหรือยังคงมองหา JRE ก็รู้สึกว่าเครื่องมือของ Java ยังถูกประชาสัมพันธ์ไม่เพียงพอ และน่าจะต้องปรับปรุงให้สามารถสร้าง JRE ได้ด้วยคำสั่งเดียว

 
brainer 2024-11-17

ผมก็ใช้อยู่แบบนั้นเหมือนกัน แต่ข้อเสียคือดูเหมือนว่าเวลาในการบิลด์จะนาน

 
kandk 2024-11-18

เวลาในการบิลด์ไม่น่าจะต่างกันนะครับ ถ้าต่างแปลว่าตั้งค่าผิด!

 
brainer 2024-11-18

อ๋อ เข้าใจแล้ว!

 
qurare 2024-11-18

ขึ้นอยู่กับกลยุทธ์ เราอาจแคชทั้งสเตจได้เลยด้วยซ้ำ เลยกลายเป็นว่าสำหรับผม เวลาบิลด์กลับสั้นลงด้วย!

 
brainer 2024-11-18

คงต้องไปทำความรู้จัก Docker ให้มากกว่านี้แล้วล่ะ!