DevOps в серията K8s bootcamp
Забележете, пълната мисловна карта „DevOps в K8s“ е достъпна на: „DevOps в мисловната карта на K8s“
В моите предишни статии „DevOps в K8s“ („DevOps в K8s — Контейнери, част първа“), представих концепцията за контейнер и как да стартирам контейнери с помощта на команда Docker. Въведох също Dockerfile
за изграждане на изображения на контейнери. В тази статия нека разгледаме някои от най-добрите практики и методи за изграждане на ефективни изображения.
Както вече знаем, Docker изгражда изображения автоматично, като чете инструкциите от даден Dockerfile
. Dockerfile
се придържа към конкретен формат и набор от инструкции и всяка инструкция в Dockerfile
създава един слой в изображението на контейнера. Слоевете са подредени и всеки е делта от промените от предишния слой.
Например, нека да разгледаме следния Dockerfile:
FROM python:alpine COPY . /app RUN make /app CMD python /app/app.py
Всяка инструкция създава един слой:
- ОТ: Създава слой от изображението на python:alpine Docker.
- КОПИРАНЕ: Добавя файлове от текущата директория на вашия Docker клиент.
- RUN: Създава вашето приложение с make.
- CMD: Указва каква команда да се изпълни в контейнера.
По-късно, когато стартираме това изображение и създадем контейнер, ние всъщност добавяме нов слой с възможност за запис (контейнерен слой) върху подлежащите слоеве. Всички промени, направени в работещия контейнер, като писане на нови файлове, модифициране на съществуващи файлове и изтриване на файлове, се записват в този слой на контейнер за запис.
Основната разлика между контейнер и изображение е горният слой за запис. Всички записи в контейнера, които добавят нови или модифицират съществуващи данни, се съхраняват в този слой за запис. Когато контейнерът бъде изтрит, записваемият слой също се изтрива. Основното изображение остава непроменено.
Производителността на Docker контейнера може да варира в зависимост от последователността от стъпки, които сме посочили във вашия Dockerfile
. Ето защо е много важно редът на инструкциите да бъде включен в Dockerfile
.
Използвайте .dockerignore
Точно като .gitignore
файл, за да изключим файлове, които не са свързани с компилацията, без да преструктурираме изходното хранилище, можем да използваме .dockerignore
файл. Този файл поддържа модели за изключване, подобни на .gitignore files
. Например:
# comment */temp* */*/temp* temp? *.md !README.md
Избягвайте да инсталирате ненужни пакети
Инсталирането на ненужни пакети в Dockerfile
ще увеличи времето за изграждане и размера на изображението. Също така, всеки път, когато правим промени в Dockerfile
, ще трябва да преминем през всички стъпки, за да изградим същото голямо изображение отново и отново. Това ще повлияе на производителността на процеса на изграждане. За да избегнете това, моля, включете само необходимите пакети и опитайте да избягвате инсталирането на същите пакети отново и отново.
Използвайте малки основни изображения
По-малките Docker изображения са по-модулни и сигурни. Освен това изграждането, бутането и изтеглянето на изображения е по-бързо с по-малки изображения. Те също са склонни да бъдат по-сигурни, тъй като включват само необходимите библиотеки и системни зависимости, необходими за изпълнение на вашето приложение.
По-долу са някои често срещани тагове за изображения:
- Пълно официално: Официално изображение в пълен размер, като например
python:3.9.3
- stretch/buster/jessie: Кодови имена за различни версии на Debian
- тънък: Сдвоена свалена версия на пълното изображение, обикновено инсталира минималните пакети, необходими за стартиране на вашия конкретен инструмент
- alpine: Базиран на проекта Alpine Linux, който е операционна система, създадена специално за използване в контейнери.
Ефемерни контейнери
Изображението, дефинирано в нашия Dockerfile, трябва да генерира контейнери, които са възможно най-ефимерни. Ефемерно означава, че контейнерът може да бъде спрян и унищожен, след това възстановен и заменен с абсолютен минимум настройка и конфигурация.
Изграждане от Stdin
Docker engine има способността да създава изображения чрез прехвърляне на Dockerfile през stdin с локален или отдалечен контекст на компилация. Например:
$ docker build -<<EOF FROM busybox RUN echo "hello world" EOF Sending build context to Docker daemon 2.048kB Step 1/2 : FROM busybox latest: Pulling from library/busybox 205dae5015e7: Pull complete Digest: sha256:7b3ccabffc97de872a30dfd234fd972a66d247c8cfc69b0550f276481852627c Status: Downloaded newer image for busybox:latest ---> 66ba00ad3de8 Step 2/2 : RUN echo "hello world" ---> Running in 085e9277353f hello world Removing intermediate container 085e9277353f ---> 9adb24b1a0fd Successfully built 9adb24b1a0fd
Пропускането на контекста на изграждане може да бъде полезно в ситуации, в които Dockerfile не изисква файловете да бъдат копирани в изображението (COPY/ADD ще се провали) и подобрява скоростта на изграждане, тъй като не се изпращат файлове към демона на Docker.
Възползвайте се от многоетапно изграждане
Многоетапните компилации ни позволяват драстично да намалим размера на окончателното си изображение, без да се мъчим да намалим броя на междинните слоеве и файлове, като използваме кеша за компилация. Например, нека да разгледаме следното Dockerfile
:
FROM golang:1.18-alpine AS prebuild # Install tools required for project RUN go get github.com/golang/dep/cmd/dep COPY Gopkg.lock Gopkg.toml /go/src/project/ WORKDIR /go/src/project/ RUN go build -o /bin/project # This results in a single layer image FROM scratch COPY --from=prebuild /bin/project /bin/project ENTRYPOINT ["/bin/project"]
Както можете да видите, можем да използваме множество FROM
изрази. Всяка FROM
инструкция може да използва различна база и всяка от тях започва нов етап от изграждането. Можем избирателно да копираме артефакти от един етап на друг, оставяйки всичко, което не искаме в крайното изображение.
Втората инструкция FROM scratch
започва нов етап на изграждане с изображението scratch
като основа. Редът COPY --from=prebuild
копира само построения артефакт от предишния етап в този нов етап.
Верижни RUN команди
Тъй като всяка команда RUN
създава кешируема единица и изгражда нов междинен слой на изображението. можем да избегнем това, като свържем всички RUN
команди. Също така, опитайте се да избегнете верижното свързване на твърде много RUN команди, които могат да се кешират, защото това би довело до създаването на голям кеш и в крайна сметка би довело до избухване на кеша.
Например:
RUN apt-get update && apt-get install -y \ bzr \ cvs \ git \ mercurial \ subversion \ && rm -rf /var/lib/apt/lists/*
Инструкции за поръчка разумно
Винаги поставяйте най-често променящите се твърдения в края на Dockerfile
. Docker кешира всяка стъпка (или слой) в определен Dockerfile
, за да ускори следващите компилации. Когато дадена стъпка се промени, кешът ще бъде анулиран не само за тази конкретна стъпка, но и за всички следващи стъпки. Например:
FROM python:3.9-slim WORKDIR /app COPY requirements.txt . RUN pip install -r /requirements.txt COPY app.py .
Обикновено включваме RUN
команди отгоре и COPY
команди отдолу. Включете командите CMD
, ENTRYPOINT
в края на Dockerfile
.
Предпочитайте COPY пред ADD
Въпреки че ADD
и COPY
са функционално подобни, но COPY
е за предпочитане. Това е така, защото е по-прозрачен от ADD
. COPY
поддържа само основното копиране на локални файлове в контейнера, докато ADD
има някои функции (като локално само извличане на tar и поддръжка на отдалечен URL), които не са очевидни веднага. Следователно, най-доброто приложение за ADD
е автоматичното извличане на локален tar файл в изображението, както в ADD rootfs.tar.xz /
.
Ако имате множество Dockerfile
стъпки, които използват различни файлове от вашия контекст, КОПИРАЙТЕ ги поотделно, а не всички наведнъж. Това гарантира, че кешът за компилация на всяка стъпка е само невалиден, принуждавайки стъпката да се изпълни отново, ако конкретно необходимите файлове се променят.
Например:
COPY requirements.txt /tmp/ RUN pip install --requirement /tmp/requirements.txt COPY . /tmp/
Предпочитайте синтаксиса на масива пред низа
Можем да напишем командите CMD
и ENTRYPOINT
по два различни начина:
- Масив (изпълнение):
ENTRYPOINT [“gunicorn”, “-w”, “4”, “-k”, “uvicorn.workers.UvicornWorker”, “main:app”]
- Низ (обвивка):
ENTRYPOINT “gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app”
За предпочитане е формата на масив (exec). Това е така, защото използването на формата на низ кара Docker да изпълнява вашия процес с помощта на bash, който не обработва правилно сигналите. Тъй като повечето обвивки не обработват сигнали към дъщерни процеси, ако използвате формата на обвивката, CTRL-C (което генерира SIGTERM) може да не спре дъщерен процес.