من الحاويات إلى kubernetes إعداد تطبيق node.js لسير عمل يعتمد على الحاويات باستخدام Docker Compose


عبد الصمد العماري

إذا كنت تعمل على تطوير تطبيقك بنشاط، فإن استخدام Docker يمكنه تبسيط سير العمل وجعل عملية نشر التطبيق على الإنتاج سلسةً، إذ يمنح العمل بالحاويات في عمليّة التطوير العديد من المزايا، نذكر منها أنّ:

  • البيئات متناسقة (consistent)، مما يعني أنه يمكنك اختيار اللغات والاعتماديات التي تريدها لمشروعك دون أن تقلق بشأن تعارضات النظام.
  • البيئات معزولة (isolated)، ممّا يسهّل اكتشاف المشكلات وإدماج الأعضاء الجدد في الفريق.
  • البيئات محمولة (portable)، ممّا يسمح لك بتحزيم ومشاركة شيفرتك مع الآخرين.

يشرح لك هذا الدرس كيفية إعداد بيئة تطوير لتطبيق Node.js بالاعتماد على Docker. ستنشئ حاويتين، واحدة من أجل تطبيق Node والأخرى لقاعدة البيانات MongoDB، وذلك باستخدام Docker Compose. ونظرًا لأن هذا التطبيق يعمل على Node وMongoDB، فسيقوم برنامج الإعداد بما يلي:

*مزامنة شيفرة التطبيق التي على المضيف مع الشيفرة التي في الحاوية لتسهيل التعديلات أثناء التطوير.

  • التحقّق من أن التعديلات في شيفرة التطبيق تعمل دون الحاجة إلى إعادة تشغيل.
  • إنشاء قاعدة بيانات محمية بمستخدم وكلمة مرور من أجل بيانات التطبيق.
  • جعل هذه البيانات ثابتة ومستقرة (persistent).

في نهاية هذا الدرس، سيكون لديك تطبيق لمعلومات سمك القرش يعمل بشكل جيّد على حاويات Docker:

persisted_data.png

المتطلبات الأساسية

لمتابعة هذا الدرس، ستحتاج إلى العناصر التالية:

الخطوة الأولى: استنساخ المشروع وتعديل الاعتماديات

تتمثل الخطوة الأولى في إنشاء هذا الإعداد في استنساخ شيفرة المشروع وتعديل ملف package.json الخاص به، والذي يتضمن اعتماديات المشروع. سوف نضيف nodemon إلى اعتماديات المشروع devDependencies، مع تحديد أننا سنستخدمها أثناء التطوير. يضمن تشغيل التطبيق باستخدام nodemon أنه سيُعاد تشغيله تلقائيًا عند إجراء تغييرات على الشيفرة.

انسخ، في البداية، مستودع nodejs-mongo-mongoose من حساب DigitalOcean Community GitHub. يتضمن هذا المستودع الشيفرة من الإعداد الموضح في كيفية دمج MongoDB مع تطبيقك Node، والذي يشرح كيفية دمج قاعدة بيانات MongoDB مع تطبيق Node موجودٍ سلفًا باستخدام Mongoose.

انسخ المستودع في مجلّد يسمى node_project:

git clone https://github.com/do-community/nodejs-mongo-mongoose.git node_project

انتقل إلى المجلّد node_project:

cd node_project

افتح ملف package.json الخاص بالمشروع باستخدام nano أو المحرّر الذي تفضّله:

nano package.json

أسفل اعتماديات المشروع وفوق قوس الإغلاق، أنشئ كائن devDependencies جديد يتضمن nodemon:

...
"dependencies": {
    "ejs": "^2.6.1",
    "express": "^4.16.4",
    "mongoose": "^5.4.10"
  },
  "devDependencies": {
    "nodemon": "^1.18.10"
  }    
}

احفظ الملف وأغلقه عند الانتهاء من التحرير.

بوضعك لشيفرة المشروع في مكانها وتعديل اعتمادياته، يمكنك الانتقال إلى إعادة بناء الشيفرة من أجل سير عملٍ يعتمد على الحاويات.

الخطوة الثانية: إعداد تطبيقك للعمل بالحاويات

يعني تعديل التطبيق من أجل سير عملٍ يعتمد على الحاويات، جعل الشيفرة أكثر نمطيةً. إذ توفر الحاويات قابلية التنقل بين البيئات، ويجب أن تعكس الشيفرة ذلك من خلال البقاء قدر الإمكان منفصلةً عن نظام التشغيل الأساسي. لتحقيق ذلك، سنعيد تشكيل شيفرتنا للاستفادة بشكل أكبر من خاصية process.env الخاصة ب Node، والتي تعيد كائنًا يحتوي على معلومات حول بيئة المستخدم في وقت التشغيل. يمكننا استخدام هذا الكائن في شيفرتنا لتعيين معلومات التكوين ديناميكيًا في وقت التشغيل باستخدام متغيرات البيئة.

لنبدأ بالملف app.js، الذي يمثّل نقطة دخول التطبيق الرئيسية لدينا. افتح هذا الملف:

nano app.js

سترى فيه تعريفًا للثابتة port، بالإضافة إلى الدالّة listen التي تستخدم هذه الثابتة لتحديد المنفذ الذي سيستمع إليه التطبيق:

...
const port = 8080;
...
app.listen(port, function () {
  console.log('Example app listening on port 8080!');
});

دعنا نعيد تعريف ثابتة port للسماح بالتعيين الديناميكي في وقت التشغيل باستخدام الكائن process.env. لذا، أجرِ التعديلات التالية على تعريف الثابتة وعلى الدالّة listen:

...
const port = process.env.PORT || 8080;
...
app.listen(port, function () {
  console.log(`Example app listening on ${port}!`);
});

يعيّن تعريفنا الثابت الجديد المنفذ port ديناميكيًا باستخدام القيمة الممرّرة في وقت التشغيل أو القيمة 8080. وبالمثل، أعدنا كتابة الدّالة لاستخدام قالب نصّي حديث، والذي سيُقحم قيمة المنفذ عند الاستماع للاتصالات. ونظرًا لأننا سنعيّن منافذنا في أماكن أخرى، فستغنينا هذه المراجعات عن الاضطرار إلى مراجعة هذا الملف بشكل مستمر عند تغيير بيئتنا.

احفظ الملف وأغلقه عند الانتهاء من التحرير.

بعد ذلك، سنعدّل معلومات اتصال قاعدة البيانات لحذف أي بيانات اعتماد في التكوينات. افتح الملف db.js الذي يحتوي على هذه المعلومات:

nano db.js

يقوم الملف حاليا بالأمور التالية:

  • يستورد Mongoose، كائن تخطيط المستندات (ODM) الذي نستخدمه لإنشاء مخطّطات ونماذج لبيانات التطبيق.
  • يعيّن بيانات اعتماد قاعدة البيانات كقيم ثابتة، بما في ذلك اسم المستخدم وكلمة المرور.
  • يتصل بقاعدة البيانات باستخدام التابع mongoose.connect.

لمزيد من المعلومات حول الملفّ، يرجى الاطلاع على الخطوة الثالثة من كيفية دمج MongoDB مع تطبيق Node الخاص بك.

ستكون خطوتنا الأولى في تعديل الملف هي إعادة تحديد الثوابت التي تتضمن معلومات حساسة. تبدو هذه الثوابت حاليًا على النحو التالي:

...
const MONGO_USERNAME = 'sammy';
const MONGO_PASSWORD = 'your_password';
const MONGO_HOSTNAME = '127.0.0.1';
const MONGO_PORT = '27017';
const MONGO_DB = 'sharkinfo';
...

يمكنك استخدام الكائن process.env لالتقاط قيم وقت التشغيل لهذه الثوابت بدلاً من الاضطرار لكتابة هذه المعلومات يدويًا. لذا عدّل هذا المقطع من الشيفرة لتبدو على هذا النحو:

...
const {
  MONGO_USERNAME,
  MONGO_PASSWORD,
  MONGO_HOSTNAME,
  MONGO_PORT,
  MONGO_DB
} = process.env;
...

احفظ الملف وأغلقه عند الانتهاء من التحرير.

في هذه المرحلة، عدّلت الملف db.js للعمل بمتغيرات البيئة في تطبيقك، ولكنك مازلت تحتاج إلى وسيلة لتمرير هذه المتغيرات إلى تطبيقك. لننشئ ملف env. بقيمٍ يمكنك تمريرها إلى التطبيق في وقت التشغيل.

افتح هذا الملف:

nano .env

سيتضمن هذا الملف المعلومات التي حذفتها من db.js وهي اسم المستخدم وكلمة المرور لقاعدة بيانات التطبيق، بالإضافة إلى إعداد المنفذ واسم قاعدة البيانات. لا تنسَ تحديث اسم المستخدم وكلمة المرور واسم قاعدة البيانات المدرجة هنا بمعلوماتك الخاصة:

MONGO_USERNAME=sammy
MONGO_PASSWORD=your_password
MONGO_PORT=27017
MONGO_DB=sharkinfo

لاحظ أننا حذفنا إعدادات المضيف التي كانت في الأصل في db.js. والآن، سنعرّف المضيف على مستوى الملف Docker Compose، إلى جانب معلومات أخرى حول الخدمات والحاويات.

احفظ هذا الملف وأغلقه عندما تنتهي من التحرير.

نظرًا لاحتواء الملف env. على معلومات حساسة، فستحتاج إلى الحرص على تضمينه في ملفات dockerignore. و gitignore. الخاصة بمشروعك حتى لا يُنسخ إلى وحدة إدارة الإصدار أو إلى حاوياتك.

افتح ملفك dockerignore.:

nano .dockerignore

أضف السطر التالي إلى أسفل الملف:

...
.gitignore
.env

احفظ الملف وأغلقه عند الانتهاء من التحرير.

يحتوي الملف gitignore. الموجود في هذا المستودع على env. أصلًا، ولكن لا تتردد في التحقق من وجوده:

nano .gitignore
...
.env
...

تمكنت في هذه المرحلة من استخراج المعلومات الحساسة من شيفرة المشروع واتخذت تدابير للتحكم في كيفية ومكان نسخ هذه المعلومات. يمكنك الآن العمل على تمتين الشيفرة الخاصّة بالاتصال بقاعدة البيانات بُغية تحسينها من أجل سير عملٍ يعتمد على الحاويات.

الخطوة الثالثة: تعديل إعدادات اتصال قاعدة البيانات

ستكون خطوتنا التالية هي جعل طريقة اتصال قاعدة البيانات أكثر متانة عن طريق إضافة شيفرة تعالج الحالات التي يفشل فيها تطبيقنا في الاتصال بقاعدة البيانات. ويُعدّ تقديم هذا المستوى من المرونة لشيفرة التطبيق ممارسةً ينصح بها عند التعامل مع الحاويات باستخدام "Compose".

افتح الملفّ db.js لتحريره:

nano db.js

ستظهر لك الشفرة التي أضفناها سابقًا، إلى جانب ثابتة العنوان url الخاصة بالمعرّف URI لاتصال Mongo والتابع connect فيMongoose:

...
const {
  MONGO_USERNAME,
  MONGO_PASSWORD,
  MONGO_HOSTNAME,
  MONGO_PORT,
  MONGO_DB
} = process.env;

const url = `mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_HOSTNAME}:${MONGO_PORT}/${MONGO_DB}?authSource=admin`;

mongoose.connect(url, {useNewUrlParser: true});

يقبل التابع connect حاليًا خيارًا يطلب من Mongoose استخدام مفسّر URL الجديد الخاص بـMongo. دعنا نضيف بعض الخيارات الإضافية إلى هذا التابع لتعريف المعاملات (paramaters) لمحاولات إعادة الاتصال. يمكننا القيام بذلك عبر إنشاء ثابتة options تتضمّن المعلومات ذات الصلة، بالإضافة إلى خيار مفسّر عناوين URL الجديد. أسفل ثوابت Mongo، أضف التعريف التالي للثابتة options:

...
const {
  MONGO_USERNAME,
  MONGO_PASSWORD,
  MONGO_HOSTNAME,
  MONGO_PORT,
  MONGO_DB
} = process.env;

const options = {
  useNewUrlParser: true,
  reconnectTries: Number.MAX_VALUE,
  reconnectInterval: 500, 
  connectTimeoutMS: 10000,
};
...

يطلب الخيار reconnectTries من Mongoose مواصلة محاولة الاتصال إلى أجل غير محدّد، في حين تحدد reconnectInterval الفترة الفاصلة بين محاولتي اتصال بوحدة الجزء من الألف من الثانية. ويحدّد connectTimeoutMS الفترة التي سينتظرها برنامج التشغيل Mongo قبل إعلان فشل محاولة الاتصال في 10 ثوانٍ.

يمكننا الآن استخدام الثابتة الجديدة options في التابع connect لضبط إعدادات اتصال Mongoose بدقّةٍ. سنضيف أيضًا كائن وعدٍ (promise) لمعالجة أخطاء الاتصال المحتملة.

يبدو التابع connect الخاصّ ب Mongoose حاليًا على هذا النحو:

...
mongoose.connect(url, {useNewUrlParser: true});

احذف التابع connect الموجود حاليًا وعوّضه بالشيفرة التالية التي تتضمن الثابتة options والوعد:

...
mongoose.connect(url, options).then( function() {
  console.log('MongoDB is connected');
})
  .catch( function(err) {
  console.log(err);
});

في حالة اتصال ناجح، تسجّل دالّتنا رسالةً مناسبةً. وإلا فإنها تُمسك الخطأ وتسجّله، مما يتيح لنا استكشاف الأخطاء وإصلاحها.

سيبدو الملف النهائي كما يلي:

const mongoose = require('mongoose');

const {
  MONGO_USERNAME,
  MONGO_PASSWORD,
  MONGO_HOSTNAME,
  MONGO_PORT,
  MONGO_DB
} = process.env;

const options = {
  useNewUrlParser: true,
  reconnectTries: Number.MAX_VALUE,
  reconnectInterval: 500,
  connectTimeoutMS: 10000,
};

const url = `mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_HOSTNAME}:${MONGO_PORT}/${MONGO_DB}?authSource=admin`;

mongoose.connect(url, options).then( function() {
  console.log('MongoDB is connected');
})
  .catch( function(err) {
  console.log(err);
});

احفظ الملف وأغلقه عندما تنتهي من التحرير.

لقد أضفت الآن مرونةً إلى شيفرة التطبيق لمعالجة الحالات التي قد يفشل فيها التطبيق في الاتصال بقاعدة البيانات. بوضعك لهذه الشيفرة في مكانها الصحيح، يمكنك الانتقال إلى تعريف خدماتك باستخدام "Compose".

الخطوة الرابعة: تعريف الخدمات باستخدام Docker Compose

بإعادة تشكيل الشيفرة، تكون مستعدًا لتحرير الملف docker-compose.yml بتعريفات خدماتك. الخدمة في Compose هي عبارة عن حاويةٍ قيد التشغيل، وتحتوي تعريفات الخدمة، التي ستتضمّنها في ملف docker-compose.yml، على معلومات حول كيفية اشتغال كل حاويةٍ صورةٍ (container image). تتيح لك الأداة "Compose" تعريف خدمات متعددة لإنشاء تطبيقات متعددة الحاويات.

قبل تعريف خدماتنا، سنضيف، مع ذلك، أداةً إلى مشروعنا تسمى wait-for للتأكد من أن التطبيق لا يحاول الاتصال بقاعدة البيانات فقط بعد اكتمال مهام بدء تشغيل قاعدة البيانات. يستخدم هذا السكربت المجمِّع netcat لاستقصاء ما إذا كان المضيف والمنفذ المعيّنان يقبلان اتصالات TCP أم لا. ويتيح لك استخدامه التحكم في محاولات التطبيق للاتصال بقاعدة البيانات عن طريق اختبار جاهزية قاعدة البيانات لقبول الاتصالات من عدمها.

رغم أنّ "Compose" يتيح لك تحديد الاعتماديات بين الخدمات باستخدام الخيار dependson، فإنّ هذا الترتيب يعتمد على ما إذا كانت الحاوية تعمل أم لا وليس على جاهزيتها. لن يكون استخدام dependson هو الأمثل لإعدادنا، لأننا نريد أن يتّصل تطبيقنا بقاعدة البيانات فقط عند اكتمال مهام بدء التشغيل فيها، بما في ذلك إضافة مستخدم وكلمة مرور إلى استيثاق قاعدة البيانات admin. لمزيد من المعلومات حول استخدام wait-for والأدوات الأخرى للتحكم في ترتيب بدء التشغيل، يرجى الاطلاع على التوصيات ذات الصلة في توثيق Compose.

افتح ملفًا يسمى wait-for.sh:

nano wait-for.sh

انسخ الشيفرة التالية في هذا الملف لإنشاء دالة الاستقصاء:

#!/bin/sh

# original script: https://github.com/eficode/wait-for/blob/master/wait-for

TIMEOUT=15
QUIET=0

echoerr() {
  if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi
}

usage() {
  exitcode="$1"
  cat << USAGE >&2
Usage:
  $cmdname host:port [-t timeout] [-- command args]
  -q | --quiet                        Do not output any status messages
  -t TIMEOUT | --timeout=timeout      Timeout in seconds, zero for no timeout
  -- COMMAND ARGS                     Execute command with args after the test finishes
USAGE
  exit "$exitcode"
}

wait_for() {
  for i in `seq $TIMEOUT` ; do
    nc -z "$HOST" "$PORT" > /dev/null 2>&1

    result=$?
    if [ $result -eq 0 ] ; then
      if [ $# -gt 0 ] ; then
        exec "$@"
      fi
      exit 0
    fi
    sleep 1
  done
  echo "Operation timed out" >&2
  exit 1
}

while [ $# -gt 0 ]
do
  case "$1" in
    *:* )
    HOST=$(printf "%s\n" "$1"| cut -d : -f 1)
    PORT=$(printf "%s\n" "$1"| cut -d : -f 2)
    shift 1
    ;;
    -q | --quiet)
    QUIET=1
    shift 1
    ;;
    -t)
    TIMEOUT="$2"
    if [ "$TIMEOUT" = "" ]; then break; fi
    shift 2
    ;;
    --timeout=*)
    TIMEOUT="${1#*=}"
    shift 1
    ;;
    --)
    shift
    break
    ;;
    --help)
    usage 0
    ;;
    *)
    echoerr "Unknown argument: $1"
    usage 1
    ;;
  esac
done

if [ "$HOST" = "" -o "$PORT" = "" ]; then
  echoerr "Error: you need to provide a host and port to test."
  usage 2
fi

wait_for "$@"

احفظ الملف وأغلقه عند الانتهاء من إضافة الشيفرة.

اجعل السكربتَ قابلًا للتنفيذ:

chmod +x wait-for.sh

بعد ذلك، افتح الملف docker-compose.yml:

nano docker-compose.yml

ابدأ بتعريف خدمة تطبيق nodejs عبر إضافة الشيفرة التالية إلى الملف:

version: '3'

services:
  nodejs:
    build:
      context: .
      dockerfile: Dockerfile
    image: nodejs
    container_name: nodejs
    restart: unless-stopped
    env_file: .env
    environment:
      - MONGO_USERNAME=$MONGO_USERNAME
      - MONGO_PASSWORD=$MONGO_PASSWORD
      - MONGO_HOSTNAME=db
      - MONGO_PORT=$MONGO_PORT
      - MONGO_DB=$MONGO_DB 
    ports:
      - "80:8080"
    volumes:
      - .:/home/node/app
      - node_modules:/home/node/app/node_modules
    networks:
      - app-network
    command: ./wait-for.sh db:27017 -- /home/node/app/node_modules/.bin/nodemon app.js

يتضمّن تعريف خدمة nodejs الخيارات التالية:

  • build: هذا يحدد خيارات التكوين، بما في ذلك context وdockerfile، والتي ستُطبّق عندما يبني Docker صورة التطبيق. إذا كنت ترغب في استخدام صورة موجودة من سجلٍّ مثل Docker Hub، فيمكنك استخدام التعليمة image بدلاً من ذلك، مع معلومات حول اسم المستخدم والمستودع ووسم الصورة.

  • context: هذا يحدّد سياق البناء الخاص ببناء الصورة، وهو في هذه الحالة مجلّد المشروع الحالي.

  • dockerfile : هذا يحدّد الملفّ Dockerfile في مجلّد المشروع الحالي لكي يستخدم Compose هذا الملفّ لبناء صورة التطبيق. لمزيد من المعلومات حول هذا الملف، يرجى الاطلاع على [كيفية إنشاء تطبيق Node.js باستخدام Docker](رابط المقال الأول في هذه السلسلة).

  • image و container_name: لإعطاء اسم لكلّ من الصورة والحاوية.

  • restart: هذا يحدّد سياسة إعادة تشغيل الحاوية. تكون القيمة الافتراضية هي no، لكننا أعدنا تعيينها في yes ما لم يتم إيقافها.

  • env_file: هذا يطلب من Compose إضافة متغيرات البيئة من ملفٍّ يسمى env. موجود في سياق البناء.

  • environment: يتيح لك استخدام هذا الخيار إضافة إعدادات اتصال Mongo التي حدّدتها في ملف env. لاحظ أننا لا نعيّن NODEENV على development، لأن هذا هو السلوك الافتراضي لـ Express إذا لم يتم تعيين NODEENV. عند الانتقال إلى الإنتاج، يمكنك تعيين هذا على production لإتاحة التخزين المؤقت للعرض والتقليل من رسائل الخطأ المطوّلة. لاحظ أيضًا أننا حددنا حاوية قاعدة البيانات db كمضيف، كما وضّحناه في الخطوة الثانية.

  • port: هذا يوجّه المنفذ 80 على المضيف إلى المنفذ 8080 على الحاوية.

  • Volumes: نضمّن هنا نوعين من الربط:

  • الأول هو وصل ترابطي (bind mount) يحمّل شيفرة التطبيق على المضيف إلى المجلّد /home/node/app في الحاوية. سيسهل هذا الأمر التطوير بسرعة، إذ سيتم إدخال أي تغييرات تجريها على شيفرة المضيف في الحاوية على الفور.

  • والثاني هو عبارة عن حجمٍ مُسمّى nodemodules. عندما ينفّذ Docker التعليمة npm install المدرجة في Dockerfile، سينشئ npm مجلّدًا جديدًا nodemodules على الحاوية يتضمن الحزم المطلوبة لتشغيل التطبيق. ومع ذلك، سيحجب الوصل الترابطي الذي أنشأناه المجلّدَ nodemodules حديث الإنشاء. ونظرًا لأن nodemodules على المضيف فارغٌ، فسيعمل الوصل على تعيين مجلّدٍ فارغ على الحاوية، مع تحييد المجلّد nodemodules الجديد ومنع التطبيق من بدء التشغيل. ويعمل الحجم nodemodules المسمى على حلّ هذه المشكلة من خلال تثبيت محتويات المجلّد /home/node/app/node_modules وربطه مع الحاوية، مع إخفاء الربط.

ضع في حسبانك النقاط التالية عند استخدام هذه الطريقة:

  • سيعمل الربط على تحميل محتويات مجلّد node_modules على الحاوية إلى المضيف وسيكون هذا المجلّد مِلكًا للجذر root، مادام الحجم المسمى أنشئ بواسطة Docker.

  • إذا كان لديك مجلّد nodemodules موجود مسبقًا على المضيف، فسيحيِّد المجلّد nodemodules الذي أنشئ على الحاوية. يفترض الإعداد الذي نبنيه في هذا الدرس أنك لا تملك مجلّدًا node_modules موجود مسبقًا وأنك لن تعمل مع npm على مضيفك. هذا يتماشى مع طريقة اثني عشر عامل لتطوير التطبيقات التي تقلل من الاعتماديات بين بيئات التنفيذ.

  • networks: هذا يحدّد أن خدمة التطبيق ستنضم إلى الشبكة app-network التي سنعرّفها في أسفل الملف.

  • command: يتيح لك هذا الخيار تعيين الأمر الذي يجب تنفيذه عندما يشغّل Compose الصورة. لاحظ أن هذا سوف يحيّد تعليمات CMD التي وضعناها في تطبيق Dockerfile الخاص بنا. نشغِّل هنا التطبيق باستخدام السكربت wait-for، الذي سيستقصي الخدمة db على المنفذ 27017 لاختبار جاهزية خدمة قاعدة البيانات من عدمها. بمجرد نجاح اختبار الاستعداد، سينفذ السكربت الأمر الذي حددناه، ‎/home/node/app/node_modules/.bin/nodemon app.js، لتشغيل التطبيق عبر nodemon. وسيضمن هذا إعادة تحميل أي تعديلات مستقبلية نجريها على شيفرتنا دون الحاجة إلى إعادة تشغيل التطبيق.

بعد ذلك، أنشئ خدمة db من خلال إضافة الشيفرة التالية أسفل تعريف خدمة التطبيق:

  • الملف ‎~/node_project/docker-compose.yml:
...
  db:
    image: mongo:4.1.8-xenial
    container_name: db
    restart: unless-stopped
    env_file: .env
    environment:
      - MONGO_INITDB_ROOT_USERNAME=$MONGO_USERNAME
      - MONGO_INITDB_ROOT_PASSWORD=$MONGO_PASSWORD
    volumes:  
      - dbdata:/data/db   
    networks:
      - app-network 

تظل بعض الإعدادات التي حددناها لخدمة nodejs كما هي، لكننا أجرينا أيضًا التعديلات التالية على تعريفات image و environment و volumes:

  • image: لإنشاء هذه الخدمة، سيسحب Compose صورة 4.1.8-xenial الخاصة ب Mongo من Docker Hub. نحن نثبّت هنا إصدارًا معينًا لتجنب التعارضات المستقبلية المحتملة عند تغير صورة Mongo. لمزيد من المعلومات حول تثبيت الإصدار، يرجى الاطلاع على توثيق Docker حول أفضل ممارسات Dockerfile.
  • MONGOINITDBROOTUSERNAME و MONGOINITDBROOTPASSWORD: تجعل صورة mongo متغيرات البيئة هذه متاحةً لكي تستطيع تعديل التهيئة الأولية لمثيل قاعدة بياناتك. وينشئ MONGOINITDBROOTUSERNAME و MONGOINITDBROOTPASSWORD معًا مستخدمًا root في استيثاق admin لقاعدة البيانات مع التأكد من تفعيل الاستيثاق عند بدء تشغيل الحاوية. لقد عيّننا MONGOINITDBROOTUSERNAME و MONGOINITDBROOTPASSWORD باستخدام قيم مأخوذة من الملف env.، والتي نمرّرها إلى الخدمة db عبر الخيار env_file. يعني القيام بذلك أن مستخدم التطبيق sammy سيكون مستخدمًا أساسيًا في مثيل قاعدة البيانات، مع إمكانية الوصول إلى جميع الصلاحيات الإدارية والتشغيلية لهذا الدور. عند العمل على الإنتاج، سترغب في إنشاء مستخدم تطبيق مخصص ذي صلاحيات محدّدة بدقّة.
اقتباس

ملاحظة: ضع في حسبانك أن هذه المتغيرات لن تكون سارية المفعول إذا شغلت الحاوية بمجلّد بيانات موجود مسبقًا.

  • dbdata:/data/db: سيثبّت الحجم المسمى dbdata البيانات المخزنة في مجلّد البيانات الافتراضي ‎/data/db الخاص بـ Mongo . سيضمن هذا أنك لن تفقد البيانات في الحالات التي توقف فيها أو تحذف الحاويات.

لقد أضفنا أيضًا خدمة db إلى شبكة app-network باستخدام الخيار networks.

كخطوة أخيرة، أضف تعريفات الحجم والشبكة إلى أسفل الملف:

...
networks:
  app-network:
    driver: bridge

volumes:
  dbdata:
  node_modules:

تتيح شبكة المستخدم الجسرية app-network التواصل بين حاوياتنا مادامت موجودة على نفس المضيف العفريت Docker. يعمل هذا على تبسيط حركة المرور والاتصال داخل التطبيق، إذ يفتح جميع المنافذ بين الحاويات على نفس الشبكة الجسرية، دون تعريض أي منفذ للعالم الخارجي. وبالتالي، فإن حاويات db و nodejs تستطيع التواصل مع بعضها البعض، وسنحتاج فقط إلى فتح المنفذ 80 للوصول الأمامي (front-end access) إلى التطبيق.

يعرّف مفتاح المستوى الأعلى volumes الحجمين dbdata و node_modules. وعندما ينشئ Docker حجومًا، تُخزّن محتوياتها في جزء من ملفات المضيف النظامية، /var/lib/docker/volumes/ ، التي يديرها Docker. تخزّن محتويات كل حجمٍ في مجلّد تحت /var/lib/docker/volumes/ وتُثبت على أي حاوية تستخدم الحجم. وبهذه الطريقة، ستكون بيانات معلومات سمك القرش التي سيقوم المستخدمون بإنشائها مثبّتة في الحجم dbdata حتى لو أزلنا الحاوية db وأعدنا إنشاءها.

سيبدو ملف docker-compose.yml النهائي كما يلي:

version: '3'

services:
  nodejs:
    build:
      context: .
      dockerfile: Dockerfile
    image: nodejs
    container_name: nodejs
    restart: unless-stopped
    env_file: .env
    environment:
      - MONGO_USERNAME=$MONGO_USERNAME
      - MONGO_PASSWORD=$MONGO_PASSWORD
      - MONGO_HOSTNAME=db
      - MONGO_PORT=$MONGO_PORT
      - MONGO_DB=$MONGO_DB
    ports:
      - "80:8080"
    volumes:
      - .:/home/node/app
      - node_modules:/home/node/app/node_modules
    networks:
      - app-network
    command: ./wait-for.sh db:27017 -- /home/node/app/node_modules/.bin/nodemon app.js 

  db:
    image: mongo:4.1.8-xenial
    container_name: db
    restart: unless-stopped
    env_file: .env
    environment:
      - MONGO_INITDB_ROOT_USERNAME=$MONGO_USERNAME
      - MONGO_INITDB_ROOT_PASSWORD=$MONGO_PASSWORD
    volumes:     
      - dbdata:/data/db
    networks:
      - app-network  

networks:
  app-network:
    driver: bridge

volumes:
  dbdata:
  node_modules:  

احفظ الملف وأغلقه عند الانتهاء من التحرير.

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

الخطوة الخامسة: اختبار التطبيق

بعد إنشاء وتحرير ملف docker-compose.yml ، يمكنك إنشاء خدماتك باستخدام الأمر docker-compose up. يمكنك أيضًا اختبار ثبات بياناتك عبر إيقاف حاوياتك وحذفها باستخدام Docker.

ابدأ أولًا ببناء صور الحاوية وإنشاء الخدمات عبر تنفيذ docker-compose up باستخدام الراية d- ، والذي سيشغّل حاويات nodejs و db في الخلفية:

docker-compose up -d

سيظهر لك الإخراج الذي يؤكد أن خدماتك أُنشئت فعلًا:

...
Creating db ... done
Creating nodejs ... done

يمكنك أيضًا الحصول على معلومات أكثر تفصيلاً حول عمليات التشغيل بعرضك لإخراج سجل الخدمات:

docker-compose logs

سيظهر لك إخراج مثل هذا إذا اشتغل كل شيء بشكل صحيح:

...
nodejs    | [nodemon] starting `node app.js`
nodejs    | Example app listening on 8080!
nodejs    | MongoDB is connected
...
db        | 2019-02-22T17:26:27.329+0000 I ACCESS   [conn2] Successfully authenticated as principal sammy on admin

يمكنك أيضًا التحقق من حالة حاوياتك باستخدام docker-compose ps:

docker-compose ps

سيظهر لك الإخراج التالي مشيرًا إلى أن حاوياتك تعمل:

 Name               Command               State          Ports        
----------------------------------------------------------------------
db       docker-entrypoint.sh mongod      Up      27017/tcp           
nodejs   ./wait-for.sh db:27017 --  ...   Up      0.0.0.0:80->8080/tcp

بعد تشغيل خدماتك، يمكنك تصفّح: http://yourserverip في المتصفح. ستظهر لك صفحة الهبوط على هذا النحو:

landing_page.png

انقر على زر الحصول على معلومات القرش. ستظهر لك صفحة بنموذج إدخال يمكنك إدخال الاسم ووصف السلوك العام لسمك القرش:

shark_form.png

أضف سمكة قرش من اختيارك في النموذج. لأغراض هذا العرض التوضيحي، سنضيف Megalodon Shark إلى حقل Shark Name، وAncient إلى حقل Shark Character:

shark_filled.png

انقر على زر الإرسال. ستظهر لك صفحة بها معلومات القرش معروضة:

shark_added.png

كخطوة أخيرة، يمكننا اختبار ثبات البيانات التي أدخلتها للتو إذا حذفنا حاوية قاعدة البيانات.

ارجع مرة أخرى إلى الطرفية، واكتب الأمر التالي لإيقاف وحذف الحاويات والشبكة:

docker-compose down

لاحظ أننا لم ندرج الخيار ‎--volumes؛ وبالتالي، لن يُحذف الحجم dbdata.

يؤكد الإخراج التالي حذف الحاويات والشبكة:

Stopping nodejs ... done
Stopping db     ... done
Removing nodejs ... done
Removing db     ... done
Removing network node_project_app-network

أعد الآن إنشاء الحاويات:

docker-compose up -d

ثم عد إلى نموذج معلومات سمك القرش:

shark_form.png

أدخل سمكة قرش جديدة من اختيارك. سنستعمل هنا Whale Shark وLarge:

whale_shark.png

بمجرد النقر على زر الإرسال، سترى أن القرش الجديد أُضيف إلى مجموعة سمك القرش في قاعدة بياناتك دون أن تفقد البيانات التي أدخلتها سابقًا:

persisted_data.png

إنّ تطبيقك يشتغل الآن على حاويات Docker مع تفعيل خاصيتي ثبات البيانات ومزامنة الشيفرة.

خاتمة

باتباع هذا الدرس، أنشأت إعداد تطويرٍ لتطبيقك Node باستخدام حاويات Docker. لقد تمكنت من جعل مشروعك أكثر نمطيّةً وأكثر قابلية للنقل من خلال استخراج المعلومات الحساسة وفصل حالة التطبيق عن شيفرة التطبيق. لقد أعددت أيضًا تكوين ملف الشيفرة المتداولة docker-compose.yml الذي تستطيع مراجعته كلما تغيرت احتياجاتك ومتطلبات التطوير الخاصة بك.

قد تكون مهتمًا، أثناء عمليتك التطويرية ، بمعرفة المزيد حول تصميم التطبيقات بسير عمل يعتمد على الحاويات وعلى Cloud Native. يرجى الاطلاع على تصميم التطبيقات لKubernetes وتحديث التطبيقات ل Kubernetes إذا كانت لغتك الإنجليزية جيدة لمزيد من المعلومات حول هذه المواضيع.

لمعرفة المزيد حول الشيفرة المستخدم في هذا الدرس، يرجى الاطلاع على كيفية إنشاء تطبيق Node.js باستخدام Docker وكيفية دمج MongoDB مع تطبيق Node الخاص بك. وللحصول على معلومات حول نشر تطبيق Node باستخدام وكيل عكسي Nginx يعتمد على الحاويات، يرجى الاطلاع على كيفية تأمين تطبيق Node.js في حاويات باستخدام Nginx و Let’s Encrypt و Docker Compose.

ترجمة -وبتصرف- للمقال Containerizing a Node.js Application for Development With Docker Compose لصاحبته Kathleen Juell





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن