Get a quote

تحسين صور Docker للـ Go على AWS ECS: البناء متعدد المراحل والحاويات الخفيفة

الصورة الافتراضية للـ Go في Docker تقترب من 900 ميجابايت. نفس التطبيق في صورة scratch يقل عن 10 ميجابايت. هذا الفارق يؤثر مباشرة على سرعة النشر في ECS. إليك كيف نبني حاويات Go للإنتاج.

الصورة الافتراضية للـ Go في Docker تقترب من 900 ميجابايت. نفس التطبيق المترجم في صورة scratch لا يتجاوز 10 ميجابايت. هذا الفارق الكبير له تأثير مباشر على أداء النشر في AWS ECS: الصور الصغيرة تُرسل إلى ECR بشكل أسرع، وتُسحب على مهام Fargate بوقت أقل، وتُقلل من مساحة الهجوم الأمنية. في هذا المقال نشرح كيف نبني صور Docker للـ Go في بيئات الإنتاج التي ننشرها في لبنان وعبر منطقة الشرق الأوسط.

لماذا يهم حجم الصورة أكثر مما يتوقع معظم الفرق؟

عندما تبدأ مهمة ECS جديدة، تقوم Fargate بسحب صورة الحاوية من ECR إلى العقدة المضيفة. إذا كانت الصورة كبيرة الحجم، يُضيف هذا السحب ثوانٍ أو عشرات الثوانٍ إلى وقت بدء التشغيل البارد. في الخدمات التي تعتمد على التوسع التلقائي، يتضاعف هذا التأخير: موجة حركة مرور تطلق حدث توسع، لكن المهام الجديدة تكون بطيئة في الوصول إلى حالة الجاهزية لأنها تنتظر سحب صورة كبيرة.

الصور الصغيرة تُقلص أيضاً من آثار الاختراق الأمني. صورة scratch لا تحتوي إلا على الملف التنفيذي المترجم وشهادات CA. لا يوجد shell، ولا مدير حزم، ولا libc. المهاجم الذي يحصل على تنفيذ أكواد داخل الحاوية لا يجد أدوات يعمل بها.

نمط Dockerfile متعدد المراحل

النهج القياسي يستخدم مرحلتين: مرحلة البناء التي تترجم الملف التنفيذي، ومرحلة نهائية خفيفة لا تحتوي إلا على ما يحتاجه التطبيق للتشغيل.

# المرحلة الأولى: الترجمة
FROM golang:1.22-alpine AS builder

WORKDIR /build

# نسخ ملفات التبعيات أولاً لتعظيم استخدام ذاكرة التخزين المؤقت للطبقات
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
    -ldflags="-s -w" \
    -o app \
    ./cmd/server

# المرحلة الثانية: الصورة النهائية
FROM scratch

COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /build/app /app

USER 65534:65534
EXPOSE 8080
ENTRYPOINT ["/app"]

العلامة CGO_ENABLED=0 تنتج ملفاً تنفيذياً مرتبطاً بشكل ثابت لا يعتمد على مكتبة C للنظام المضيف. بدون هذه العلامة، يحاول الملف التنفيذي تحميل libc من بيئة التشغيل، وهي غير موجودة في صورة scratch.

فهم التخزين المؤقت للطبقات في بناءات Go

ترتيب تعليمات COPY يحدد مدى فعالية Docker في تخزين الطبقات مؤقتاً. المبدأ الأساسي: انسخ الملفات التي تتغير نادراً قبل الملفات التي تتغير كثيراً.

ملفات وحدة Go تتغير فقط عند إضافة تبعيات جديدة أو تحديثها، وهو أمر نادر مقارنة بتغييرات الكود المصدري. بنسخها أولاً وتشغيل go mod download في طبقة منفصلة، يخزن Docker خطوة تنزيل الوحدات بالكامل. البناءات اللاحقة التي تغير فيها الكود المصدري فقط تتخطى التنزيل كلياً.

scratch مقابل distroless مقابل Alpine

scratch: الخيار الأدنى حجماً. لا حزم نظام تشغيل، لا shell. الملف التنفيذي هو الشيء الوحيد الذي يعمل. خيارنا الأول لخدمات Go التي تُجري اتصالات HTTP خارجية فقط.

gcr.io/distroless/static-debian12: قاعدة تحافظ عليها Google بدون shell لكن مع ملفات نظام Debian الأساسية بما فيها بيانات المنطقة الزمنية وشهادات CA. مفيدة عندما يحتاج تطبيقك إلى بيانات المنطقة الزمنية.

alpine:3.x: الأثقل من الثلاثة بحوالي 7 ميجابايت، لكن مفيد عند الحاجة إلى shell للتنقيح.

في خدمات منصة RTYLR، نستخدم scratch للخدمات HTTP البحتة وdistroless للخدمات التي تحتاج إلى معالجة المناطق الزمنية في حسابات التواريخ.

ذاكرة التخزين المؤقت للطبقات في ECR لبناءات CI

بدون إعداد خاص، كل بناء CI يبدأ من الصفر ويعيد تنزيل جميع التبعيات. مع ECR يمكن استخدام السجل كمصدر تخزين مؤقت:

- name: Build and push
  uses: docker/build-push-action@v5
  with:
    cache-from: type=registry,ref=${{ env.ECR_REGISTRY }}/myservice:cache
    cache-to: type=registry,ref=${{ env.ECR_REGISTRY }}/myservice:cache,mode=max

هذا يقطع أوقات البناء من 3-4 دقائق إلى أقل من 60 ثانية لمعظم خدمات Go.

بيانات المنطقة الزمنية في صور scratch

مكتبة Go القياسية تقرأ بيانات المنطقة الزمنية من نظام الملفات عند وقت التشغيل. في صورة scratch، لا يوجد نظام ملفات بخلاف ما نسخته صراحةً.

الحل الأبسط هو استخدام قاعدة بيانات المنطقة الزمنية المضمنة في Go 1.15:

import _ "time/tzdata"

هذا يُضمِّن قاعدة البيانات كاملة في الملف التنفيذي بإضافة حوالي 450 كيلوبايت فقط.

دروس من بيئات الإنتاج

  • استخدم دائماً البناء متعدد المراحل. لا ترسل أبداً ملفاً تنفيذياً Go في صورة golang القاعدية.
  • انسخ go.mod و go.sum قبل الكود المصدري لتعظيم ضربات ذاكرة التخزين المؤقت للطبقات.
  • استخدم CGO_ENABLED=0 و ldflags للحصول على ملفات تنفيذية ثابتة ومخففة.
  • scratch هو الاختيار الصحيح لمعظم خدمات Go الخالصة.
  • استخدم ECR كمصدر تخزين مؤقت في CI لتقليص أوقات البناء بشكل كبير.

هل تبني خدمات SaaS على AWS وتريد تحسين بنية النشر؟ نساعد الفرق الهندسية في لبنان والشرق الأوسط في بناء بنية تحتية إنتاجية منذ البداية. تواصل معنا عبر https://voxire.com/get-a-quote/

العودة إلى المدونة
Chat on WhatsApp