بناء صور Docker في خطوط CI/CD
لديك ملف Dockerfile يعمل بشكل مثالي على حاسوبك المحمول. تقوم بتشغيل docker build، يتم بناء الصورة، ويعمل تطبيقك. ولكن عندما تدفع نفس ملف Dockerfile إلى خط أنابيب (pipeline)، تبدأ الأمور في التصرف بشكل مختلف.
البناء الذي استغرق دقيقتين على جهازك يستغرق الآن خمس عشرة دقيقة. الصورة التي عملت بالأمس تفشل اليوم بدون سبب واضح. وعندما تحتاج إلى إعادة بناء إصدار قديم للتراجع (rollback)، تتصرف الصورة الناتجة بشكل مختلف عن الأصلية.
هذه المشكلات ليست عشوائية. إنها تنبع من الفجوة بين بناء الصور على جهاز المطور وبنائها في خط أنابيب آلي. فهم هذه الفجوة هو الخطوة الأولى لإصلاحها.
ما الذي يتغير عند الانتقال إلى خط أنابيب
عند بناء صورة في خط أنابيب، تتغير ثلاثة أشياء بشكل جوهري.
أولاً، يجب أن يتم البناء على جهاز شخص آخر. قد يكون لهذا الجهاز موارد مختلفة، وظروف شبكة مختلفة، وأنظمة ملفات مختلفة. يجب أن يعمل ملف Dockerfile الخاص بك بغض النظر عن مكان تنفيذه.
ثانياً، يجب على خط الأنابيب إعادة بناء الصورة في كل مرة يتغير فيها الكود. هذا ليس اختيارياً. إذا تخطيت إعادة البناء، فسيقوم نشرك بتشغيل كود قديم. ولكن إعادة البناء في كل مرة يعني أن كل commit يؤدي إلى تشغيل عملية البناء الكاملة، ويجب أن تكون هذه العملية سريعة بما يكفي للحفاظ على حركة التطوير.
ثالثاً، يجب أن يكون البناء قابلاً لإعادة الإنتاج (reproducible). يجب أن ينتج نفس الكود المصدري نفس الصورة، بغض النظر عن وقت أو مكان تشغيل البناء. بدون إعادة الإنتاجية، لا يمكنك الوثوق بأن التراجع إلى commit قديم سيعيد نفس سلوك التطبيق بالضبط.
يوضح الرسم البياني أدناه المراحل النموذجية لخط أنابيب بناء Docker، من الكود المصدري إلى الدفع إلى السجل (registry).
التحكم في سياق البناء (Build Context)
سياق البناء هو المجلد الذي ترسله إلى Daemon الخاص بـ Docker عند تشغيل docker build. على حاسوبك المحمول، هذا عادةً ما يكون مجلد مشروعك. في خط الأنابيب، يحدث نفس الشيء، لكن عواقب السياق الكبير تكون أسوأ.
يتم إرسال كل ملف في سياق البناء إلى Daemon الخاص بـ Docker. إذا كان مستودعك يحتوي على node_modules، أو بيئات Python الافتراضية، أو ملفات ثنائية مُجمَّعة، فسيتم نقل هذه الملفات على الرغم من أنها غير مطلوبة للبناء. هذا يبطئ كل تشغيلة pipeline.
الحل هو ملف .dockerignore. يعمل مثل .gitignore ولكن لبناءات Docker. قم بإدراج كل شيء غير مطلوب للصورة: مجلدات التبعيات، أدلة ذاكرة التخزين المؤقت، تاريخ .git، بيانات الاختبار، والوثائق. سياق بناء خفيف يعني بناءات أسرع وحركة شبكة أقل.
اجعل التخزين المؤقت (Cache) يعمل لصالحك
يبني Docker الصور في طبقات (layers). كل تعليمة في ملف Dockerfile الخاص بك تنشئ طبقة واحدة. عند إعادة البناء، يتحقق Docker مما إذا كانت كل طبقة قد تغيرت. إذا كانت الطبقة لم تتغير، يعيد Docker استخدام النتيجة المخزنة مؤقتاً من البناء السابق.
آلية التخزين المؤقت هذه هي أعظم حليف لك للبناءات السريعة. لكنها تعمل فقط إذا بقيت ذاكرة التخزين المؤقت على قيد الحياة بين تشغيلات pipeline.
في التطوير المحلي، تعيش ذاكرة التخزين المؤقت على جهازك. في خط الأنابيب، تختفي ذاكرة التخزين المؤقت عندما ينتهي الـ runner ما لم تقم بحفظها صراحةً. توفر بعض أنظمة CI تخزيناً مؤقتاً مدمجاً لطبقات Docker. إذا كان نظامك لا يوفر ذلك، فأنت بحاجة إلى تكوينه يدوياً أو قبول أن كل بناء يبدأ من الصفر.
حتى مع عمل التخزين المؤقت، فإن ترتيب التعليمات في ملف Dockerfile الخاص بك يحدد مقدار ذاكرة التخزين المؤقت التي تستخدمها بالفعل. القاعدة الذهبية هي: انسخ الأشياء التي تتغير بشكل أقل أولاً.
لتطبيق Node.js، هذا يعني نسخ package.json و package-lock.json قبل باقي كود المصدر. قم بتشغيل npm install مباشرة بعد نسخ هذه الملفات. ثم انسخ كود التطبيق. مع هذا الترتيب، يتم إعادة تشغيل تثبيت التبعيات فقط عندما تتغير تبعياتك بالفعل، وليس عند تعديل سطر واحد من كود التطبيق.
ينطبق نفس المبدأ على أي لغة. يجب أن تنسخ مشاريع Python requirements.txt أو pyproject.toml أولاً. يجب أن تنسخ مشاريع Go go.mod و go.sum أولاً. النمط عالمي: افصل التبعيات المستقرة عن كود التطبيق المتغير.
استخدام وسائط البناء (Build Arguments) للمرونة
يجب ألا يقوم ملف Dockerfile الخاص بك بتضمين قيم ثابتة (hardcode) تتغير بين البيئات. يجب أن يأتي إصدار الصورة الأساسية، أو اسم البيئة، أو رمز الوصول لسجل خاص من خارج ملف Dockerfile.
يوفر Docker ARG لهذا الغرض. يمكنك تعريف عنصر نائب في ملف Dockerfile الخاص بك، ويملؤه خط الأنابيب أثناء البناء.
ARG BASE_IMAGE_VERSION=20.04
FROM ubuntu:${BASE_IMAGE_VERSION}
في خط الأنابيب الخاص بك، تقوم بتمرير القيمة الفعلية:
docker build --build-arg BASE_IMAGE_VERSION=22.04 .
هذا يحافظ على عمومية ملف Dockerfile الخاص بك وقابلية إعادة استخدامه. يمكن لملف Dockerfile واحد أن يخدم بناءات التطوير، والاختبار، والإنتاج دون تكرار.
ضمان بناءات قابلة لإعادة الإنتاج (Reproducible Builds)
إعادة الإنتاجية تعني أن بناء نفس الكود المصدري في أوقات مختلفة ينتج نفس الصورة. بدون ذلك، لا يمكنك الوثوق باستراتيجية التراجع الخاصة بك، أو سجل التدقيق، أو فحص الأمان.
ثلاثة أشياء شائعة تعطل إعادة الإنتاجية.
أولاً، استخدام وسوم latest للصور الأساسية. يتغير وسم latest بمرور الوقت. latest اليوم ليس هو latest غداً. قم بتثبيت الصورة الأساسية على وسم إصدار محدد مثل ubuntu:22.04 أو node:20-alpine.
ثانياً، عدم تثبيت إصدارات التبعيات. قد يحدد ملف package.json الخاص بك نطاق إصدار مثل ^4.0.0. هذا النطاق يتحول إلى إصدارات مختلفة بمرور الوقت. استخدم ملفات القفل مثل package-lock.json أو yarn.lock لتجميد الإصدارات الدقيقة.
ثالثاً، تضمين طوابع زمنية للبناء أو بيانات وصفية للبناء داخل الصورة. إذا كانت عملية البناء الخاصة بك تكتب التاريخ الحالي في ملف داخل الصورة، فسيختلف هذا الملف بين البناءات حتى عندما يكون الكود المصدري متطابقاً. تجنب ذلك إلا إذا كان لديك سبب تشغيلي محدد له.
تخزين الصورة في سجل (Registry)
بمجرد أن يبني خط الأنابيب الصورة، فإنها تحتاج إلى مكان لتعيش فيه حيث يمكن للخوادم ومجموعات Kubernetes سحبها. هذا المكان هو سجل الحاويات (container registry).
يجب أن يقوم خط الأنابيب الخاص بك بوسم الصورة بمعرفات ذات معنى. النمط الشائع هو استخدام commit SHA كوسم أساسي، مع وسوم إضافية للفروع أو الإصدارات الدلالية.
docker tag myapp:${COMMIT_SHA} registry.example.com/myapp:${COMMIT_SHA}
docker push registry.example.com/myapp:${COMMIT_SHA}
هذا يمنحك مرجعاً دائماً لكل صورة تم بناؤها على الإطلاق. يمكنك دائماً العودة إلى أي commit وسحب الصورة الدقيقة التي تم بناؤها منه.
قائمة التحقق العملية
قبل دفع بناء Docker الخاص بك إلى خط أنابيب، تحقق من هذه العناصر:
.dockerignoreيستبعد مجلدات التبعيات، وذاكرة التخزين المؤقت، و.git- ملف Dockerfile ينسخ ملفات التبعيات قبل كود التطبيق
- وسوم الصورة الأساسية مثبتة على إصدارات محددة، وليس
latest - إصدارات التبعيات مقفلة باستخدام ملفات القفل
- يتم استخدام وسائط البناء للقيم الخاصة بالبيئة
- خط الأنابيب يحفظ ذاكرة التخزين المؤقت لطبقات Docker بين التشغيلات
- الصور موسومة بـ commit SHA ويتم دفعها إلى سجل
ماذا يعني هذا لخط الأنابيب الخاص بك
بناء الصور في خط أنابيب لا يقتصر فقط على تشغيل docker build على جهاز مختلف. يتعلق الأمر بتصميم ملف Dockerfile الخاص بك وخط الأنابيب الخاص بك للعمل معاً. ملف Dockerfile جيد التنظيم يحترم ترتيب الطبقات وسياق البناء سيبني بشكل أسرع. خط الأنابيب الذي يحافظ على ذاكرة التخزين المؤقت ويوسم الصور بشكل صحيح سيعطيك قطعاً أثرية موثوقة وقابلة لإعادة الإنتاج.
الصورة التي تبنيها اليوم يجب أن تكون نفس الصورة التي يمكنك إعادة بنائها بعد ستة أشهر من نفس commit. هذا الاتساق هو ما يجعل عمليات النشر قابلة للتنبؤ والتراجع آمناً.