Контейнеризация #4. Work it!

Дисклеймер

В прошлый раз ты научился создавать контейнеры и даже заставлять их что-то делать. Это похвально, но нет предела совершенству. У нас еще много работы впереди, так что готовься регулярно впитывать вкусную, интересную и полезную информацию по администрированию Docker. И смотреть на котиков.

Алсо, не забывай свои корни:

Напоминалка

В предыдущей статье я рассказывал тебе об основах создания и управления докер-контейнерами. А сегодня речь пойдет про best practices контейнеростроительного дела, среди них:

  • Фильтрация файлов на стадии билда
  • Теги и лейлбы
  • Практики безопасности
  • Уменьшение размеров образа и выбор подходящего репозитория для контейнера

Просто игнорируй их

Часто проект, который надо запихнуть в контейнер, бывает совсем не маленьких размеров. И еще чаще, папка с ним содержит несколько модулей и контекстов, длинный список либ, всякую документацию, zip-ы с файлами для прогонки тестов и тому подобные вещи. Большая часть всего этого в контейнере не нужна, поэтому не ленись поддерживать свой .dockerignore в актуальном состоянии.

Если ты думаешь, что ты супер-пупер педант и поддерживаешь папку своего приложения в идеальном порядке, то вот тебе другой пример. Если у тебя есть, например, файл README и ты в нем что-то поменял, а собираешь контейнер ты примерно вот так

COPY ./container_app /usr/src/app

то я тебя поздравляю, при следующей сборке контейнера контекст сборки не поднимется из кеша, потому что ты поменял README и кеш от контекста теперь другой. Наслаждайся очередным часом сборки)

Tag me!

Тег является обязательны условием создания любого промышленного Docker-изображения. Фактически, тег это строка, являющаяся алиасом на ID контейнера. По умолчанию создается тег latest, который, в общем-то, не говорит вообще ни о чем. Например, ты можешь создать новое изображение с другим базовым образом, но тем же функционалом и бум, у тебя два тега latest, два списка уязвимостей, багов и несовместимостей на один и тот же функционал. И пользователь об этом вообще не догадывается. Ты молодец, не надо так.

Думай о тегах для изображения как о способе поддержки списка версий продукта. Вышло обновление образа — запили для него новый тег. Желательно также в теге указывать отличительную особенность новой версии. В общем, смотри пример.

# Вот так НЕ НАДО
docker build -t my_image:latest .

# Пример отражения особенностей изображения: облегченная версия
docker build -t my_image:9.1-slim .

Не пост, а мета

Одним из правил хорошего тона для создания Docker-контейнеров является добавление вспомогательной информации в контейнеры с помощью инструкции LABEL в Dockerfile. Эта практика позволяет пользователям получать следующую, часто необходимую им, информацию:

  • Мейл создателя образа, чтобы знать в кого метать фекалии
  • Список версий контейнера
  • Ссылка на исходный код проекта (если это опенсорс)
  • Статистика Jenkins

Еще очень неплохо брать какую-нибудь схему описания лейблов, наполнять мета-информацию контейнера согласно ней, а потом прикреплять ссылку на эту схему в последний из LABEL-ов. Пример стандарта, соответствующего RFC5785, приведен вот тут.

Прятки с конфигами

Я сейчас скажу очевидную вещь, готовься. Хранить пароли и логины в общедоступном Dockerfile — ПЛОХО. Серьезно, создавай конфиг-файлы с чувствительной информацией вне Dockerfile, а потом прокидывай их внутрь контейнера. Как подключать эти конфиги к запускаемому сервису — отдельная история для каждого случая, тут уж разбирайся сам. Я лучше покажу тебе новую фишку Docker (вроде уже вышла из альфы) — секретные команды, которые позволяют автоматически отключить кеширование файлов при их управлении. Пример, как всегда, ниже.

FROM alpine

# Показать секрет в их дефолтном расположении
RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecrets

# Показать секрет в их указанном расположении
RUN --mount=type=secret,id=mysecret,dst=/foobar cat /foobar

PID 1, прием

Все процессы в UNIX-системах представлены в виде дерева. Корневым является init(). У него соответственно PID = 1. И этот процесс частично отвечает за то, чтобы управлять ресурсами завершенных дочерних процессов.

Так вот, в Docker каждый процесс запускается в своем пространстве имен. Ииии, у каждого запущенного PID = 1. А никакого функционала, аналогичного функционалу нормального процесса init() ты, конечно, для него не написал.

Поясню на примере. Ты запустил в своем докер-контейнере только один процесс, который, например, запускает веб-сервер. И вот, в один момент, этому процессу понадобилось запустить какой-нибудь bash- скрипт, запускающий perl. Сервер отключает bash-скрипт по тайм-ауту. А процесс perl продолжает работать. После окончания работы, процесс perl перейдет в zombie-состояние, и его родителем станет процесс веб-сервера, который, скорее всего, не знает, как освобождать ресурсы ядра.

Поэтому может случиться такое, что твой код наспамил кучу процессов, они остались в состоянии zombie, заполнили собой таблицу процессов ядра и бум, ты не можешь больше создавать процессы. Хотя случается такое редко.

Для этой проблемы есть несколько решений:

  • Запускать команду с помощью bash. Он корректно выводит процессы из zombie-состояния, хотя и не прокидывает сигналы дочерним процессам.
# Опция -e запрещает bash-y запускать скрипт напрямую
CMD ["/bin/bash", "-c", "set -e && /custom_path"]
  • Использовать Baseimage — docker. Так ты запустишь полноценную виртуалку.
  • Использовать tini. Этот способ наиболее предпочтительный, надежный и проверенный.

Логи мои логи

Контейнеры идеально подходят для приложений без сохранения состояния, которые в идеале должны быть эфемерными. Контейнер запустился, поработал и не оставил никаких следов после. Собственно, все изменения в Docker-контейнере после окончания его работы теряются, если не сделать commit. Но commit создает новый слой в контейнере, тем самым увеличивая размер изображения и создавая ненужную мета-информацию.

Поэтому логи нужно выносить из контейнеров на локальную машину/сервер/бд, где они будут храниться долго и счастливо. Лучше всего выбрать какое-нибудь готовое решение для логирования. Так и картинку красивую можно получить, и разного дополнительного функционала полно. Актуальный список полезностей — тут.

Многоэтапность — твой бро

В Docker с версии 17.05 появилась такая вещь, как многоэтапная сборка. Это когда ты берешь и пишешь в своем Dockerfile несколько инструкций FROM. Зачем это нужно и почему так надо делать — покажу на примере.

Рассмотрим часто встречающуюся ситуацию:

  • У тебя есть первый контейнер, в нем ты собираешь свое будущее приложение, чтобы создать нужные артефакты, которые собственно и выполняют бизнес-логику. Часто в этом контейнере есть библиотеки, инструменты разработчика все остальное, что нужно при разработке и тестах, но совершенно не вперлось в продакшне.
FROM golang:1.7.0
WORKDIR /go/src/github.com/me/amasingApp/
RUN go get -d -v golang.org/x/net/html  
COPY app.go .
RUN go get -d -v golang.org/x/net/html \
  && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
  • Ты хочешь получить второй контейнер, который содержит только скомпилированное приложение и все что необходимо для его запуска, безо всяких лишних деталей.
FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/runningApp
COPY app .
CMD ["./runningApp"]
  • И ты пишешь скрипт, который собирает первый контейнер, копирует из него все необходимое во второй и запускает его.

#!/bin/sh
echo Building alexellis2/href-counter:build

# Билдим первый контейнер
docker build --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy \  
    -t me/amasingApp:build-config . -f Dockerfile.build

# Даем ему имя, чтобы не обращаться по ID или хешу
docker create --name extract me/amasingApp:build-config  

# Копируем необходимые артефакты себе на локальную машину в папку /prepared
docker cp extract:/go/src/github.com/alexellis/href-counter/app ./prepared  

# Удаляем уже не нужный контейнер
docker rm -f extract

echo Building me/amasingApp:1.0-slim

# Билдим второй контейнер, в котором будет только приложение и все что нужно для запуска
docker build --no-cache -t me/amasingApp:1.0-slim .

# Удаляем папку с временными файлами
rm ./prepared

Так делали все до 17-й версии, потому что на выходе ты получал контейнер с необходимым функционалом и минимальным размером, и все было прекрасно. Кроме того,что приходилось писать скрипт, копировать файлы на локальную машину и создавать два контейнера вместо одного. Теперь, слава богу, можно просто сделать вот так:

# Первое изображение, необходимо для того, чтобы создать артефакты
#Задали имя builder первому изображению, чтобы не обращаться по номеру.
FROM golang:1.7.0 as builder
WORKDIR /go/src/github.com/me/amasingApp/
RUN go get -d -v golang.org/x/net/html  
COPY app.go .
RUN go get -d -v golang.org/x/net/html \
  && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/runningApp
#Копируем нужные файлы из первого изображения
COPY --from=builder /go/src/github.com/me/amasingApp/runningApp .
CMD ["./runningApp"]

Какие плюсы? А вот какие: промежуточные артефакты, необходимые для сборки приложения не сохраняются ни на локальной машине, ни в итоговом контейнере, не надо писать лишних скриптов, создается только 1 контейнер, а результат точно такой же.

На вкус и цвет фломастеры разные

На DockerHub есть сразу несколько поддерживаемых официальных образов: ubuntu, busybox, centos, openuse, alpine ( это если не учитывать уже подготовленных изображений для разработки на каком-то языке). Они отличаются набором базовых утилит, пакетными менеджерами и итоговыми размерами.

Чаще всего, спор идет о выборе между ubuntu и alpine. В первом куча готовых утилит, файловый менеджер apt, поддержка manylinux1 по дефолту. Второй отвечает на это минимальным количеством уязвимостей, крохотными размерами ( важно при запуске большого количества контейнеров) и заточенностью под запуск в RAM.

Alpine — твой выбор, если ты точно знаешь, что тебе нужно, умеешь гуглить названия пакетов для apk (они не такие же как в apt) и хочешь собрать изображение минимального размера, пусть и потратив на поиск необходимых для приложения компонентов больше времени.

Ubuntu — если не особо охота заморачиваться с поиском необходимых пакетов и конфликтами зависимостей, когда нужно запихнуть приложение в контейнер как можно скорее, а желательно вчера, при этом размер изображения тебя не сильно волнует.

Остальные образы являются промежуточными вариантами, со своими плюсами и минусами. Для того, чтобы детальнее в них разобраться, лезь на гитхабы и смотри открытые issue. А в целом, лучшего среди этих образов все равно нет, каждый заточен под что-то свое. Правда с выбором лучше определиться до того, как вы будете выкатывать свою структуру в виде Docker-образов, а иначе гарантированно погрязнете в багах и конфликтах.

Вместо послесловия

Сейчас ты узнал список практик, принятых при работе с Docker, очень краткий и сжатый. Если хочешь разобраться в этом всем получше — нужно много лазить по гиту, практиковаться и собирать изображения по 3 раза в день за 20 минут до еды. Но в любом случае понимание придет со временем и опытом, а для начала и такой список запомнить будет неплохо.

А в следующий раз я дам тебе справку по docker — compose и тому, как настраивать взаимодействие контейнеров между собой. Обязательно оставь свое мнение в комментах внизу, чтобы я понял, надо оно тебе или нет.

Возможно, Вам понравится:

guest
0 комментариев
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x
()
x