# Docker Основы :::info **Вопросы для собеса** https://habr.com/ru/companies/southbridge/articles/528206/ ::: ## что такое контейнеры и для чего они нужны Контейнеры представляют из себя средства инкапсуляции приложения вместе с его зависимостями. Преимущества контейнеров: * контейнеры совместно используют ресурсы основной ОС, что делает их более эффективными * переносимость * возможность запускать десятки контейнеров, что дает имитацию работы промышленной распределенной системы. * быстрый запуск без возни с конфигурациями и зависимостями ### Сравнение контейнеров с ВМ На рисунке 1.1 показаны 3 приложения, работающие в отдельных ВМ на одном хосте. Здесь требуется гипервизор (2-го типа, который работает поверх хостовой ОС) для создлания и запуска ВМ, управляющим доступом к нижележащей ОС и к аппаратуре, а также интерпретирубщий системные вызовы. Для каждой ВМ необходима полная копия ОС, запускаемое приложения и все библиотеки. ![](https://hackmd.io/_uploads/ry7-q7mp3.jpg) На рисунке 1.2 описано как те же приложения будут работать в системе с контейнерами. В отличии от ВМ ядро-хоста совместно используется (разделяется) работающими контейнерами. Механизмы контейнера, отвечающие за пуск и остановку = гипервизор, однако не влекут доп накладных ресурсов, тк они равнозначнв процессам ОС. ![](https://hackmd.io/_uploads/BJlXqQXp3.jpg) ### Docker, контейнеры и файловая система Docker состоит из 2 компонентов: * Docker Engine - механизм, отвечающий за создание и функционирования контейнеров * Docker Hub - облачный сервис для распостранения контейнеров Open Container Project (Open Container Initiative) - общий стандарт формата контейнеров и механизма запуска Docker Swarm - менеджер кластеров, инстурмент командной строки для поддержки работы Docker-хостов Некоторые команды: docker inspect <имя контейнера> - информация о контейнере **docker rm -v $(docker ps -aq -f status=exited)** - удаление всех остановленных контейнеров **docker run -it --name <имя контейнера> --hostname <имя хоста> cowsay ubuntu bash** **docker commit <имя контейнера> <имя репозитория>/<имя создаваемого образа>** - превращение контейнера в образ Для контейнеров Docker использует файловую систему Union File System, которая позволяет монтировать несколько файловых систем в общую иерархию, которая выглядит как единая файловая система. Файловая система конкретного образа смонтирована как уровень только для чтения, а любые изменения в работающем контейнере происходят на уровне с разрешенной записью, монтированного поверх основной файловой системы образа. Поэтому Docker при поиске изменений (docker diff <имя>) работающей системе должен рассматривать только самый верхний уровень, на котором возможна запись. UFS (каскадно-объединенное монтировани) позволяет подключать несколько файловых систем с перекрытием (или наложением лруг на друга). Каталоги могут содержать файлы из нескольких файловых системе,но если двум файлам в точности соотвествует один и тот же путь, то файл смонтированный самым последним, скроет все раннее монтированные файлы. Узнать файловую систему можно через docker info | grep Storage Образы Docker состоят из нескольких уровней (layers). Каждый уровень представляет собой защищенную от записи файловую систему. Для каждой инструкции в Dockerfile создается свой уровень, которы размещается поверх предыдущих. Во время преобразования образа в контейнер (run or create) механизм докер выбирает нужный образ и добавляет на самом верхнем уровне файловую систему с возможностью записи (одновременно инициализируются различные параметры, такие как ip адресс, имя, идентификатор) :::warning Docker, по порядку, делает следующее: 1. скачивает образ ubuntu: docker проверяет наличие образа ubuntu на локальной машине, и если его нет — то скачивает его с Docker Hub. Если же образ есть, то использует его для создания контейнера; 2. создает контейнер: когда образ получен, docker использует его для создания контейнера; 3. инициализирует файловую систему и монтирует read-only уровень: контейнер создан в файловой системе и read-only уровень добавлен образ; 4. инициализирует сеть/мост: создает сетевой интерфейс, который позволяет docker-у общаться хост машиной; 5. Установка IP адреса: находит и задает адрес; 6. Запускает указанный процесс: запускает ваше приложение; Обрабатывает и выдает вывод вашего приложения: подключается и логирует стандартный вход, вывод и поток ошибок вашего приложения, что бы вы могли отслеживать как работает ваше приложение. ::: ::: success Контейнер может быть в следующих состояниях: * created * Restarting * Running * Paused * Exited * Stopped (exited, отличие в том, что он минимум 1 раз был running) ::: ::: success Для хранения образов применяется иерархическая система * реестр - сервис, отвечающий за хранение и распространение образов. (Docker Hub) * Репозиторий - набор взаимосвязанных образов (различные версии) * Тег - алфавитно-цифровой идентификатор, присылаемый образам внутри репозитория (1.1, latest) ::: :::info Пример генерации образа и выгрузки: docker build -y amount/cowsay Docker push amount/cowsay amount - репозиторий cowsay - имя образа ::: :::info Пространства имён для образов: * Пространство имён «user». Начинаются с /: amount/cowsay и созданы пользователями (amount) * Пространство имён root: официальные образы (Debian, nginx) * Имена с префиксами в виде имени хоста или ip-адреса представляют образы, хранящиеся в сторонних реестрах. ::: ## Архитектура Docker ![](https://hackmd.io/_uploads/S1qz4WW33.jpg) 1. В центре расположен **демон Docker (Docker daemon)**, отвественный за создание, запуск и контроль работы контейнеров, а так же за создание и хранение образов. Демон запускается ОС (команда docker daemon) 2. **Клиент (client)** используется для диалога с демоном по протоколу HTTP. По умолчанию это соединение устанавливается через сокет домена Unix, но так же может использовать TCP-сокет для поддержки соединения с удаленными клиентами или дискриптор файла для сокетов, управляемых systemd. 3. **Реестры Docker** используются для хранения и распорстранения образов. :::info **Про сокеты** https://zalinux.ru/?p=6293 ::: ### Базовые технологии Демон Docker использует драйвер выполнения (execution driver) для создания контейнеров. По умолчанию используется собственный драйвер Docker runc. :::info Дра́йвер — компьютерное программное обеспечение, с помощью которого другое программное обеспечение получает доступ к аппаратному обеспечению некоторого устройства ::: Драйвер runc связан со следующими механизмами ядра: * **cgroups** - механизм отвечающий за управление ресурсами, используемыми контейнерами * **пространства имен (namespaces)** отвечает за изоляцию контейнеров , гарантирует, что файловая система, имя хоста, пользователи, сетевая средат процессы любого контейра польностью изолированы от отсальной части системы :::info **Про cgroups** https://habr.com/ru/companies/selectel/articles/303190/ ::: ### Сопровождающие технологии Технологии Docker * **Swarp** - решение задачи кластерезации от Docker. Он позховояет сгрупировать несколько докер-хостов, после чего пользователь может работатьь с этой группой как с единным ресурсом. * **Docker compose** - инструмент для создания и выполнеения приложений, скомпонованных из нескольких контейнеров * **Docker machine** - устанавливает и конфигурирует докер-хосты на локальных и удаленных ресурсах. * **Kinematic** - графических интерфейс для винды и мака * **Docker trusted registry (Artifactory)** - локальное программное обеспечение для хранения и управления образами докер ## Как создаются образы Для команды *docker build* необходим Dockerfile и контекст создания образа. **Контекст создания** - набор локальных файлов и каталогов, к которым можно образаться из инструкций ADD и (или) COPY в Dockerfile :::success Пример: docker build -t test/cow-dockerfile . Контекст создания определяет ".", т е текущий каталог ::: Каждая инструкция Dockerfile приводит к появлению нового уровня образа, который так же может участвать в запуске контейнера. Новый уровень создается во время запуска контейнера с использованием образа предыдущего уровня. После успешного завершения выполнения инструкции Dockerfile вспомогательный контейнер удаляется, если в команде не был задан аргумент *--rm=false*. Так как результатом выполнения каждой инструкции является создание статического образа - в сущности, это фафловая система и некоторые метаданные, - все активные процессы в данной инструкции буду завершены. :::warning *docker history* - весь набор уровней ::: Создадим образ из следующего Dockerfile: :::danger FROM busybox:latest RUN echo "This should work" RUN /bin/bash -c echo "This will won't" ::: ![](https://hackmd.io/_uploads/rJ_xBHz33.jpg) ![](https://hackmd.io/_uploads/SJVuSSfnh.jpg) ### Кэшерирование Дляускоренного создания образов в Docker выполняется кэширование каждого уровня. При выполнении команды docker build, Docker выполняет инструкции из Dockerfile поочередно, создавая слои образа. Каждая инструкция добавляет новый слой, который является изменением по сравнению с предыдущим слоем. Когда Docker выполняет инструкцию, он проверяет, существует ли в кэше слой с такими же параметрами. Если существует, Docker может переиспользовать этот слой, что ускоряет процесс сборки. Преимущества использования кэширования в Docker: * Ускорение сборки: При повторных сборках образа Docker может переиспользовать предварительно созданные слои, избегая повторных вычислений и сокращая время сборки. * Эффективное использование ресурсов: Кэширование позволяет сократить потребление ресурсов, так как Docker не пересоздает слои, если они не изменились. * Повышение производительности: Благодаря сокращению времени сборки и использованию кэшированных слоев, разработчики могут более быстро создавать, тестировать и развертывать образы. Однако есть некоторые случаи, когда использование кэширования может привести к нежелательным результатам: * Изменения в Dockerfile: Если вы вносите изменения в Dockerfile (например, меняете инструкции), это может привести к изменению слоев и требовать полной пересборки образа без использования кэша. * Изменения зависимостей: Если зависимости приложения (например, пакеты или библиотеки) меняются, Docker также может потребовать полной пересборки, так как изменения в зависимостях могут повлиять на слои. * Кэширование устарелых данных: Иногда Docker может кэшировать данные, которые на самом деле устарели, и это может вызвать некорректное поведение приложения. Чтобы более гибко управлять кэшированием, Docker предоставляет флаг --no-cache, который позволяет полностью отключить использование кэша при сборке образа. ### Инструкции Dockerfile ![](https://hackmd.io/_uploads/S1PLgp4nn.png) :::info **Сайт по командам** https://habr.com/ru/companies/ruvds/articles/439980/ ::: ## Установление связи контейнеров с внешним миром Если мы запустили веб-сервер внутри контейнера, то для обеспечения связи сервера с внешним миром необходимо открыть порт с помощью аргумента -p или -P. :::success docker run -d -p 8000:80 nginx ::: Аргумент -p 8000:80 сообщил механизму Docker о необходимости перенаправления порта 8000 хоста на порт 80 в контейнере. В качестве альтернативе можно использовать аргумент -P, тогда Docker должен автоматически выбрать свободный порт. ## Соединение между контейнерами Соединения механизма Docker - простейший способ обеспечения обмена информацией между контейнерами на одном хосте. При использовании принятой по умолчанию сетевой модели Docker обмен данными между контейнерами будет происходить во внутренней сети Docker, то есть все коммуникационные операции останутся невидемыми из сети хоста. ![](https://hackmd.io/_uploads/HyPGVULnh.png) :::info Подробнее: https://habr.com/ru/articles/260053/ ::: :::danger Основной минус: статичность - -при перезапуске контейнеров соединения должны сохраняться, но они не обновляются, если контейнер-адресат заменен ::: ## Управление данными с помощью томов и контейнеров данных Тома Docker - каталоги, которые не являются частью файловой системы UnionFS конкретного контейнера, а представляют собой обычные каталоги в файловой системе хоста. **Способы инициализации томов:** 1. **С помощью флага -v** ![](https://hackmd.io/_uploads/ByGhdNmp2.jpg) 2. **C помощью инструкции VOLUME** ::: success FROM debian:wheezy VOLUME /data ::: **Установка прав доступа к тому в файле Dockerfile** 1. **С помощью Dockerfile** ::: success FROM debian:wheezy RUN useradd foo RUN mkdir /data && touch /data/x RUN chown -R foo:foo /data VOLUME /data ::: При запуске контейнера из этого образа Docker скопирует все файлы из каталога тома в образе в соотвутствующий том контейнера. Но этого не произойдет, если в качестве тома задан существующий каталог хоста ::: danger В данном случае последние 2 инструкции будут бесполезны, потому что они будут запускаться в томе временного контейнера, используемого для создания соответствующего уровня FROM debian:wheezy RUN useradd foo VOLUME /data RUN touch /data/x RUN chown -R foo:foo /data ::: 2. **Расширение аргумента -v** :::info docker run -v /home/adrian/data:/data debian ls /data ::: Здесь каталог файловой системы хоста /home/adrian/data монтируется как /data в контейнере. Все файлы, уже существующие в каталоге /home/adrian/data, становятся доступными внутри контейнера. Если каталог /data ранее существовал в контейнере, то его содержимое будет скрыто созданным томом. В отличии от других вариантов вызова, никакие файлы из образа не копируются в том, и этот том не будет удален механизмом Docker (docker rm -v не удаляет том, который монтируется на каталог, указанный пользователем) Можно совместно использовать одни и те же данные в нескольких контейнерах **docker run --name dbdata postgres echo "Data-only container"** (все тома, определенные в этом образе инициализируются до выполнения ehco) **docker run -d --volumes-from dbdata --name db1 postgres** :::danger Том невозможно удалить, пока существует хотя бы 1 контейнер, установивший связь с этим томом ::: :::info Тома удаляются если: * контейнер, содержащий тома, был удален командой docker rm -v * в команду docker run был включен флаг --rm * отсуствие контейнеров, связанных с удаляемыми томами * удаляемому тому не соответсвует какой-либо каталог ФС хоста ::: ## Часто используемые команды Docker ### Флаги docker run :::info * **-a, --attach**: подключает потоки к терминалу (по умолчанию stdout и stderror) **docker -a stdin debian** * **-d, --detach**: запускает контейнер в фоновом режиме в режиме "отключения всех потоков" * **-i, --interective**: поддерживает доступность открытого потока stdin * **--restart:** (**docker run --restart on-failure:10 postgres**) - если контейнер завершает работу с ненулевым кодом, то выполняется 10 попыток рестарта * **--rm**: автоматически удаляет контейнер после завершения сеанса * **-t, --tty**: создает терминал * **-e, --env**: определяет переменные среды **(docker run -e var1=val -e var2=val2)** * **-h, --hostname**: имя хоста **(docker run -h "myhostname" debian)** * **--name** - им контейнера * **-v, --volume**: **(docker run -it -v /data debian /bin/bash)** * **--volumes-from** - вольюмы взятые из другого контейнера * **--expose**: определяет номер или диапазон номеров портов, предназначенных для использования в контейнере * **--link**: настраивает интерфейс частной закрытой сети * **-p, --publish**: делает порт доступынм с хоста * **-P, --publish-all**, объявляет все порты,открытые в контейнере, доступными на хосте. Для каждого объявляемого порта произвольным образом выбирается свободный порт с большим номером. Чтобы увидеть установленные порты воспользоваться **docker port** ::: ### Управление контейнерами **docker attach [option] [name]** - взаимодействие с основным процессом внутри контейнера ![](https://hackmd.io/_uploads/SJ4ELnPTh.jpg) **docker create** - создает контейнер, но не запускает его **docker start** - запускает созданный контейнер **docker cp** - копирует файлы между файловыми системами контейнера и хоста **docker exec** - запускает заданную задачу внутри контейнера **docker kill** - посылает сигнал основному процессу (PID=1) **docker (un)pause** - временно приостанавливает все процессы внутри контейнера. Процессы не получают никакоих сигналов приостановки (как docker stop). Команда docker pause использует внутреннюю функциональную возможность freezing механизма cgroups в ядре Linux **docker restart*** = docker stop + docker start **docker rm** - удаляет контейнер. Аргумент -f позволяет удалять работающие контейнеры. С помощью аргумента -v можно удалить тома, созданные удаляемым контейнером (Если данные тома не смонтированы на каталоги и не используются лругими контейнерами) **docker start** - запускает контейнер **docker stop** - останавливает контейнер, но не удаляет его :::danger Чтобы выйти из контейнера, запущенном в интерактивном режиме и не остановить его нужно использовать **Ctrl+P or Ctrl+Q** ::: ### Информация о контейнере :::warning **docker info** - информация о системе docker и хосте ::: **docker diff** - показывает изменения в файловой системе контейнера по сравнению с файловой системой образа, который был использован для запуска этого контейнера ![](https://hackmd.io/_uploads/rJsPU2w6h.jpg) **docker-events** - выводит в реальном времени события от демона демону **docker inspect** - предоставляет подробную информацию о заданных контейнерах или образах. **docker logs** - выводит журналы (logs) для контейнера. Выводится все, что было записано в STDERR, STDOUT **docker port** - выводит список отображений открытых портов для заданного контейнера ![](https://hackmd.io/_uploads/ryx9U2DT2.jpg) ![](https://hackmd.io/_uploads/rke5U2P6h.jpg) **docker ps** - список контейнеров **docker top** - предоставляет информацию о процессах, выполняющихся внутри заданного контейнера. ### Работа с образами **docker build** - создает образ из файла Dockerfile **docker commit** - создает образ из указанного контейнера. По умолчанию контейнеры временно преостанавливаются перед созданием образа, но это можно исправить при помщи --pause=false **docker export** - экспортирует содержимое файловой системы заданного контейнера в виде tar архива, напрявляя его в STDOUT(docker export <container> > lates.tar) **docker history** - выводит информацию о каждом уровне в образе **docker images** - выводит список локальных образов. Существует полезный флаг -q, возвращающий только идентификаторы образов, что удобно для передачи в поток ввода других команд: **висячие образы - слои, не связанные с используемыми образами** ![](https://hackmd.io/_uploads/Hksfj3dah.jpg) **docker import** - создает образ из архивного файла, содержащего файловую систему и созданного командой docker export. Следует отметить, что образ, созданный командой docker import, состоит только из 1 уровня, и в нем отсутствуют параметры кнфигурации Docker: объявленные порты, значения инструкций CMD. ![](https://hackmd.io/_uploads/HkDHohO62.jpg) **docker load**: загружает репозиторйи из tar архив, передаваемый через STDIN. В отличии от docker import образы содержат метаданные **docker save**: сохраняет именованные образы или репозитории в tar архи, передаваемый в STDOUT (для записи в файл -о). ![](https://hackmd.io/_uploads/BJtIohOa3.jpg) ![](https://hackmd.io/_uploads/HkyDs3O62.jpg) **docker rmi**: удаляет заданный образ **docker tag**: связывает имя репозитория и тега с заданным образом ![](https://hackmd.io/_uploads/B1ZOi3da3.jpg) ### Команды для работы с реестро **docker login** - процедура входа или регистрации **docker logout** - процедура выхода **docker pull** - загрузка образа из реестра **docker push** - выгрузка образа или репозитория в заданный реестр **docker search** - выводит список общедоступных репозиториев из реестра. ### Часто использую docker stop $(docker ps -l -q) docker rm $(docker ps -lq) docker rm $( docker ps -q -f status=exited) docker image prune -a: удаление всех неиспользуемых образов sudo docker system prune: удаляет все неиспользуемые контейнеры, сети, образы (как висящие, так и не имеющие ссылок) и, при необходимости, тома. # Использование Docker в разработке ## Каталог хоста с исходными кодами как отдельная файловая система внутри контейнера Пусть у нас есть простое Flask приложение, которое выводит "Hello, Docker!" Созадим следующий Dockerfile :::success FROM python:3.4 RUN pip install Flask==2.3.3 WORKDIR /app COPY app /app CMD ["python", "identidock.py"] ::: Соберем образ: **docker build -t identitock** Смонтируем каталог хоста с исходными кодами как отдельную файловую систему внутри контейнера (для того, чтобы при изменении не собирать образ заново и не перезапускать контейнер) **docker run -d -p 5000:5000 -v "$(pwd)"/app: /app identidock** В данном примере мы смонтировали тот же каталог, который был добавлен в образ с помощью команды COPY. Таким образом сейчас мы используем один и тот же каталог на хосте и внутри контейнера, а не его копию из образа. Именно поэтому мы можем редактировать файл исходного файл исходного кода и сразу видеть результат внесенных изменений. ## Переключению среды разработки Для того, чтобы свести к минимумум различия между средами разработки и эксплуатации можно использовать один и тот же образ с переключением среды через переменную окружения. :::info uWSGI - готовый к промышленной эксплуатации сервер приложений, который также может работать в связке с обычным веб-сервером, таким как nginx. ::: Пусть у нас есть простое Flask приложение, которое выводит "Hello, Docker!" Созадим следующий Dockerfile :::success FROM python:3.11.4 RUN groupadd -r uwsgi && useradd -r -g uwsgi uwsgi (1) RUN pip install Flask==2.3.3 && pip install uWSGI==2.0.22 WORKDIR /app COPY app /app EXPOSE 9090 9191 (2) USER uwsgi (3) CMD ["uwsgi", "--http", "0.0.0.0:9090", "--wsgi-file", "/app/identidock.py",\ "--callable", "app", "--stats", "0.0.0.0:9191"] (4) ::: (1): Создание группы и пользователя uwsgi ю (2): Объявление портов (3): определение имени пользователя uwsgi для всех последующих строк (4): Здесь uWSGI получает указание запустить http-сервер, прослушивающий порт 9090 и инициализирующий приложение app из файла app/identidock.py. Также инициализирующий сервер наблюдения за состоянием с прослушиванием порта 9191. Вынесем инструкцию, связанную с uWSGI в bash скрипт: https://github.com/using-docker/using_docker_in_dev/blob/master/identidock/cmd.sh ``` #!/bin/bash set -e if [ "$ENV" = 'DEV' ]; then echo "Running Development Server" exec python "identidock.py" else echo "Running Production Server" exec uwsgi --http 0.0.0.0:9090 --wsgi-file /app/identidock.py \ --callable app --stats 0.0.0.0:9191 fi ``` При выполнении любой команды в оболочке Bash по умолчанию создается подоболочка, и порождается новый дочерний процесс (forked) для выполнения команды. Однако при использовании команды exec команда, следующая за exec, заменяет текущую оболочку. Это означает, что под-оболочка не создается, а текущий процесс заменяется этой новой командой. Dockerfile будет выглядеть следующим образом: :::success FROM python:3.11.4 RUN groupadd -r uwsgi && useradd -r -g uwsgi uwsgi RUN pip install Flask==2.3.3 && pip install uWSGI==2.0.22 WORKDIR /app COPY app /app COPY cmd.sh / EXPOSE 9090 9191 USER uwsgi CMD ["/cmd.sh"] ::: Для определения среды используем команду с флагом -е: **docker run -e "ENV=DEV" -p 5000:9090 indentidock** С помощью комынды docker logs мы сможем получить информацию из журналов uWSGI, что будет полезным во время промышленной эксплуатации :::info Docker run -d -P --name port-test identidock C помощью команды docker port port-test можно определить с какими портами хоста связаны порты в контейнере. ::: :::info **uwsgi nginx** https://www.8host.com/blog/nastrojka-uwsgi-i-nginx-dlya-obsluzhivaniya-prilozhenij-python-v-ubuntu-14-04/ ::: ## Автоматизация с использованием Compose Инструмент Docker Compose предназначен для быстрой найстройки и запуска различных вариантов сред разработки Docker. Пример: :::success services: identidock: (1) build: . (2) ports: (3) - "5000:5000" environment: (4) ENV: DEV volumes: (5) - ./app:/app ::: (1): имя создаваемого контейнера (2): ключ build сообщает, что образ данного контейнера будет браться из Dockerfile (может содержать imgae:<image_name>) (3): объявление портов (4): ключ enviroment для определение среды (5): определение томов Теперь, вместо того, чтобы останавливать контейнер и перезапускать его с новой переменной окружения, мы можем менять переменную окружения в docker-compose файле и перезапускать его. ## Развертывание нескольких взаимодействующих сервисов При сосдании проекта из нескольких приложений, мы можем настроить их взаимодействие во внутренней сети Docker, то есть все коммуникационные операции останутся невидемыми из сети хоста. Рассмотрим на примере веб-приложения, которое состоит из окошка для ввода имя. Причем после ввода приложение генерирует идентификационную пинтограмму по введенному имени. ![](https://hackmd.io/_uploads/HJmpupxRh.png) Для этого понадобится Docker-сервис изображений dnmonster. Развернем данное приложение, обязательно указав имя контейнера **docker pull amouat/dnmonster** **docker run -d --name dnmonster amouat/dnmonster** Затем развернем приложение из прошлой части, указав линк на прошлый контейнер в формате: <имя>:<имя под которым контейнер будет известен запускаемому> **docker run -d -p 5000:9090 -e "ENV=prod" --link dnmonster:dnmonster indentidock:latest** Теперь продемонстрируем как можно сделать все то же самое, только с использованием docker-compose: :::success services: identidock: image: indentidock:latest ports: - "5000:9090" environment: ENV: PROD volumes: - ./app:/app links: - dnmonster dnmonster: image: amouat/dnmonster:latest ::: ![](https://hackmd.io/_uploads/rk5P26lA3.png) Кроме того, если делать изменения при ENV: DEV, то изменения будут применться без пересборки проекта (из прошлых пунктов) **Преимущества микросервисной архитектуры:** * Проще масштабировать прогруммную среду, распостраняя ее на большое количество сетевых компьютеров * Отдельные микросервисы можно быстро и легко заменять на более современные аналоги или откатываться к предыдущим версиям ## Работа с репозиториями и реестром Считается плохим тоном использование тег latest, который используется автоматически. Для смены тега следует использовать следующую команду: ![](https://hackmd.io/_uploads/Byl8UIaQA3.png) ### Организация собственного реестра Простейшим способом создания окального реестра является использование официального образа. Быстрый запуск реестра осуществляется командой: :satellite: **docker run -d -p 5000:5000 registry:2** :satellite: Теперь у нас есть работающий реестр, и мы можем присваивать образам соответствующие теги и выгружать их в этот реестр. При использовании механизма docker-machine остается возможность указания адреса localhost, поскольку он правильно интепретируется механизмом Docker (в отличии от клиента), который работает на том же хосте, что и реестр. ![](https://hackmd.io/_uploads/r17yacr03.png) Если попробовать взаимодействие с данным реестром с другого хоста, то ничего не получится, потому что демон Docker запретит соединение, т.к на этот хост не имеет действительного сертификата TLS. До этого соединение разрешалось потому что в механизме Docker предусмотрено исключение для загрузки с серверов, расположенных на локальном хосте. Данную проблему можно решить следующим путем: установить на хосте реестра самоподписанный сертификат и скопировать его на все хосты демонов docker, которым должен быть предоставлен доступ к этому реестру. ``` openssl req `# создать новый сертификат/ключ` \ -x509 -sha256 `# создать сертификат, а не запрос на подпись сертификата` \ -days 365 `# на 1 год` \ -newkey rsa:2048 `# сгенерировать новый ключ размером 2048 бит с алгоритмом RSA` \ -keyout -keyout /registry_certs/domain.key `# файл для сохранения ключа` \ -out registry_certs/domain.crt `# файл для сохранения сертификата` ``` Теперь необходимо скопировать сертификат в каждую систему демона docker (/etc/docker/certs.d/<название реестра:порт>), которому потребуется доступ к реестру. ``` sudo mkdir -p /etc/docker/certs.d/registry:5000 sudo cp registry_certs/domain.crt /etc/docker/certs.d/registry:5000/ca.crt sudo systemctl restart docker ``` После чего необходимо запустить реестр ``` docker run -d -p 5000:5000 -v $(pwd)/registry_certs: /certs -e REGISTRY_HTTP_TLS_CERTIFICAT = /certs/domain.crt -e REGISTRY_HTTP_TLS_KEY = /certs/domain.key --restart=always --name registry registry:2 ``` **Хранилище** По умолчанию образ реестра использует драйвер файловой системы , который сохораняет все данные и образы в соотвествующей файловой системе. Для этого необходимо объявить том в ранее определенном корневом каталоге и связать его с надежным хранилищем файлов. Например, в подкаталог /var/lib/registry **Аутентификация** Существует 2 способа организации процедуры 1. Установить и настроить защищающий реестр прокси-сервера, например nginx, который будет отвечать за утенфикацию пользователей. 2. Токен-аутенфикация с использованием json web tokens. При применении этого метода реестр будет отказывать в обслуживании клиентам, не предъявившим корректного токена, но будет перенаправлять их на сервер аутенфикации **Сокращение размера образа** Общий конечный размер образа представляет собой сумма размеров всех его уровней. (Если создать на одном уровне файл, например 50 MB, а на следующем удалить, то размер будет на 50MB тяжелее, нежели если бы файл был создан и удален на одном уровне) # Непрерывная интеграция и тестирование с использованием Docker ## Включение модульных тетов Филосовия Docker в отношении использования одного и того же образа в процессах разработки, тестирования и эксплуатации предполагает включение тестов в образ. Для этого необходимо включить в образ файл с тестами (для приложений на python можно использовать библиотеку unittest) **Пример**: https://github.com/using-docker/ci-testing/blob/master/identidock/app/tests.py Для переключения в режим тестирования можно использовать переменную окружения при которой запускается файл tests **Полезные модули при тестировании:** Модуль Mock: макеты-пустышки в тестировании https://habr.com/ru/articles/141209/ **Недостатки:** * Увеличение размера образа **Преимущества:** * Простота (тесты и сама программа в одном образе) * Надежность ## Создание контейнера для Jenkins Jenkins - широко известный сервер непрерывной интеграции. Сервер необходимо настраивать таким образом, чтобы при любых изменениях в проекте он автоматически проверял внесенные изменения, создавал новые образы и выполнял для них определенные операции тестирования. Для этого существует 2 решения: :::info Docker-сокет – это точка входа (endpoint), используемая для обмена данными между клиентом и демоном. По умолчанию это IPC-сокет, доступный через файл /var/run/docker.sock, но Docker также поддерживает TCP-сокеты, определяемые по сетевому адресу, и сокеты механизма systemd. В этой главе предполагается использование сокета по умолчанию /var/run/docker.sock. Так как доступ к такому сокету организован через дескриптор файла, можно просто смонтировать эту точку входа как том внутри контейнера. ::: 1. смонтировать размещенный на хосте Docker-сокет внутри контейнера, тем самым позволяя серверу Jenkins успешно создавать образы-«братья» того же уровня. 2. Другое решение состоит в использовании методики Docker-в-Docker (Docker-in-Docker – DinD), при которой Docker-контейнер может создавать собственные контейнеры-потомки. Для такого подхода необходима особая конфигурация, касающаяся запуска контейнера в привилегированном режиме. ![](https://hackmd.io/_uploads/B1zr-cjA2.png) :::danger Главное отличие между методиками в том, что контейнеры созданные по методике DiD полностью изолированны от контейнера хост. При выполнениикоманды docker ps внутри DiD-контейнера будут показаны только контейнеры, созданные демоном DiD Docker. При использовании методики монтирования сокета команла покажет все онтейнеры все зависимости от места запуска команды ::: # Docker machine **Docker Machine** — это утилита, которая нужна вам для того, чтобы устанавливать Docker Engine на удаленные физические или виртуальные сервера, а также управлять ими при помощи команды docker-machine. Вы можете использовать Docker Machine для создания Docker хостов на вашем Mac или Windows ПК, в корпоративной сети вашей компании, в вашем датацентре или в различных Облаках. Используя команды docker-machine, вы можете запустить, проверить, остановить, перезапустить управляемый ей хост, обновить клиент и демон Docker, а также настроить клиент Docker на работу с удаленным хостом. **Сценарии использования:** 1. У меня есть устаревший ПК, и я хочу иметь возможность запустить Docker на Mac или Windows :::success Если вы работаете на устаревшем Mac или Windows ноутбуке или рабочей станции, которые не удовлетворяют последним требованиям Docker для Mac или Docker для Windows, значит, вам необходима Docker Machine, чтобы все таки «запустить Docker» (имеется в виду, Docker Engine) у себя локально. Во время такой установки Docker Machine на ваш Mac или Windows при помощи Docker Toolbox, на самом деле у вас на ПК устанавливается локальная виртуальная машина, внутри которой и будет работать Docker Engine. Docker Machine в данном случае даст вам возможность настроить подключение клиента Docker (команда docker) к Docker Engine в этой виртуальной машине. ::: 2. Я хочу установить Docker на удаленные виртуальные машины или физические серверы :::success Docker Engine — это нативное приложение для Linux систем. Если у вас Linux в качестве основной системы и вам нужно иметь возможность запускать команду docker, все что вам нужно сделать — это скачать и установить Docker Engine. Тем не менее, если вам нужен эффективный способ работать с множеством Docker хостов в сети, в Облаке или же локально, вам нужна Docker Machine. Вне зависимости от того, используете ли вы Mac, Windows или Linux, вы можете установить Docker Machine и использовать команду docker-machine для подготовки и управления большим количеством Docker хостов. Она автоматически устанавливает на хост Docker, т.е. устанавливает Docker Engine, затем помогает настроить и клиент docker. ::: **Какова разница между Docker Engine и Docker Machine?** Когда говорят «Docker», обычно имеют в виду Docker Engine, т.е. клиент-серверное программное обеспечение, состоящее из демона Docker, REST API, который определяет интерфейсы взаимодействия с демоном и клиентский консольный командный интерфейс, т.е. клиента docker, который общается с демоном при помощи обертки над REST API. ![](https://hackmd.io/_uploads/rJ7l8Gnx6.png) Docker Machine — это утилита для подготовки и управления вашими докеризированными хостами (имеются в виду хосты с установленным на них Docker Engine). Обычно Docker Machine устанавливается на вашу локальную систему. У Docker Machine есть свой консольный клиент docker-machine так же, как клиент для Docker Engine — docker. Вы можете использовать Docker Machine для установки Docker Engine на один или более виртуальных серверов. Эти виртуальные серверы могут быть локальными или удаленными. ![](https://hackmd.io/_uploads/Sk7fIGneT.png) :::info https://cloud.croc.ru/blog/about-technologies/docker-machine/ ::: # Конфигурация хоста ## Выбор драйвера файловой системы Драйверы хранилища Docker контролируют, как изображения и контейнеры хранятся в вашей файловой системе. Это механизм, который позволяет вам создавать изображения, запускать контейнеры и изменять доступные для записи слои. Вот различия между каждым драйвером и ситуациями, в которых их следует использовать. Когда запускается новый контейнер, Docker сначала извлекает слои изображения, созданные путем построения его файла Dockerfile. Слои сохраняются на вашем хост-компьютере, поэтому вам не нужно снова загружать изображение, пока вы не захотите получить обновления. В рамках процесса извлечения Docker идентифицирует и повторно использует слои, которые у него уже есть, избегая избыточных загрузок. Как только слои изображения станут доступны, Docker запустит контейнер и добавит сверху дополнительный слой. Это доступный для записи уровень, который может изменять контейнер. Все нижние уровни неизменяемы и являются производными от их определений Dockerfile. Слой с возможностью записи хорошо работает с небольшими накладными расходами, когда вы просто добавляете файлы в файловую систему контейнера. Они попадают в слой с возможностью записи наверху стека. Однако изменения в существующие файлы вызывают больше проблем: они существуют на нижних уровнях, доступных только для чтения, но теперь в них нужно записывать. Подход Docker заключается в «копировании при записи», то есть файл копируется из исходного слоя в доступный для записи уровень в момент модификации. Это операция с интенсивным вводом-выводом, которая может привести к снижению производительности. Различные драйверы хранилища несут ответственность за реализация поддержки копирования при записи. Каждый драйвер предлагает уникальный компромисс между производительностью и эффективностью использования диска. Вы можете проверить текущий драйвер, запустив **docker info | grep “Storage Driver”** ### overlay2 Драйвер overlay2 теперь используется по умолчанию во всех активно поддерживаемых дистрибутивах Linux. Для этого требуется резервная файловая система ext4 или xfs. overlay2 предлагает хороший баланс между производительностью и эффективностью для операций копирования при записи. Когда требуется копирование при записи, драйвер просматривает слои изображения, чтобы найти нужный файл, начиная с самого верхнего слоя. Результаты кешируются, чтобы ускорить процесс в следующий раз. Как только файл найден, он копируется на доступный для записи уровень контейнера. Затем копия модифицируется с изменениями, запрошенными контейнером. С этого момента контейнер видит только новую скопированную версию файла. Оригинал в нижнем слое изображения становится непрозрачным для контейнера. overlay2 работает на уровне файлов, а не на уровне блоков. Это повышает производительность за счет максимальной эффективности использования памяти, но может привести к увеличению доступных для записи слоев при внесении большого количества изменений. ### btrfs и zfs Эти два драйвера работают на уровне блоков и идеально подходят для операций с интенсивной записью. Каждому из них требуется соответствующая резервная файловая система. Использование этих драйверов приводит к тому, что ваш каталог / var / lib / docker хранится на томе btrfs или zfs. Каждый слой изображения получает свой собственный каталог в папке вложенных томов. Пространство выделяется каталогам по требованию по мере необходимости, что позволяет поддерживать низкий уровень использования диска до тех пор, пока не будут выполнены операции копирования при записи. Базовые слои изображения хранятся в файловой системе в виде подтомов. Другие слои становятся снимками, содержащими только те различия, которые они вносят. Изменения записываемого уровня обрабатываются на уровне блоков, добавляя еще один снимок с экономией места. Вы можете создавать снимки подобъектов и другие снимки в любое время. Эти моментальные снимки продолжают обмениваться неизмененными данными, сводя к минимуму общее потребление памяти. Использование одного из этих драйверов может улучшить работу с контейнерами, интенсивно использующими запись. Если вы пишете много временных файлов или кешируете много операций на диске, btrfs или zfs могут превзойти overlay2. Тот, который вам следует использовать, зависит от вашей файловой системы резервного копирования – обычно zfs предпочтительнее как более современная альтернатива btrfs. # Ведение журналов событий и контроль **Принятая по умолчанию подсистема ведения журналов событий в Docker** Начнем с описания самого простого решения, которое включено в базовый комплект Docker. Если не заданы соответствующие аргументы и не установлено специальное ПО для ведения журналов, то Docker будет фиксировать все события, отправляя сообщения о них в стандартный поток вывода STDOUT и в стандартный поток ошибок STDERR. В дальнейшем эти записи можно извлечь командой docker logs. У подсистемы ведения журналов событий, принятой по умолчанию в Docker, есть некоторые недостатки. Она может работать только со стандартными потоками STDOUT и STDERR, что создает трудности, если ваше приложение фиксирует события только в файле. Кроме того, отсутствует ротация журналов, и если вы попытаетесь использовать приложения, подобные yes (которое просто периодически записывает слово «yes» в стандартный поток вывода STDOUT), для подтверждения работы контейнера, то очень скоро на диске не останется свободного места. ![](https://hackmd.io/_uploads/Hyrus89-T.png) Существуют некоторые другие методы ведения журналов, которые можно применить с помощью аргумента --log-driver в команде docker run. Механизм ведения журналов, принятый по умолчанию, заменяется на переданный через аргумент --log-driver при инициализации демона Docker. Возможные варианты значений, определяющих механизмы ведения журналов: * json-file – механизм, принятый по умолчанию, который описан выше; * syslog – драйвер syslog, который будет рассматриваться ниже; * journald – драйвер для ведения журналов под управлением systemd; * gelf – драйвер Graylog Extend Log Format (GELF); * fluentd – перенаправляет журнальные записи в fluentd * none – отключает ведение журналов событий. **Объединение журналов** Необходимо объединить все журналы (в том числе и с разных хостов) в единой точке, чтобы появилась возможность обрабатывать их аналитическими и контролирующими инструментальными средствами. Для решения этой задачи предлагаются две основные методики: 1) запуск внутри всех контейнеров второго процесса, который работает как посредник, пересылающий все журнальные записи в объединяющий сервис; 2) сбор всех журнальных записей на конкретном хосте или в отдельном независимом контейнере и перенаправление их в объединяющий сервис. Первая методика вполне работоспособна и даже иногда применяется, но она приводит к росту размеров образов и к не вполне оправданному увеличению количества активных процессов, поэтому мы будем рассматривать только вторую методику. Доступ с хоста к журналам контейнера можно организовать несколькими способами: 1. использование интерфейса прикладного программирования Docker API. Несомненным преимуществом является официальная поддержка API, а накладные расходы, связанные с применением HTTP-соединения, невелики. 2) при использовании драйвера syslog можно воспользоваться его функциональностью для автоматического перенаправления журнальных записей, 3) можно организовать прямой доступ к файлам журналов из каталога Docker. Чтобы добавить механизм ведения журналов событий в наше приложение identidock, воспользуемся так называемым стеком ELK. ELK – это Elasticsearch, Logstash и Kibana: * Elasticsearch – механизм текстового поиска, работающий почти в реальном времени. Создан как легко масштабирующийся при наличии множества сетевых узлов для обеспечения обработки больших объемов данных, поэтому наилучшим образом подходит для поиска в многочисленных журнальных записях; * Logstash – инструмент для чтения необработанных журнальных записей с последующим их синтаксическим разбором и фильтрацией перед отправкой в другой сервис. * Kibana (https://github.com/elastic/kibana) – основанный на JavaScript графический интерфейс для Elasticsearch. ![](https://hackmd.io/_uploads/r1707P9WT.png) **Хранение и ротация журналов** После выбора драйвера журналов и средств анализа журнальных записей необходимо решить, каким образом организовать хранение журналов, и определить срок их хранения. Если эти вопросы оставить без внимания, то, вероятнее всего, контейнеры будут использовать механизм ведения журналов по умолчанию и постепенно «съедят» все пространство на жестком диске, что приведет к критическому сбою хоста. Утилиту logrotate ОС Linux можно использовать для управления размерами файлов журналов. Обычно используется несколько так называемых поколений (версий) файлов журналов, и через регулярные интервалы времени файлы журналов перемещаются по линии поколений. Например, в дополнение к текущей версии журнала могут существовать поколения «отец», «дед» и «прадед» как отдельные версии журналов. Версии «дед» и «прадед» подвергаются процедуре сжатия для более компактного хранения. Каждый день текущая версия журнала перемещается в поколение «отец», в свою очередь, бывший «отец» сжимается и перемещается в поколение «дед», бывший «дед» становится «прадедом», а бывший «прадед» удаляется. :::success Если приложение фиксирует события только в файле, а не выводит сообщения в поток STDOUT/STDERR, то и здесь сохраняются некоторые возможности их обработки. Если вы уже используете интерфейс Docker API для ведения журналов (например, с помощью контейнера Logspout), то самым простым решением будет запуск процесса (обычно это tail -F), который просто выводит содержимое файла журнала в стандартный поток STDOUT ::: ## Жихненный цикл контейнеров ![](https://hackmd.io/_uploads/r12TXwcWa.png) # Сетевая среда и обнаружение сервисов ## Посредники Использование посредников (ambassadors) представляет собой один из способов установления соединений между контейнерами, расположенными на разных хостах. Это промежуточные контейнеры (прокси-контейнеры), которые замещают реальный контейнер (или сервис) и перенаправляют трафик в действительный сервис. Посредники обеспечивают разделение обязанностей, что делает их полезными во многих ситуациях, а не только при установлении соединений между сервисами, расположенными на разных хостах. **Преимущества:** * Позволяют отделить эксплуатационную сетевую архитектуру от архитектуры этапа разработки без внесения каких-либо изменений в код. Разработчики могут использовать локальные версии баз данных и прочих ресурсов, точно зная, что посредники могут переориентировать приложение на использование указанных кластерных сервисов или удаленных ресурсов без каких-либо изменений в исходном коде. * Посредники способны оперативно, без паузы в работе переключаться на другой внутренний сервис, в то время как использование прямого соединения с конкретным сервисом потребует перезапуска контейнера-клиента. **Недостатки:** * для них требуется дополнительная настройка, соответственно, и дополнительные издержки, а кроме того, посредники представляют собой потенциальные точки отказа. ![Снимок экрана (401).png](https://hackmd.io/_uploads/rJ92MlH7T.png) ## Обнаружение сервисов etcd – это распределенное хранилище данных в формате «ключ-значение». Каждый член в кластере etcd запускает экземпляр бинарного файла etcd, который обменивается информацией с другими членами. Клиенты получают доступ к etcd через интерфейс REST, предоставляемый всеми членами кластера. ZooKeeper – централизованное, надежное, обеспечивающее высокую степень доступности хранилище, используемое для координации работы сервисов в Mesos и Hadoop. Решение реализовано на языке Java, доступ осуществляется через Java API, хотя доступны интерфейсы к некоторым другим языкам программирования. От клиентов требуются поддержка активных соединений с серверами ZooKeeper и поддержка механизма keepalive, то есть необходимо написание значительного объема кода. ZooKeeper – зрелое, стабильное и тщательно протестированное решение. Если у вас уже есть инфраструктура, использующая ZooKeeper, то, возможно, это будет лучшим вариантом. В противном случае дополнительные трудозатраты по интеграции и созданию инфраструктуры на основе ZooKeeper, вероятнее всего, не оправдаются, особенно если вы не используете язык Java; ## Варианты организации сетевой среды