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) може да не спре дъщерен процес.

Заключение