بناء الصورة دون المساس بقاعدة البيانات

مرحبًا بالجميع.

أنا أدير مثيلًا صغيرًا جدًا من Discourse (منذ سنوات بالفعل، مع شبه انعدام المشاكل): https://discuss.cubeisland.de/.
لقد كنت أستخدم عملية النشر القياسية المعتمدة على launcher على آلة افتراضية مخصصة (على أجهزتي الخاصة في مركز بيانات). الشيء الوحيد الذي غيّرتُه على مر السنين هو الانتقال إلى قاعدة بيانات PostgreSQL مشتركة تعمل خارجيًا.

في الآونة الأخيرة، بدأت في نقل التطبيقات من الآلات الافتراضية المخصصة إلى مجموعة Docker (Docker Swarm) كخطوة تمهيدية للانتقال في النهاية إلى مجموعة Kubernetes، وذلك في المقام الأول لتوفير الموارد وجعل أجزاء من البنية التحتية أكثر “مرونة”.

اليوم هو اليوم الذي نظرت فيه إلى مثيل Discourse الصغير هذا كأحد الآلات الافتراضية القليلة المتبقية التي تعمل كتطبيقات مخصصة. “إنه يعمل بالفعل على Docker، فكم يمكن أن يكون من الصعب نشره على مجموعة Swarm؟” هكذا فكرت. ومن خلال ما قرأته، يبدو أن ذلك ممكن بالفعل. يمكنني ببساطة أخذ الصورة من المثيل الحالي قيد التشغيل، ودفعها إلى السجل الداخلي الخاص بنا، وتشغيلها في مجموعة Swarm، وستعمل كل شيء على ما يرام، وهو أمر رائع.

نظرت في ملفات launcher، وخاصة القوالب والنماذج، واستنتجت أنه قد يكون فكرة جيدة فصل Redis في مثل هذا النشر، وربما يمكنني إعداد مهمة CI لبناء صور جديدة عند إضافة إضافات أو عندما أرغب في التحديث. لذا قمت باستنساخ discourse_docker محليًا، ونسخت تعريف حاوية app.yml الخاص بي إلى النسخة المستنسخة، وحاولت تشغيل ./launcher bootstrap app لبناء صورة يمكنني بعد ذلك دفعها إلى سجلي، دون نشرها فورًا.

إلى فاجأت، حاولت السكربت الاتصال بخادم PostgreSQL “الإنتاجي” لمحاولة ترحيل قاعدة البيانات، ولحسن الحظ لم يكن لديه وصول من محطة العمل المحلية الخاصة بي.

تفحصت هنا، ويبدو أن هذا هو كيفية عمل الأمر، مما يجعلني أتساءل:

  1. كيف يمكنني بناء حاوية لمثيل جديد، حيث لا أملك قاعدة بيانات بعد؟ هل سأحتاج إلى إعداد قاعدة بيانات الإنتاج قبل أن أتمكن من بناء الصورة؟
  2. أفترض أن هذا هو الوقت الوحيد الذي يتم فيه تشغيل db:migrate، لذا إذا كان لدي عدة مثيلات متشابهة (مثل الإنتاج والاختبار)، فستحتاج إلى ترقية أحد المثيلات لبناء الصورة الجديدة، ثم لا يمكنني استخدام نفس الصورة للمثيل الثاني، حتى لو كانت الصورة متطابقة.
  3. كيف يمكنني بناء صور للمثيلات التي لا يكون خادم قاعدة البيانات فيها متاحًا من النظام الذي يبني الصورة (وهو أمر ليس نادرًا كما قد يبدو)؟

بعد قراءة عدة منشورات (بالتأكيد بما في ذلك هذا المنشور)، أنا أدرك تمامًا أسباب عملية البناء كما هي حاليًا، وأرى قيمتها بالنسبة لـ 99% من الأشخاص الذين ينشرون Discourse بشكل عادي على آلاتهم الافتراضية الكاملة القياسية. وأنا معتاد جدًا على نماذج الحاويات “الكل في واحد”، ولا أعترض على ذلك. في النهاية، تكمن القيمة الرئيسية لـ Docker في حقيقة أن مورد البرمجيات يمكنه إعداد تكوينات مُحسَّنة مسبقًا وتجميعها في بيئة تشغيل قابلة للتكرار، مما يلغي الحاجة إلى الكثير من المعرفة المحددة بالتطبيق من جانب العمليات. لذا أنا متحمس تمامًا لاستخدام أدواتكم المقدمة، فلماذا أتوقع من شخص آخر بناء حاويات أفضل من مورد البرمجيات نفسه؟ ولماذا أريد فصل nginx عن تطبيق Ruby عندما لا توجد فائدة تُذكر، فقط لجعل النشر أكثر “نقاءً” (مهما كان ذلك يعني…)؟

ومع ذلك، من الغريب رؤية حاوية تقوم بتعديل حالة التشغيل أثناء عدم تشغيلها تقريبًا. لقد قمت بتشغيل العديد من التطبيقات في حاويات، وقمت بتغليف العديد منها بنفسي، وبعضها لم يكن مخصصًا للعمل في حاويات من الأساس.

أول مثال يخطر ببالي، لتطبيق يتعامل مع متطلبات/مشاكل مشابهة لتلك الخاصة بـ Discourse بطريقة مماثلة، هو GitLab. بينما يقدمون الآن مخطط Helm أنيقًا لنشر Kubernetes “المفكك بالكامل” كما ينبغي، فإنني أظن (دون النظر إلى أي أرقام) أن نسبة مماثلة تبلغ 99% من منشوراتها الصغيرة والمتوسطة الحجم تستخدم صورة Docker Omnibus الخاصة بـ GitLab (أو حزمة نظام التشغيل، وهي في الأساس نفس الشيء). لديهم عملية تهيئة مماثلة، ولكنها تعتمد على Chef داخل الحاوية، ويتم تنفيذها جميعًا عند كل بدء تشغيل، وتقوم بالأشياء المعتادة مثل ترحيل قاعدة البيانات وتجميع الأصول.

نعم، قد يستغرق بدء تشغيل GitLab عدة دقائق بسبب ذلك، لكن لم يكن ذلك أبدًا مشكلة في المنشورات التي رأيتها (بعضها في شركات كبرى). خاصة مع أنظمة التنسيق الحديثة مثل Docker Swarm وKubernetes وأي شيء آخر، والتي يمكنها تشغيل ترقيات متدحرجة لك، حيث يتم إيقاف المثيل القديم فقط إذا كان المثيل الجديد يعمل وقد نجح في فحص الصحة والاستعداد. لذا قد لا تكون عملية النشر الطويلة مشكلة في الواقع. ولكن حتى بدون ترقيات متدحرجة متطورة، والتي قد تعمل أو لا تعمل، يمكنك أيضًا تجاوز الكثير من وقت التوقف في العديد من المواقف.

إذن: هل من الممكن تكوين launcher لتجاوز العمليات المعتمدة على قاعدة البيانات أثناء بناء الصورة، وبدلاً من ذلك القيام بهذه العمليات أثناء بدء تشغيل الحاوية؟

أنا بالتأكيد مستعد لاستثمار بعض الوقت في هذا بنفسي، لكن وقتي في المساء محدود، لذا فإن أي تلميحات ستكون موضع ترحيب كبير.

أنا أيضًا منفتح على عمليات مختلفة تمامًا إذا كنت تعتقد أن هذا غير منطقي أو حتى غير ممكن أو ما إلى ذلك.

شكرًا لأي تغذية راجعة!

5 إعجابات

أردتُ فعل نفس ما فعلتُ أنت - فنحن ندير discourse على Amazon ECS، لذا كان علينا أن نتمكن من بناء صورة الويب فقط ودفعها إلى سجل. لم أرغب في العبث بعملية بناء discourse لأننا نرغب في البقاء أقرب ما يمكن إلى التثبيت المدعوم.

بدلاً من ذلك، نستخدم سكريبت launcher العادي لبناء إعداد مكون من حاويتين على جهاز محلي، ولكن نتجاهل حاوية البيانات ونقوم بدفع حاوية الويب إلى السجل. وعند وقت التشغيل، نجاوز تفاصيل اتصال Postgres و Redis عبر متغيرات البيئة.

تتم عملية نشر الصورة الجديدة في ثلاث خطوات:

  1. تشغيل عمليات الترحيل الآمنة قبل النشر. اطلب من ECS تنفيذ هذا الأمر (مع الصورة الجديدة):

     SKIP_POST_DEPLOYMENT_MIGRATIONS=1 rake db:migrate
    
  2. نشر الصورة الجديدة. قم بتحديث خدمة ECS.

  3. تشغيل عمليات الترحيل بعد النشر. اطلب من ECS تنفيذ هذا الأمر:

     SKIP_POST_DEPLOYMENT_MIGRATIONS=0 rake db:migrate
    

من المحتمل أن تشغيل حاوية بيانات محلية أثناء بناء الصورة هو أمر مضيعة، لكنه يعني أننا يمكننا استخدام web.template.yml القياسي دون القلق بشأن الأجزاء التي تحاول التواصل مع قاعدة البيانات أو Redis.

8 إعجابات

شكرًا لك على ذلك! لقد استنتجت أيضًا أنه يمكنني تشغيل قاعدة بيانات PostgreSQL أثناء بناء الصورة والتخلص منها بمجرد انتهاء بناء التطبيق الفعلي.

إعجابَين (2)

أخيراً خصصت الوقت لتنفيذ هذا!

طبقت بناء الصورة باستخدام خط أنابيب gitlab-ci الذي يشغل postgres و redis كخدمات أثناء البناء ثم يتخلص منهما بعد ذلك:

الآن يتبقى لي فقط أتمتة النشر مع ترحيلات قاعدة البيانات

إعجابَين (2)

هذا الشيء يعمل منذ أكثر من عام دون لمسه أبدًا، ولا حتى لإصدار 2.8.

إعجابَين (2)

لقد نقلت بناء الصورة إلى github: GitHub - pschichtel/discourse-docker: A reusable Discourse container built using the launcher tool.

تم نشر الصورة في pschichtel/discourse:stable-web_only

يبدو أن هذا قد تعطل أخيرًا. عند الترقية من 3.0.6 إلى 3.1.0، لم يتم إجراء أي عمليات ترحيل لقاعدة البيانات. ومع ذلك، نجحت عملية تشغيل bundle exec rake db:migrate النهائية داخل الحاوية قيد التشغيل، على الرغم من أنها لم تتم إلا بعد إعادة تشغيل أخرى للحاوية.

يجب عليك الترحيل مرة أخرى عندما تبدأ الصورة الجديدة بدون تعيين تلك البيئة. توجد مهمة “rake” ستقوم بذلك ولكن لا يمكنني تذكرها أو العثور عليها من هاتفي. شيء مثل ensure_post_migrations.

على حد علمي، لم ألاحظ أي خلل. أنا أتابع بشكل أساسي فرع إصدار النسخة التجريبية، وبقدر ما أعلم، فقد تم تشغيل عمليات الترحيل بشكل صحيح لكل خطوة في سلسلة 3.1.0.beta…

لقد وجدت db:ensure_post_migrations عبر rake -AT.

ما الفرق بين db:migrate مع SKIP_POST_DEPLOYMENT_MIGRATIONS=0 و db:ensure_post_migrations؟

حسنًا، بعد إلقاء نظرة على الكود، فهمت ما تفعله db:ensure_post_migrations. من المفترض استخدامه في نفس تنفيذ rake قبل db:migrate لضمان تعيين SKIP_POST_DEPLOYMENT_MIGRATIONS إلى 0. نصي البرمجي يضمن ذلك بالفعل:

.gitlab-ci.yml:

./migrate.sh pre || echo "Redis not running during pre migrations, skipping..."
docker stack deploy --prune --resolve-image always -c "$STACK.yml" "$STACK"
./docker-stack-wait.sh -t 180 "$STACK"
./migrate.sh post

migrate.sh:

#!/usr/bin/env sh

if [ "$(docker ps -q --filter "label=com.docker.stack.namespace=${STACK}" --filter "label=com.docker.swarm.service.name=${STACK}_${DISCOURSE_REDIS_HOST}" | wc -l)" = "0" ]
then
    echo "No redis container found, unable to run migrations!"
    exit 1
fi

if [ "$1" = "pre" ]
then
    skip_post=1
else
    skip_post=0
fi


docker run \
    --rm \
    --name "discourse-migration-${DISCOURSE_DB_HOST}-${DISCOURSE_DB_NAME}" \
    --network "${STACK}_discourse" \
    --workdir /var/www/discourse \
    -u discourse \
    -e SKIP_POST_DEPLOYMENT_MIGRATIONS="$skip_post" \
    -e LANG="${LANG}" \
    -e DISCOURSE_DEFAULT_LOCALE="${DISCOURSE_DEFAULT_LOCALE}" \
    -e DISCOURSE_HOSTNAME="${DISCOURSE_HOSTNAME}" \
    -e DISCOURSE_DEVELOPER_EMAILS="${DISCOURSE_DEVELOPER_EMAILS}" \
    -e DISCOURSE_SMTP_ADDRESS="${DISCOURSE_SMTP_ADDRESS}" \
    -e DISCOURSE_SMTP_PORT="${DISCOURSE_SMTP_PORT}" \
    -e DISCOURSE_DB_USERNAME="${DISCOURSE_DB_USERNAME}" \
    -e DISCOURSE_DB_PASSWORD="${DISCOURSE_DB_PASSWORD}" \
    -e DISCOURSE_DB_HOST="${DISCOURSE_DB_HOST}" \
    -e DISCOURSE_DB_NAME="${DISCOURSE_DB_NAME}" \
    -e DISCOURSE_REDIS_HOST="${DISCOURSE_REDIS_HOST}" \
    "$DISCOURSE_IMAGE" \
    bundle exec rake db:migrate

يقوم بتشغيل db:migrate مع SKIP_POST_DEPLOYMENT_MIGRATIONS=1 في الصورة الجديدة على Docker Swarm بينما لا يزال Discourse يعمل بالإصدار القديم. ثم يقوم بنشر الصورة الجديدة إلى Swarm وينتظر حتى تتقارب. في النهاية، يقوم بتشغيل db:migrate مرة أخرى، ولكن مع SKIP_POST_DEPLOYMENT_MIGRATIONS=0.

لقد نجح هذا بشكل موثوق لكل إصدار لأكثر من عامين الآن. بالنظر إلى أنه نجح معك يا @simonk، هل قمت بشيء مختلف جوهريًا مقارنة بنصي البرمجي؟

إعجاب واحد (1)

لا، ما زلت أتبع نفس العملية التي حددتها هنا، والتي على حد علمي هي نفسها تقريبًا مثل عملك. أنا أستخدم rake db:migrate بدلاً من bundle exec rake db:migrate، ولكن لا يمكنني تخيل أن ذلك سيحدث فرقًا كبيرًا.

لم أستخدم docker stack أو swarm مطلقًا. هل هناك أي احتمال لوجود خطأ في مكان ما في البرامج النصية الخاصة بك قد يتسبب في استخدام البرنامج النصي migrate.sh للصورة القديمة بدلاً من الصورة الجديدة؟

لم أتحقق من ذلك صراحة، سأبحث في الأمر. بالتأكيد سيستخدم السرب أحدث صورة، ولكن ربما لم يستخدم نص CI أحدث صورة لسبب ما.

لقد بحثت في هذا الآن مع التحديث 3.1.1. بالفعل، كان برنامج CI النصي يستخدم إصدارًا أقدم من الحاوية.