Автор оригинала: Vyacheslav.
Большинство докерфайлов начинаются с родительского изображения. BaseImage приложения является специальным Докер Изображение, которое настроен для правильного использования в рамках контейнеров докеров и в курсе О ваших требованиях к приложению, ожиданиям + набор дополнительных инструментов.
Обычно те, кто включает, но не ограничивается:
- Модификации для удобства докера.
- Применение конкретные библиотеки и рамки
- Некоторые инструменты администрирования, которые особенно полезны для устранения неисправностей внутри докера
- Механизмы для легкого запуска нескольких процессов, не нарушая философию докера
Возможно, каждая компания или агентство, посвященное докеренным приложениям, имеют или реализовано такое. До сих пор вы можете найти его в виде сложной оболочки и макияжа, особенно если BaseImage выделяется для нескольких ОС-эс.
Чем ближе Аналогичный контейнер
1.0.0 Выпуск — это больше шансов, что вы попробуете постараться принимать Уход за следующим приложением базового изображения приложения.
Давайте кратко погрузимся в инструменты и компоненты, которые вам могут понадобиться.
Изображение здание ботистота с непревзойденным контейнером
Папки и файлы организации
Вы можете запустить процесс здания, используя любой инструмент, который вам нравится. С моего опыта — я нахожу до сих пор со следующим подходом: https://github.com/Voronenko/container-image-boilerplate
требования .txt
— Определяет любые конкретные инструменты PY & libs, которые вы хотите использовать вместе с Anisible-Container. .Projmodules
— Список зависимостей (ролей, развертываемых, и т. Д.) Необходимо скомпилировать базовое изображение. Формат, похожий на GitModules, но без прямых ссылок для совершения. Пример:
[submodule "roles/sa-nginx-container"] path = roles/softasap.sa-nginx-container url = https://github.com/softasap/sa-nginx-container.git [submodule "roles/sa-uwsgi-container"] path = roles/softasap.sa-uwsgi-container url = https://github.com/softasap/sa-uwsgi-container.git [submodule "roles/sa-include"] path = roles/softasap.sa-include url = https://github.com/softasap/sa-include.git [submodule "deployables/application"] path = deployables/application url = https://github.com/voronenko-p/django-sample.git
init.sh
&& init_quick.sh
— разрешить внешние зависимости в определенных местах (роли под ролями, развертывами под развертывами, и т. Д.)
Ansible.cfg
— По умолчанию пусто, но вы можете настроить параметры для вашего процесса сборки в соответствии с документацией.
version.txt
— информационный файл Gitflow на основе выпуска (x.y.z)
image.txt
— Дополнительная информация о изображении, построенном для метки и толкания деталей.
Makefile
&& Docker-helper.sh
— Оркестровские коммунальные услуги, обсуждаемые в следующей главе.
Docker-Compose.yml
— Обычно я также предоставляю конфигурацию Docker-Compose, поэтому изображение может быть немедленно попробовать.
Container.yml
— определение изображения, которое вы собираетесь построить
И, конечно, .gitignore.
— Мы определенно не хотим совершать внешние ресурсы. P-ENV
— Виртуальная среда, созданная во время сборки, Несеменное развертывание
— Переходный выход, производимый самим асетим-контейнером.
roles/* ansible-deployment/* p-env/* deployables/* *.retry
Создание процесса оркестора
Makefile Image Fallements после фаз: инициализация
чистый
Сбрасывает проект до начального состояния, удалив все строить каталоги и артефакты
clean: @rm -rf .Python p-env roles ansible-deployment
Инициализация
Анализируют .Projmodules и проверяет необходимые роли и развертываемые
initialize: @init_quick.sh
P-Env/Bin/Ansible-Container
Внутренняя задача — создает и инициализирует виртуальную среду с помощью Ansible-Container под P-ENV/Directory.
p-env/bin/ansible-container: p-env/bin/pip @touch $@ p-env/bin/pip: p-env/bin/python p-env/bin/pip install -r requirements.txt p-env/bin/python: virtualenv -p $(python) --no-site-packages p-env @touch $@
строить
Выполняет Ansible-Container для Container.yml, обеспечивая путь к ролям. Название проекта влияет на то, как будет называться изображения.
build: p-env/bin/ansible-container @p-env/bin/ansible-container --debug --project-name $(ROLE_NAME) build --roles-path ./roles/ -- -vvv @echo "Application docker image was build"
пробежать и остановиться
Потенциально, Ansible-Container позволяет немедленно запустить и останавливать изображения таким образом, как Docker-Compose. Из моего опыта, это не всегда работает,
- Container.yml основан на 2-й версии спецификации Docker-compose, в то время как мне обычно нужно как минимум 3.1 в производстве. В любом случае предусмотрены шаги — они могут работать в вашем случае. Я обычно заканчиваю с отдельным docker-compose.yml v3.1 + в каталоге.
run: p-env/bin/ansible-container @echo p-env/bin/ansible-container --debug --project-name $(ROLE_NAME) run --roles-path ./roles/ -- -vvv @p-env/bin/ansible-container --debug --project-name $(ROLE_NAME) run --roles-path ./roles/ -- -vvv @echo "Application environment was started" stop: p-env/bin/ansible-container @echo p-env/bin/ansible-container --debug --project-name $(ROLE_NAME) stop @p-env/bin/ansible-container --debug --project-name $(ROLE_NAME) stop @echo "Application environment was stopped"
Тег и толчок
Обычно в результате успешной сборки вы хотите подтолкнуть артефакт в какой-то реестр. Это высоко зависит от вашего проекта, и вы, скорее всего, настроили его для ваших потребностей.
Пример. Пример: правильно теги API и изображения Nginx построены и подталкивают их в Docker Hub.
tag: @docker tag $(ROLE_NAME)-api:latest softasap/sa-container-box-examples:$(ROLE_NAME).api.$(ROLE_VERSION) @docker tag $(ROLE_NAME)-api:latest softasap/sa-container-box-examples:$(ROLE_NAME).api.latest @docker tag $(ROLE_NAME)-nginx:latest softasap/sa-container-box-examples:$(ROLE_NAME).nginx.$(ROLE_VERSION) @docker tag $(ROLE_NAME)-nginx:latest softasap/sa-container-box-examples:$(ROLE_NAME).nginx.latest push: @docker push softasap/sa-container-box-examples:$(ROLE_NAME).api.$(ROLE_VERSION) @docker push softasap/sa-container-box-examples:$(ROLE_NAME).api.latest @docker push softasap/sa-container-box-examples:$(ROLE_NAME).nginx.$(ROLE_VERSION) @docker push softasap/sa-container-box-examples:$(ROLE_NAME).nginx.latest
Составные помощники
Запуск, остановка и демонстрация Docker-Compose управляемых контейнеров.
compose-up: @docker-compose up --no-recreate compose-stop: @docker-compose stop compose-down: compose-stop @docker-compose rm --force
Это все. Вернуться к первичной цели.
Для базового изображения я обычно хочу:
- Согласованная организация папки (я не хочу догадаться каждый раз, когда и как мне нужен логика для бега),
- Уплотняя система init.
- Дополнительная поддержка для работы нескольких процессов на контейнер (до тех пор, пока контейнер остается односторонним логическим блоком — это не противоречит Docker Philosophy).
- В зависимости от проекта, который я хочу использовать разные базовые изображения и не хотеть погружаться в специфику с компиляцией каждый раз.
- Возможность запустить логику под пользовательскими пользователями внутри контейнера и синхронизировать, при необходимости файлов и папок с хост-системой.
И окончательный момент: как можно дольше я хочу, чтобы мой код организовал лучше, чем серия команд Bash, которая обычно трудно читала и трудно настроить Таким образом, мы обычно делаем с программами. И это где Аналогичный контейнер
помогу.
Почему действительный init для контейнеров важен
Хотя люди говорят, что надлежащим докерским микромервицем следует запускать один выделенный процесс, не всегда достижимо в реальном мире. Более правильно сказать, что Docker предлагает философию проведения единой логической службы на контейнер. Логическая служба может состоять из множественных процессов ОС. Иногда вам действительно нужно бежать более одного сервиса в контейнере докера. Это особенно верно, если вы адаптируете некоторое приложение, которое ранее работало в автономной среде VPS.
Почему процесс INIT важен: работающие процессы могут быть визуализированы на дереве: каждый процесс может порождать дочерние процессы, и каждый процесс имеет родитель, кроме максимального процесса. Этот максимальный процесс — это процесс инициализации. Он начинается, когда вы запускаете свой контейнер и всегда имеете PID 1. Этот процесс INIT отвечает за запись остальной части системы, включая запуск вашего приложения. Когда какой-то процесс завершается, он превращается в SMTH, называемый «несущественным процессом», также известный как «процессом зомби» (https://en.wikipedia.org/wiki/zombie_process). По простым словам, это те, которые расторжены, но не были (все же) ждали их родительских процессов.
Но что, если родительский процесс прекращается (намеренно или непреднамеренно)? Что происходит, тогда к его порожденным процессам? У них больше нет родительского процесса Итак, они становятся «сиротами» (https://en.wikipedia.org/wiki/orphan_process).
И именно здесь инициативный процесс пинает. Это становится новым родителем (принимает) детские детские процессы, даже если они никогда не были созданы непосредственно процессом INIT. Ядро операционной системы автоматически обрабатывает усыновление. Более того: операционная система ожидает, что процесс инициативы тоже пожинать детей.
Что если нет? Пока зомби не удаляется из системы с помощью ожидания, он потребляет слот в таблице процессов ядра, и если эта таблица заполнила, это невозможно создать дальнейшие процессы в самой хост-системе. Кроме того, реализована система init init, часто приводит к неправильному обращению с процессами и сигналами и может привести к таким проблемам, как контейнеры, которые не могут быть изящены, или протекающие контейнеры, которые должны были быть уничтожены.
Больше чтения на тему:
- https://medium.com/@gchudnov/trapping-signals-in-docker-containers-7a57fdda7d86
- https://blog.phusion.nl/2015/01/20/docker-and-the-pid-1-zombie-reaping-problem/
Upstart, Systemd, Sysv обычно слишком тяжелыми (излишек), который должен использоваться внутри докера (+ не всегда легко возможно). Каковы варианты?
Кандидаты для процесса инициализации контейнера
В момент написания статьи чаще всего использовали подходы понижению:
Пользовательский письменный скрипт init
Согласно Docker Documentation, https://docs.docker.com/engine/admin/multi-service_container/посмотрите на пример доказательства концепции такого скрипта ниже
#!/bin/bash # Start the first process ./my_first_process -D status=$? if [ $status -ne 0 ]; then echo "Failed to start my_first_process: $status" exit $status fi # Start the second process ./my_second_process -D status=$? if [ $status -ne 0 ]; then echo "Failed to start my_second_process: $status" exit $status fi # Naive check runs checks once a minute to see if either of the processes exited. # This illustrates part of the heavy lifting you need to do if you want to run # more than one service in a container. The container will exit with an error # if it detects that either of the processes has exited. # Otherwise it will loop forever, waking up every 60 seconds while /bin/true; do ps aux |grep my_first_process |grep -q -v grep PROCESS_1_STATUS=$? ps aux |grep my_second_process |grep -q -v grep PROCESS_2_STATUS=$? # If the greps above find anything, they will exit with 0 status # If they are not both 0, then something is wrong if [ $PROCESS_1_STATUS -ne 0 -o $PROCESS_2_STATUS -ne 0 ]; then echo "One of the processes has already exited." exit -1 fi sleep 60 done
Будет работать, но на самом деле не гарантирует пожинать … Давайте рассмотрим более надежные альтернативы.
Глупость
Dumb-init — это простой процессорный руководитель и система init, предназначенная для работы в виде PID 1 внутри минимальных контейнеров (таких как Docker). Он развернут как маленький, статически связанный двоичный бинарный в C.
Dumb-init позволяет просто префикнуть вашу команду с Dumb-init. Он действует как PID 1, и сразу же появляется в вашей команде в качестве дочернего процесса, заботясь о правильном обращении и пересылающих сигналам, поскольку они получаются.
Project Repo: https://github.com/yelp/dumb-init
Тихо
Tini рекламирует себя как крошечный, но действительный init для контейнеров. Обещания:
- Защита от программного обеспечения, которое случайно создает процессы зомби
- Обеспечивает работу обработчиков сигналов по умолчанию для программного обеспечения, которое вы запускаете в своем документе Docker.
- Легко ввести: Docker Images, работаю без TINI, будет работать с TINI без каких-либо изменений.
Отправлено как сложный бинарный для HUGH разнообразие платформ.
Project Repo: https://github.com/krallin/tini
Запустить его
Runit — это кроссплатформенная схема init inix с руководством обслуживания, замена Sysvinit и других схем init. Он работает на GNU/Linux, BSD, может быть легко адаптирован к другим операционным системам Unix. Руководство программы предназначена для запуска в качестве процесса UNIX NO 1, он автоматически запускается автоматически заменой init/sbin/init, если это запускается ядром.
Сайт проекта: http://smarden.org/runit/
S6.
S6 является проектом, фактически бравшими руками по адресу http://skarnet.org/software/s6/overview.html. S6 содержит коллекцию коммунальных услуг, вращающихся вокруг контроля и управления процессором, регистрацией и инициализацией системы. Более того, специально для Docker существует вспомогательный проект, так называемый «S6-наложение» https://github.com/just-containers/s6-verlay
S6 предоставляет:
- Легкий процесс инициатива с поддержкой инициализации (CONT-INIT.D), доработка (cont-final.d), а также исправления прав собственности (Fix-attrs.d).
- S6-наложение обеспечивает правильную функциональность PID 1 внутри контейнера Docker. Процессы зомби будут правильно очищены.
- Поддержка нескольких процессов в одном контейнере («Услуги»)
- Пригодным для использования со всеми базовыми изображениями — Ubuntu, Centos, Fedora
- Распределен как один файл .tar.gz, чтобы сохранить количество слоев вашего изображения.
- Целый набор коммунальных услуг, включенных в S6 и S6-портативные Utils. Они включают удобные и композитные утилиты.
- Журнал Вращающийся вне коробки через Logutil-Service, который использует S6-журнал под капотом.
My_init.
Часть проекта Phance BaseImage Project https://github.com/phusion/BaseImage-Docker, который в настоящее время нацелен на Ubuntu 16:04, Ubuntu 14:04 OS-ES. Состоит из пользовательских письменных файлов PY и обернутая необязательного рутина.
Предоставляет
- Защита от программного обеспечения, которое случайно создает процессы зомби — пожинает, реализуется как часть скрипта управления.
- Кроме того, поддерживает файлы запуска в каталогах init.d и rc.local.
- Поддерживает дополнительные дополнительные услуги внутри контейнера через Runit: Cron, SSH
- обрабатывает дополнительную магию с окружающей средой
Требуется: Python внутри вашего контейнера. В настоящей форме только ограничена базовая система Ubuntu.
Руководство?
Это известный менеджер процессов обычно используется с приложениями Python. Я часто видел людей, пытающихся использовать его как систему init. Но: Supervisor явно упоминает, что это не должно управлять своим процессом INIT. Если у вас есть какой-то сам подпроцесс Fork, он не будет очищен руководителем. Таким образом, вы в любом случае должны выбрать другую систему init.
Хорошо, если вы в любом случае использовали его с вашим заявлением ранее.
Более ?
Если вы знаете больше кандидатов, пожалуйста, прокомментируйте.
Кандидаты для работы нескольких услуг внутри контейнера.
От упомянутых выше и в то же время легкий, хуже, чтобы упомянуть:
Супервизор
Классический руководитель, который даже не требует корневых привилегий. CTL-скрипт, который действует аналогично системным функциям SystemD. Поддерживает перезапуск процессов, а также обработчики событий на основе протоколов оболочки.
Запустить его
Рукослины с швейцарским ножом набор утилит, один из них runsvdir.
(http://smarden.org/runit/runsvdir.8.html) Это позволяет определять набор «обслуживания определений» в некоторых каталогах и ухаживает на запуск их при запуске.
Типичные примеры взаимодействия:
/usr/bin/sv статус/etc/Сервис/
— Получить статус услуг, перечисленных в папке конфигурации
/usr/bin/sv -w 10 down/etc/service/*
— Выключить все услуги с таймаутом 10
S6 (в объеме проекта S6-Overlay)
В отличие от супервайзера S6 использует структуру папки для управления службами, аналогичной руганию. S6-наложение требует их под/etc/services.d перед запуском init (затем копирует их в/var/run/s6/services).
services └ nginx └ run
Теперь каждый из этих файлов запуска является исполняемым, который S6 выполняет, чтобы начать процесс.
#!/bin/bash set -e exec nginx -c /etc/nginx/nginx.conf
Услуги контролируются двоичными S6-SVC. Он имеет количество вариантов, но основная идея заключается в том, что вы даете ей каталог службы. Так, например, если я хотел отправить SIGHUP в Nginx, я бы сделал S6-SVC -H/var/run/s6/services/nginx. Примечание: это/var/run, а не/etc/services.d; Это хуг отличия от рутина. И, наконец, то -h для SIGHUP.
S6-Overlay поставляется с несколькими встроенными версиями, поэтому вы можете загрузить тот, который соответствует вашу настройку Linux. Если вы хотите использовать S6 напрямую, пользователи Alpine и несколько других ароматизаторов Linux могут просто установить его из своего менеджера по пакетам. Мы бегаем Debian, и для этого нет PPA, поэтому нам придется компилировать S6 самостоятельно
Более ?
Если вы знаете больше кандидатов, пожалуйста, прокомментируйте.
Рабочие процессы как другой пользователь
По умолчанию контейнеры Docker работают как пользователь root. Это плохо, потому что:
- Приложение может изменить вещи, которые оно не должно быть
- Если приложение делится папкой с базовым хостом, все созданные файлы будут принадлежать корню
- Если контейнер скомпрометируется — ну, все же плохо, если они root.
Если вы хотите понять, насколько UID и GID работают в контейнерах Docker, посмотрите на эту статью: https://medium.com/@mccode/understanding-how-uid-and-gid-work-in-docker-containers-c37a01d01cf
Каковы варианты:
Докерский родной
Установить на уровне докерафила
# Create an app user so our program doesn't run as root. RUN groupadd -r app &&\ useradd -r -g app -d /home/app -s /sbin/nologin -c "Docker image user" app ... USER app
или пройти как параметры на Docker Run
docker run --rm --group-add audio --group-add nogroup --group-add 777 busybox id uid=0(root) gid=0(root) groups=10(wheel),29(audio),99(nogroup),777
Сценарий успеха, но не всегда возможно, если контейнер связывается с хостом.
каприз
Это утилита, которая поставляется с руганием (http://smarden.org/runit/chpst.8.html) — вы можете легко использовать его, если у вас уже есть Runit в рамках вашей системы init.
Phance’s Setuser
Если вы выбрали образ Phance в качестве основы, у вас есть setuser
по пути https://github.com/phuse/baseimage-docker/blob/master/image/bin/setuser.
Sudo.
Ну, вы можете установить Sudo внутри контейнера. Некоторые люди делают. Но ты хочешь?
С информацией выше у вас есть уже несколько идей для построения вашего базового изображения.
Извините за повторение, но позвольте мне еще раз подчеркнуть: если вы хотите альтернативу пользовательским макияжам и тоннам сложных файлов Shell, попробуйте Аналимный контейнер. Анализируемый контейнер обеспечивает лучшую альтернативу command && command && command
(и так далее) синтаксис вы боролись с созданием контейнеров. Поскольку Anisible находится в основе неисправного контейнера, вы можете сделать контейнер, полностью предсказуемый и повторяемый и более читаемый.
Я должен признать, что, хотя он уже прошел 0.9.1, несовместный контейнер по-прежнему в ранних возрастах с некоторыми громоздкими побочными эффектами время от времени (https://medium.com/@v_voronenko/evaluation-ansible-container -AS-A-Tool-for-custom-docker-Containers-build-500A0395A4C8), но он действительно становится более надежным каждому незначительному выпуску, поэтому, если вы попробуете его сейчас — ваше производство будет готово к понятию, когда он выпущен .. Отказ
Ранее беглый внутренний контейнер требуется установку многих ненужных пакетов, что вызывает большие размеры изображения. Ansible-Cointainer ввел другой подход: комбинация дирижера (управление контейнером с помощью аварийных и необходимых инструментов) и целевой контейнер. Это позволяет сохранить целевой размер изображения небольшого размера, и этот подход будет работать на любом базовом изображении, что позволяет устанавливать Anbible и Python.
Цель нынешнего доказательства концепции: Создайте роль Bootstrap для создания базового изображения приложения и, таким образом, упростить работу приложения.
Мы хотим: выберите предпочитаемую систему init: Tini, Dumb-init или init подход к Phance (Phusice-init)
container_init: "phusion-init" # "dumb-init" "tini-init" container_init_directory: /etc/my_init.d
Мы хотим: иметь возможность выбрать внутренний сервисный слой: Руководитель, руководитель или S6.
container_svc: "runit" # "supervisord"
После концепции базового изображения Phance мы хотим дополнительных услуг CRON, SSHD и SYLLOG в контейнере.
option_container_cron: true option_container_sshd: true option_container_syslog_ng: true
Если мы когда-нибудь хотели SSH внутри контейнера, давайте предоставляем KeyPair доверять.
container_ssh_private_key: "{{role_dir}}/files/keys/insecure_key" container_ssh_public_key: "{{role_dir}}/files/keys/insecure_key.pub"
Полученная игра компактна и читабена, как ваша обычная источника.
- include: tasks_system_services.yml - include: tasks_cron.yml when: option_container_cron - include: tasks_sshd.yml when: option_container_sshd - include: tasks_syslog_ng.yml when: option_container_syslog_ng
Более того — мы не ограничиваемся одним подходом — мы можем бесплатно выбрать систему INITY и SERVICE из списка поддерживаемых. Вы всегда можете добавить новый.
Для того, чтобы обеспечить некоторую совместимость между системами, я обычно
а) Поместите бегунов услуг в/etc/service/
Взгляните на пример роли, который может использоваться для создания такого базового изображения: https://github.com/softasap/sa-container-bootstrap
Типичная игра-контейнерная игра, используемая для создания изображения приложения, используя свою базовую игру, можно выглядеть как:
Система init init основана с руководителем в качестве менеджера услуг
version: "2" settings: conductor_base: ubuntu:16.04 volumes: - temp-space:/tmp # Used to copy static content between containers services: tini-supervisord: from: ubuntu:16.04 container_name: api roles: - { role: "softasap.sa-container-bootstrap", container_init: "tini-init", container_svc: "supervisord" } - { role: "softasap.sa-nginx-container", container_init: "tini-init", container_svc: "supervisord" } - { role: "../custom-roles/app-nginx-stub-deploy", container_init: "tini-init", container_svc: "supervisord" } expose: - '8000' - '22' volumes: - temp-space:/tmp # Used to copy static content between containers environment: IN_DOCKER: "1" volumes: temp-space: docker: {}
Система init init основана с руководителем в качестве менеджера услуг
version: "2" settings: conductor_base: ubuntu:16.04 volumes: - temp-space:/tmp # Used to copy static content between containers services: dumb-runit: from: ubuntu:16.04 container_name: api roles: - { role: "softasap.sa-container-bootstrap", container_init: "dumb-init" } - { role: "softasap.sa-nginx-container", container_init: "dumb-init" # optionally uses runit for services management and upstart. } - { role: "../custom-roles/app-nginx-stub-deploy", container_init: "dumb-init" } expose: - '8000' - '22' volumes: - temp-space:/tmp # Used to copy static content between containers environment: IN_DOCKER: "1" volumes: temp-space: docker: {}
Близко к базовому изображению Фюзии
version: "2" settings: conductor_base: ubuntu:16.04 volumes: - temp-space:/tmp # Used to copy static content between containers services: phusion-runit: from: ubuntu:16.04 container_name: api roles: - { role: "softasap.sa-container-bootstrap" } - { role: "softasap.sa-nginx-container", container_init: "phusion-init" # uses runit for services management and upstart. } - { role: "../custom-roles/app-nginx-stub-deploy" } expose: - '8000' - '22' volumes: - temp-space:/tmp # Used to copy static content between containers environment: IN_DOCKER: "1" volumes: temp-space: docker: {}
Еще примеры на https://github.com/voronenko/devops-docker-baseimage-demo
Немногие цифры на произведенные размеры
Base system: ubuntu 16.04 Init system: phusion_init Base image with all services: 268 MB / 101 MB on docker.hub Base image with demo nginx app installed: 313 MB / 133 MB on docker hub phusion/baseimage: 225 MB / 84 MB on docker hub
Резюме
Ошибный контейнер, как только он достигает стабильной версии, надеюсь, в этом году, по-видимому, является очень перспективным инструментом для внедрения комплексных трубопроводов на основе Docker Bate. Кроме того, Anisible Community позволяет вам повторно использовать количество доступных ролей — что потенциально может оптимизировать путь внедрения вашего развертывания.
Оригинал: «https://www.codementor.io/@slavko/using-ansible-container-to-build-your-next-application-base-image-c4eq2ise1»