Docker المتقدم وأمن الحاويات

BuildKit وأداء البناء

18 دقيقة الدرس 2 من 28

BuildKit وأداء البناء

كان محرك بناء Docker القديم — المُستدعى بواسطة docker build قبل عصر BuildKit — معالجًا تسلسليًا للطبقات؛ لا يستطيع تشغيل المراحل المستقلة بالتوازي، ويُخزّن الأسرار في تاريخ الصورة، وكانت ذاكرة التخزين المؤقت لديه خشنة الحبيبات. BuildKit، الذي أصبح المحرك الافتراضي منذ Docker 23.0، يعيد كتابة هذه القواعد من أساسها. في Google وMeta وكل شركة كبيرة أخرى، فهم آليات BuildKit الداخلية ليس اختياريًا: دورة بناء مدتها 90 ثانية كان يمكن أن تكون 18 ثانية تمثّل ساعات عمل مهندسين حقيقية تُهدر يوميًا على مستوى الأسطول.

كيف يختلف BuildKit معماريًا

يُمثّل BuildKit ملف Dockerfile كـرسم بياني دوري موجّه (DAG) من عمليات LLB (Low-Level Build) بدلًا من قائمة تعليمات مسطّحة. يُرسَل هذا الرسم إلى الديمون buildkitd، الذي يمكنه جدولة العقد المستقلة بالتوازي، وتخطّي الرسوم الفرعية التي نتائجها مُخزَّنة مسبقًا، وبث الطبقات كـblobs قابلة للعنونة بالمحتوى. النتيجة: بناء متعدد المراحل متزامن فعلًا لا مجرد مظهر.

BuildKit DAG scheduling: parallel stages merged into final image Base: node:20 FROM node:20-alpine Base: golang:1.22 FROM golang:1.22-alpine Stage: frontend npm ci && npm run build Stage: gobuilder go build -o /app ./... Stage: test go test ./... (parallel) Stage: final COPY --from=gobuilder COPY --from=frontend BuildKit schedules these in parallel Cache mount: /root/.cache persists across builds
يجدول BuildKit المراحل المستقلة بالتوازي ويدمج مخرجاتها في الصورة النهائية.

تفعيل BuildKit

منذ Docker Engine 23.0، أصبح BuildKit هو الافتراضي. في البيئات القديمة أو بيئات CI، فعّله صراحةً:

# لكل استدعاء (متغير بيئة) DOCKER_BUILDKIT=1 docker build -t myapp:latest . # بشكل دائم في إعدادات داعمة Docker (‎/etc/docker/daemon.json) { "features": { "buildkit": true } } # تحقق من تفعيل BuildKit — ابحث عن "BuildKit" في رأس مخرجات البناء docker build --progress=plain -t myapp:latest . 2>&1 | head -5

تثبيت الذاكرة المؤقتة (Cache Mounts): أكبر مكسب بخطوة واحدة

يُرفق تثبيت الذاكرة المؤقتة (--mount=type=cache) مجلدًا دائمًا بتعليمة RUN يبقى عبر دورات البناء دون أن يتحول إلى طبقة. هذه هي الطريقة المعيارية لتخزين مديري الحزم مؤقتًا. بدون تثبيت الذاكرة المؤقتة، يُعيد كل go mod download أو pip install تنزيل جيجابايتات من الاعتماديات عند كل إغفال للذاكرة المؤقتة.

# syntax=docker/dockerfile:1 FROM golang:1.22-alpine AS builder WORKDIR /src COPY go.mod go.sum ./ # تثبيت الذاكرة المؤقتة: يُخزَّن cache وحدات Go خارج رسم الطبقات. # المعرّف "go-mod" مشترك عبر جميع دورات البناء على هذا المضيف. RUN --mount=type=cache,target=/root/go/pkg/mod \ go mod download COPY . . # تثبيت ثانٍ لـcache البناء — تجميع تدريجي RUN --mount=type=cache,target=/root/.cache/go-build \ CGO_ENABLED=0 GOOS=linux go build -trimpath -o /app/server ./cmd/server FROM gcr.io/distroless/static-debian12 COPY --from=builder /app/server /server ENTRYPOINT ["/server"]
نطاق تثبيت الذاكرة المؤقتة مهم. بشكل افتراضي يكون النطاق مقيدًا بالـid والمستخدم الحالي. اضبط sharing=locked (بناء واحد في كل مرة) أو sharing=private (نسخة مستقلة لكل بناء) عند تشغيل دورات بناء متزامنة في CI لتجنب تلف الذاكرة المؤقتة. الافتراضي هو sharing=shared — مناسب لمعظم الحالات.

أسرار البناء: لا مكان لها في الطبقات

الحل القديم ما قبل BuildKit للأسرار (ARG، ENV، النسخ متعدد المراحل) كان دائمًا يترك السر في طبقة وسيطة على الأقل يمكن الوصول إليها عبر docker history. أسرار BuildKit تُثبَّت كـtmpfs أثناء تعليمة RUN فقط ولا تُكتب في أي طبقة على الإطلاق. هذه هي الطريقة الآمنة الوحيدة لاستهلاك بيانات الاعتماد في وقت البناء.

# syntax=docker/dockerfile:1 FROM python:3.12-slim WORKDIR /app COPY requirements.txt . # يُثبَّت السر على /run/secrets/pip_token أثناء هذه RUN فقط. # لا يظهر في docker history أو أي طبقة. RUN --mount=type=secret,id=pip_token \ pip install \ --extra-index-url https://$(cat /run/secrets/pip_token)@pypi.company.internal/simple \ --no-cache-dir \ -r requirements.txt COPY src/ ./src/ # استدعاء البناء — مرّر السر من بيئة المضيف أو ملف # docker build --secret id=pip_token,env=PIP_TOKEN . # docker build --secret id=pip_token,src=/run/secrets/pip_token .
لا تستخدم ARG لتمرير الأسرار أبدًا. حتى لو لم يُطبع ARG صراحةً، تُخزَّن قيمته في manifest الصورة ويمكن استردادها بـdocker history --no-trunc. في سجلات الحزم الداخلية على نطاق Google، هذه ثغرة أمنية نشطة. استخدم --mount=type=secret حصريًا.

تمرير وكيل SSH في وقت البناء

تتطلب اعتماديات Git الخاصة وصول SSH أثناء البناء. يوفر BuildKit --mount=type=ssh الذي يُعيد توجيه socket وكيل SSH من المضيف إلى عملية البناء — دون أن تلمس أي ملف مفاتيح الصورة قط:

# في Dockerfile RUN --mount=type=ssh \ git clone git@github.com:company/private-lib.git /tmp/lib # استدعاء البناء — تأكد من تحميل المفتاح في ssh-agent ssh-add ~/.ssh/id_ed25519 docker build --ssh default . # في GitHub Actions (CI) - uses: webfactory/ssh-agent@v0.9.0 with: ssh-private-key: ${{ secrets.DEPLOY_KEY }} - run: docker build --ssh default .

docker buildx والبناء متعدد المنصات

buildx هو إضافة CLI التي تكشف واجهة BuildKit الكاملة. ميزته الأكثر أهمية في الإنتاج هي بناء الصور متعددة المنصات — بناء linux/amd64 وlinux/arm64 في أمر واحد ودفع manifest متعدد المعماريات. في AWS وGCP، يوفّر arm64 (Graviton / Tau T2A) تخفيضًا في التكلفة بنسبة 20–40% للأحمال الثقيلة على المعالج.

# إنشاء builder يدعم التجميع المتقاطع عبر QEMU docker buildx create --name multiarch --driver docker-container --bootstrap docker buildx use multiarch # بناء ودفع صورة متعددة المعماريات في خطوة واحدة docker buildx build \ --platform linux/amd64,linux/arm64 \ --tag registry.example.com/myapp:1.2.3 \ --tag registry.example.com/myapp:latest \ --push \ . # فحص manifest الناتج docker buildx imagetools inspect registry.example.com/myapp:latest
استخدم المبنيات الأصلية (native builders) قدر الإمكان. المحاكاة عبر QEMU بطيئة — بناء مدته 3 دقائق على amd64 قد يستغرق 25 دقيقة عند محاكاته على arm64. في CI، خصّص عداء ubuntu-latest (amd64) وعداء ubuntu-latest-arm64، ابن كل منصة بشكل أصلي، ثم ادمج الـmanifests بـdocker buildx imagetools create. هذا ما تفعله سجلات الصور الكبيرة داخليًا.

الذاكرة المؤقتة المضمّنة وخلفيات الذاكرة البعيدة

يمكن لـBuildKit تصدير ذاكرته المؤقتة إلى سجل الصور حتى يعيد استخدامها عداؤو CI الجدد. مع --cache-to و--cache-from، يحقق العداء الجديد أوقات بناء قريبة من أوقات الذاكرة الدافئة:

# تصدير الذاكرة المؤقتة إلى السجل (mode=max يخزّن جميع الطبقات الوسيطة) docker buildx build \ --cache-from type=registry,ref=registry.example.com/myapp:cache \ --cache-to type=registry,ref=registry.example.com/myapp:cache,mode=max \ --tag registry.example.com/myapp:${GIT_SHA} \ --push \ . # GitHub Actions — النمط الكامل - name: Build and push uses: docker/build-push-action@v6 with: push: true tags: registry.example.com/myapp:${{ github.sha }} cache-from: type=registry,ref=registry.example.com/myapp:cache cache-to: type=registry,ref=registry.example.com/myapp:cache,mode=max platforms: linux/amd64,linux/arm64
mode=max مقابل mode=min. mode=min (الافتراضي) يُخزّن طبقات المرحلة النهائية فقط — مفيد حين تريد أصغر blob للذاكرة المؤقتة. mode=max يُخزّن كل مرحلة وسيطة، ما يُنتج معدلات إصابة أفضل للبناء متعدد المراحل بتكلفة صورة cache أكبر. في monorepos ذات مراحل قاعدة مشتركة كثيرة، يكسب mode=max دائمًا تقريبًا.

أنماط الفشل في الإنتاج

  • تسمّم الذاكرة المؤقتة عبر وسوم متغيرة: استخدام --cache-from مع وسم cache كـ:latest تكتب عليه وظيفة أخرى بالتزامن يؤدي إلى دورات بناء غير حتمية. ثبّت وسوم الذاكرة المؤقتة على فرع أو مرجع ثابت.
  • تثبيتات ذاكرة مؤقتة قديمة في CI: تثبيتات الذاكرة المؤقتة لـBuildKit محلية للعقدة. على عداؤي CI المؤقتين، يكون التثبيت فارغًا دائمًا. استخدم خلفيات الذاكرة المؤقتة للسجل، لا التثبيتات المحلية، في CI.
  • نفاد ذاكرة buildkitd في monorepos الكبيرة: زد حدود ذاكرة buildkitd واضبط --oci-worker-snapshotter=overlayfs على Linux للحصول على إزالة تكرار أفضل للطبقات.
  • تسريب الأسرار عبر build args: راجع التحذير أعلاه. راجع سجلات CI بحثًا عن الرموز المميزة التي مُررت عبر ARG — تظهر بنص عادي في مخرجات البناء المطوّلة.