کانتینرهای لینوکسی و داکر

در حال نوشتن مقاله‌ای در مورد «کوبرنیتس» بودم که متوجه شدم بدون شرح کانتینرها نمی‌توانم آن مقاله را ادامه بدهم. در این مقاله یاد می‌گیریم کانتینر چیست و چرا برایمان مهم است.

بگذارید فورا کارمان را با یک تعریف مختصر شروع کنیم:

کانتینر یک ماشین مجازی بسیار سبک با قابلیت دسترسی مستقیم اما کنترل شده به منابع سیستم است.

این تعریف چند کلمه‌ی کلیدی دارد که به آنها خواهیم پرداخت. اول اینکه صحبت از یک «ماشین مجازی» است. دیگر اینکه «سبک» است. یعنی ایجاد و حذف آن سریع و کم هزینه است و می‌تواند «مستقیما» به سخت‌افزار و سایر منابع سیستم دسترسی «کنترل شده» داشته باشد.

بیایید اول نگاهی گذار داشته باشیم به انواع ماشین مجازی و بعد ببینیم کانتینر چه فرقی با اینها دارد. اگر با این بخش آشنا هستید مسقیم بپرید به بخش بعدی.

ماشین‌های مجازی و امنیت آنها

از چند دهه پیش همواره این نیاز وجود داشته تا برنامه‌هایی که برای یک سیستم عامل نوشته شده‌اند روی یک سیستم عامل دیگر اجرا کرد. این یکی از رایج‌ترین دلایل استفاده از برنامه‌های شبیه‌ساز بوده است. برنامه‌هایی مثل Virtual Box و Qemu می‌توانند سخت‌افزارهای مختلف را شبیه‌سازی کنند. مثلا می‌توان روی یک کامپیوتر با سی‌پی‌یو Intel یا AMD با معماری x86 یک ماشین مجازی با سی‌پی‌یو با معماری arm و سخت‌افزار دلخواه شبیه‌سازی کرد. سیستم‌عامل و برنامه‌هایی که درون این ماشین مجازی اجرا می‌شوند باید آن معماری را پشتیبانی کنند. برنامه‌های داخل این ماشین بسیار کند اجرا خواهند شد چرا که شبیه‌ساز باید همواره میلیاردها دستور سطح پایین زبان ماشین را بین این دو معماری ترجمه کند.

برنامه‌های شبیه‌ساز می‌توانند ماشین مجازی با معماری دستگاه مادر (دستگاهی که شبیه‌سازی روی آن انجام می‌شود) نیز بسازند. در این صورت سرعت به شکل قابل توجهی افزایش پیدا می‌کند. البته در اکثر سی‌پی‌یوهای امروزی اکستنش‌هایی برای پشتیبانی از این نوع شبیه‌سازی وجود دارد که باید مستقیما در BIOS فعال شود. در این صورت یک شبیه‌ساز می‌تواند با سرعتی تقریبا برابر با سرعت ماشین مادر سیستم‌عامل خودش را اجرا کند. در این حالت می‌توان سیستم‌های عامل مختلف با معماری یکسان (مثلا x86) را روی این ماشین مجازی نصب کرد. افراد معمولا برای ایزوله کردن یک برنامه این کار را انجام می‌دهند. یا شاید برنامه‌ای دارند که فقط روی یک سیستم عامل قدیمی اجرا می‌شود و نیاز به شبیه‌ساز دارند.

فارغ از نوع شبیه‌سازی مسئله‌ی مهمتر ایزوله کردن یک برنامه است. تصور کنید یک دانشجو می‌خواهد یک ویروس یا کرم اینترنتی را آزمایش بکند. ماشین مجازی راه خوبی برای این قبیل کارهاست. البته باید دید که آیا یک برنامه می‌تواند از شبیه‌ساز فرار بکند یا نه؟

سرویس‌دهنده‌های اینترنتی ماقبل بوجود آمدن سرویس‌های «ابری» از ماشین‌های مجازی برای سرویس‌دهی به کاربران و برنامه‌نویس‌ها استفاده می‌کردند. برای داشتن یک وبسایت یا راه‌اندازی یک سرویس اینترنتی معمولا افراد یک ماشین مجازی سفارش می‌دادند و برنامه‌های دلخواهشان را روی آن نصب می‌کردند. این روش ارزانتر از سفارش یک سرور انحصاری بود. در این روش ماشین‌های مجازی مختلف یک سرور مادر را با هم شریک بودند. هنوز هم این روش رایج است. در این روش اگر یک برنامه از زندانی که برایش ساخته شده فرار بکند به ماشین مجازی دسترسی کامل خواهد داشت ولی فرار از ماشین مجازی و رسیدن به سرور اصلی چالش سختی خواهد بود.

برخی از سرویس‌دهنده‌ها هم یک سرور واقعی یا یک ماشین مجازی را مستقیما به کاربران مختلف می‌فروختند. در این روش همه کاربران منابع این ماشین مجازی یا واقعی را شریک بودند. این روش هنوز هم در سرویس‌دهنده‌های فوق ارزان رایج است. در این روش اگر یک برنامه‌ بتواند از زندانی که برایش ساخته‌شده فرار بکند به راحتی به همه سیستم عامل و نیز اطلاعات سایر کاربران دسترسی خواهد داشت.

سرویس‌دهنده‌های ابری و نیاز به یک راه‌حل جدید

یک ماشین مجازی یک سیستم عامل کامل را شبیه‌سازی می‌کند. یک کپی کامل از همه اجزای سیستم. ساختن و حذف کردن آن نیز زمانبر است. برای یک کاربر دسکتاپ سودمند است ولی برای یک سرویس‌دهنده‌های ابری که به شکل اتوماتیک هزاران و میلیونها ماشین مجازی می‌سازد و پس از پایان کار نابود می‌کند اصلا قابل قبول نیست. از سوی دیگر کنترل آن سخت است. یک کاربر می‌خواهد یک برنامه با صدها پراسس را در یک ماشین مجازی راه بیاندازد ولی یک سرویس‌دهنده‌ی ابری یا یک برنامه‌نویس تنها می‌خواهد برنامه‌اش را در اینترنت (در «ابر») روشن کند و کاری انجام بدهد. شاید فقط چند میلی‌ثانیه تا چند ثانیه زمان احتیاج دارد و اصلا داده‌ای برای ذخیره‌سازی ندارد. اینجاست که ماشین‌مجازی دیگر قابل استفاده نیست چرا که برای اجرای یک پراسس سبک و سریع خیلی خیلی بزرگ و حجیم و پرهزینه است.

کانتینرها: یک سیستم عامل در یک پراسس

کانتینرها برای حل مشکلات سرویس‌دهنده‌های ابری بوجود آمدند. سرویس‌دهنده‌های ابری نیاز دارند تا پروسس‌های کوچک (یک تکه کد) را به سرعت و به شکلی امن و ایزوله شده بر روی سیستم عامل اجرا کنند. از سوی دیگر این کار باید بدون تغییر یک برنامه انجام بشود. یعنی برنامه‌ای که در یک کانتینر اجرا می‌شود یک سیستم‌عامل عادی می‌بیند و با آن صحبت می‌کند. منابع سیستم از قبیل سی‌پی‌یو و حافظه و دیسک و شبکه و مانند اینها در اختیارش هستند و کاملا عادی اجرا می‌شود. ولی یک کانتینر بی‌خبر است از اینکه در جهانی به سر می‌برد که کرنل برایش خلق کرده است و در آن زندانی است و فقط منابعی را می‌بیند و به قدری می‌بیند که برایش مقدر شده است.

یک کانتینر بی‌خبر است از اینکه در جهانی به سر می‌برد که کرنل برایش خلق کرده است و در آن زندانی است و فقط منابعی را می‌بیند و به قدری می‌بیند که برایش مقدر شده است.

ایستاده بر شانه‌های کرنل لینوکس

از جایی که کرنل لینوکس روی تقریبا همه‌ی سرورهای دنیا در حال اجراست، تکنولوژی‌های زیربنایی کانتینرها نیز ریشه در کرنل لینوکس دارند و اینجا هم در مورد کانتینرهای لینوکسی می‌نویسم چرا که تنها از این کرنل استفاده کرده‌ام. البته سیستم‌های عامل دیگر هم از سالها پیش (چه بسا پیش از لینوکس) قابلیت‌های مشابه داشته‌اند که می‌توانید برای اطلاعات بیشتر ‎به اینترنت مراجعه کنید.

لینوکس اجازه می‌دهد که چندین user-space ایزوله شده همزمان روی یک سیستم اجرا بشوند. هر دستوری که توسط لینوکس اجرا می‌شود یا در user space اتفاق می‌افتد یا در kernel space. مثلا باز کردن یک فایل در حریم کرنل اتفاق می‌افتد ولی کدهایی که با منابع سیستم کاری ندارند نه. لینوکس ویژگی‌هایی دارد که خلق کانتینرها را امکانپذیر کرده است از جمله cgroups و namespaces.

در لینوکس namespaceها روشی برای مدیریت منابع در پراسس‌ها هستند. یک پراسس بسته به namespaceای که در آن قرار گرفته منابعی را می‌بیند و منابعی را نمی‌بیند. با دستور lsns می‌توانید لیست namespaceهای موجود را ببینید. جلوتر مثالی با داکر خواهیم دید. cgroups یا control groups هم حد و حدود منابعی که پراسس‌ها استفاده می‌کنند را تعیین می‌کند. مثلا می‌توان یک پراسس (و همه‌ی پراسس‌هایی که ایجاد خواهد کرد) را فقط به ۵۰ درصد سی‌پی‌یو سیستم و ۱۰ درصد حافظه محدود کرد. برای اطلاعات بیشتر man namespaces و man cgroups را ببینید.

تکنولوژی‌های متفاوت کانتینرها در لینوکس

بر اساس فیچرهای لینوکس تکنولوژی‌های مختلفی برای ساخت و مدیریت کانتینرها بوجود آمده‌اند. اینجا من فقط به Docker می‌پردازم. برای مطالعه بیشتر LXC و Snaps را ببینید. بویژه Snap برای برنامه‌های دسکتاپ عالی است و روی اوبونتو به صورت پیش‌فرض نصب است (در مقاله‌ی بعدی کوبرنیتس را با Snap نصب می‌کنیم).

داکر

احتمالا اسم «داکر» به گوشتان خورده است. ابزاری که کانتینرها را مشهور کرد. همانطور که پیشتر رفت داکر از امکانات کرنل استفاده می‌کند تا کانتینرهای سبک‌وزن و کنترل شده بسازد و مدیریت کند. داکر یک کلاینت و یک سرویس دارد که باید روی سیستم در حال اجرا باشد. به جز اینها داکر یک هاب اینترنتی دارد، جایی که imageهای مختلفی ساخته شده و ثبت و نگهداری می‌شوند. به این وبسایت container registry گفته می‌شود. روش کار با داکر اینست که کاربر یک ایمیج را انتخاب می‌کند و داکر این ایمیج را دانلود می‌کند و یک کانتینر از روی این ایمیج می‌سازد. من داکر را روی آرچ نصب کرده‌ام:

➜  ~ systemctl status docker.service
● docker.service - Docker Application Container Engine
   Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; vendor pres>
   Active: active (running) since Wed 2019-10-23 19:56:18 CEST; 2 days ago
     Docs: https://docs.docker.com
 Main PID: 2852 (dockerd)
    Tasks: 56 (limit: 38377)
   Memory: 228.4M
   CGroup: /system.slice/docker.service
           ├─2852 /usr/bin/dockerd -H fd://
           └─2878 containerd --config /var/run/docker/containerd/containerd.tom>

داکر را فقط با کاربر روت یا کاربری که عضو گروه داکر باشد می‌توان کنترل کرد. می‌توانید کاربر خودتان را این گروه اضافه کنید:

➜  ~ sudo usermod -aG docker mehdi

یک لاگ‌اوت و لاگ‌این لازم است تا این تغییر رسمیت پیدا کند.

حالا من روی آرچ یک کانتینر اوبونتو ۱۸.۰۴ استارت می‌کنم:

➜  ~ docker run -it ubuntu
root@8fbd551af6b1:/# 

یا یک نسخه از CentOS:

➜  ~ docker run -it centos     
Unable to find image 'centos:latest' locally
latest: Pulling from library/centos
729ec3a6ada3: Pull complete 
Digest: sha256:f94c1d992c193b3dc09e297ffd54d8a4f1dc946c37cbeceb26d35ce1647f88d9
Status: Downloaded newer image for centos:latest
[root@4f610aba524e /]# 

ایمیج اوبونتو قبلا دانلود شده بود پس فورا استارت شد. ایمیج سنت‌اواس باید دانلود می‌شد. توجه کنید که ایمیج‌ها در داکر لایه‌لایه هستند و داکر فقط لایه‌های جدید را دانلود می‌کند. این مخصوصا برای برنامه‌نویس‌هایی که برنامه‌هایشان را می‌خواهند در کانتینرها بسته‌بندی کنند مهم است.

اگر در یک ترمینال دیگر دستور docker ps را اجرا کنیم لیست کانتینرهای در حال اجرا را خواهیم دید.

➜  ~ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
4f610aba524e        centos              "/bin/bash"         2 minutes ago       Up 2 minutes                            vigorous_diffie
8fbd551af6b1        ubuntu              "bash"              4 minutes ago       Up 4 minutes                            priceless_chatterjee                          0,04s
➜  ~ 

دقت کنید که کانتینر یک محیط runtime است. یعنی وقتی معنی دارد که در حال اجراست. در ضمن می‌تواند تغییر کند یعنی می‌توانیم فایل‌سیستم درون آن را در حین اجرا دستکاری کنیم ولی ایمیج را نمی‌توانیم تغییر بدهیم. اگر کانتینر را حذف کنیم و دوباره استارت کنیم تغییرات ما از بین می‌روند. کانتینر هیچ گونه دسترسی به فایل سیستم دستگاه مادر ندارد و هر آنچه می‌بیند مجازی است. در صورت نیاز می‌تواند یک پوشه را داخل کانتینر ماونت کرد یا از volumeهای داکر استفاده کرد که به آنها نمی‌پردازم.

مثال بعدی nextcloud است. در حالت سنتی باید تمام اجزای nextcloud را نصب می‌کردیم یا یک ماشین مجازی کند و تنبل راه می‌انداختیم تا آن را تست کنیم، حالا فقط با یک فرمان اینکار را انجام می‌دهیم:

➜  ~ docker run -it -p8008:80 nextcloud
Initializing nextcloud 17.0.0.9 ...
Initializing finished
New nextcloud instance
AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 172.17.0.4. Set the 'ServerName' directive globally to suppress this message
AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 172.17.0.4. Set the 'ServerName' directive globally to suppress this message
[Sat Oct 26 09:03:37.159898 2019] [mpm_prefork:notice] [pid 1] AH00163: Apache/2.4.38 (Debian) PHP/7.3.11 configured -- resuming normal operations
[Sat Oct 26 09:03:37.159935 2019] [core:notice] [pid 1] AH00094: Command line: 'apache2 -D FOREGROUND'

پارامتر -it یک محیط اینتراکتیور به ما می‌دهد (مثلا اگر بخواهیم bash را اجرا کنیم) و -p8008:80 پورت مجازی ۸۰ داخل کانتینر را به پورت ۸۰۰۸ هاست وصل می‌کند. 127.0.0.1:8008 را باز کنید و nextcloud را امتحان کنید. برای توقف کافی است Ctrl+C را بگیرید. اگر دوباره دستور docker ps را اجرا کنید خواهید دید که اینبار یک پورت زیر ستون PORTS لیست شده است:

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                          NAMES
2a6f37acf559        nextcloud           "/entrypoint.sh apac…"   4 seconds ago       Up 3 seconds        80/tcp, 0.0.0.0:80->8080/tcp   hungry_kilby
4f610aba524e        centos              "/bin/bash"              11 minutes ago      Up 11 minutes                                      vigorous_diffie
8fbd551af6b1        ubuntu              "bash"                   13 minutes ago      Up 13 minutes                                      priceless_chatterjee

بسته‌بندی یک برنامه داخل یک کانتینر

اگر لینوکس‌کار قدیمی باشید حتما با دردسرهای نصب یک برنامه آشنا هستید. آپگرید برنامه‌ها کار سختی بود. کانفلیکت وابستگی‌های برنامه‌های مختلف چیز رایجی بود. امکان پیش‌بینی وضعیت یک سیستم تا حدودی غیرممکن بود چرا که سیستم عامل همواره در تغییر بود و برنامه ممکن بود با تغییرات جدید سازگار نباشد و دیگر کار نکند یا از آن بدتر کاربکند ولی نتایج پیش‌بینی نشده‌ای تولید بکند. حفظ امنیت این برنامه‌ها هم مشکل بود. حالا ما همه اینها را در یک کانتینر بسته‌بندی و آپلود می‌کنیم و هرجا بخواهیم با یک خط کامند پیاده و اجرا می‌کنیم. بدون ذره‌ای کانفلیکت و نگرانی. روش کار اینست که برنامه‌ساز یک فایل به نام Dockerfile داخل پروژه اضافه می‌کند و ایمیج پایه (مثلا اوبونتو یا آلپاین) را مشخص کرده و گام به گام پکیج‌ها و فایل‌ها و متغیرهای محیطی و مانند اینها را که باید در این ایمیج وجود داشته باشند مشخص می‌کند. همچنین پورت‌هایی که کانتینرهای ساخته شده از این ایمیج به جهان خارج از آن عرضه می‌کنند نیز مشخص می‌شود. در انتها هم برنامه‌ای که باید موقع استارت این کانتینر اجرا شود مشخص می‌شود. با کمک می‌تواند به صورت لوکال با کامندلاین داکر یا روی DockerHub این ایمیج را ساخت. دقت کنید که آنچه در این فایل نوشته می‌شود فقط دستوراتی برای ساختن ایمیج است. کانتینر چیزی است ناپایدار که در نهایت از روی این ایمیج ساخته شده و اجرا می‌شود. جهان برنامه‌های داخل کانتینر منحصر به فایل‌ها و تنظیماتی خواهد بود که ما برای ایمیج تعریف می‌کنیم. به عنوان مثال داکرفایل زیر را که برای یک پکیج پایتون نوشته‌ام ببینید:

FROM frolvlad/alpine-python3
RUN apk update
RUN apk add expat-dev python3-dev boost-dev zlib-dev bzip2-dev g++ boost-python3
RUN pip install -Iv osmium==2.14.3

WORKDIR /build
COPY o2g /build/o2g/
COPY pyproject.toml README.md /build/
ENV FLIT_ROOT_INSTALL 1
RUN pip install --user flit && ~/.local/bin/flit install --deps none
RUN find /usr/lib/ -name libboost_* -not -name libboost_python* -delete
RUN find /usr/lib/python3.7 -type f -name "*.pyc" -delete
RUN find /usr/lib/ -name "__pycache__" -type d -delete

FROM frolvlad/alpine-python3
COPY --from=0 /usr/lib/libboost_python3*.so* \
              /usr/lib/libstdc++.so* \
              /usr/lib/libgcc_s.so* \
              /usr/lib/
COPY --from=0 /usr/lib/python3.7/site-packages/ /usr/lib/python3.7/site-packages/
COPY --from=0 /usr/bin/o2g /usr/bin/o2g

RUN pip --no-cache-dir install --no-compile bottle

ENV LC_ALL=C.UTF-8
WORKDIR /app
COPY web/app.py web/index.html /app/
CMD ["python", "app.py"]
EXPOSE 3000

این مثالی نسبتا پیشرفته است. این ایمیج یک ساخت دومرحله‌ای را دنبال می‌کند (برای کاستن از حجم ایمیج نهایی). در مرحله اول ابزارآلات لازم برای بیلد نصب می‌شوند ولی فقط خروجی به مرحله دوم کپی می‌شود و باقی حذف می‌شود. ایمیج بیس آلپاین است (یک ایمیج بسیار کم حجم و محبوب میان برنامه‌سازان). تعدادی برنامه نصب می‌شود. وب سرور ساده‌ای هم نصب می‌شود و متغیرهای محیطی تنظیم می‌شوند و در نهایت برنامه‌ای که باید موقع استارت اجرا شود هم تنظیم می‌شود و پورت خروج هم همینطور. این ایمیج روی داکرهاب آپلود شده است و برای تست این برنامه به چیز جز داکر نیاز ندارید:

➜  ~ docker run -it -p3000:3000 hiposfer/o2g
Bottle v0.12.17 server starting up (using WSGIRefServer())...
Listening on http://0.0.0.0:3000/
Hit Ctrl-C to quit.

172.17.0.1 - - [26/Oct/2019 09:28:41] "GET / HTTP/1.1" 200 1516

gtfs

برای نوشتن یک داکرفایل رفرنس داکرفایل را ببینید. برای دیدن پارامترهای داکر طبق معمول docker --help و man docker. اگر کنجکاو هستید حتما docker-compose را هم ببینید. چرا که داکر می‌تواند چند کانتینر را همزمان اجرا کند و آنها را روی پورت‌های مختلف به یکدیگر وصل کند طوری که کانتینرها باز هم بی‌خبرند از اینکه از چند بخش تشکیل شده‌اند. یک کاربر خوب این روش جداکردن دیتابیس و وب‌سرور و سایر اجزای سیستم از برنامه‌ی اصلی است.

خلاصه

مروری داشتیم روی ماشین‌های مجازی و کاربردهاش و بعد شرح دادیم چرا آنها جوابگوی نیاز سیستم‌های ابری نبودند. سپس گذری داشتیم به لینوکس کرنل و نگاهی انداختیم به قابلیت‌های مهم کرنل که کانتینرها روی آنها سوار شده‌اند. در انتها هم به داکر پرداختیم و دیدیم چطور می‌توان کانتینرها را اجرا کرد و یک نمونه هم از یک داکرفایل دیدیم که یک برنامه به کمک آن در یک کانتینر بسته‌بندی می‌شود.