تو این نوشته قراره به سوال جواب بدیم که داکر چطوری ایمیجها رو میسازه و مدیریت میکنه و همینطور با مفاهمین Dockerfile و Docker Layers بیشتر آشنا میشیم.
قبل از اینکه بریم سراغ نحوه ایجاد ایمیجها توسط داکر، بیاییم یکم راجع به Dockerfile صحبت کنیم.
داکرفایل(Dockerfile) در واقع اسکریپتی هست که شامل همهی دستورات و اطلاعات مورد نیاز داکر برای ایجاد image رو تعریف میکنه.
اسکریپت Dockerfile معمولا با دستورات مهمتر مثل FROM (که مشخص میکنه base ایمیج ما چی هست، مثلا سیستمعاملی که میخواییم برای ایمیج داشته باشیم) شروع میشه و با با دستور RUN (که میتونیم باهاش هر دستوری رو اجرا کنیم)، CMD (که میتونیم باهاش دستور پیشفرض اجرایی کانتینر رو مشخص کنیم) و دستورات دیگه مثل LABEL، EXPOSE، ENV،ADD تکمیل و ایجاد میشه.
نکته: تو این نوشته دستورات داکرفایل مورد بحث نیس و تو نوشتهای دیگه راجع بهشون صحبت میکنیم و اینکه نیاز نیست این دستورات رو بلد باشید تا این نوشته رو متوجه بشید.
خب با این تعریف بیاییم یه داکرفایل ساده با محتوای زیر ایجاد کنیم و راجع به این صحبت کنیم که داکر چطوری از روی این داکرفایل ایمیج رو ایجاد میکنه:
FROM ubuntu:20.04 LABEL maintainer="Mehdi Zarepour" RUN apt-get update && apt-get install -y curl WORKDIR /app COPY . /app CMD ["echo", "Hello Docker"]
بیاییم این دستورات رو یکی یکی بررسی کنیم و ببینیم داکر با استفاده از این دستورات چطوری ایمیج رو ایجاد می کنه:
۱. دستور FROM ubuntu:20.04: دستور FROM به داکر میگه که قراره Base ایمیجمون چی باشه، که اینجا گفتیم میخواییم ubuntu باشه (معمولا اولین دستور داکرفایل FROM هست). پس تو این مرحله ایمیجمون به شکل زیر میشه، یعنی سیستم عامل به ایمیج اضافه میشه:
۲.دستور "LABEL maintainer="Mehdi Zarepour: دستور LABEL یه متادیتا به اسم maintainer و مقدار Mehdi Zarepour به ایمیج اضافه میکنه (LABEL ساختار key-value داره، پس مقدار key و value می تونه هرچیزی که دوست دارید باشه) این دستور کار خاصی نمی کنه، صرفا داره مشخص می کنه ایجاد کننده و نگهدارنده این ایمیج کیه. تو این مرحله ایمیجمون به شکل زیر میشه، یعنی نگهدارنده یه جایی تو فایل ایمیج ذخیره میشه:
۳. دستور RUN apt-get update && apt-get install -y curl: دستور RUN، هر دستوری که جلوش بیاد رو اجرا میکنه که تو مثال ما داره curl رو روی Ubuntu نصب میکنه، پس curl به Ubuntuای که توی فایل ایمیج داشتیم اضافه میشه:
۴. دستور WORKDIR /app: دستور WORKDIR دایرکتوری جاری رو تغییر میده به آدرسی که جلوش مشخص کردیم یعنی app/ (می تونیم بگیم این دستور با cd /app یکی هستن، با این تفاوت که وقتی از WORKDIR استفاده می کنیم، اگه دایرکتوری وجود نداشته باشه ایجادش میکنه ولی cd خطا میده اگه app/ وجود نداشته باشه). پس تو این مرحله دایرکتوری app/ به ایمیج اضافه میشه:
۵. دستور COPY . /app: دستور COPY فایل های برنامه یا نرمافزار ما رو از دایرکتوری جاری سیستم لوکالمون به ایمیج و دایرکتوری app/ اضافه میکنه.
۶. دستور CMD ["echo", "Hello Docker"]: دستور CMD مشخص میکنه دستور پیشفرض اجرایی برای کانتینری که از روی این ایمیج ساخته میشه چی هست.
خب وقتی همهی دستورات توی داکرفایل توسط داکر خونده شد، این میشه نسخه نهایی ایمیجی که از روش میسازه. ساده بود نه؟ :)
باید بگم که این تعریف اشتباهست یا حداقل کامل نیست! داکر برای ایجاد ایمیج تنها یک فایل مثلا با فرمت ISO نمیسازه که شامل دپندنسیهایی که تو Dockerfile مشخص کردیم باشه، بلکه به ازای هر کدام از دستوراتی که ما توی Dockerfile تعریف میکنیم اصطلاحا یک «لایه Docker Layer» ایجاد می کنه. خب پس بریم ببینم این لایه ها چی هستن و چطوری داکر باهاشون ایمیج ها رو ایجاد می کنه.
ایمیجهای داکر از چندیدن لایه تشکیل میشن که این لایه ها مجموعه ای از فایلها و یا دایرکتوریهای read-only هستن که بصورت stack روی هم قرار می گیرن. هر لایه از روی دستورات نوشته شده تو Dockerfile ایجاد میشه، یعنی از روی هر کدوم از این دستورات مثل FROM، RUN، COPY و غیره یک لایه ایجاد میشه که هر کدوم از این لایه ها یه تغییری روی image میزارن و درنهایت image نهایی ساخته میشه، هر کدوم از این لایه ها به صورت کلی دو نوع تغییر روی image میزارن:
برای درک بهتر به تصویر زیر نگاه کنید:
بیاییم یکم با جزییات بیشتر به هر کدوم لاز این لایه ها نگاه کنیم و ببینیم هر کدوم از این لایه ها چیکار می کنن:
پس مجموع این لایه ها روی هم در نهایت image رو بوجود میارن، حالا سوال اینجاس، داکر چطوری این لایه ها رو ذخیره میکنه؟ و چطوری یکپارچه شون میکنه و از روشون یک image ایجاد میکنه؟ اول بریم سراغ نحوه ذخیره سازی سازی این لایهها.
گفتیم هر کدوم از لایههای داکر یه فایل یا دایرکتوری read-only هستن، یعنی بعد از ایجاد شدن دیگه قابل تغییر نیستن. داکر این فایل ها و دایرکتوری ها رو توی دایرکتوری مشخصی ذخیره و مدیریت میکنه، بزاریم الکی پیچیدهش نکنیم و فرض کنیم داکر این لایهها رو در دایرکتوری /var/lib/docker/ ذخیره میکنه.
/var/lib/docker/ │ ├── <layer1>/ (Ubuntu 20.04 files) │ ├ └── /usr/ │ ├ └── /etc/ │ ├ └── /lib/ │ ├── <layer2>/ (Maintainer label) │ ├── <layer3>/ (curl installation) │ ├ └── /usr/bin/curl │ ├── <layer4>/(/app directory) │ ├ └── /app │ ├── <layer5>/ (Copied files) │ ├ └── /app/index.js │ └── <layer6>/(CMD instruction)
نکته: نموداری که بالا کشیدم صرفا برای شبیهسازی هست و در واقع ذخیرهسازی این فایلها و دایرکتوریها به شکل پیچیدهتری پیاده و مدیریت میشه و واقعا نیازی به رفتن به جزییات بیشتر نیست.
همونطور که شکل بالا نشون میده، داکر میاد تغییرات لایهها رو توی دایرکتوریهای read-only ذخیره میکنه، مثلا برای لایه اول که شامل Ubuntu میشه، همه فایل ها و دایرکتوری های Ubuntu رو توی یه دایرکتوری مثلا به اسم layer1 ذخیره میکنه و همین کار رو برای لایههای دیگه هم میکنه، مثلا فایلها و دایرکتوریهای cURL رو هم توی یه دایرکتوری به اسم layer3 ذخیره میکنه.
پس هر لایه یک دایرکتوری read-only هست که توسط داکر ایجاد و نگهداری میشه که شامل تغییراتی هست که هر لایه قراره روی image ایجاد میکنه.
خب حالا بریم سراغ سوال دوم که داکر چطوری از روی این لایهها (دایرکتوریهای read-only) image رو ایجاد میکنه.
خب با وجود این لایهها بنظر داکر کار سختی نداره! فقط کافیه که این فایل ها و دایرکتوریها رو بیاره کنار هم و تو یک سیستم فایل مشترک قرار بده، یعنی:
/my-image │ └── /usr/ │ ├ └── /usr/bin/curl │ └── /etc/ │ └── /lib/ │ └── /app │ ├ └── /app/index.js
اگه توجه کنید، همهی دایرکتوریها با هم merge شدن و یک سیستمفایل واحد متشکل از همهی نیازمندیهایی که تو Dockerfile ایجاد کردیم هست، یعنی ما Ubuntu رو با همهی فایلها و تنظیماتش داریم، curl هم به دایرکتوری bin اضافه شده و همینطور کد برنامهمون تو دایرکتوری app قرار داده شده، و تمام! اینطوری برنامه ما کنار همهی دپندنسیهاش در یک پکیج به اسم Docker Image بستهبندی شده.
از اونجایی که ما خیلی کنجکاویم، این سوال پیش میاد که داکر چطوری این دایرکتوریها رو با هم merge میکنه و یک سیستمفایل واحد برای ایمیج ایجاد میکنه؟ ?
داکر برای ایجاد این فایلسیستم واحد از UnionFS استفاده میکنه، UnionFS نوعی از فایل سیستم هست که این اجازه رو به ما میده که چند سیستم فایل رو با هم merge کنیم و یک سیستمفایل واحد ایجاد کنیم. به مثال زیر نگاه کنید تا ببینیم UnionFS چطوری این کارو میکنه:
فرض کنیم ما سه تا دایرکتوری با نامهای A, B و C داریم که محتوایت توشون به شکل زیره:
A/ └── file1 B/ └── file2 C/ └── file3
با استفاده از UnionFS ما میتونیم این دایرکتوریها رو با هم مرج کنیم و یک دایرکتوری واحد به شکل زیر ایجاد کنیم:
Union(A, B, C)/ ├── file1 ├── file2 └── file3
تو این حالت file1 از دایرکتوری A اومده، file2 از دایرکتوری B اومده و file3 از دایرکتوری C اومده و در نهایت روی هم یک دایرکتوری واحد ایجاد کردن. تو دنیای داکر هر کدوم از این دایرکتوری ها (A, B, C) همون لایههایی هستن که داکر از روی Dockerfile ایجاد میکنه و در نهایت از UnionFS استفاده میکنه و یک فایل سیسم واحد در اختیار Image قرار میده.
نکته: اگه فایل مشترکی توی هر کدوم از این دایرکتوریها (یا همون لایهها) وجود داشته باشه توسط UnionFS مرج میشه و محتویات آخرین دایرکتوری نگهداری میشه. مثلا اگه یه دایرکتوری D هم داشته باشیم که file1 توش باشه، UnionFS محتوایات فایلی که در آخرین دایرکتوری یعنی D قرار گرفته شده رو جایگزین فایلی می کنه که در دایرکتوری A بود. پس حواستون باشه که این اتفاق برای محتوایات لایههایی که داکر ایجاد میکنه هم میافته و لایههای بالاتر محتویات لایههای پایین تر رو override میکنن.
کنجکاوی: داکر معمولا از overlay2 به عنوان UnionFS برای مرج کردن لایهها یا دایرکتوری ها استفاده میکنه.
پس مراحل ایجاد Docker Image به شکل زیر میشه:
گفتیم این لایهها که Image از روشون ساخته میشه read-only هستن، احتمالا این اتفاق افتاده براتون که از روی یه Image یک Container ایجادکنید و مقدار یه فایلی رو تغییر بدید و وقتی دوباره یه Container جدید از رو Image بسازید میبینید که تغییرات reset شدن و دیگه نیستن! این قضیه ثابت میکنه که ایمیج read-only هست و ما نمیتونیم تغییری توش ایجاد کنیم، چرا؟ چون لایههای که ایمیج از روشون ساخته میشه read-only هستن.
خب این ته ماجرا نیس! چون ما نیاز داریم تو یه Container تغییری توی Image ایجاد کنیم، مثلا یه فایل جدید بسازیم و یا محتویات یه فایلی رو تغییر بدیم، راه حلی که داکر برای این مسئله داره اینه که زمانی که میخواییم یه کانتینر رو run کنیم، یه لایه جدید به اسم لایه container روی همهی لایههایی که برای ایمیج داریم اضافه میکنه که فرقش اینه که توی این لایه ما دسترسی write هم داریم پس میتونیم توی این لایه تغییراتمون رو اعمال کنیم.
اگه یادتون باشه گفتیم توی UnionFS لایههای بالایی میتونن لایههای پایینی رو override کنن، پس عملا ما بدون اینکه واقعا تغییری روی فایلسیسم Imageها ایجاد کنیم، با استفاده از لایه کانتینر overrideشون میکنیم و تغییراتی که نیاز داریم رو اعمال میکنیم. به شکل زیر نگاه کنید:
Container Filesystem ├── Read-write layer (specific to this container) │ ├── <layerN> │ ├── ... │ ├── <layer2> │ ├── <layer2> │ └── <layer1> └── Read-only layers (from the image)
دوست دارم خیلی بیشتر راجع به Layering تو Docker و مزایاش بگم، مثلا چطوری باعث میشه تو مصرف هارد صرفهجویی بشه یا چطوری داکر از لایهها برای cache کردن استفاده میکنه. ولی بنظر میاد نوشته طولانی بشه پس تا همینجا تمومش میکنم و اگه عمری بود تو نوشتههای بعدی راجع بهشون حرف میزنیم.
حتما کامنت بزارین و نظرتون رو بگید، خیلی بهم کمک میکنه، بگید مثلا این کنجکاویها رو کجاها بیشتر بریم توش.
در پایان اینکه اگه نوشته رو دوست داشتید با دوستان به اشتراک بزارید که هم بهشون کمک کنه و هم انرژی بشه برای من که ادامه بدم :)
و اینکه میتونید منو با آیدی mehdi_zarepour تو توییتر پیدا کنید، نوشتههای جدید رو اونجا توییت میکنم. فعلا نوشتههام راجع به داکر خواهد بود.