Skip to content

فصل ۲ — ایجاد و اجرای کانتینرها

Kubernetes پلتفرمی برای ایجاد، deploy و مدیریت برنامه‌های توزیع‌شده است. این برنامه‌ها اشکال و اندازه‌های بسیار متفاوتی دارند، اما در نهایت همه از یک یا چند برنامه تشکیل شده‌اند که روی ماشین‌های فردی اجرا می‌شوند. این برنامه‌ها ورودی می‌گیرند، داده را دستکاری می‌کنند و نتیجه را برمی‌گردانند. قبل از اینکه حتی به ساخت سیستم توزیع‌شده فکر کنیم، باید ابتدا در نظر بگیریم چگونه imageهای container برنامه را بسازیم که این برنامه‌ها را در بر می‌گیرند و قطعات سیستم توزیع‌شدهٔ ما را تشکیل می‌دهند.

برنامه‌های application معمولاً از language runtime، کتابخانه‌ها و source code شما تشکیل شده‌اند. در بسیاری موارد، برنامهٔ شما به کتابخانه‌های shared خارجی مثل libc و libssl وابسته است. این کتابخانه‌های خارجی معمولاً به‌صورت اجزای shared در OS نصب‌شده روی ماشین خاص ارسال می‌شوند.

این وابستگی به کتابخانه‌های shared مشکل ایجاد می‌کند وقتی برنامه‌ای که روی laptop برنامه‌نویس توسعه یافته به کتابخانهٔ sharedی وابسته است که هنگام rollout به OS production در دسترس نیست. حتی وقتی محیط توسعه و production دقیقاً همان نسخهٔ OS را دارند، مشکل پیش می‌آید وقتی developerها فراموش می‌کنند فایل‌های asset وابسته را در بسته‌ای که به production deploy می‌کنند بگنجانند.

روش‌های سنتی اجرای چند برنامه روی یک ماشین نیاز دارد همهٔ این برنامه‌ها همان نسخه‌های کتابخانه‌های shared روی سیستم را به اشتراک بگذارند. اگر برنامه‌های مختلف توسط تیم‌ها یا سازمان‌های مختلف توسعه یافته باشند، این وابستگی‌های shared پیچیدگی و coupling بی‌مورد بین این تیم‌ها اضافه می‌کند.

یک برنامه فقط در صورت deploy reliable روی ماشینی که باید اجرا شود، موفق اجرا می‌شود. اغلب state of the art برای deployment شامل اجرای scriptهای imperative است که ناگزیر failure caseهای پیچیده و بzantine دارند. این کار rollout نسخهٔ جدید همه یا بخشی از سیستم توزیع‌شده را labor-intensive و دشوار می‌کند.

در فصل ۱ قوی استدلال کردیم برای ارزش imageها و زیرساخت immutable. این immutability دقیقاً همان چیزی است که container image فراهم می‌کند. همان‌طور که خواهیم دید، به‌راحتی همهٔ مشکلات مدیریت وابستگی و encapsulation توصیف‌شده را حل می‌کند.

هنگام کار با برنامه‌ها اغلب مفید است آن‌ها را به روشی بسته‌بندی کنیم که اشتراک با دیگران آسان باشد. Docker، موتور container runtime پیش‌فرض، بسته‌بندی executable و push به registry راه‌دور را آسان می‌کند تا بعداً دیگران pull کنند. در زمان نگارش، container registry در همهٔ public cloudهای اصلی در دسترس است و سرویس‌های ساخت image در cloud نیز در بسیاری از آن‌ها موجود است. همچنین می‌توانید registry خود را با سیستم‌های متن‌باز یا تجاری اجرا کنید. این registryها به کاربران کمک می‌کند imageهای private را مدیریت و deploy کنند، در حالی که سرویس‌های image-builder یکپارچگی آسان با سیستم‌های continuous delivery می‌دهند.

برای این فصل و بقیهٔ کتاب، با یک برنامهٔ نمونهٔ ساده کار می‌کنیم که برای نشان دادن این workflow ساختیم. برنامه را در GitHub پیدا می‌کنید.

Container image یک برنامه و وابستگی‌هایش را در یک artifact واحد زیر root filesystem بسته‌بندی می‌کند. محبوب‌ترین فرمت container image، فرمت Docker image است که توسط Open Container Initiative به فرمت OCI image استاندارد شده است. Kubernetes هم imageهای سازگار با Docker و OCI را از طریق Docker و runtimeهای دیگر پشتیبانی می‌کند. Docker image همچنین metadata اضافی برای container runtime دارد تا instance برنامهٔ در حال اجرا بر اساس محتوای container image راه بیندازد.

این فصل موضوعات زیر را پوشش می‌دهد:

  • نحوهٔ بسته‌بندی برنامه با فرمت Docker image
  • نحوهٔ راه‌اندازی برنامه با Docker container runtime

Container Imageها

برای تقریباً همه، اولین تعامل با هر فناوری container با container image است. Container image بستهٔ binary است که همهٔ فایل‌های لازم برای اجرای برنامه داخل OS container را در بر می‌گیرد. بسته به نحوهٔ اولین آزمایش با containerها، یا image را از filesystem محلی می‌سازید یا image از پیش موجود را از container registry دانلود می‌کنید. در هر صورت، وقتی container image روی کامپیوتر شما حاضر است، می‌توانید آن image را اجرا کنید تا برنامهٔ در حال اجرا داخل OS container تولید شود.

فرمت Docker Image

محبوب‌ترین و گسترده‌ترین فرمت container image، فرمت Docker image است که توسط پروژهٔ متن‌باز Docker برای بسته‌بندی، توزیع و اجرای containerها با دستور docker توسعه یافت. سپس کار توسط Docker, Inc. و دیگران برای استانداردسازی فرمت container image از طریق پروژهٔ Open Container Initiative (OCI) آغاز شد. در حالی که استاندارد OCI در میانهٔ ۲۰۱۷ به milestone release 1.0 رسید، پذیرش این استانداردها آهسته پیش می‌رود. فرمت Docker image همچنان de facto standard است و از سری لایه‌های filesystem تشکیل شده است. هر لایه فایل‌ها را از لایهٔ قبلی در filesystem اضافه، حذف یا تغییر می‌دهد. این نمونه‌ای از overlay filesystem است. سیستم overlay هم هنگام بسته‌بندی image و هم هنگام استفادهٔ واقعی از image به‌کار می‌رود. در runtime، پیاده‌سازی‌های concrete مختلفی از چنین filesystemهایی وجود دارد، از جمله aufs، overlay و overlay2.

Container Layering

عبارت‌های «فرمت Docker image» و «container image» ممکن است کمی گیج‌کننده باشند. image یک فایل واحد نیست بلکه specification برای فایل manifest است که به فایل‌های دیگر اشاره می‌کند. manifest و فایل‌های مرتبط اغلب به‌عنوان یک واحد توسط کاربران در نظر گرفته می‌شوند. سطح indirection امکان storage و transmittal کارآمدتر را می‌دهد. API مرتبط با این فرمت برای آپلود و دانلود image به image registry است. Container imageها با سری لایه‌های filesystem ساخته می‌شوند که هر لایه لایه‌های قبل را به ارث می‌برد و تغییر می‌دهد. برای توضیح دقیق، بیایید چند container بسازیم. توجه کنید برای صحت ترتیب لایه‌ها باید bottom up باشد، اما برای فهم آسان‌تر رویکرد معکوس می‌گیریم:

.
└── container A: فقط یک سیستم‌عامل پایه، مثل Debian
    └── container B: بر پایهٔ #A، با افزودن Ruby v2.1.10
    └── container C: بر پایهٔ #A، با افزودن Golang v1.6

در این نقطه سه container داریم: A، B و C. B و C از A fork شده‌اند و جز فایل‌های container پایه چیزی به اشتراک نمی‌گذارند. ادامه می‌دهیم با ساخت روی B با افزودن Rails (نسخهٔ 4.2.6). همچنین ممکن است بخواهیم برنامهٔ legacyای را پشتیبانی کنیم که نسخهٔ قدیمی‌تر Rails (مثلاً 3.2.x) می‌خواهد. می‌توانیم image container برای آن برنامه بر پایهٔ B بسازیم و روزی برنامه را به نسخهٔ ۴ migrate کنیم:

. (ادامه از بالا)
└── container B: بر پایهٔ #A، با افزودن Ruby v2.1.10
    └── container D: بر پایهٔ #B، با افزودن Rails v4.2.6
    └── container E: بر پایهٔ #B، با افزودن Rails v3.2.x

از نظر مفهومی، هر لایهٔ container image روی لایهٔ قبلی ساخته می‌شود. هر ارجاع parent یک pointer است. در حالی که مثال اینجا مجموعهٔ ساده‌ای از containerهاست، containerهای real-world دیگر می‌توانند بخشی از directed acyclic graph بزرگ‌تر باشند.

Container imageها معمولاً با فایل پیکربندی container ترکیب می‌شوند که دستورالعمل راه‌اندازی محیط container و اجرای application entry point را می‌دهد. پیکربندی container اغلب اطلاعاتی دربارهٔ راه‌اندازی networking، namespace isolation، محدودیت منابع (cgroups) و محدودیت‌های syscall که باید روی instance container در حال اجرا اعمال شود دارد. root filesystem container و فایل پیکربندی معمولاً با فرمت Docker image بسته‌بندی می‌شوند.

Containerها به دو دستهٔ اصلی تقسیم می‌شوند:

  • System containerها
  • Application containerها

System containerها می‌خواهند virtual machine را تقلید کنند و اغلب process boot کامل اجرا می‌کنند. اغلب مجموعه‌ای از سرویس‌های سیستمی معمول VM مثل ssh، cron و syslog دارند. وقتی Docker تازه بود، این نوع containerها بسیار رایج‌تر بودند. با گذشت زمان، poor practice تلقی شدند و application containerها محبوبیت یافتند.

Application containerها با system containerها فرق دارند چون معمولاً یک برنامه اجرا می‌کنند. اگرچه اجرای یک برنامه در هر container ممکن است محدودیت غیرضروری به نظر برسد، سطح granularity ایده‌آلی برای ترکیب برنامه‌های scalable می‌دهد و design philosophyای است که Podها به‌شدت از آن بهره می‌برند. نحوهٔ کار Podها را در فصل ۵ به‌تفصیل بررسی می‌کنیم.

ساخت Application Imageها با Docker

به‌طور کلی، سیستم‌های container orchestration مثل Kubernetes روی ساخت و deploy سیستم‌های توزیع‌شدهٔ ساخته‌شده از application container متمرکزند. در نتیجه، برای بقیهٔ این فصل روی application container تمرکز می‌کنیم.

Dockerfileها

Dockerfile می‌تواند برای خودکارسازی ساخت Docker container image استفاده شود.

بیایید با ساخت application image برای برنامهٔ سادهٔ Node.js شروع کنیم. این مثال برای بسیاری از زبان‌های dynamic دیگر مثل Python یا Ruby بسیار مشابه است.

ساده‌ترین اپ npm/Node/Express دو فایل دارد: package.json (مثال ۲-۱) و server.js (مثال ۲-۲). آن‌ها را در یک دایرکتوری بگذارید و سپس npm install express --save را اجرا کنید تا وابستگی به Express برقرار و نصب شود.

مثال ۲-۱. package.json

json
{
    "name": "simple-node",
    "version": "1.0.0",
    "description": "A sample simple application for Kubernetes Up & Running",
    "main": "server.js",
    "scripts": {
       "start": "node server.js"
    },
    "author": ""
}

مثال ۲-۲. server.js

javascript
var express = require('express');

var app = express();
app.get('/', function (req, res) {
  res.send('Hello World!');
});
app.listen(3000, function () {
  console.log('Listening on port 3000!');
  console.log(' http://localhost:3000');
});

برای بسته‌بندی به‌عنوان Docker image باید دو فایل اضافی بسازیم: .dockerignore (مثال ۲-۳) و Dockerfile (مثال ۲-۴). Dockerfile دستور پخت ساخت container image است، در حالی که .dockerignore مجموعهٔ فایل‌هایی را که هنگام کپی به image نادیده گرفته شوند تعریف می‌کند. توضیح کامل syntax Dockerfile در وب‌سایت Docker در دسترس است.

مثال ۲-۳. .dockerignore

node_modules

مثال ۲-۴. Dockerfile

dockerfile
# Start from a Node.js 10 (LTS) image
FROM node:10

# Specify the directory inside the image in which all commands will run
WORKDIR /usr/src/app

# Copy package files and install dependencies
COPY package*.json ./
RUN npm install

# Copy all of the app files into the image
COPY . .

# The default command to run when starting the container
CMD [ "npm", "start" ]

هر Dockerfile روی container imageهای دیگر ساخته می‌شود. این خط مشخص می‌کند از image node:10 روی Docker Hub شروع می‌کنیم. این image از پیش پیکربندی‌شده با Node.js 10 است.

این خط work directory را در image container برای همهٔ دستورات بعدی تنظیم می‌کند.

این دو خط وابستگی‌های Node.js را initialize می‌کنند. ابتدا فایل‌های package را به image کپی می‌کنیم. این شامل package.json و package-lock.json می‌شود. دستور RUN سپس دستور صحیح را در container برای نصب وابستگی‌های لازم اجرا می‌کند.

حالا بقیهٔ فایل‌های برنامه را به image کپی می‌کنیم. این شامل همه‌چیز به‌جز node_modules است، چون از طریق .dockerignore حذف شده است.

در پایان، دستوری را مشخص می‌کنیم که هنگام اجرای container باید اجرا شود.

دستور زیر را برای ساخت Docker image simple-node اجرا کنید:

$ docker build -t simple-node .

وقتی می‌خواهید این image را اجرا کنید، با دستور زیر می‌توانید. می‌توانید به http://localhost:3000 بروید تا به برنامهٔ در حال اجرا در container دسترسی داشته باشید:

$ docker run --rm -p 3000:3000 simple-node

در این نقطه image simple-node ما در Docker registry محلی جایی که image ساخته شد زندگی می‌کند و فقط برای یک ماشین در دسترس است. قدرت واقعی Docker از توانایی اشتراک image در هزاران ماشین و جامعهٔ گسترده‌تر Docker می‌آید.

بهینه‌سازی اندازهٔ Image

چند gotcha وجود دارد وقتی مردم شروع به آزمایش با container image می‌کنند که به imageهای بیش‌ازحد بزرگ منجر می‌شود. اولین نکته این است که فایل‌هایی که توسط لایه‌های بعدی در سیستم حذف می‌شوند در واقع هنوز در imageها هستند؛ فقط inaccessible شده‌اند. موقعیت زیر را در نظر بگیرید:

.
└── layer A: شامل فایل بزرگی به نام 'BigFile'
    └── layer B: 'BigFile' را حذف می‌کند
        └── layer C: بر پایهٔ B با افزودن یک binary استاتیک

ممکن است فکر کنید BigFile دیگر در این image نیست. بعد از همه، وقتی image را اجرا می‌کنید، دیگر در دسترس نیست. اما در واقع هنوز در لایهٔ A هست، یعنی هر بار image را push یا pull می‌کنید، BigFile هنوز از شبکه منتقل می‌شود، حتی اگر دیگر به آن دسترسی نداشته باشید.

گودال دیگری که مردم در آن می‌افتند مربوط به image caching و build است. به یاد داشته باشید هر لایه delta مستقل از لایهٔ زیرین است. هر بار لایه‌ای را تغییر می‌دهید، هر لایهٔ بعد از آن تغییر می‌کند. تغییر لایه‌های قبلی یعنی باید rebuild، repush و repull شوند تا image را به development deploy کنید.

برای درک کامل‌تر، دو image را در نظر بگیرید:

.
└── layer A: شامل یک OS پایه
    └── layer B: source code server.js را اضافه می‌کند
        └── layer C: بستهٔ 'node' را نصب می‌کند

در مقابل:

.
└── layer A: شامل یک OS پایه
    └── layer B: بستهٔ 'node' را نصب می‌کند
        └── layer C: source code server.js را اضافه می‌کند

واضح است هر دو image یکسان رفتار می‌کنند و در واقع اولین بار pull شدن چنین است. اما در نظر بگیرید وقتی server.js تغییر می‌کند چه می‌شود. در یک مورد فقط تغییر باید pull یا push شود، اما در مورد دیگر هم server.js و هم لایهٔ node باید pull و push شوند، چون لایهٔ node به لایهٔ server.js وابسته است. به‌طور کلی می‌خواهید لایه‌ها را از کم‌احتمال‌ترین تغییر به پراحتمال‌ترین تغییر مرتب کنید تا اندازهٔ image برای push و pull بهینه شود. به همین دلیل در مثال ۲-۴ فایل‌های package*.json را کپی و وابستگی‌ها را قبل از کپی بقیهٔ فایل‌های برنامه نصب می‌کنیم. developer فایل‌های برنامه را بسیار بیشتر از وابستگی‌ها به‌روز و تغییر می‌دهد.

امنیت Image

وقتی به امنیت می‌پردازیم، میانبر وجود ندارد. هنگام ساخت imageهایی که در نهایت در cluster production Kubernetes اجرا می‌شوند، حتماً best practiceهای بسته‌بندی و توزیع برنامه را دنبال کنید. مثلاً container با password داخل نسازید — و این شامل نه‌فقط لایهٔ نهایی، بلکه هر لایه‌ای در image است. یکی از مشکلات غیرمنطقی معرفی‌شده توسط لایه‌های container این است که حذف فایل در یک لایه آن فایل را از لایه‌های قبلی حذف نمی‌کند. هنوز فضا می‌گیرد و هر کسی با ابزار مناسب می‌تواند به آن دسترسی داشته باشد — مهاجم زیرک می‌تواند imageای بسازد که فقط از لایه‌هایی تشکیل شده که password را دارند.

secretها و image هرگز نباید مخلوط شوند. اگر چنین کنید، هک می‌شوید و شرم را به کل شرکت یا بخش می‌آورید. همه می‌خواهیم روزی در تلویزیون باشیم، اما راه‌های بهتری برای آن وجود دارد.

Multistage Image Buildها

یکی از رایج‌ترین راه‌های ساخت تصادفی imageهای بزرگ، انجام compile واقعی برنامه به‌عنوان بخشی از ساخت application container image است. compile کد در build image طبیعی به نظر می‌رسد و آسان‌ترین راه ساخت container image از برنامه است. مشکل این است که همهٔ ابزارهای توسعهٔ غیرضروری، که معمولاً بسیار بزرگ‌اند، داخل image باقی می‌مانند و deployment را کند می‌کنند.

برای حل این مشکل، Docker multistage build معرفی کرد. با multistage build، به‌جای تولید یک image، Dockerfile می‌تواند چند image تولید کند. هر image یک stage در نظر گرفته می‌شود. artifactها می‌توانند از stageهای قبلی به stage فعلی کپی شوند.

برای ملموس‌سازی، نگاه می‌کنیم چگونه برنامهٔ نمونهٔ kuard را بسازیم. این برنامهٔ نسبتاً پیچیده‌ای است که frontend React.js (با process build خودش) دارد که سپس در برنامهٔ Go embed می‌شود. برنامهٔ Go API server backend اجرا می‌کند که frontend React.js با آن تعامل دارد.

یک Dockerfile ساده ممکن است این‌طور باشد:

dockerfile
FROM golang:1.11-alpine

# Install Node and NPM
RUN apk update && apk upgrade && apk add --no-cache git nodejs bash npm

# Get dependencies for Go part of build
RUN go get -u github.com/jteeuwen/go-bindata/...
RUN go get github.com/tools/godep

WORKDIR /go/src/github.com/kubernetes-up-and-running/kuard

# Copy all sources in
COPY . .

# This is a set of variables that the build script expects
ENV VERBOSE=0
ENV PKG=github.com/kubernetes-up-and-running/kuard
ENV ARCH=amd64
ENV VERSION=test

# Do the build. This script is part of incoming sources.
RUN build/build.sh

CMD [ "/go/bin/kuard" ]

این Dockerfile container imageای با executable استاتیک تولید می‌کند، اما همچنین همهٔ ابزارهای توسعهٔ Go و ابزارهای ساخت frontend React.js و source code برنامه را دارد که هیچ‌کدام برای برنامهٔ نهایی لازم نیستند. image، در همهٔ لایه‌ها، بیش از ۵۰۰ مگابایت می‌شود.

برای دیدن نحوهٔ انجام با multistage build، این Dockerfile چندمرحله‌ای را بررسی کنید:

dockerfile
# STAGE 1: Build
FROM golang:1.11-alpine AS build

# Install Node and NPM
RUN apk update && apk upgrade && apk add --no-cache git nodejs bash npm

# Get dependencies for Go part of build
RUN go get -u github.com/jteeuwen/go-bindata/...
RUN go get github.com/tools/godep

WORKDIR /go/src/github.com/kubernetes-up-and-running/kuard

# Copy all sources in
COPY . .

# This is a set of variables that the build script expects
ENV VERBOSE=0
ENV PKG=github.com/kubernetes-up-and-running/kuard
ENV ARCH=amd64
ENV VERSION=test

# Do the build. Script is part of incoming sources.
RUN build/build.sh

# STAGE 2: Deployment
FROM alpine

USER nobody:nobody
COPY --from=build /go/bin/kuard /kuard

CMD [ "/kuard" ]

این Dockerfile دو image تولید می‌کند. اولی build image است که Go compiler، toolchain React.js و source code برنامه را دارد. دومی deployment image است که فقط binary کامپایل‌شده را دارد. ساخت container image با multistage build می‌تواند اندازهٔ نهایی container image را صدها مگابایت کاهش دهد و در نتیجه زمان deployment را به‌شدت سرعت بخشد، چون معمولاً latency deployment به performance شبکه وابسته است. image نهایی تولیدشده از این Dockerfile حدود ۲۰ مگابایت است.

می‌توانید این image را با دستورات زیر بسازید و اجرا کنید:

$ docker build -t kuard .
$ docker run --rm -p 8080:8080 kuard

ذخیرهٔ Imageها در Registry راه‌دور

فایدهٔ container image چیست اگر فقط روی یک ماشین در دسترس باشد؟

Kubernetes به این واقعیت تکیه می‌کند که imageهای توصیف‌شده در Pod manifest روی هر ماشین در cluster در دسترس باشند. یک گزینه برای رساندن این image به همهٔ ماشین‌ها export کردن image kuard و import روی هر کدام است. نمی‌توانیم چیزی خسته‌کننده‌تر از مدیریت Docker image به این روش تصور کنیم. process دستی import و export Docker image پر از خطای انسانی است. فقط نه بگویید!

استاندارد در جامعهٔ Docker ذخیرهٔ Docker image در registry راه‌دور است. گزینه‌های بسیاری برای Docker registry وجود دارد و انتخاب شما عمدتاً بر اساس نیازهای امنیت و قابلیت‌های همکاری است.

به‌طور کلی، اولین انتخابی که باید دربارهٔ registry بگیرید استفاده از registry خصوصی یا عمومی است. Registryهای عمومی به هر کسی اجازهٔ دانلود image ذخیره‌شده در registry را می‌دهند، در حالی که registryهای خصوصی برای دانلود image نیاز به authentication دارند. در انتخاب عمومی در برابر خصوصی، در نظر گرفتن use case مفید است. Registryهای عمومی برای اشتراک image با جهان عالی‌اند، چون استفادهٔ آسان و بدون authentication از container image را می‌دهند. می‌توانید نرم‌افزار را به‌راحتی به‌صورت container image توزیع کنید و اطمینان داشته باشید کاربران در همه‌جا تجربهٔ یکسان دارند.

در مقابل، registry خصوصی برای ذخیرهٔ برنامه‌هایی که خصوصی سرویس شما هستند و نمی‌خواهید جهان از آن‌ها استفاده کند بهتر است.

در هر صورت، برای push image باید به registry authenticate شوید. معمولاً با دستور docker login می‌توانید، هرچند برای برخی registryها تفاوت‌هایی هست. در مثال‌های اینجا به registry Google Cloud Platform به نام Google Container Registry (GCR) push می‌کنیم؛ cloudهای دیگر از جمله Azure و Amazon Web Services (AWS) هم container registry میزبانی‌شده دارند. برای کاربران جدید که imageهای عمومی قابل خواندن میزبانی می‌کنند، Docker Hub جای خوبی برای شروع است.

وقتی login کردید، می‌توانید image kuard را با پیش‌وند دادن Docker registry هدف tag کنید. همچنین می‌توانید شناسهٔ دیگری اضافه کنید که معمولاً برای نسخه یا variant آن image است و با دو نقطه (😃 جدا می‌شود:

$ docker tag kuard gcr.io/kuar-demo/kuard-amd64:blue

سپس می‌توانید image kuard را push کنید:

$ docker push gcr.io/kuar-demo/kuard-amd64:blue

حالا که image kuard روی registry راه‌دور در دسترس است، وقت deploy با Docker است. چون به Docker registry عمومی push کردیم، همه‌جا بدون authentication در دسترس خواهد بود.

Docker Container Runtime

Kubernetes API برای توصیف deployment برنامه می‌دهد، اما به container runtime تکیه می‌کند تا application container را با APIهای خاص container بومی OS هدف راه بیندازد. روی سیستم Linux یعنی پیکربندی cgroup و namespace. رابط این container runtime با استاندارد Container Runtime Interface (CRI) تعریف شده است. CRI API توسط برنامه‌های مختلف پیاده‌سازی می‌شود، از جمله containerd-cri ساخته‌شده توسط Docker و پیاده‌سازی cri-o مشارکت‌شده توسط Red Hat.

اجرای Containerها با Docker

اگرچه در Kubernetes معمولاً containerها توسط daemon روی هر node به نام kubelet راه می‌افتند، شروع با container با ابزار خط فرمان Docker آسان‌تر است. ابزار CLI Docker می‌تواند برای deploy container استفاده شود. برای deploy container از image gcr.io/kuar-demo/kuard-amd64:blue، دستور زیر را اجرا کنید:

$ docker run -d --name kuard \
  --publish 8080:8080 \
  gcr.io/kuar-demo/kuard-amd64:blue

این دستور container kuard را start می‌کند و پورت ۸۰۸۰ روی ماشین محلی را به ۸۰۸۰ در container map می‌کند. گزینهٔ --publish می‌تواند به -p کوتاه شود. این forwarding لازم است چون هر container IP خودش را می‌گیرد، پس گوش دادن روی localhost داخل container باعث گوش دادن روی ماشین شما نمی‌شود. بدون port forwarding، اتصال‌ها برای ماشین شما inaccessible می‌مانند. گزینهٔ -d مشخص می‌کند در پس‌زمینه (daemon) اجرا شود، در حالی که --name kuard به container نام دوستانه می‌دهد.

کاوش برنامهٔ kuard

kuard رابط وب ساده‌ای expose می‌کند که می‌توانید با اشارهٔ مرورگر به http://localhost:8080 یا از خط فرمان بارگذاری کنید:

$ curl http://localhost:8080

kuard همچنین توابع جالب متعددی expose می‌کند که بعداً در این کتاب کاوش می‌کنیم.

محدود کردن مصرف منابع

Docker توانایی محدود کردن میزان منابع مصرفی برنامه‌ها را با expose کردن فناوری cgroup زیرین ارائه‌شده توسط kernel لینوکس می‌دهد. این قابلیت‌ها همچنین توسط Kubernetes برای محدود کردن منابع مصرفی هر Pod استفاده می‌شوند.

محدود کردن منابع حافظه

یکی از مزایای کلیدی اجرای برنامه‌ها در container توانایی محدود کردن استفاده از منابع است. این به چند برنامه اجازه می‌دهد روی همان سخت‌افزار هم‌زیستی کنند و استفادهٔ منصفانه تضمین شود.

برای محدود کردن kuard به ۲۰۰ مگابایت حافظه و ۱ گیگابایت swap، از flagهای --memory و --memory-swap با دستور docker run استفاده کنید.

container kuard فعلی را stop و حذف کنید:

$ docker stop kuard
$ docker rm kuard

سپس container kuard دیگری با flagهای مناسب برای محدود کردن مصرف حافظه start کنید:

$ docker run -d --name kuard \
  --publish 8080:8080 \
  --memory 200m \
  --memory-swap 1G \
  gcr.io/kuar-demo/kuard-amd64:blue

اگر برنامه در container حافظهٔ زیادی مصرف کند، terminate می‌شود.

محدود کردن منابع CPU

منبع حیاتی دیگر روی ماشین CPU است. محدود کردن استفاده از CPU با flag --cpu-shares در دستور docker run:

$ docker run -d --name kuard \
  --publish 8080:8080 \
  --memory 200m \
  --memory-swap 1G \
  --cpu-shares 1024 \
  gcr.io/kuar-demo/kuard-amd64:blue

پاکسازی

وقتی ساخت image تمام شد، می‌توانید با دستور docker rmi حذفش کنید:

docker rmi <tag-name>

یا:

docker rmi <image-id>

Imageها می‌توانند از طریق نام tag (مثلاً gcr.io/kuar-demo/kuard-amd64:blue) یا از طریق image ID حذف شوند. مانند همهٔ مقادیر ID در ابزار docker، image ID می‌تواند تا زمانی که unique بماند کوتاه شود. معمولاً فقط سه یا چهار کاراکتر از ID لازم است.

مهم است توجه کنید مگر اینکه صریحاً image را حذف کنید، برای همیشه روی سیستم شما می‌ماند، حتی اگر image جدیدی با نام یکسان بسازید. ساخت image جدید فقط tag را به image جدید منتقل می‌کند؛ image قدیمی را حذف یا جایگزین نمی‌کند.

در نتیجه، هنگام iterate کردن در ساخت image جدید، اغلب imageهای بسیار متفاوتی می‌سازید که فضای غیرضروری روی کامپیوتر می‌گیرند.

برای دیدن imageهای فعلی روی ماشین، از دستور docker images استفاده کنید. سپس tagهایی که دیگر استفاده نمی‌کنید را حذف کنید.

Docker ابزاری به نام docker system prune برای پاکسازی عمومی می‌دهد. این همهٔ containerهای متوقف‌شده، همهٔ imageهای بدون tag و همهٔ لایه‌های image استفاده‌نشده cacheشده در process build را حذف می‌کند. با احتیاط استفاده کنید.

رویکرد کمی پیشرفته‌تر، راه‌اندازی cron job برای اجرای image garbage collector است. مثلاً ابزار docker-gc image garbage collector رایجی است که به‌راحتی می‌تواند به‌صورت cron job تکراری اجرا شود، یک‌بار در روز یا یک‌بار در ساعت، بسته به تعداد imageهایی که می‌سازید.

خلاصه

Application containerها abstraction تمیزی برای برنامه‌ها می‌دهند و وقتی با فرمت Docker image بسته‌بندی شوند، برنامه‌ها ساخت، deploy و توزیع آسان می‌شوند. Containerها همچنین isolation بین برنامه‌های روی همان ماشین می‌دهند که به اجتناب از تضاد وابستگی کمک می‌کند.

در فصل‌های آینده خواهیم دید توانایی mount کردن دایرکتوری‌های خارجی یعنی نه‌تنها برنامه‌های stateless، بلکه برنامه‌هایی مثل mysql و دیگرانی که دادهٔ زیاد تولید می‌کنند را هم می‌توان در container اجرا کرد.