اذهب إلى المحتوى

بناء الصور وتهيئة بيئة العمل للتطبيقات المبنية ضمن الحاويات


ابراهيم الخضور

استخدمنا في جزئية سابقة من هذه السلسلة صورتين مختلفتين هما "ubuntu" و "node" ونفّذنا بعض الأعمال يدويًا لتشغيل تطبيق "Hello, World". ستساعدنا الأدوات والأوامر التي تعلمناها سابقًا في هذه الجزئية من السلسلة، إذ نتعلم فيه بناء الصور وتهيئة بيئة عمل التطبيق. سنبدأ ببناء واجهة خلفية نمطية باستخدام Express/Node.js ثم نبني عليها مستخدمين خدمات أخرى مثل قواعد بيانات MongoDB.

ملفات Dockerfile

بإمكاننا إنشاء صورة جديدة تتضمن التطبيق "!Hello, World" بدلًا من تعديل الحاوية بنسخ ملفات جديدة إليها، وتساعدنا في ذلك أداة تُدعى Dockerfile، وهي ملفٌ نصي بسيط يحتوي كل التعليمات الخاصة بإنشاء صورة. سنبدأ إذًا بإنشاء مثال عن Dockerfile من تطبيق "!Hello, World".

أنشئ مجلدًا جديدًا على جهازك ثم أنشئ ضمنه الملف "Dockerfile " إن لم تكن قد فعلت ذلك مسبقًا. ولنضع كذلك الملف "index.js" الذي يضم الشيفرة ('!console.log('Hello, World إلى جواره. ستبدو هيكلية المجلد على النحو التالي:

├── index.js
└── Dockerfile

سنخبر الصورة من خلال "Dockerfile" بثلاثة أمور:

  • استخدم node:16 أساسًا للصورة.
  • ضع الملف "index.js" ضمن الصورة، كي لا نُضطر إلى نسخه يديويًا إلى الحاوية.
  • استخدم node لتنفيذ شيفرة الملف "index.js" عندما نشغّل الحاوية من الصورة.

توضع هذه النقاط الثلاث ضمن ملف "Dockerfile" وأفضل مكان لإنشاء هذا الملف هو جذر المشروع، وسيبدو هذا الملف على النحو التالي:

FROM node:16

WORKDIR /usr/src/app

COPY ./index.js ./index.js

CMD node index.js
  • FROM: تخبر هذه التعليمة برنامج دوكر Docker أنّ أساس الصورة هو node:16.
  • COPY: تنسخ هذه التعليمة الملف index.js من الجهاز المضيف إلى ملف بنفس الاسم ضمن الصورة.
  • CMD: تخبر هذه التعليمة البرنامج ما يجب أن يحدث عند تنفيذ الأمر docker run. وهذه التعليمة هي تعليمة تنفيذ افتراضية يمكن استبدالها بالمعامل الذي يُعطي بعد اسم الصورة. اكتب الأمر docker run --help إن نسيت.
  • WORKDIR: وضعت هذه التعليمة للتأكد من أننا لن نفعل شيئًا يتداخل مع محتوى الحاوية. إذ سيضمن أنّ كل التعليمات التي ستليه ستكون ضمن المجلد "usr/src/app/" الذي يُعد مجلد العمل في هذه الحالة. فإن لم يكن هذا المجلد موجودًا في الصورة، سيُنشأ تدريجيًا.

لم نحدد مجلد عمل WORKDIR، فقد نجازف بتغيير ملفات هامة بطريق الخطأ. لو تحققت من الجذر / للصورة بتنفيذ الأمر docker run node:16 ls ستجد العديد من المجلدات والملفات في هذه الصورة.

نستطيع الآن استخدام الأمر docker build لبناء الصورة بناءً على ملف Dockerfile، لكننا سنضيف الراية t- إلى هذا الأمر كي تساعدنا في إعادة تسمية الصورة:

$ docker build -t fs-hello-world . 
[+] Building 3.9s (8/8) FINISHED
...

ستكون نتيجة تنفيذ الأمر هي:

"docker please build with tag fs-hello-world the Dockerfile in this directory" 

والتي تشير إلى بناء صورة بالاسم "fs-hello-world" بالاعتماد على ملف Dockerfile الموجود في المجلد. يمكنك الإشارة إلى أي ملف Dockerfile لكن في حالتنا البسيطة يكفي وضع . للإشارة إلى هذا الملف ضمن المجلد لهذا انتهى الأمر بالنقطة.

يمكنك تشغيل الحاوية الآن باستخدام الأمر docker run fs-hello-world ويمكن نقل الصورة أو تنزيلها أو حذفها فهي في طبيعتها ملفات. إذ يمكنك تشكيل قائمة بالصور التي يضمها حاسوبك باستخدام الأمر docker image ls أو حذف الصورة ‍‍docker image rm. اطلع على بقية الأوامر المتاحة بتنفيذ أمر المساعدة docker image --help.

صور أكثر فائدة

ينبغي أن يكون نقل خادم Express إلى حاوية ببساطة نقل تطبيق "!Hello, World"، إذ يكمن الاختلاف الوحيد بين الحالتين في وجود ملفات أكثر في حالة الخادم، لكن ستغدو الأمور أبسط بوجود التعليمة COPY.

لنحذف الآن الملف "index.js" وننشئ خادم Express باستخدام express-generator الذي يساعدنا على بناء هيكلية بسيطة للتطبيق.

$ npx express-generator
  ...

  install dependencies:
    $ npm install

  run the app:
    $ DEBUG=playground:* npm start

لنشغل التطبيق الآن كي نرى ما فعلنا، وانتبه أن أمر التشغيل قد يختلف في جهازك، فالمجلد في المثال السابق يُدعى "playground".

$ npm install
$ DEBUG=playground:* npm start
  playground:server Listening on port 3000 +0ms

يمكنك الانتقال الآن إلى العنوان "http://localhost:3000" حيث يعمل التطبيق.

إن ضم الملفات في حاويات هي عملية سهلة نوعًا ما بناءً على ما واجهناه حتى الآن:

  • استخدم الأساس node.
  • اضبط مجلد العمل كي لا يتداخل عملك مع بقية محتويات الصورة.
  • انسخ كل ملفات المجلد إلى الصورة.
  • ابدأ بتنفيذ الأمر DEBUG=playground:* npm start بعد الأمر CMD.

لنضع ملف Dockerfile التالي في جذر المشروع:

FROM node:16

WORKDIR /usr/src/app

COPY . .

CMD DEBUG=playground:* npm start

سنبني الآن الصورة انطلاقًا من ملف باستخدام الأمر:

docker build -t express-server .

وسنشغلها باستخدام الأمر:

 docker run -p 3123:3000 express-server

تبلّغ الراية p- دوكر بضرورة فتح منفذ الجهاز المضيف وتوجيهه إلى منفذ للحاوية ولهذا الأمر الصيغة التالية: p host-port:application-port-

$ docker run -p 3123:3000 express-server

> playground@0.0.0 start
> node ./bin/www

Tue, 29 Jun 2021 10:55:10 GMT playground:server Listening on port 3000

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

سيبدأ التطبيق عمله الآن، لهذا سنختبره بإرسال الطلب GET إلى العنوان "/http://localhost:3123". بالنسبة لإيقاف الخادم فهو أمر عصيبٌ حاليًا، لهذا افتح نافذة أخرى للطرفية ونفذ الأمر docker kill لإيقاف التطبيق؛ إذ يرسل هذا الأمر الإشارة SIGKILL إلى التطبيق ويجبره على الإنهاء. ستحتاج إلى اسم أو معرّف الحاوية ID مثل وسيط لتنفيذ الأمر السابق. وتجدر الإشارة أنه يكفي استخدام بداية المعرّف id عند تمريره وسيطًا، إذ سيعرف دوكر مباشرةً الحاوية المقصودة.

$ docker container ls
  CONTAINER ID   IMAGE            COMMAND                  CREATED         STATUS         PORTS                                       NAMES
  48096ca3ffec   express-server   "docker-entrypoint.s…"   9 seconds ago   Up 6 seconds   0.0.0.0:3123->3000/tcp, :::3123->3000/tcp   infallible_booth

$ docker kill 48
  48

لنعمل من الآن وصاعدًا على نفس المنفذ في كلا الجانبين p-. وهكذا لن تضطر إلى تذكُّر ما المنفذ الذي عليك اختياره.

إصلاح المشاكل المحتملة الناتجة عن عملية النسخ واللصق

لا بُد من تغيير بعض الخطوات لإنشاء ملف Dockerfile متقدم، وقد لا يعمل المثال الذي أوردناه سابقًا على الإطلاق، لأننا أهملنا خطوة هامة.

عندما تنفِّذ الأمر npm install على حاسوبك، فقد يُثبّت مدير حزم Node بعض الاعتمادات التي تتعلق بنظام التشغيل أثناء تقدم التثبيت. وقد ننقل صدفةً أجزاءً غير وظيفية إلى الصورة عند استخدام التعليمة COPY، ويحدث ذلك بسهولة إن نسخنا المجلد "node_modules" إلى الصورة.

من المهم جدًا إبقاء تلك النقاط في ذاكرتنا عند بناء الصورة، فمن الأفضل أن ننفذ معظم الأعمال مثل npm install أثناء عملية البناء ضمن الحاوية بدلًا من تنفيذها قبل البناء؛ إذ أن القاعدة الجوهرية هنا هي نسخ الملفات التي ستدفعها إلى غيت هب GitHub فقط، ولا ينبغي نسخ الاعتماديات ومتطلبات البناء كونها أشياء يمكن تثبيتها أثناء بناء الصورة.

يمكنك استخدام الملف "dockerignore." لحل المشكلة، وهو ملفٌ شبيه بملف التجاهل "gitignore." لمنع نسخ الملفات غير المطلوبة إلى الصورة. يُوضع هذا الملف إلى جوار الملف Dockerfile، وإليك مثالًا عن محتوياته:

.dockerignore
.gitignore
node_modules
Dockerfile

لكننا سنحتاج إضافةً إلى ملف "dockerignore." في حالتنا إلى تثبيت الاعتماديات خلال خطوة البناء، لهذا سيتغير ملف Dockerfile إلى الشكل:

FROM node:16

WORKDIR /usr/src/app

COPY . .

RUN npm install
CMD DEBUG=playground:* npm start

قد يكون تنفيذ الأمر npm install خطرًا، لهذا يزوّدنا npm بأداة أفضل لتثبيت الاعتماديات وهو الأمر ci.

تُلخّص الاختلافات بين ci و install على النحو التالي :

  • قد يُحدّث install الملف "package-lock.json".
  • قد يُثبِّت install نسخةً مختلفةً من الاعتمادية إن ظهرت المحارف "^" أو "~" في نسخة الاعتمادية.
  • سيحذف ci المجلد "node_modules" قبل تثبيت أي شيء.
  • سيتبع ci الملف "package-lock.json" ولا يبدّل أي ملف.

باختصار: يقدم ci نسخًا يمكن الاعتماد عليها، بينما يُستخدم install عند تثبيت اعتماديات جديدة.

طالما أننا لن نثبِّت أي شيء جديد في خطوة البناء، ولا نريد تغييرات فجائية في النسخ، سنستخدم الأمر ci:

FROM node:16

WORKDIR /usr/src/app

COPY . .

RUN npm ci
CMD DEBUG=playground:* npm start

كما يمكننا تحسين الحالة أكثر باستخدام الأمر npm ci --only=production كي لا نهدر الوقت في تثبيت الاعتماديات.

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

ينبغي أن يعمل الملف من جديد، لهذا حاول تنفيذ الأمر التالي:

docker build -t express-server . && docker run -p 3000:3000 express-server

لاحظ كيف وصلنا هنا أمري باش bash باستخدام &&، وسنحصل تقريبًا على نفس النتيجة إذا نفذنا كلا الأمرين كلًّا على حدة؛ لكن عندما تربط أمرين باستخدام &&، فلن يُنفَّذ الأمر الآخر إذا فشل تنفيذ أحدهما.

ضبطنا سابقًا متغير البيئة :DEBUG=playground من خلال الأمر CMD لتشغيل npm، كما يمكننا أيضًا استخدام التعليمة ENV في ملف Dockerfiles لضبط متغيرات البيئة، فلنفعل ذلك:

FROM node:16

WORKDIR /usr/src/app

COPY . .

RUN npm ci 

ENV DEBUG=playground:*
CMD npm start

أفضل الممارسات المتعلقة باستخدام Dockerfiles

عليك اتباع القاعدتين الجوهريّتين التاليتين عند إنشاء الصور:

  • حاول أن تبني صورةً آمنة قدر المستطاع.
  • حاول أن تُنشئ صورةً صغيرة قدر الإمكان.

تُعد الصور الأصغر أكثر أمانًا لأن مجال الهجوم عليها محدود، كما يمكن نقلها بسرعة أكبر ضمن أنابيب النشر.

وأخيرًا لا بُد من إصلاح آخر نقطة أهملناها وهي تشغيل التطبيق مثل جذر بدلًا من مستخدم بصلاحيات منخفضة:

FROM node:16

WORKDIR /usr/src/app

COPY --chown=node:node . .
RUN npm ci 

ENV DEBUG=playground:*

USER node
CMD npm start

التمرين 12.5: إنشاء حاوية لتطبيق Node

يضم المستودع الذي نسخته في التمرين الأول تطبيق لائحة مهام todo-app. اطلع على الواجهة الخلفية "todo-app/todo-backend" للتطبيق واقرأ الملف "اقرأني README". لن نقترب حاليًا من الواجهة الأمامية "todo-frontend"

الخطوة الأولى: وضع الواجهة الخلفية "todo-backend" ضمن حاوية بإنشاء الملف "todo-app/todo-backend/Dockerfile" ثم بناء الصورة.

الخطوة الثانية: تشغيل الصورة على المنفذ الصحيح، والتأكد أن عدّاد الزيارات سيزداد عند استخدامه عبر المتصفح على العنوان "/http://localhost:3000" (أو على أي منفذ آخر قد تهيّئه).

تلميح: شغّل التطبيق خارج الحاوية أولًا للتحقق منه قبل وضعه في الحاوية.

استخدام الأداة docker-compose

أنشأنا في جزئية سابقة من هذه السلسلة خادمًا وعلمنا أنه يعمل على المنفذ 3000 وشغّلناه باستخدام الأمر:

docker build -t express-server . && docker run -p 3000:3000 express-server

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

تُعد الأداة Docker-compose من الأدوات الرائعة الأخرى التي تساعدك على إدارة الحاوية، لهذا سنبدأ استخدام هذه الأداة خلال رحلتنا في دراسة الحاويات، إذ ستساعد على توفير بعض الوقت عند تهيئة الحاوية.

ثبّت الأداة Docker-compose ثم تأكد من عملها على النحو التالي:

$ docker-compose -v
docker-compose version 1.29.2, build 5becea4c

سنحوّل الآن الأوامر السابقة إلى ملف yaml يمكن تخزينه في مستودع غيت Git.

أنشئ الملف "docker-compose.yml" وضعه في جذر المشروع إلى جوار ملف Dockerfile، ثم ضع المحتوى التالي ضمنه:

version: '3.8'            # نسخة جديدة لا بد أن تعمل

services:
  app:                    # اسم الخدمة وقد يكون أي شيء
    image: express-server # صرّح عن الصورة التي تريد استخدامها
    build: .              # حدد مكان بناء الصورة إن لم تكن موجودة
    ports:                # حدد المنفذ الذي يرتبط به التطبيق
      - 3000:3000

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

سنتمكن الآن من استخدام الأمر docker-compose up في بناء وتشغيل التطبيق، وإن أردت إعادة بناء الصور، استخدم الأمر docker-compose up --build.

بإمكانك أيضًا تشغيل التطبيق في الخلفية باستخدام الأمر docker-compose up -d (الراية d- لفصل التطبيق) وإيقافه بتنفيذ الأمر docker-compose down.

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

التمرين 12.6: الأداة docker-compose

أنشئ الملف "todo-app/todo-backend/docker-compose.yml" الذي يعمل مع تطبيق node من التمرين السابق. وعليك الانتباه إلى عدّاد الزيارات فهو الميزة الوحيدة التي ينبغي أن تعمل.

استخدام الحاويات في مرحلة التطوير

يمكن استخدام الحاويات أثناء تطوير التطبيقات بطرق متعددة لتسهيل عملك، ومن إحدى فوائدها تجاوز تثبيت وتهيئة الأدوات مرتين.

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

يستخدم التطبيق الذي تعرفنا عليه في التمارين السابقة MongoDB، لهذا دعونا نستخدم Docker Hub لإيجاد صورة MongoDB، إذ أنه المكان الافتراضي الذي نسحب الصور منه، كما يمكنك استخدام مسجلات أخرى أيضًا، لكن طالما أننا نتعمق في دوكر فهو خيارٌ جيد. يمكنك أن تجد من خلال بحث سريع الصورة المطلوبة على العنوان https://hub.docker.com/_/mongo.

أنشئ ملف yaml يُدعى "todo-app/todo-backend/docker-compose.dev.yml" يحتوي ما يلي:

version: '3.8'

services:
  mongo:
    image: mongo
    ports:
      - 3456:27017
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: example
      MONGO_INITDB_DATABASE: the_database

يُوضَّح معنى أول متغيري بيئة معرفين في الشيفرة السابقة في صفحة Docker Hub:

"تُنشِئ المتغيرات المُستخدمة على التوازي مستخدمًا جديدًا وتضبط كلمة المرور له. يُنشأ هذا المستخدم في قاعدة بيانات إدارة الاستيثاق ويعطى دور الجذر root، وهو دور المستخدم الأعلى superuser."

يخبر متغير البيئة الأخير MONGO_INITDB_DATABASE قاعدة البيانات MongoDB أن تنشئ قاعدة بيانات بهذا الاسم. بإمكانك استخدام الراية f- لتخصيص ملف لتشغيل أمر Docker Compose مثل:

 docker-compose -f docker-compose.dev.yml up

شغًل الآن MongoDB من خلال الأمر:

 docker-compose -f docker-compose.dev.yml up -d

أما الراية d- فلتشغيل العملية في الخلفية.

يمكنك متابعة سجلات الخرج بتنفيذ الأمر:

docker-compose -f docker-compose.dev.yml logs -f

وتُستخدم الراية f- للتأكد من متابعة السجلات.

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

نفِّذ الأمر القديم npm install على جهازك لإعداد تطبيق Node، ثم شغل التطبيق باستخدام متغيرات البيئة اللازمة. يمكنك تعديل الشيفرة لجعل متغيرات البيئة متغيرات افتراضية أو استخدم الملف env.. لا ضرر من وضع هذه المفاتيح على غيت هب لأنها تُستخدم ضمن بيئة التطوير المحلية، لذلك سأضعها هناك عن طريق الأمر npm run dev لمساعدتك في النسخ واللصق.

$ MONGO_URL=mongodb://localhost:3456/the_database npm run dev

لن يكون ذلك كافيًا، بل نحتاج إلى إنشاء مستخدم نستوثِق منه ضمن الحاوية، إذ سيقود الولوج إلى العنوان " http://localhost:3000/todos" إلى خطأ في الاستيثاق.

[nodemon] 2.0.12
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node ./bin/www`
(node:37616) UnhandledPromiseRejectionWarning: MongoError: command find requires authentication
    at MessageStream.messageHandler (/Users/mluukkai/opetus/docker-fs/container-app/express-app/node_modules/mongodb/lib/cmap/connection.js:272:20)
    at MessageStream.emit (events.js:314:20)

ربط وتركيب وتهيئة قاعدة البيانات

ستجد في صفحة MongoDB Docker Hub تحت عنوان " تهيئة نسخة جديدة Initializing a fresh instance" معلومات عن تنفيذ شيفرة لتهيئة قاعدة البيانات ومستخدم لها.

في المشروع التجريبي ملف "todo-app/todo-backend/mongo/mongo-init.js" يضم المحتوى التالي:

db.createUser({
  user: 'the_username',
  pwd: 'the_password',
  roles: [
    {
      role: 'dbOwner',
      db: 'the_database',
    },
  ],
});

db.createCollection('todos');

db.todos.insert({ text: 'Write code', done: true });
db.todos.insert({ text: 'Learn about containers', done: false });

يهيئ الملف قاعدة البيانات مع مستخدم وبعض المهام المخزّنة في القاعدة، وعلينا في الخطوة التالية نقلها إلى الحاوية وتشغيلها.

من الممكن إنشاء صورة جديدة من mongo ثم نسخ الملف إلى الداخل، أو استخدام الأمر لتركيب الملف ضمن الحاوية، لكننا سنختار الطريقة الأخيرة.

إن التركيب بالربط Bind mount هي عملية ربط ملف على حاسوبك المحلي بملف في الحاوية، ويمكننا تنفيذ ذلك باستخدام الراية v- مع الأمر بالصيغة التالية: v FILE-IN-HOST:FILE-IN-CONTAINER-. يُعرّف التركيب بالربط تحت المفتاح volumes في الملف "docker-compose"، أما صياغة التصريح فتكون على النحو التالي "مضيف ثم حاوية":

  mongo:
    image: mongo
    ports:
     - 3456:27017
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: example
      MONGO_INITDB_DATABASE: the_database
    volumes:
     - ./mongo/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js

إن النتيجة هي أنّ الملف "mongo-init.js" في مجلد mongo على حاسوبك سيكون نفسه الملف "mongo-init.js" في المجلد "docker-entrypoint-initdb.d/" من الحاوية، وسيؤدي تعديل أحدهما إلى تعديل الآخر. لا حاجة لتغيير أي شيء أثناء التشغيل، وهذا هو مفتاح تطوير البرمجيات ضمن الحاويات.

نفّذ الأمر التالي للتأكد من وجود كل شيء في مكانه:

 docker-compose -f docker-compose.dev.yml down --volumes 

ثم ابدأ لائحة جديدة بتنفيذ الأمر التالي لتهيئة قاعدة البيانات:

 docker-compose -f docker-compose.dev.yml up 

إذا واجهك خطأ على النحو التالي:

mongo_database | failed to load: /docker-entrypoint-initdb.d/mongo-init.js
mongo_database | exiting with code -3

فقد يكون لديك مشكلةً في إذن القراءة، وهذا أمرٌ قد يحدث عند التعامل مع الأقراص volumes. بإمكانك في حالتنا استخدام الأمر chmod a+r mongo-init.js الذي يمنح أيًا كان إمكانية قراءة الملف، لكن كن حذرًا عند استخدام التعليمة chmod لأن السماح بإذونات أكبر قد يقود إلى مشاكل أمنية، لهذا استخدم تلك التعليمة على الملف "mongo-init.js" الموجود على جهازك فقط.

سيعمل تطبيق express الآن عبر متغيرات البيئة الصحيحة:

$ MONGO_URL=mongodb://the_username:the_password@localhost:3456/the_database npm run dev

لنتأكد أن الطلب إلى العنوان http://localhost:3000/todos سيعيد كل المهام الموجودة في قاعدة البيانات، فمن المفترض أن يعيد المهمتان اللتان هيأناهما، ولا بد من استخدام Postman لاختبار وظائف التطبيق الأساسية مثل إضافة وحذف مهام من قاعدة البيانات.

البيانات المقيمة في الأقراص

لا تُخزّن الحاويات بياناتنا افتراضيًا، فعندما تغلق حاوية قد تستطيع أو لا تستطيع استعادة البيانات. وبصورةٍ عامة هناك طريقتان مختلفتان لتخزين البيانات:

  • التصريح عن مكان ضمن منظومة الملفات (عملية الربط بالتركيب bind mount).
  • ترك الأمر لبرنامج دوكر كي يقرر تخزين البيانات (استخدام الأقراص volume)

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

services:
  mongo:
    image: mongo
    ports:
     - 3456:27017
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: example
      MONGO_INITDB_DATABASE: the_database
    volumes:
      - ./mongo/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js
      - ./mongo_data:/data/db

ستُنشئ الإعدادات السابقة مجلدًا يُدعى mongo_data ضمن منظومة الملفات في حاسوبك ثم تربطه بالحاوية بالاسم data/db/. أي أنّ البيانات الموجودة في المجلد data/db/ ستُخزّن خارج الحاوية لكن بإمكانها الوصول إليها. وتذكر إضافة المجلد إلى ملف التجاهل "gitignore.".

يمكن تحقيق الأمر ذاته باستخدام أقراص التخزين المسماة:

services:
  mongo:
    image: mongo
    ports:
     - 3456:27017
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: example
      MONGO_INITDB_DATABASE: the_database
    volumes:
      - ./mongo/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js
      - mongo_data:/data/db
volumes:
  mongo_data:

يُنشأ القرص الآن ويدار من قبل دوكر، وبإمكانك قبل تشغيل التطبيق استعراض الأقراص الموجودة بتنفيذ الأمر docker volume ls، أو فحصّها docker volume inspect، أو حذفها docker volume rm. ليس اختيار مكان تخزين البيانات محليًا في هذا الخيار أمرًا قليل الأهمية موازنةً بالخيار السابق.

التمرين 12.7: كتابة القليل من الشيفرة للتعامل مع MongoDB

نفترض في هذا التمرين أنك أنجزت جميع الإعدادات التي تحدثنا عنها سابقًا بعد التمرين 12.5. وسيبقى تشغيل التطبيق خارج الحاوية وستوضع قاعدة البيانات MongoDB فقط ضمن الحاوية.

لا توجد طريق ملائمة حتى الآن للحصول على مهمة واحدة (GET* /todos/:id*) وتحديث مهمة واحدة (PUT* /todos/:id*). جد حلًا لهاتين المشكلتين.

تنقيح المشاكل في الحاويات

اقتباس

قد تصل أثناء كتابة الشيفرة على الأغلب إلى حالة لا يعمل فيها أي شيء. -ماتي لوكينين Matti Luukkainen.

لا بُد من تعلم استعمال بعض الأدوات لتنقيح التطبيقات ضمن الحاويات، لأننا لا نستطيع استخدام الأمر console.log دائمًا. عندما تظهر الثغرات في شيفرتك، فلا بد وأن يعمل شيء ما في شيفرتك لتنطلق منه. وعمومًا هناك وضعان لتبدأ منهما: الأول هو تطبيق يعمل والثاني هو تطبيق لا يعمل، لهذا سنطلع على بعض الأدوات التي تساعد في تنقيح التطبيق في الحالة الثانية.

يمكن أن تنتقل أثناء تطوير البرنامج في عملك خطوة خطوة لتتاكد طوال الوقت أن كل شيء يعمل كما تتوقع. لكن لا ينطبق هذا الأمر عند ضبط الإعدادات، فقد تخفق الإعدادات التي تكتبها حتى لحظة الإنتهاء منها؛ لهذا إذا كتبت ملف "docker-compose.yml" طويل أو ملف Dockerfile ولم يعمل، عليك التروي برهة والتفكير بالطرق المختلفة التي تتأكد منها أن شيء ما يعمل بالشكل المطلوب.

لا تزال طريقة التشكيك بكل شيء واردة هنا، وكما قلنا في القسم الثالث: عليك أن تكون منظّما، وطالما أنّ المشكلة قد تكون في أي مكان، عليك التشكيك بكل شيء، وإزالة كل مصادر الخطأ واحدًا تلو الآخر. فالتوقف والتفكير بالمشكلة بدلًا من كتابة مزيدٍ من الإعدادات هي الطريقة الأنسب في الحصول على حل بسيط، كما أن البحث السريع باستخدام محركات البحث قد يساعدك في التقدم.

الأمر exec

يمكن استخدام الأمر exec للقفز مباشرةً إلى الحاوية وهي تعمل. لهذا سنهيّئ خادم ويب في الخلفية ونجري بعض الإعدادات كي نشغّله ليعرض الرسالة "!Hello, exec" ضمن المتصفح. سنستخدم الخادم Nginx القادر على تخديم الملفات الساكنة، ويدعم الملف "index.html" الذي يمثل الصفحة الافتراضية ويسمح لنا بتعديلها أو استبدالها:

$ docker container run -d nginx

الأسئلة المطروحة الآن هي:

  • أين سنتوجه عبر المتصفح؟
  • هل يعمل الخادم بالفعل؟

لكن نعرف كيف نجيب على هذه الأسئلة وذلك باستعراض الحاويات التي تعمل:

$ docker container ls
CONTAINER ID   IMAGE           COMMAND                  CREATED              STATUS                      PORTS     NAMES
3f831a57b7cc   nginx           "/docker-entrypoint.…"   About a minute ago   Up About a minute           80/tcp    keen_darwin

لقد حصلنا على جواب السؤال الأول فعلًا، فيبدو أن الخادم ينصت إلى المنفذ 80. لهذا سنطفئ الخادم ثم نعيد تشغيله مستخدمين الراية p- للسماح للمتصفح بالوصول إليه:

$ docker container stop keen_darwin
$ docker container rm keen_darwin

$ docker container run -d -p 8080:80 nginx

لنلقي نظرةً على التطبيق بالانتقال إلى العنوان http://localhost:8080، وستجد أنّ التطبيق يعرض رسالة خاطئة، لهذا سنقفز إلى الحاوية مباشرةً لإصلاحها. أبقِ متصفحك مفتوحًا، فلا نريد إغلاق الحاوية عند إصلاحها، بل سنستخدم أوامر الطرفية ضمن الحاوية، ولا تنسى استخدام الراية it- لضمان قدرتك على التفاعل مع الحاوية:

$ docker container ls
CONTAINER ID   IMAGE     COMMAND                  CREATED              STATUS              PORTS                                   NAMES
7edcb36aff08   nginx     "/docker-entrypoint.…"   About a minute ago   Up About a minute   0.0.0.0:8080->80/tcp, :::8080->80/tcp   wonderful_ramanujan

$ docker exec -it wonderful_ramanujan bash
root@7edcb36aff08:/#

بعد أن قفزنا داخل الحاوية، لا بد من إيجاد الملف الخاطئ واستبداله، إذ يتضح لنا من خلال بحث سريع باستخدام محرك بحث أن الملف هو "usr/share/nginx/html/index.html/".

لننتقل إلى المجلد ونحذف الملف:

root@7edcb36aff08:/# cd /usr/share/nginx/html/
root@7edcb36aff08:/# rm index.html

لو انتقلنا الآن إلى العنوان http://localhost:8080 فسنحصل على الصفحة 404 لأننا حذفنا الملف الصحيح، لهذا سنستبدله بملف آخر يضم المحتوى الصحيح:

root@7edcb36aff08:/# echo "Hello, exec!" > index.html

أعد تحميل الصفحة وسترى كيف يعرض المتصفح الرسالة الصحيحة. وهكذا نرى كيف نستفيد من الأمر exec في التفاعل مع الحاوية. ستُفقد كل التغييرات التي أجريتها عند حذف الحاوية، ولتحفظ هذه التغييرات لا بد من دفعها باستخدام الأمر commit.

التمرين 12.8: واجهة سطر أوامر Mongo

استخدم script لتسجيل ما تفعله، ثم احفظ الملف بالاسم "script-answers/exercise12_8.txt".

حاول الولوج إلى قاعدة البيانات في التمرين السابق أثناء تشغيل MongoDB باستخدام سطر أوامر CLI. يمكنك تنفيذ الأمر باستخدام exec، ثم أضف بعد ذلك مهمة جديدة عبر سطر الأوامر CLI. شغّل سطر الأوامر وأنت ضمن الحاوية باستخدام الأمر mongo.

يتطلب سطر أوامر mongo رايتي اسم مستخدم وكلمة مرور للاستيثاق على نحوٍ صحيح،. وستعمل الرايات u root -p example- جيدًا، وتكون القيم مأخوذةً من الملف docker-compose.dev.yml.

  • الخطوة الأولى: شغّل MongoDB.
  • الخطوة الثانية: استخدم الأمر exec للدخول إلى الحاوية.
  • الخطوة الثالثة: افتح واجهة سطر أوامر mongo.
  • عندما تصل إلى واجهة سطر أوامر mongo يمكنك أن تطلب منها عرض قواعد البيانات:
> show dbs
admin         0.000GB
config         0.000GB
local         0.000GB
the_database  0.000GB

للولوج إلى قاعدة البيانات الصحيحة:

> use the_database

ولإيجاد المجموعات:

> show collections
todos

يمكنك الآن الوصول إلى البيانات الموجودة ضمن تلك المجموعات:

> db.todos.find({})
{ "_id" : ObjectId("611e54b688ddbb7e84d3c46b"), "text" : "Write code", "done" : true }
{ "_id" : ObjectId("611e54b688ddbb7e84d3c46c"), "text" : "Learn about containers", "done" : false }

أضف مهمةً جديدة نصها: "Increase the number of tools in my toolbelt" مع ضبط حالتها على false. راجع التوثيق لتتعلم إضافة المدخلات إلى قاعدة البيانات، وتأكد من رؤية المهمة الجديدة في تطبيق Express وعند الاستعلام عنه عبر سطر أوامر Mongo CLI.

قاعدة البيانات Redis

Redis هي قاعدة بيانات من الشكل (مفتاح-قيمة) مما يجعلها قاعدة لحفظ البيانات بهيكلية أدنى من MongoDB مثلًا، فلن تجد فيها مجموعات أو جداول بل كميات من البيانات يمكن الحصول عليها وفقًا لقيم المفاتيح المرتبط بالبيانات (القيم).

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

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

التمارين 12.9 - 12.11

التمرين 12.9: إعداد Redis للمشروع

هُيّئ خادم Express مسبقًا للتعامل مع Redis ويفتقد فقط إلى متغير البيئة REDIS_URL، إذ يستخدم التطبيق متغير البيئة للاتصال بقاعدة البيانات. اطلع على عمل Redis مع Docker Hub ثم أضفها إلى الملف "todo-app/todo-backend/docker-compose.dev.yml" من خلال تعريف خدمة جديدة بعد mogo.

services:
  mongo:
    ...
  redis:
    ???

بما أن صفحة Docker Hub لا تضم جميع المعلومات الكافية، استخدم غوغل مثلًا في البحث. ستجد المنفذ الافتراضي للقاعدة بإجراء البحث الموضح في الصورة التالية:

01_redis_port.png

لا نعلم بعد إن كان الإعداد سيعمل ما لم نجرّب. لن يستخدم التطبيق Redis من تلقاء نفسه طبعًا، وهذا ما سنراه في التمرين التالي.

حالما تهيئ Redis وتشغله، أعد تشغيل الواجهة الخلفية ومرر لها متغير البيئة ‍‍REDIS_URL بالصيغة redis://host:port.

$ REDIS_URL=insert-redis-url-here MONGO_URL=mongodb://localhost:3456/the_database npm run dev

يمكنك الآن اختبار الإعداد بإضافة السطر التالي:

const redis = require('../redis')

إلى مثال خادم Express في الملف "routes/index.js". إن لم يحدث شيء فقد طُبِّق الإعداد بصورةٍ صحيحة وإلا سينهار الخادم:

events.js:291
      throw er; // Unhandled 'error' event
      ^

Error: Redis connection to localhost:637 failed - connect ECONNREFUSED 127.0.0.1:6379
    at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1144:16)
Emitted 'error' event on RedisClient instance at:
    at RedisClient.on_error (/Users/mluukkai/opetus/docker-fs/container-app/express-app/node_modules/redis/index.js:342:14)
    at Socket.<anonymous> (/Users/mluukkai/opetus/docker-fs/container-app/express-app/node_modules/redis/index.js:223:14)
    at Socket.emit (events.js:314:20)
    at emitErrorNT (internal/streams/destroy.js:100:8)
    at emitErrorCloseNT (internal/streams/destroy.js:68:3)
    at processTicksAndRejections (internal/process/task_queues.js:80:21) {
  errno: -61,
  code: 'ECONNREFUSED',
  syscall: 'connect',
  address: '127.0.0.1',
  port: 6379
}
[nodemon] app crashed - waiting for file changes before starting...

التمرين 12.10

ثُبتت قاعدة البيانات https://www.npmjs.com/package/redis في المشروع مسبقًا وأضيفت إليه دالتين تعيدان وعودًا وهما getAsync و setAsync:

  • تقبل الدالة setAsync قيمةً ومفتاحًا، ويُستخدم المفتاح لتخزين القيمة.
  • تقبل الدالة getAsync مفتاحًا وتعيد قيمته المقابلة في الوعد promise.

أنجز عدادًا للمهام يخزّن عدد المهام التي تُنشئها في قاعدة البيانات Redis:

  • الخطوة الأولى: زد العداد بمقدار واحد في أي لحظة يُرسل فيها طلب لإضافة مهمة.
  • الخطوة الثانية: أنشئ وصلة GET/statics ساكنة يمكنك طلب معلومات وصفية منها، وينبغي أن تُعاد بصيغة JSON على النحو التالي:
{
  "added_todos": 0
}

التمرين 12.11

استخدم script لتسجيل ما تفعله، ثم تحفظ الملف بالاسم "script-answers/exercise12_11.txt".

إن لم يسلك التطبيق السلوك المطلوب، فقد يساعدك الولوج المباشر إلى قاعدة البيانات في الدلالة على المشاكل. لنلقي نظرةً إذًا على طريقة استخدام واجهة سطر أوامر Redis في الوصول إلى قاعدة البيانات:

  • انتقل إلى حاوية redis باستخدام الأمر docker exec، ثم افتح الواجهة redis-cli.
  • ابحث عن المفتاح الذي استخدمته باستخدام الأمر * KEYS.
  • تحقق من قيمة المفتاح من خلال الأمر GET.
  • راجع أوامر redis-cli للبحث عن الأمر المناسب لضبط قيمة العداد على 9001.
  • تأكد من أن القيمة الجديدة ستعمل عند تحديث الصفحة http://localhost:3000/statistics.
  • احذف المفتاح باستخدام واجهة سطر الأوامر وتأكد أن العداد سيعمل عند إضافة مهام جديدة.

الذاكرة المقيمة وقاعدة البيانات Redis

أشرنا سابقًا أنّ قاعدة البيانات Redis لا تحتفظ بالبيانات افتراضيًا، لكنه أمر يمكن حله بسهولة، إذ كل ما علينا فعله هو تشغيل Redis بأمر مختلف كما توضح صفحة Docker hub:

services:
  redis:
    # أي شيء آخر
    command: ['redis-server', '--appendonly', 'yes'] # CMD تعديل 
    volumes: # التصريح عن القرص
      - ./redis_data:/data

ستقيم البيانات الآن في المجلد على الجهاز المُضيف، وتذكّر إضافة المجلد إلى الملف gitignore..

إمكانات أخرى لقاعدة البيانات Redis

تقدم Redis عدّة ميزات إضافةً إلى عمليات إضافة وضبط وحذف المفاتيح، فهي تحدد مثلًا فترة صلاحية المفتاح وهذه ميزة شديدة الأهمية عند استعمالها مثل مخزن مؤقت.

كما يمكن استخدام Redis في إنجاز نماذج نشر-اشتراك publish-subscribe -أواختصارًا PubSub-، وهي آلية تواصل غير متزامنة للتطبيقات الموزّعة. تعمل Redis في هذه الحالة مثل وسيط بين تطبيقين أو أكثر، ينشر بعضها رسائل بإرسالها إلى Redis وعند وصول هذه الرسائل تُبلغ Redis جميع الأطراف بأنها اشتركت بهذه الرسائل.

التمرين 12.12 البيانات المقيمة و Redis

تأكد أن البيانات لا تُخزّن افتراضيًا في قاعدة البيانات Redis بأن تكون قيمة العداد 0 بعد تنفيذ الأمرين docker-compose -f docker-compose.dev.yml down و docker-compose -f docker-compose.dev.yml up.

أنشئ بعد ذلك قرصًا للبيانات عن طريق تعديل الملف "todo-app/todo-backend/docker-compose.dev.yml"، وتأكد من بقاء البيانات بعد تنفيذ الأمرين docker-compose -f docker-compose.dev.yml down وdocker-compose -f docker-compose.dev.yml up.

ترجمة -وبتصرف- للفصل Building and Configuring Environments من سلسلة Deep Dive Into Modern Web Development

اقرأ أيضًا


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

أفضل التعليقات

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



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...