Инструменты пользователя

Инструменты сайта


learning:diamol

Различия

Показаны различия между двумя версиями страницы.

Ссылка на это сравнение

learning:diamol [18.05.2023 10:13] – [17. Оптимизация образов по размеру, скорости и безопасности] viacheslavlearning:diamol [30.07.2024 19:21] (текущий) – внешнее изменение 127.0.0.1
Строка 1: Строка 1:
 +====== Docker in a month ======
 +<code bash>
 +# Клон учебного репозитория
 +git clone https://github.com/sixeyed/diamol.git
 +# грохнуть все контейнеры, в т. ч., запущенные
 +docker container rm -f $(docker container ls -aq)
 +# грохнуть все образы diamol
 +docker image rm -f $(docker image ls -f reference='diamol/*' -q)
 +</code>
 +===== 2. Начало =====
 +<code bash>
 +# Интерактивно зайти внутрь контейнера (-it - interactive, tty)
 +docker container run -it diamol/base
 +
 +# Список запущенных контейнеров. ID контейнера = его hostname
 +docker ps
 +CONTAINER ID   IMAGE         COMMAND     CREATED              STATUS              PORTS     NAMES
 +aa9454c9f05e   diamol/base   "/bin/sh"   About a minute ago   Up About a minute             modest_fermi
 +
 +# Список процессов внутри контейнера (aa - начало ID контейнера)
 +docker container top aa
 +UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
 +root                3138                3112                0                   11:22               pts/              00:00:00            /bin/sh
 +
 +# Логи контейнера - здесь то, что было введено в командной строке интерактивно
 +docker logs 9c
 +/ # hostname
 +9cfdb08bc3c8
 +/ # date
 +Mon Apr 10 11:34:58 UTC 2023
 +
 +# Подробная информация о контейнере
 +docker container inspect 9c
 +[
 +    {
 +        "Id": "9cfdb08bc3c8184aec9a5837d04386ad650f4a1f107b20133d8f90671e1ffd77",
 +        "Created": "2023-04-10T11:34:52.389807692Z",
 +        ...
 +
 +# Список работающих контейнеров
 +docker ps # или
 +docker container ls
 +# Список всех контейнеров
 +docker ps -a # или
 +docker container ls -a
 +
 +# Контейнеры не исчезают после того, как они выполнили задачу и остановились. Можно снова их запустить, посмотреть в логи, скопировать файлы в/из них.
 +# Докер не удаляет контейнеры, пока не поступит команда на это.
 +# Запуск контейнера с постоянно работающим процессом в фоне (''%%--detach%%'') и на порту 8088 (''%%--publish%%''), на внутренний 80 порт контейнера:
 +docker run -d -p 8088:80 diamol/ch02-hello-diamol-web
 +
 +# Потребление контейнером CPU/Mem/Net/disk
 +docker container stats 9c
 +
 +# Удалить контейнер (--force, -f для запущенных контейнеров)
 +docker container rm -f 9c
 +# Удалить все контейнеры
 +docker container rm --force $(docker container ls --all --quiet)
 +</code>
 +
 +===== 3. Создание собственных образов =====
 +<code bash>
 +# Скачать образ из реестра по умолчанию (default registry)
 +docker image pull diamol/ch03-web-ping
 +Using default tag: latest
 +latest: Pulling from diamol/ch03-web-ping
 +e7c96db7181b: Pull complete # слой
 +bbec46749066: Pull complete # слой
 +89e5cf82282d: Pull complete # слой
 +5de6895db72f: Pull complete # слой
 +f5cca017994f: Pull complete # слой
 +78b9b6c949f8: Pull complete # слой
 +Digest: sha256:2f2dce710a7f287afc2d7bbd0d68d024bab5ee37a1f658cef46c64b1a69affd2
 +Status: Downloaded newer image for diamol/ch03-web-ping:latest
 +docker.io/diamol/ch03-web-ping:latest
 +
 +# Запустить, задав имя (это проверялка доступности сайта)
 +docker run -d --name web-ping diamol/ch03-web-ping
 +
 +# В логах:
 +docker container logs web-ping |less
 +** web-ping ** Pinging: blog.sixeyed.com; method: HEAD; 3000ms intervals
 +Making request number: 1; at 1681130396655
 +Got response status: 200 at 1681130397047; duration: 392ms
 +Making request number: 2; at 1681130399657
 +Got response status: 200 at 1681130400349; duration: 692ms
 +</code>
 +
 +У контейнера могут быть некие стандартные значения параметров, но их можно изменить при запуске с помощью предварительно заданных переменных окружения.\\
 +У контейнера свои переменные, отдельные от хостовых.
 +<code bash>
 +# Запуск с переменной окружения, чтобы изменить сайт для проверки
 +docker run --name web-ping --env TARGET=bva.dyndns.info diamol/ch03-web-ping
 +** web-ping ** Pinging: bva.dyndns.info; method: HEAD; 3000ms intervals
 +Making request number: 1; at 1681131611538
 +Got response status: 200 at 1681131612215; duration: 677ms
 +Making request number: 2; at 1681131614540
 +Got response status: 200 at 1681131615097; duration: 557ms
 +</code>
 +
 +==== Dockerfile ====
 +Dockerfile - это скрипт для запаковки приложения в контейнер.
 +<code bash>
 +# Базовый образ
 +FROM diamol/node
 +
 +# Значения по умолчанию для переменных окружения
 +ENV TARGET="blog.sixeyed.com"
 +ENV METHOD="HEAD"
 +ENV INTERVAL="3000"
 +
 +# Рабочий каталог - если его нет, то он создаётся
 +WORKDIR /web-ping
 +# Копирование файлов с хоста внутрь образа, здесь один файл app.js
 +COPY app.js .
 +
 +# Команда, которую запускает контейнер после старта
 +CMD ["node", "/web-ping/app.js"]
 +</code>
 +
 +Сборка и использование образа
 +<code bash>
 +# --tag - имя образа, . - из текущей папки.
 +docker image build --tag web-ping .
 +
 +# вывести список образов на w
 +docker image ls 'w*'
 +REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
 +web-ping     latest    bebf5120fd51   15 minutes ago   75.3MB
 +
 +# Запустить собранный образ
 +docker container run -e TARGET=bva.dyndns.info -e INTERVAL=5000 web-ping
 +** web-ping ** Pinging: bva.dyndns.info; method: HEAD; 5000ms intervals
 +Making request number: 1; at 1681137797563
 +Got response status: 200 at 1681137798217; duration: 654ms
 +Making request number: 2; at 1681137802567
 +Got response status: 200 at 1681137803136; duration: 569ms
 +</code>
 +==== Слои ====
 +<code bash>
 +# История создания образа послойно
 +docker image history web-ping
 +# В списке образов можно увидеть их размер
 +docker image ls
 +</code>
 +Образ - это коллеция слоёв. Слои - это файлы, хранимые в кэше Докера. Слои могут быть общими у разных образов и контейнеров: если множество контейнеров используют приложения Node.js, они все будут иметь слой, содержащий среду Node.js.
 +
 +Размер в списке образов - логический, т. е., тот, который был бы реально, если нет общих слоёв. Если они есть, то реальный размер получается меньше.
 +<code bash>
 +# Сколько всего места занимает Докер (реальный размер)
 +docker system df
 +TYPE                TOTAL               ACTIVE              SIZE                RECLAIMABLE
 +Images              27                  4                   4.249GB             4.142GB (97%)
 +Containers          9                                     73B                 0B (0%)
 +Local Volumes                                           311MB               311MB (100%)
 +Build Cache                                             0B                  0B
 +</code>
 +
 +Если слой используется в нескольких образах/контейнерах, то они становятся нередактируемыми (read-only) - иначе бы изменения в одном образе приводили бы к перестроению всех остальных.
 +
 +==== Оптимизация Докерфайлов через использование кэша слоёв ====
 +В примере с web-ping есть файл приложения. Если его изменить и пересоздать образ, то получится новый слой. Так как слои идут последовательно, накладываясь один на другой, то изменение слоя влечёт за собой изменение всех последующих. Если из примера выше изменить файл app.js и создать образ, то будет видно, что из 7 слоёв будут заново сделаны только последние 2, остальные будут взяты из кэша, т. к. там ничего не менялось.
 +<code bash>
 +docker image build -t web-ping:v2 .
 +</code>
 +Для каждого слоя генерится контрольная сумма, если она не меняется - Докер берёт слой из кэша, если меняется - собирается новый слой и все последующие, даже если контрольные суммы у них были теми же. Любой Докерфайл надо составлять так, чтобы наиболее часто меняющиеся данные/строки/слои были в конце файла, что позволит пересоздавать меньше слоёв и экономить тем самым место на диске, время сборки и трафик, если образы предоставляются в общий доступ.
 +
 +В примере выше можно оптимизировать Докерфайл, перенеся выше строку команд и слепив строки переменных в одну, что при сборке образа даст 5 слоёв вместо 7, а при изменении app.js и пересборке образа пересоздаваться будет только последний слой.
 +<code bash>
 +FROM diamol/node
 +
 +CMD ["node", "/web-ping/app.js"]
 +
 +ENV TARGET="yandex.ru" \
 +METHOD="HEAD" \
 +INTERVAL="3000"
 +
 +WORKDIR /web-ping
 +
 +COPY app.js .
 +</code>
 +
 +Создание образа без использования Dockerfile, пример.
 +<code bash>
 +# Запуск контейнера с именем ch03-lab из образа diamol/ch03-lab
 +docker container run -it --name ch03-lab diamol/ch03-lab
 +# Изменение файла ch03.txt
 +echo "Vasya" >> ch03.txt
 +# Выход из контейнера, он завершает работу (в данном случае)
 +exit
 +# Сделать образ ch03-lab-image-new из контейнера ch03-lab
 +docker container commit ch03-lab ch03-lab-image-new
 +# Создать контейнер из образа ch03-lab-image-new и вывести содержимое файла ch03.txt
 +docker container run ch03-lab-image-new cat ch03.txt
 +</code>
 +
 +===== 4. Из исходного кода - в образ =====
 +Внутри докерфайла можно запускать команды. Команды выполняются во время сборки, и изменения файловой системы в результате их выполнения сохраняются как слои. Это делает докерфайлы невероятно гибким форматом упаковки - можно распаковывать zip-файлы, запускать установщики Windows и т. д. Рассмотрим вариант упаковки приложений из исходного кода. Очень удобно написать докерфайл, который ставит все утилиты/зависимости/инструменты и встраивает их в образ. Затем этот образ используется для компиляции приложения.
 +
 +==== multi-stage ====
 +Это "поэтапный" докерфайл (multi-stage Dockerfile), потому что в сборке несколько этапов - несколько строк FROM. Тем не менее, на выходе будет один образ с содержимым последнего этапа. Этапы запускаются независимо, но можно копировать файлы и папки из прошлых этапов. Смысл поэтапного докерфайла - избавление от необходимости настройки зависимостей, рабочих мест разработчиков, унификация всего процесса. Необходимо только поставить Докер, дальше всё будет происходить по написанному сценарию.
 +<code bash>
 +# AS - название этапа (необязательно)
 +FROM diamol/base AS build-stage
 +RUN echo 'Building...' > /build.txt
 +
 +FROM diamol/base AS test-stage
 +# COPY с аргументом --from предписывает копировать файлы с предыдущего этапа, а не с хоста
 +COPY --from=build-stage /build.txt /build.txt
 +# Запись файла, вывод сохраняется как слой. Вызываемые команды должны присутствовать в образе из FROM
 +RUN echo 'Testing...' >> /build.txt
 +
 +FROM diamol/base
 +COPY --from=test-stage /build.txt /build.txt
 +CMD cat /build.txt
 +</code>
 +Что тут происходит: на 1 этапе создаётся файл, на втором он копируется из 1-го и в него добавляется текст, а третий копирует файл из 2-го этапа и выводит его содержимое.
 +
 +Каждый этап изолирован. Можно использовать разные базовые образы с разным набором инструментов и запускать какие угодно команды. Если на каком-то этапе возникает ошибка, вся сборка терпит неудачу.
 +
 +==== Java app with Maven ====
 +
 +<code bash>
 +FROM diamol/maven AS builder
 +
 +WORKDIR /usr/src/iotd
 +COPY pom.xml .
 +RUN mvn -B dependency:go-offline
 +
 +COPY . .
 +RUN mvn package
 +
 +# app
 +FROM diamol/openjdk
 +
 +WORKDIR /app
 +COPY --from=builder /usr/src/iotd/target/iotd-service-0.1.0.jar .
 +
 +EXPOSE 80
 +ENTRYPOINT ["java", "-jar", "/app/iotd-service-0.1.0.jar"]
 +</code>
 +Здесь первый этап (builder) использует образ diamol/maven, где установлен OpenJDK Java development kit и Maven build tool, копирует файл pom.xml (конфиг Maven) в рабочую папку и запускает установку зависимостей. Затем идёт копирование всех файлов в текущем каталоге в текущий каталог контейнера, и в конце запускается компиляция приложения и упаковка в файл jar. На втором этапе используется образ diamol/openjdk с Java 11, но уже без Maven. Создаётся рабочий каталог и туда копируется файл jar, созданный на предыдущем этапе, куда Maven уже положил все необходимые зависимости. Дальше публикуется порт 80 и приложение запускается (ENTRYPOINT - это аналог CMD).
 +
 +Во время сборки будет обширный вывод, и одна из строк будет
 +<code bash>
 +Step 9/11 : COPY --from=builder /usr/src/iotd/target/iotd-service-0.1.0.jar .
 +</code>
 +, что свидетельствует о копировании готового файла из этапа сборки.
 +
 +Контейнеры получают доступ друг к другу через виртуальную сеть по виртуальным IP-адресам, которые им присваивает Докер.
 +<code bash>
 +# Создать сеть
 +docker network create nat
 +</code>
 +Теперь можно при запуске контейнера указывать, что он подключен к сети nat.
 +<code bash>
 +docker container run --name iotd -d -p 800:80 --network nat image-of-the-day
 +</code>
 +Ещё раз - при таком подходе необходим Докер, не нужно заморачиваться со всем остальным. Ещё нужно обратить внимание, что в финальном образе нет утилит сборки, нет команд mvn, они все остались на первом этапе.
 +
 +==== node.js ====
 +Это другой подход к сборке, подобный Python, PHP и Ruby. Если для Java заливается исходный код и он компилируется в файл JAR, а затем файл JAR передаётся в финальный образ (то же и с .NET и файлами DLL), то здесь нет компиляции, а нужен Javascript. Соответственно, для обоих этапов используется один и тот же образ с node.js и npm. Этапы сборки здесь нужны для того, чтобы не тащить с собой зависимости (''npm install'', pip для Python, Gems для Ruby) в финальный образ.
 +<code bash>
 +FROM diamol/node AS builder
 +
 +WORKDIR /src
 +COPY src/package.json .
 +
 +RUN npm install
 +
 +# app
 +FROM diamol/node
 +
 +EXPOSE 80
 +CMD ["node", "server.js"]
 +
 +WORKDIR /app
 +COPY --from=builder /src/node_modules/ /app/node_modules/
 +COPY src/ .
 +</code>
 +Выполнить:
 +<code bash>
 +cd ch04/exercises/access-log
 +docker image build -t access-log .
 +docker container run --name accesslog -d -p 801:80 --network nat access-log
 +</code>
 +
 +==== Go ====
 +Go компилируется в исполняемый бинарник, не требующий для запуска установленной среды выполнения (.NET, Node.js и т. п.). Поэтому образы получаются очень маленькими. Так же работают Rust и Swift, но Go популярнее. Образы Докера для Go делаются по аналогии с Java, но с некоторыми отличиями. Билдер использует образ с Go tools, а финальный этап использует базовый. На финальном этапе копируется индексный файл HTML, и так как Go - это бинарник, надо его сделать исполняемым.
 +<code bash>
 +FROM diamol/golang AS builder
 +
 +COPY main.go .
 +RUN go build -o /server
 +RUN chmod +x /server
 +
 +# app
 +FROM diamol/base
 +
 +EXPOSE 80
 +ENV IMAGE_API_URL="http://iotd/image" \
 +    ACCESS_API_URL="http://accesslog/access-log"
 +CMD ["/web/server"]
 +
 +WORKDIR web
 +COPY --from=builder /server .
 +COPY index.html .
 +</code>
 +
 +<code bash>
 +cd ch04/exercises/image-gallery
 +docker image build -t image-gallery .
 +docker container run -d -p 802:80 --network nat image-gallery
 +</code>
 +
 +Если посмотреть на размеры образов, то можно увидеть, как экономится место в финальном образе в случае разбиения докерфайла на этапы:
 +<code bash>
 +docker image ls -f reference=diamol/golang -f reference=image-gallery
 +REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
 +image-gallery       latest              5880a505b363        3 minutes ago       25.5MB
 +diamol/golang       latest              119cb20c3f56        6 weeks ago         803MB
 +</code>
 +Поэтапный докерфайл делает проект полностью портативным. Вне зависимости от того, какой CI-сервис используется - Jenkins или какой-то облачный - везде это будет работать одинаково.
 +
 +Итак, ключевые особенности такого подхода:
 +  - Стандартизация, унификация инструментов, простота применения
 +  - Производительность: каждый этап имеет свой собственный кэш и использует его по возможности
 +  - Управляемость: всю предварительную работу - зависимости, сборщики и прочие инструменты - можно выносить в ранние стадии, и они не войдут в конечный образ.
 +
 +===== 5. Доступ к образам: Dockerhub и прочие =====
 +От сборки и запуска образов переходим к выкладыванию их в общий доступ. По умолчанию в Докере прописан реестр Dockerhub. Формат пути к образу:
 +<code bash>
 +# docker.io - реестр
 +# diamol - аккаунт в реестре
 +# golang - репозиторий (имя образа)
 +# v2 - Тэг (по умолчанию - latest)
 +docker.io/diamol/golang:v2
 +</code>
 +Тэг - самая важная часть, которая используется для различения версий/вариантов образа, например,
 +<code bash>
 +openjdk:13 # свежий релиз
 +openjdk:8u212-jdk # конкретный релиз 8 версии
 +openjdl:for-Vasya-Windows # вариант для Васи под Windows
 +</code>
 +Если при сборке не указать тэг, то он будет latest. Тем не менее, это может не отражать сути - latest может быть не самой свежей версией. Поэтому при загрузке образа в реестр тэг нужно всегда указывать.
 +==== Загрузка образа в реестр ====
 +<code bash>
 +dockerId="vasya"
 +# вход в реестр
 +docker login --username $dockerId 
 +# Установить метку (reference) на образ
 +# Если потом вывести список образов, то будет два одинаковых образа по размеру и ID, но с разными метками - старой и новой.
 +docker image tag image-gallery $dockerId/image-gallery:v1
 +# Загрузить образ в реестр
 +docker image push $dockerId/image-gallery:v1
 +</code>
 +Загрузка образа происходит послойно, и это ещё одна причина, по которой образы нужно оптимизировать, чтобы они были минимального размера. В реестре, так же, как и локально, слои используются образами совместно.
 +
 +==== Собственный реестр ====
 +У Докера есть [[https://docs.docker.com/registry/deploying/|базовый сервер реестра]], который позволяет скачивать и загружать образы с использованием слоёв, но у него нет веб-интерфейса.
 +<code bash>
 +# --restart=always для того, чтобы контейнер запускался после перезагрузки самого Докера, REGISTRY_STORAGE_DELETE_ENABLED - возможность удалять образы
 +docker run -d -p 5000:5000 --restart=always -e REGISTRY_STORAGE_DELETE_ENABLED=true --name registry registry:2
 +
 +# Прописать псевдоним registry.logal для наглядности
 +echo $'\n127.0.0.1 registry.local' | sudo tee -a /etc/hosts
 +
 +# Пометить образы
 +docker image tag lab4:v2 registry.local:5000/gallery/lab4:v1
 +docker image tag image-gallery registry.local:5000/gallery/api:v1
 +docker image tag access-log registry.local:5000/gallery/logs:v1
 +docker image tag image-of-the-day:v1 registry.local:5000/gallery/ui:v1
 +# Загрузить образы
 +docker image push registry.local:5000/gallery/lab4:v1
 +docker image push registry.local:5000/gallery/api:v1
 +docker image push registry.local:5000/gallery/logs:v1
 +docker image push registry.local:5000/gallery/ui:v1
 +
 +# Если тэгов у образа несколько, можно загрузить их все одной командой (-a или --all-tags)
 +docker image push -a registry.local:5000/gallery/ui
 +</code>
 +https://docs.docker.com/engine/reference/commandline/push/#all-tags
 +
 +Факультативно может понадобиться прописать этот реестр в конфигурацию Докера, т. к. по умолчанию подключение к незащищённым (HTTP) реестрам запрещено.
 +<file json /etc/docker/daemon.json>
 +{
 +  "insecure-registries": [
 +    "registry.local:5000"
 +  ]
 +}
 +</file>
 +
 +Проверить - ''docker info'', параграф ''Insecure registries:''. Стандартно там прописана сеть ''127.0.0.0/8''.\\
 +Список образов: http://registry.local:5000/v2/_catalog
 +
 +==== Эффективный выбор тэгов ====
 +Основная идея - использовать ''[major].[minor].[patch]'', где
 +  * major - новые функции
 +  * minor - добавление, но не удаление функций
 +  * patch - исправление ошибок
 +
 +Это даёт пользователю возможность выбора, какой ветки придерживаться. Например, можно указать
 +<code bash>
 +registry.local:5000/gallery/ui # всегда брать latest
 +registry.local:5000/gallery/ui:2 # все релизы в рамках конкретной мажорной версии
 +registry.local:5000/gallery/ui:2.3 # все патчи в рамках минорной
 +registry.local:5000/gallery/ui:2.3.65 # конкретный выпуск
 +</code>
 +
 +==== "Золотые" образы ====
 +Проблема доверия к образам, выложенном на Докерхабе, решается с помощью
 +  * Проверенных издателей - компании вроде Microsoft, IBM и т. п., которым присваивается такой статус.
 +  * Официальных образов - обычно опенсорс-образы, собираемые совместно разработчиками и командой Докера. Докерфайлы для этих образов можно посмотреть на Гитхабе.
 +
 +"Золотые" образы - это официальные образы с разными добавлениями, необходимыми тому или иному разработчику, типа сертификатов, каких-то особых настроек и так далее. Например,
 +<code bash>
 +FROM mcr.microsoft.com/dotnet/core/sdk:3.0.100
 +
 +LABEL framework="dotnet"
 +LABEL version="3.0"
 +LABEL description=".NET Core 3.0 SDK"
 +LABEL owner="golden-images@sixeyed.com"
 +
 +WORKDIR src
 +COPY global.json .
 +</code>
 +Сборка золотых образов:
 +<code bash>
 +docker image build -t golden/dontencore-sdk:3.0 dotnet-sdk/
 +docker image build -t golden/aspnet-core:3.0 aspnet-runtime/
 +</code>
 +
 +Пример сборки с использованием золотого образа:
 +<code bash>
 +FROM golden/dotnetcore-sdk:3.0 AS builder
 +COPY . .
 +RUN dotnet publish -o /out/app app.csproj
 +
 +FROM golden/aspnet-core:3.0
 +COPY --from=builder /out /app
 +CMD ["dotnet", "/app/app.dll"]
 +</code>
 +Здесь используется обычная поэтапная сборка, но используется локальный золотой образ. Официальные образы могут меняться часто, и можно обновлять золотой образ пореже, например, раз в квартал. Ещё одна полезная опция - в CI-пайплайне можно задать проверку докерфайлов, и если кто-то попытается собрать приложение без использования золотого образа, то сборка завершится с ошибкой.
 +
 +==== HTTP API v2 ====
 +<code bash>
 +# Список репозиториев
 +http://registry.local:5000/v2/_catalog
 +# Вывести тэги репозитория gallery/ui
 +http://registry.local:5000/v2/gallery/ui/tags/list
 +# Манифест для gallery/ui:v1
 +http://registry.local:5000/v2/gallery/ui/manifests/v1
 +# Дайджест для gallery/ui:v1
 +curl -s --head http://registry.local:5000/v2/gallery/ui/manifests/v1 -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' |grep -i content-digest |cut -d ' ' -f 2 |tr -d '\r'
 +# Удалить
 +registry='http://registry.local:5000/v2' 
 +repo='gallery/ui'
 +tag='v1'
 +digest=`curl -s --head $registry/$repo/manifests/$tag -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' |grep -i content-digest |cut -d ' ' -f 2 |tr -d '\r'`
 +curl -X DELETE $registry/$repo/manifests/$digest
 +</code>
 +https://docs.docker.com/registry/spec/api\\
 +<WRAP round tip 60%>
 +<code bash>
 +curl -X DELETE $registry/$repo/manifests/$digest
 +curl: (3) URL using bad/illegal format or missing URL
 +</code>
 +You get a \r (carriage return). You can remove it with ''%%tr -d '\r'%%''\\
 +https://stackoverflow.com/questions/67214881/curl-throws-error-3-with-variable-but-not-with-manually-written-url
 +</WRAP>
 +
 +===== 6. Постоянное хранение данных - тома (volumes) и точки монтирования (mounts) =====
 +Каждый контейнер имеет свою изолированную файловую систему, которая при остановке контейнера автоматически не удаляется. Вот так можно вытащить файлы из контейнера, даже остановленного, на локальную машину:
 +<code bash>
 +docker container cp <containername>:/path/to/file.txt file.txt
 +</code>
 +Файловая система контейнера состоит из слоёв, которые доступны только на чтение, и самого верхнего слоя, доступного на запись, который имеет срок жизни такой же, как и сам контейнер. Даже если редактируются файлы из нижележащих слоёв, это делается на верхнем уровне с помощью процесса copy-on-write, копирующего файлы с недоступных для редактирования слоёв на верхний слой. В целом по логике приблизительно напоминает систему снапшотов на виртуальной машине.
 +
 +Если требуется сохранять данные и после удаления контейнера, в этом случае используются //тома (volumes)// или //точки монтирования (mounts).//
 +==== Тома ====
 +Можно вручную создавать тома и подключать их к контейнерам, а можно использовать докерфайл с командой VOLUME.
 +<code bash>
 +FROM diamol/dotnet-aspnet
 +WORKDIR /app
 +ENTRYPOINT ["dotnet", "ToDoList.dll"]
 +
 +VOLUME /data
 +COPY --from=builder /out/ .
 +</code>
 +
 +Приложение to-do, которое будет хранить свои данные на томе
 +<code bash>
 +docker container run --name todo1 -d -p 8010:80 diamol/ch06-todo-list
 +# Показать ID тома, путь на хосте и путь в контейнере 
 +docker container inspect --format '{{.Mounts}}' todo1
 +[{volume 48d9319e1601b951e8f578b77bf03b38f414119bd0509d16fa7b31eab8565433 /var/lib/docker/volumes/48d9319e1601b951e8f578b77bf03b38f414119bd0509d16fa7b31eab8565433/_data /data local  true }]
 +# список томов
 +docker volume ls
 +</code>
 +Тома, которые прописаны в образах, создаются как отдельные для каждого контейнера. Например, если запустить новый контейнер с to-do, то у него будет свой новый том, и список задач будет пустым. Можно подключить один том к нескольким контейнерам. 
 +<code bash>
 +# подключить контейнеру app2 том app1
 +docker container run -d --name t3 --volumes-from todo1 diamol/ch06-todo-list
 +# проверить каталог, куда подключается том в контейнере t3 (например, /data), есть ли там данные тома todo1
 +docker exec t3 ls /data
 +</code>
 +Тем не менее, просто так цеплять нескольким контейнерам один том чаще всего плохая идея, т. к. данные могут быть повреждены, когда несколько контейнеров одновременно пытаются читать или писать одни и те же файлы. Тома очень полезны при обновлении приложения, когда контейнер со старой версией стирается и запускается новый, использующий данные того же тома, ранее прикрученного к старому контейнеру. Поэтому лучше управлять томами отдельно и создавать именованные тома (named volumes).
 +<code bash>
 +target='/data'
 +docker volume create todo-list
 +docker container run -d -p 8011:80 -v todo-list:$target --name todo-v1 diamol/ch06-todo-list
 +# ... добавляется какая-то информация в http://host:8011 ...
 +# удаление контейнера
 +docker container rm -f todo-v1
 +# создание нового с прикручиванием старого тома
 +docker container run -d -p 8011:80 -v todo-list:$target --name todo-v2 diamol/ch06-todo-list:v2
 +</code>
 +Команда VOLUME в докерфайле и ''-v'' при запуске контейнера - это разные вещи. Докерфайл создаёт образ, и все контейнеры, которые будут созданы на основе этого образа, будут в любом случае создавать том, даже если это не указано в команде ''docker run''. Этот том будет иметь случайный ID, и данные с этого тома, конечно, можно использовать потом, но если сможешь его найти.
 +
 +Команда ''-v'' при запуске контейнера подключает том вне зависимости от того, было это задано при сборке образа или нет. Если в образе уже задан том, то ключ запуска перекрывает изначальную настройку при совпадении пути в контейнере. Если ты сам делаешь образ, то там необходимо указывать параметр VOLUME, если приложение stateful, так как нельзя рассчитывать, что пользователь сообразит, что надо указывать ''-v'' при запуске. Если же ты используешь готовые образы, то лучше всегда указывать ''-v'', т. к. потом замучаешься искать эти пути по умолчанию и где что лежит.
 +
 +
 +==== Точки монтирования ====
 +Bind mounts - прямое использование файловой системы хоста: на нём создаётся каталог, который используется внутри контейнера. Путь может вести на быстрый диск, RAID-массив, хранилище и т. д. По сути, то же самое ([[https://docs.docker.com/storage/volumes/#choose-the--v-or---mount-flag|The biggest difference]] is that the ''-v'' syntax combines all the options together in one field, while the ''%%--mount%%'' syntax separates them.)
 +<code bash>
 +source="$(pwd)/databases" && target='/data'
 +mkdir $source
 +docker container run --mount type=bind,source=$source,target=$target -d -p 8012:80 diamol/ch06-todo-list
 +curl http://localhost:8012 # создаёт todo-list.db
 +ls $source # проверить наличие файла
 +</code>
 +
 +Bind mount двунаправленный - можно создавать файлы в контейнере и редактировать их на хосте и наоборот. Так как контейнеры должны запускаться от непривилегированной учётки, чтобы свести к минимуму риск при атаке на систему, для того чтобы читать и писать файлы на хосте, требуется указывать в докерфайле инструкцию USER, имеющую соответствующие права на хосте.\\
 +Если писать файлы не нужно, можно смонтировать каталог на хосте только для чтения контейнером - это один из вариантов брать конфигурацию для контейнера с хоста без переделывания образа.
 +<code bash>
 +docker run --name todo-configured --mount type=bind,source='/home/user/config',target='/app/config',readonly -d -p 8013:80 diamol/ch06-todo-list
 +</code>
 +В общем, монтировать можно всё, к чему имеет доступ хост - это могут быть отказоустойчивые или распределённые хранилища, тем не менее, есть ограничения использования привязок.
 +  - Что, если целевой каталог контейнера (target) уже существует и там уже есть файлы? При монтировании source полностью заменяет target - файлы, которые там были, будут недоступны.<code bash>docker container run --mount type=bind,source="$(pwd)/new",target=/init diamol/ch06-bind-mount</code>
 +  - Что если монтируется один файл в существующий каталог в контейнере? Внутри контейнера каталог, где содержится файл, будет взят из образа, а сам файл будет заменён (в Windows-контейнерах монтирование одного файла не поддерживается).<code bash>docker container run --mount type=bind,source="$(pwd)/new/123.txt",target=/init/123.txt diamol/ch06-bind-mount</code>
 +  - При использовании распределённых хранилищ (SMB, Azure, S3 и т. д.) надо быть готовым к тому, что система может не заработать, так как распределённое хранилище может не поддерживать операций, которые требует контейнер для работы с привязкой, например, поддержку жёстких ссылок.
 +
 +==== Строение файловой системы контейнера ====
 +Каждый контейнер содержит виртуальный диск, собранный из нескольких источников, это т. н. единая файловая система (union filesystem). Источники - это слои образов, точки монтирования томов и привязок, и сверху находится слой для записи. Контейнер работает с этой ФС как с одним разделом. Тем не менее, разные каталоги на этом диске могут быть смонтированы в разные места.
 +
 +  * Слой записи - хранит любые изменения за время существования контейнера. Удаляется вместе с контейнером.
 +  * Локальные точки монтирования - общий доступ между хостом и контейнером. Удобен тем, что можно редактировать файлы на хосте и изменения будут сразу отражаться в контейнере без необходимости пересборки образа.
 +  * Распределённые точки монтирования - общий доступ между сетевым хранилищем и контейнером. Полезная опция, но имеет свои недостатки, такие как пониженная производительность и отсутствие поддержки всех возможностей локальной ФС. Их можно использовать как ресурсы только для чтения или общий кэш, либо как хранилище общих данных для множества контейнеров на нескольких хостах в сети.
 +  * Тома - общие данные между контейнером и объектом хранения под управлением Докера. Используются для постоянного хранения данных приложения. При обновлении приложения данные берутся с этого тома и используются в дальнейшем.
 +  * Слои образа - базовая файловая система контейнера, верхний слой перекрывает нижний, т. е., перезаписывает его файлы. Слои доступны только для чтения и могут быть частью разных контейнеров.
 +
 +Пример с точкой монтирования, где лежит конфиг, и томом, где хранятся данные:
 +<code bash>
 +docker volume create todo6
 +docker run --name todo6 -dp 8003:80 -v todo6:/new-data \
 +--mount type=bind,source='/home/user/docker/todo6-conf',target='/app/config',readonly \
 +diamol/ch06-lab
 +</code>
 +===== 7. Multi-container apps (Docker Compose) =====
 +Возможность указать желаемую конфигурацию в едином файле.
 +
 +Пример секции services в docker-compose.yml:
 +<code yaml>
 +accesslog:
 +  image: diamol/ch04-access-log
 +
 +iotd:
 +  image: diamol/ch04-image-of-the-day
 +  ports:
 +    - "80"
 +
 +image-gallery:
 +  image: diamol/ch04-image-gallery
 +  ports:
 +    - "8010:80"
 +  depends_on:
 +    - accesslog
 +    - iotd
 +</code>
 +iotd - публикуется порт 80 контейнера на любой порт хоста, а image-gallery запустится только после accesslog и iotd.
 +
 +Размножить iotd до 3 экз., просмотреть логи, где при обновлении страниц в браузере видно, что обращения идут на разные экземпляры контейнеров iotd.
 +<code bash>
 +docker-compose up -d --scale iotd=3
 +# browse to http://localhost:8010 and refresh
 +docker-compose logs --tail=1 iotd # показать последнюю запись из каждого экземпляра iotd
 +</code>
 +
 +Остановить, стартовать, посмотреть статус.
 +<code bash>
 +docker-compose stop # Показываются контейнеры
 +Stopping image-of-the-day_iotd_3          ... done
 +Stopping image-of-the-day_iotd_2          ... done
 +Stopping image-of-the-day_image-gallery_1 ... done
 +Stopping image-of-the-day_accesslog_1     ... done
 +Stopping image-of-the-day_iotd_1          ... done
 +docker-compose start # Показываются сервисы (запускаются в заданном порядке)
 +Starting accesslog     ... done
 +Starting iotd          ... done
 +Starting image-gallery ... done
 +docker container ls # Если остановленные контейнеры существуют, то они и запускаются (см. CREATED и STATUS)
 +CONTAINER ID   IMAGE                          COMMAND                  CREATED          STATUS          PORTS                                     NAMES
 +732d36e8af7d   diamol/ch04-image-of-the-day   "java -jar /app/iotd…"   6 minutes ago    Up 10 seconds   0.0.0.0:49156->80/tcp, :::49156->80/tcp   image-of-the-day_iotd_3
 +81b762117f5a   diamol/ch04-image-of-the-day   "java -jar /app/iotd…"   6 minutes ago    Up 10 seconds   0.0.0.0:49157->80/tcp, :::49157->80/tcp   image-of-the-day_iotd_2
 +abce83ac3a53   diamol/ch04-image-gallery      "/web/server"            14 minutes ago   Up 8 seconds    0.0.0.0:8010->80/tcp, :::8010->80/tcp     image-of-the-day_image-gallery_1
 +01cff95179c8   diamol/ch04-access-log         "docker-entrypoint.s…"   14 minutes ago   Up 10 seconds   80/tcp                                    image-of-the-day_accesslog_1
 +c523d48d79de   diamol/ch04-image-of-the-day   "java -jar /app/iotd…"   14 minutes ago   Up 9 seconds    0.0.0.0:49158->80/tcp, :::49158->80/tcp   image-of-the-day_iotd_1
 +
 +
 +
 +</code>
 +
 +В примере выше, когда контейнер запускался в 3-х экз., это не было отражено в самом файле docker-compose, поэтому, если перезапустить конфигурацию, то она вернётся к изначальному виду.
 +<code bash>
 +# down - удалить приложение, контейнеры, сети и тома, упомянутые в docker-compose.yml (если они не помечены как external)
 +docker-compose down
 +docker-compose up -d
 +docker container ls
 +</code>
 +
 +==== Как контейнеры взаимодействуют ====
 +В докере есть свой DNS, и контейнеры в пределах виртуальной сети видят друг друга по имени. Если контейнер запрашивает имя, которого не существует в виртуальной сети, Докер запрашивает внешний DNS. Это можно проверить, зайдя внутрь контейнера и запрашивая имена через nslookup. Если одинаковых контейнеров несколько, то внутренний DNS Докера будет выдавать все экземпляры, но раз от раза тасуя их очерёдность, что обеспечивает простую балансировку нагрузки типа round-robin.
 +
 +Если грохнуть контейнер через Docker CLI мимо docker-compose, то при повторной команде ''docker-compose up'' недостающие контейнеры будут запущены снова.
 +<code bash>
 +docker rm -f image-of-the-day_accesslog_1
 +image-of-the-day_accesslog_1
 +
 +docker-compose up -d --scale iotd=3
 +image-of-the-day_iotd_1 is up-to-date                                                                                            
 +image-of-the-day_iotd_2 is up-to-date                                                                                            
 +image-of-the-day_iotd_3 is up-to-date                                                                                            
 +Creating image-of-the-day_accesslog_1 ... done
 +image-of-the-day_image-gallery_1 is up-to-date         
 +</code>
 +==== Пример конфигурации docker-compose ====
 +<code yaml>
 +services:
 +
 +  todo-db:
 +    image: diamol/postgres:11.5
 +    ports:
 +      - "5433:5432"
 +    networks:
 +      - app-net
 +
 +  todo-web:
 +    image: diamol/ch06-todo-list
 +    ports:
 +      - "8020:80"
 +    environment:
 +      - Database:Provider=Postgres
 +    depends_on:
 +      - todo-db
 +    networks:
 +      - app-net
 +    secrets:
 +      - source: postgres-connection
 +        target: /app/config/secrets.json
 +</code>
 +''environment'' - переменные окружения, создаваемые внутри контейнера, ''secrets'' читаются Докером и предоставляются как файлы внутри контейнера. В данном случае, в распоряжении приложения будет файл ''/app/config/secrets.json'' с содержащимися там учётными данными под названием postgres-connection.
 +
 +Secrets обычно нужны при кластеризации типа Docker Swarm или K8s. Они хранятся в БД кластера и могут быть зашифрованы, что даёт возможность более-менее безопасно хранить всякие учётные данные типа сертификатов, ключей API и т. д. На одиночном хосте нет кластерной БД для секретов, поэтому можно читать секреты из файлов. Вот соответствующая секция в docker-compose.yml:
 +<code yaml>
 +secrets:
 +  postgres-connection:
 +    file: ./config/secrets.json
 +</code>
 +В какой-то степени чтение секретов из файлов напоминает bind mounts, потому что файлы хоста используются в контейнере. Тем не менее, использование опции секрета позволяет впоследствии легче мигрировать с кластер.
 +
 +Файлы docker-compose.yml делают проще настройку для нескольких окружений, например, теста и разработки - можно указать разные порты и другие настройки. В примере выше todo-app используется вместе с БД Postgres - работает всё так же, но данные теперь пишутся в БД, которая может управляться отдельно.
 +
 +<code bash>
 +# Показать только контейнеры, относящиеся к текущей конфигурации docker-compose
 +docker-compose ps
 +</code>
 +Разделение упаковки приложения и настроек - ключевая функция Докера. Приложение может быть собрано через CI-пайплайн, и тот же образ потом проходит тесты и переходит в прод с различными настройками для каждой среды. Это гарантия, что в прод приходит тот же образ, который прошёл тесты.
 +
 +
 +==== Какие проблемы решает docker-compose ====
 +Исчезает разница между документацией и конфигурацией. Файл docker-compose.yml фактически является описанием конфигурации системы, который довольно легко читается. Он всегда актуален и нет необходимости создавать текстовые файлы с описанием.
 +
 +Позволяет создавать мультиконтейнерные сервисы в рамках одного хоста. Тем не менее, он не следит за состоянием контейнеров, как оркестраторы - если контейнер упал или был удалён, то docker-compose ничего сделать не сможет, надо руками перезапускать конфигурацию. Тем не менее, это не значит, что он не подходит для пром. использования - он хорош для начала, когда идёт миграция с ВМ или железа. Докер-композ не даёт HA и NLB, но этого не было и на ВМ и железке.
 +
 +===== 8. Проверка здоровья и зависимостей =====
 +В продуктивной среде используется Swarm или K8s, где есть функционал сабжа. При создании контейнеров нужно указать информацию, которая будет использоваться для проверки здоровья. Если контейнер перестал корректно работать, он удаляется и заменяется новым.
 +==== Проверки здоровья в образах ====
 +Докер на базовом уровне проверяет каждый контейнер при запуске. Контейнер запускает какой-либо свой основной процесс, а когда тот завершён - переходит в состояние exited. В кластере платформа может перезапустить или пересоздать контейнер, но это самая простая проверка. Например, процесс может работать, но криво, выдавая ошибки, а Докер будет считать, что всё нормально.
 +
 +Для того, чтобы Докер был в курсе, в Dockerfile есть команда HEALTHCHECK, где можно прописать специфическую проверку для конкретного образа. Например, в этом случае
 +<code bash>
 +# --fail - код возврата, запускать каждые 10 сек
 +HEALTHCHECK --interval=10s \
 +CMD curl --fail http://localhost/health # /health - специально предусмотренный путь для проверки (в этом приложении), который должен выдавать код возврата 200, если всё нормально.
 +</code>
 +''%%--fail%%'' - выдаёт 0, если всё нормально. Если возврат будет не 0, то проверка считается непройденной.
 +
 +Если вывести простыню о состоянии контейнера, то там в разделе State появится раздел Health.
 +<code bash>
 +docker container inspect $(docker container ls --last 1 --format '{{.ID}}')
 +</code>
 +Теперь управляющий софт будет в курсе проблемы и сможет предпринять соответствующие действия. На одиночном сервере, даже если контейнер помечен как неисправный, Докер не делает ничего, чтобы не было простоя и потерялись данные, и контейнер продолжает работать, а проверки продолжаются, и если проверка будет успешной, контейнер снова вернётся в состояние healthy.
 +
 +==== Запуск с проверкой зависимостей ====
 +В отличие от docker-compose, в кластере нельзя гарантировать, что контейнеры запустятся в нужной последовательности, например, веб-морда может запуститься раньше API и приложение обломится, хотя контейнеры будут в порядке, поэтому нужно настраивать зависимости. Проверка зависимостей отличается от проверки здоровья - она запускается до запуска приложения и проверяет, всё ли готово для его запуска. Если что-то не готово, контейнер не запускается. У Докера нет встроенной функции проверки зависимостей типа HEALTHCHECK, так что нужно добавлять это в команду запуска.
 +
 +В Докерфайле:
 +<code bash>
 +# Если API отвечает по ссылке, то запустить приложение
 +CMD curl --fail http://numbers-api/rng && \
 +dotnet Numbers.Web.dll
 +</code>
 +
 +==== Написание собственных инструментов для проверки ====
 +Curl - хорошая вещь для проверок, лучше написать свой механизм проверки на том же языке, на котором написано само приложение - не нужно ставить дополнительных компонентов, можно задействовать более сложную логику проверок, использовать не только URL и т .д. Например, в поэтапном докерфайле сначала собирается приложение, на втором этапе - проверяльщик, а на третьем всё это копируется в основной образ, и HEALTHCHECK задействует уже не curl, а самописный проверяльщик. Проверяльщик, конечно, может быть задействован и в строке проверки зависимостей.
 +
 +Ещё одно преимущество написания собственных проверок - образ становится портативным, потому что вся логика проверки находится внутри образа, и она будет работать на любой платформе (Compose, Swarm, K8s), а каждая платформа использует свой метод объявления и использования проверок.
 +
 +==== Проверки здоровья и зависимостей в Docker Compose ====
 +В Docker Compose у healthcheck есть тонкие настройки (в данном случае используется проверка, встроенная в образ):
 +<code yaml>
 +numbers-api:
 +  image: diamol/ch08-numbers-api:v3
 +  ports:
 +    - "8087:80"
 +  healthcheck:
 +    interval: 5s     # интервал между проверками
 +    timeout: 1s      # время ожидания выполнения проверки (после истечения проверка считается неуспешной)
 +    retries: 2       # кол-во неуспешных проверок для признания контейнера нездоровым
 +    start_period: 5s # через какое время после запуска начинать проверки (чтобы дать приложению запуститься)
 +  networks:
 +    - app-net
 +</code>
 +Эти опции регулируются в зависимости от ситуации. Необходимо также учитывать, что проверки потребляют ресурсы, поэтому в продуктивной среде перебарщивать с частотой их выполнения и количеством не стоит.
 +
 +Если проверка не строена в образ, можно задать её в ''docker-compose.yml'':
 +<code yaml>
 +  healthcheck:
 +    test: ["CMD", "dotnet", "Utilities.HttpCheck.dll", "-t", "150"]
 +    interval: 5s
 +    timeout: 1s
 +    retries: 2
 +    start_period: 10s
 +  # тут по желанию можно добавить автоперезапуск
 +  restart: on-failure
 +  # и зависимости
 +  depends_on: [serviceName]
 +</code>
 +Если не задать зависимости, то зависимый контейнер, стартовав одновременно с тем, от чего он зависит, обломится, но будет перезапускаться, пока зависимость не стартует. Это не очень красиво, но, тем не менее, работоспособно. Историю проверок можно посмотреть в логах контейнера.
 +
 +Зачем указывать зависимости внутри образа, если можно задать их в докер-композе - дело в том, что докер-композ контролирует зависимости только на одном хосте, и что будет твориться в кластере, предсказать невозможно.
 +
 +==== От проверок к самовосстановлению ====
 +Запуск приложения как распределённой системы из мелких компонентов повышает гибкость и быстродействие, но усложняет управление. Если компонентов много и между ними куча зависимостей, то прописывать их все тяжело, но заниматься этим - не лучшая идея. Если на одном хосте можно указать, что веб-морда зависит от бекенда, то если у тебя K8s на десятке серверов и тебе нужно 20 бекендов и 50 веб-контейнеров, ты прописываешь веб-морде запуск после бекенда, запускается 19 бекендов и один не запускается или запускается очень долго - в результате приложение не работает вообще, потому что веб-контейнеры не запустились все, ожидая, пока все контейнеры бекенда поднимутся, хотя если бы не была прописана зависимость, то даже при одном запущенном бекенде и 50 веб-мордах приложение бы работало.
 +
 +Здесь как раз могут помочь проверки здоровья и зависимостей. Лучше дать платформе запускать контейнеры как ей нужно - максимум, на всех серверах и как можно быстрее, но если какие-то контейнеры не видят зависимостей, они просто перезапускаются или пересоздаются до тех пор, пока их зависимости не запустятся и не станут для них доступными. Пусть в момент запуска приложение не будет развёрнуто на 100%, но, во всяком случае, оно уже будет работать. Идея самовосстанавливающихся приложений - это переложить работу с временными ошибками на плечи платформы. Например, если приложение кривое и вызывает переполнение памяти, то платформа заменит контейнер новым. Это не исправит причину, но приложение хотя бы будет работать.
 +
 +С проверками не надо переусердствовать.\\
 +Проверки здоровья запускаются периодически, и они не должны выполнять слишком много работы. Лучше всего, если проверяться будут ключевые компоненты приложения, без того, чтобы занимать этим много времени и вычислительных ресурсов.\\
 +Проверки зависимостей запускаются только во время старта контейнера и экономия вычислительных ресурсов здесь неактуальна, но необходимо быть внимательным к тому, что проверяется. Некоторые зависимости находятся вне нашего контроля, и если платформа не умеет исправлять проблемы, то она не поможет, если контейнер упадёт.
 +
 +===== 9. Мониторинг =====
 +Контроль и возможность обзора работы приложений очень важен, без него нельзя переводить приложение в продуктивную среду. Ниже будут рассмотрены Prometheus (сборщик метрик) и Grafana (визуализация), которые будут запущены рядом с приложением в контейнерах, что означает применимость такой схемы и единый подход в любом окружении.
 +==== Структура мониторинга для контейнерных приложений ====
 +Традиционный мониторинг обычно подразумевает какую-то панель с отображением списка серверов и их дисковое пространство, загрузка процессора, памяти и т. д. - и оповещения в случае проблем. Контейнеры более динамичны - платформа может крутить сотни контейнеров, постоянно удаляющихся и создающихся. Соответственно, подход к мониторингу должен быть другим - необходимо подключение к платформе и обнаружение всех запущенных приложений без статического списка IP-адресов их контейнеров, что и делает Прометей. Он собирает данные о метриках контейнеров (они все собраны с API, который предоставляет эти метрики) и также данные из самого Докера.
 +
 +Прометей предоставляет единый подход к мониторингу различных приложений - одни и те же типы метрик подходят для приложений, написанных на разных языках, язык запросов один. Докер поддерживает метрики для Прометея. Несмотря на то, что это экспериментальная функция, она существует довольно давно и работает стабильно, поэтому эту информацию полезно иметь под рукой, чтобы знать об общем состоянии системы. Включить эти метрики можно так:
 +<code bash>
 +sudo nano /etc/docker/daemon.json
 +# добавить перед последней закрывающей }
 +# если файла нет, то нужно его создать
 +{
 +  "metrics-addr": "0.0.0.0:9323",
 +  "experimental": true
 +}
 +# Потом нужно дать права группе docker на этот файл, если он был создан
 +sudo chown user:docker /etc/docker/daemon.json
 +# перезапустить Докер
 +sudo systemctl restart docker
 +</code>
 +После этого метрики будут доступны по адресу http://docker:9323/metrics
 +
 +После этого можно ставить самого Прометея
 +<code bash>
 +# так в книжке, базовая процедура инсталляции - ссылка ниже.
 +# здесь переменная указывает на хост, чтобы брать с него метрики
 +docker run -e DOCKER_HOST=192.168.1.10 -dp 9090:9090 diamol/prometheus:2.13.1
 +</code>
 +https://prometheus.io/docs/prometheus/latest/installation/#using-docker
 +
 +После выполнения запуска Прометея веб-интерфейс доступен по адресу http://docker:9090, там можно посмотреть на доступные метрики Докера, и что в разделе targets он отображается как нормально работающий.
 +
 +{{:learning:pasted:20210202-115819.png?600|Метрики Докера}} {{:learning:pasted:20210202-115733.png?600|Статус}}
 +
 +Докер выдаёт кучу метрик, как высоко-, так и низкоуровневых, а приложения будут выдавать свои наборы, и можно составить из них панель, где будет отображаться состояние системы в целом.
 +
 +==== Метрики приложения ====
 +У Прометея есть клиентские библиотеки для всех основных языков программирования, например, promhttp для Go, micrometer для Java REST API, prom-client для Node.js. Клиент собирает информацию, что делается в контейнере и какая на него нагрузка применительно к среде, в которой он выполняется, т. е., клиент будет выдавать специфические для своей среды метрики.
 +
 +Помимо метрик самого Докера и метрик окружения (runtime), должны быть ещё и метрики самого приложения, чтобы была полная картина. Метрики приложения могут быть сконцентрированы на операциях, например, показывать среднее время выполнения запроса, а могут на бизнес-аналитике - кол-во пользователей в системе или регистрации на новом сервисе. Нестандартные метрики нужно создавать самому.
 +
 +У Прометея есть несколько типов метрик, самые простые - это счётчики (counters) и датчики (gauges). Значение счётчика может либо оставаться стабильным, либо расти, датчик может менять значения в любую сторону.
 +
 +Полезно мониторить в метриках:
 +  * При обращении ко внешним системам - как долго длится запрос и успешен ли ответ. Будет видно, тормозит ли внешняя система и не нарушает ли она работу вашей системы.
 +  * Все те события, что хорошо бы иметь в логах - метрика-счётчик меньше ест ресурсов, чем логирование, и проще показывает, как часто происходит событие.
 +  * Подробности о приложении и поведении пользователей - то, о чём бизнес желает получать отчёты. Метрики дадут картину в реальном времени, в отличие от исторических отчётов.
 +
 +==== Запуск Прометея в контейнере для сбора метрик ====
 +Прометей использует pull-сбор, т. е., сам лезет за информацией, а не ждёт её. Это называется //скоблением// (scraping), и когда Прометей устанавливается, нужно настроить конечные точки, которые он будет //скоблить.// Также, можно настроить Прометея так, чтобы он обнаруживал все контейнеры в кластере. В докер-композе на одном сервере используется простой список служебных имён, и Прометей находит контейнеры через Докер-DNS.
 +
 +Вот пример конфигурации, где скоблится два компонента приложения:
 +<code yaml>
 +# Общая настройка - 10 сек между проверками
 +global:
 +  scrape_interval: 10s
 +
 +# job для каждого компонента
 +scrape_configs:
 +  - job_name: "image-gallery"
 +    metrics_path: /metrics
 +    static_configs:
 +      - targets: ["image-gallery"]
 +  
 +  - job_name: "iotd-api"
 +    metrics_path: /actuator/prometheus
 +    static_configs:
 +      - targets: ["iotd"]
 +
 +  - job_name: "access-log"
 +    metrics_path: /metrics
 +    # использовать Докер-DNS для автопоиска
 +    dns_sd_configs:
 +      - names:
 +        - accesslog
 +        type: A
 +        port: 80
 +</code>
 +Здесь контейнеры будут обнаруживаться автоматически, но для image-gallery и iotd подразумевается наличие только одного контейнера (static_configs) - если их размножить, то из-за NLB метрики будут сниматься каждый раз с разных контейнеров, так как Прометей в этом случае берёт первый IP-адрес для доменного имени. Access-log же настроен на поддержку множества IP (dns_sd_configs).
 +
 +Одна из самых мощных функций Прометея - запись расширенной информации в метриках, что даёт возможность получать информацию разной степени подробности. Например, вывод метрики access_log_total выглядит так:
 +<code>
 +access_log_total{hostname="3c57931b20b4",instance="192.168.48.6:80",job="access-log"} 167
 +access_log_total{hostname="4245de46cccb",instance="192.168.48.7:80",job="access-log"} 171
 +access_log_total{hostname="731f99d38048",instance="192.168.48.3:80",job="access-log"} 152
 +</code>
 +Но можно получить общее число, выполнив суммирование и фильтр ярлыков (labels):
 +<code bash>
 +sum(access_log_total) without(hostname, instance)
 +
 +{job="access-log"} 490
 +# На соседней вкладке graph график тоже будет суммированным
 +</code>
 +Запрос //sum()// написан на прометеевском языке //PromQL.// Это мощный язык запросов, но необязательно сильно погружаться в него, чтобы настроить хорошо структурированную панель мониторинга, чаще всего достаточно простых запросов и фильтрации ярлыков.
 +
 +Интерфейс Прометея хорош для проверки конфигурации, доступности всех объектов мониторинга и отработки запросов, но это не графическая панель.
 +
 +==== Визуализация метрик с помощью Grafana ====
 +Гугл в книжке [[https://sre.google/sre-book/table-of-contents/|Site Reliability Engineering]] описывает т. н. "золотые сигналы" для мониторинга сайтов - это задержка (latency), трафик (traffic), ошибки (errors) и насыщенность (saturation).
 +
 +Пример настройки панели: {{ :learning:pasted:20210203-155111.png?480}}
 +
 +  * HTTP 200 Responses - сколько запросов успешно обработано.<code go>// Можно вместо 200 поставить 500, тогда будет показывать кол-во сбойных запросов
 +sum(image_gallery_requests_total{code="200"}) without(instance)</code>
 +  * In-flight requests - число одновременных запросов, что показывает пики трафика.<code go>// это по всем контейнерам
 +sum(image_gallery_in_flight_requests) without(instance)</code>
 +  * Memory in use - сколько памяти съело приложение. Это хороший индикатор насыщенности.<code go>go_memstats_stack_inuse_bytes{job="image-gallery"}</code> 
 +  * Active Goroutines - сколько запущено Goroutines. Показывает активность Go-приложения, сколько ест ресурсов процессора.<code go>sum(go_goroutines{job=\"image-gallery\"}) without(instance)</code>
 +
 +Остальные показатели используют примерно такие же несложные запросы. Не нужно сильно заморачиваться с PromQL - достаточно выбирать правильные метрики и подходящее их отображение.
 +
 +В мониторинге актуальные значения менее ценны, чем отображение тенденций (трендов), так как знание того, что приложение занимает 200 МБ памяти, не так нужно, как знание того, что произошёл неожиданный скачок её потребления. Это даёт возможность принять какое-то решение, например, увеличить кол-во контейнеров.
 +
 +Добавить панель в Графану можно с помощью кнопки с плюсиком вверху. В том же ряду есть кнопка Share dashboard, и там есть складка Export, где выгружается json, с помощью которого можно сделать свой образ, в котором сразу будет всё настроено, например
 +<code bash>
 +FROM diamol/grafana:6.4.3
 +
 +COPY datasource-prometheus.yaml ${GF_PATHS_PROVISIONING}/datasources/
 +COPY dashboard-provider.yaml ${GF_PATHS_PROVISIONING}/dashboards/
 +COPY dashboard.json /var/lib/grafana/dashboards/
 +</code>
 +Таким же образом можно добавлять и пользователей, например, пользователя только на чтение всех панелей. У Графаны есть "список воспроизведения" (playlist), и если добавить этого пользователя и все панели в этот список, то на экране панели под этим пользователем будут циклически переключаться.
 +
 +==== Уровни наблюдения ====
 +Обозримость (observability) - важнейшее требования для продуктивной системы. Необходимо несколько панелей наблюдения - инфраструктурная (загрузка серверов - проц, память, диск и т. д.), по каждому компоненту, и суммарная панель, которая даёт общую картину и позволяет оперативно отреагировать, если что-то начинает идти не так.
 +
 +==== Лабораторная ====
 +Надо написать конфиг для Прометея и собрать образ:
 +<WRAP group>
 +<WRAP half column>
 +<file yaml prometheus.yml>
 +global:
 +  scrape_interval: 10s
 +
 +scrape_configs:
 +  - job_name: 'todo'
 +    metrics_path: /metrics
 +    static_configs:
 +      - targets: ['todo']
 +</file>
 +</WRAP>
 +
 +<WRAP half column>
 +<file bash Dockerfile>
 +FROM diamol/ch09-prometheus
 +COPY prometheus.yml /etc/prometheus/
 +</file>
 +<code bash>
 +# Сборка
 +docker image build -t my-prometheus -f prometheus/Dockerfile prometheus/
 +</code>
 +</WRAP>
 +</WRAP>
 +
 +Потом пишется docker-compose.yml с образом ''diamol/ch09-grafana'' и запускается, там создаётся панель с нужными параметрами, выгружается JSON в ''grafana/dashboard.json'', собирается образ
 +<file bash Dockerfile>
 +FROM diamol/ch09-grafana
 +COPY dashboard.json /var/lib/grafana/dashboards/
 +</file>
 +<code bash>
 +# Сборка
 +docker image build -t my-grafana -f grafana/Dockerfile grafana/
 +</code>
 +
 +Потом в ''docker-compose.yml'' образ Графаны меняется на свой и запускается:
 +<file yaml docker-compose.yml>
 +version: '3.7'
 +services:
 +  todo:
 +    image: diamol/ch09-todo-list
 +    ports:
 +      - 8080:80
 +  prometheus:
 +    image: my-prometheus
 +    ports:
 +      - 9090:9090
 +  grafana:
 +    image: my-grafana
 +    ports:
 +      - 3000:3000
 +</file>
 +===== 10. Запуск нескольких окружений с помощью docker-compose =====
 +Нужно, когда одна версия продуктивная, другая тестовая, третья в разработке и т. д. Просто так запустить два раза приложение не выйдет, потому что Докер-композ думает, что вы запускаете приложение, которое уже и так запущено:
 +<code bash>
 +$ docker-compose -f numbers/docker-compose.yml up -d
 +Creating network "numbers_app-net" with the default driver
 +Creating numbers_numbers-api_1 ... done
 +Creating numbers_numbers-web_1 ... done
 +$ docker-compose -f todo-list/docker-compose.yml up -d
 +Creating network "todo-list_app-net" with the default driver
 +Creating todo-list_todo-web_1 ... done
 +$ docker-compose -f todo-list/docker-compose.yml up -d
 +todo-list_todo-web_1 is up-to-date
 +</code>
 +
 +Докер-композ использует понятие //проект (project)// для определения разных ресурсов, принадлежащих одному приложению, и он использует имя папки, где лежит docker-compose.yml как имя проекта по умолчанию. У создаваемых ресурсов имя проекта идёт как префикс, и для контейнеров ещё добавляется числовой счётчик как суффикс. Например, в приложении app1, где есть сервис web и том disk, при создании ресурсов контейнеры будут называться app1_web_1, app1_web_2 и т. д., а volume - app1_disk.
 +
 +Но имя проекта можно переопределить, тогда можно будет запускать параллельно множество экземпляров приложения на одном Докер-движке, так как для движка имена новых ресурсов не совпадают со старыми, соответственно, они создаются:
 +<code bash>
 +$ docker-compose -f todo-list/docker-compose.yml -p todo-test up -d
 +Creating network "todo-test_app-net" with the default driver
 +Creating todo-test_todo-web_1 ... done
 +</code>
 +
 +Нюанс в том, что для каждого экземпляра нужно выяснять порт, к которому нужно подключаться извне, чтобы попасть в контейнер, так как хостовый порт присваивается динамически. 
 +<code bash>
 +docker-compose -f ./todo-list/docker-compose.yml -p todo-test up -d
 +docker ps
 +docker container port todo-test_todo-web_1 80 # Выяснить порт
 +0.0.0.0:49157
 +:::49157
 +</code>
 +Чтобы сделать ситуацию более управляемой, можно, конечно, дублировать докер-композ-файлы и вносить туда изменения, но имеется более удобный способ.
 +
 +==== Docker Compose override files ====
 +При простом дублировании и редактировании файлов их содержимое будет практически одинаковым, что неудобно для редактирования и создаёт путаницу и рассинхронизацию конфигураций.\\ 
 +Докер-композ позволяет создавать перекрывающие файлы, где будут отражены только изменения базовой конфигурации. К примеру, в основном файле настроено только то, что используется везде, для среды разработки добавляется сеть и доп. сервисы, а для теста - другие сервисы, сеть и добавляются тома.
 +
 +В этом случае для изменения какой-то среды нужно редактировать только один файл, а если нужно изменить что-то для всех окружений - отредактировать основной файл.
 +<code yaml>
 +# from docker-compose.yml - the core app specification:
 +services:
 +  todo-web:
 +    image: diamol/ch06-todo-list
 +    ports:
 +      - 80
 +    environment:
 +      - Database:Provider=Sqlite
 +    networks:
 +      - app-net
 +# and from docker-compose-v2.yml - the version override file:
 +services:
 +  todo-web:
 +    image: diamol/ch06-todo-list:v2
 +</code>
 +В данном случае, меняется образ. Указывая изменения в перекрывающих файлах, необходимо сохранять структуру его написания.
 +
 +Чтобы запустить основной файл с перекрывающим, нужно указать их последовательно. Порядок важен, иначе можно получить не то, что нужно.
 +<code bash>
 +# config в конце - проверка конфигурации без реального запуска, вывод результата
 +# config сортирует вывод по алфавиту, так что результат будет выглядеть непривычно
 +docker-compose -f ./numbers/docker-compose.yml -f ./numbers/docker-compose-test.yml config
 +</code>
 +
 +<WRAP group>
 +<WRAP half column>
 +Основной файл: описывает только сервисы без портов и сетей.
 +<file yaml numbers/docker-compose.yml>
 +version: "3.7"
 +
 +services:
 +  numbers-api:
 +    image: diamol/ch08-numbers-api:v3
 +    networks:
 +      - app-net
 +
 +  numbers-web:
 +    image: diamol/ch08-numbers-web:v3
 +    environment:
 +      - RngApi__Url=http://numbers-api/rng
 +    networks:
 +      - app-net
 +
 +networks:
 +  app-net:
 +</file>
 +
 +Среда разработки: порты и сеть, без проверок.
 +<file yaml numbers/docker-compose-dev.yml>
 +version: "3.7"
 +
 +services:
 +  numbers-api:
 +    ports:
 +      - "8087:80"
 +    healthcheck:
 +      disable: true
 +
 +  numbers-web:
 +    entrypoint:
 +      - dotnet
 +      - Numbers.Web.dll
 +    ports:
 +      - "8088:80"
 +
 +networks:
 +  app-net:
 +    name: numbers-dev
 +</file>
 +</WRAP>
 +
 +<WRAP half column>
 +Тест: сеть, проверки. Сервис API остаётся внутренним и не публикуется.
 +<file yaml numbers/docker-compose-test.yml>
 +version: "3.7"
 +
 +services:
 +  numbers-api:
 +    healthcheck:
 +      interval: 20s
 +      start_period: 15s
 +      retries: 4
 +
 +  numbers-web:
 +    ports:
 +      - "8080:80"
 +    restart: on-failure
 +    healthcheck:
 +      test: ["CMD", "dotnet", "Utilities.HttpCheck.dll", "-t", "250"]
 +      interval: 20s
 +      timeout: 10s
 +      retries: 4
 +      start_period: 10s
 +
 +networks:
 +  app-net:
 +      name: numbers-test
 +</file>
 +
 +Тест на юзерах: сеть, стандартный порт 80, авторестарт сервисов, более строгая проверка.
 +<file yaml numbers/docker-compose-uat.yml>
 +version: "3.7"
 +
 +services:
 +  numbers-api:
 +    healthcheck:
 +      interval: 10s
 +      retries: 2
 +    restart: always
 +    ports:
 +      - "8090:80"
 +
 +  numbers-web:
 +    restart: always
 +    ports:
 +      - "80:80"
 +    healthcheck:
 +      interval: 10s
 +      retries: 2
 +
 +networks:
 +  app-net:
 +    name: numbers-uat
 +
 +</file>
 +</WRAP>
 +</WRAP>
 +
 +<code bash>
 +# запуск трёх сред - разработка, тест и User acceptance test
 +docker-compose -f ./numbers/docker-compose.yml -f ./numbers/docker-compose-dev.yml -p app-dev -d
 +docker-compose -f ./numbers/docker-compose.yml -f ./numbers/docker-compose-test.yml -p app-test up -d
 +docker-compose -f ./numbers/docker-compose.yml -f ./numbers/docker-compose-uat.yml -p app-uat up -d
 +
 +docker ps
 +CONTAINER ID   IMAGE                        COMMAND                  CREATED              STATUS                        PORTS                                   NAMES
 +b4f10fb1eeac   diamol/ch08-numbers-web:v3   "/bin/sh -c 'dotnet …"   About a minute ago   Up About a minute             0.0.0.0:80->80/tcp, :::80->80/tcp       numbers-uat_numbers-web_1
 +7e2e8739eb06   diamol/ch08-numbers-api:v3   "dotnet Numbers.Api.…"   About a minute ago   Up About a minute (healthy)   0.0.0.0:8090->80/tcp, :::8090->80/tcp   numbers-uat_numbers-api_1
 +daef59fda817   diamol/ch08-numbers-api:v3   "dotnet Numbers.Api.…"   About a minute ago   Up About a minute (healthy)   80/tcp                                  numbers-test_numbers-api_1
 +d19477e16c43   diamol/ch08-numbers-web:v3   "/bin/sh -c 'dotnet …"   About a minute ago   Up About a minute (healthy)   0.0.0.0:8080->80/tcp, :::8080->80/tcp   numbers-test_numbers-web_1
 +f86e7079c986   diamol/ch08-numbers-web:v3   "dotnet Numbers.Web.…"   About a minute ago   Up About a minute             0.0.0.0:8088->80/tcp, :::8088->80/tcp   numbers-dev_numbers-web_1
 +a16bb1a08e64   diamol/ch08-numbers-api:v3   "dotnet Numbers.Api.…"   About a minute ago   Up About a minute             0.0.0.0:8087->80/tcp, :::8087->80/tcp   numbers-dev_numbers-api_1
 +</code>
 +Теперь мы имеем три изолированные среды на одном сервере, каждый из веб-сервисов видит только свой API, т. к. сети разные. Если что-то поломается в одной среде, то это не затронет другие.
 +
 +Для того, чтобы удалить среду, недостаточно просто команды ''docker-compose down'', нужно указывать имя проекта, если оно было задано, и все файлы, чтобы Докер мог удалить все ресурсы, принадлежащие среде.
 +<code bash>
 +# Не удалит ничего
 +docker-compose down
 +
 +# Это работает, если при запуске не было указано имя проекта.
 +# Если имя проекта было указано, в этом примере попытается удалить сеть, но не сможет,
 +# т. к. контейнеры с префиксом по умолчанию он не найдёт (префикс другой - app-test),
 +# а сеть не может быть удалена, если в ней есть контейнеры.
 +docker-compose -f ./numbers/docker-compose.yml -f ./numbers/docker-compose-test.yml down
 +Removing network numbers-test
 +ERROR: error while removing network: network numbers-test id 8193d858fd0f2c9716d750478c253d032251571586d12f61648987baadc75315 has active endpoints
 +# Если имя проекта было указано, то его необходимо указывать вместе с файлами
 +docker-compose -f ./numbers/docker-compose.yml -f ./numbers/docker-compose-test.yml -p numbers-test down
 +Stopping numbers-test_numbers-api_1 ... done
 +Stopping numbers-test_numbers-web_1 ... done
 +Removing numbers-test_numbers-api_1 ... done
 +Removing numbers-test_numbers-web_1 ... done
 +Removing network numbers-test
 +</code>
 +
 +==== Добавление в конфигурацию переменных и секретов ====
 +Помимо изолирования приложения с помощью сетей и настройки разницы между окружениями с помощью перекрывающих файлов, нужно также менять и настройки самого приложения. Большинство приложений могут считывать свои настройки из переменных и конфигурационных файлов, и докер-композ поддерживает оба этих подхода.
 +
 +Возьмём приложение to-do, где нужно настраивать 3 параметра:
 +  - Уровень логирования: высокий для разработчиков и менее подробный для теста и прода.
 +  - Тип базы данных: простой файл или отдельная БД.
 +  - Строка подключения к БД: учётные данные для подключения, если БД не файл.
 +
 +Конфигурация приложения в виде секрета.
 +<code yaml>
 +services:
 +  todo-web:
 +    image: diamol/ch06-todo-list
 +    secrets:
 +      # Название секрета, откуда брать данные (должен быть задан в докер-композ файле)
 +      - source: todo-db-connection
 +        # Целевой файл внутри контейнера
 +        target: /app/config/secrets.json
 +</code>
 +Базовый конфиг выше неполноценен, т. к. в нём нет секции secrets. Но вот, например, перекрывающий (override) файл, где эта секция задана:
 +<code yaml>
 +services:
 +  todo-web:
 +    ports:
 +      - 8089:80
 +    # Environment добавляет переменную внутрь контейнера, которая указывает приложению использовать SQLite.
 +    # Это самый простой путь задавать параметры, и понятно, что именно настраивается.
 +    environment:
 +      - Database:Provider=Sqlite
 +    # env_file - путь к текстовому файлу, где заданы переменные в формате name=value
 +    # Удобно для задания переменных для нескольких компонентов сразу, чтобы не дублировать их в конфиге
 +    env_file:
 +      - ./config/logging.debug.env
 +
 +# Secrets - секция верхнего уровня, как services.
 +# Указывает, где на хосте лежит файл для todo-db-connection в базовом конфиге.
 +secrets:
 +  todo-db-connection:
 +    file: ./config/empty.json
 +</code>
 +
 +Помимо задания переменных внутри контейнера, можно брать переменные с хоста (в фигурных скобках). Это более гибкий подход, т. к. не нужно редактировать сами докер-композ-файлы. Применимо в случае, если нужно, к примеру, развернуть тестовое окружение на другом сервере с другими настройками.
 +<code yaml>
 +todo-web:
 +  ports:
 +    - "${TODO_WEB_PORT}:80"
 +  environment:
 +    - Database:Provider=Postgres
 +  env_file:
 +    - ./config/logging.information.env
 +  networks:
 +    - app-net
 +</code>
 +
 +Если Докер-композ обнаруживает файл .env в текущем каталоге, он берёт его за основу как env-файл. В этом файле могут быть заданы основной и перекрывающий файл, имя проекта и т. д., избавляющее от необходимости каждый раз добавлять это в командную строку, поэтому достаточно в папке выполнить docker-compose up -d. Пример файла .env:
 +<code bash>
 +# Порты
 +TODO_WEB_PORT=8877
 +TODO_DB_PORT=5432
 +# Файлы и имя проекта:
 +COMPOSE_PATH_SEPARATOR=;
 +COMPOSE_FILE=docker-compose.yml;docker-compose-test.yml
 +COMPOSE_PROJECT_NAME=todo_ch10
 +</code>
 +Собственно говоря, файл .env является настройками docker-compose по умолчанию, избавляя от необходимости указывать перекрывающий (override) файл. Этот файл помогает ориентироваться, какие файлы относятся к какому проекту. Нужно учитывать, что докер-композ смотрит только на файл .env, ему нельзя задать другое имя.
 +
 +Резюме:
 +  * **Свойство environment** - самый простой вариант, но не нужно указывать там пароли, ключи API и т. п., потому что они будут в открытом виде.
 +  * **environment_file** - удобно, когда у докер-сервисов много общих настроек. Докер-композ читает env-файл локально как список переменных, так что можно использовать это при подключении к удалённому Докер-движку.
 +  * **secret** - наиболее гибкий подход: можно указывать секретные данные, секреты поддерживаются всеми контейнерными движками. Секрет читается из файла на хосте в случае docker-compose, но он может храниться и в кластере. Независимо от места хранения, содержимое секрета передаётся в файл внутрь контейнера для чтения приложением.
 +  * **.env** - задание параметров окружения по умолчанию.
 +
 +[[https://docs.docker.com/compose/environment-variables/envvars/|Change pre-defined environment variables]] (насчёт ''COMPOSE_PROJECT_NAME'' и т. п.)\\
 +[[https://docs.docker.com/compose/environment-variables/envvars-precedence/|Приоритет переменных в зависимости от места их вызова]]
 +
 +==== Поля расширения (extension fields) - устранение дублирования текста конфигурации ====
 +Поле расширения - это блок файла YAML, на который можно ссылаться несколько раз в композ-файле. Поля расширения должны определяться на верхнем уровне вне прочих блоков - сервисов, сетей и т. п. Перед именем поля ставится &.
 +<code yaml>
 +x-labels: &logging
 +  logging:
 +    options:
 +      max-size: '100m'
 +      max-file: '10'
 +      
 +x-labels: &labels
 +  app-name: image-gallery
 +</code>
 +Здесь в случае блока logging задаются настройки логирования, так что этот блок может использоваться непосредственно внутри блока service. Labels содержит ключ/значение ярлыка и может быть использован только внутри существующего блока labels.\\
 +Вызываются поля расширения с помощью конструкции ''%%<<: *имяБлока%%''
 +<code yaml>
 +services:
 +  iotd:
 +    ports:
 +      - 8080:80
 +    <<: *logging
 +    labels:
 +      <<: *labels
 +      public: api
 +</code>
 +
 +<code yaml>
 +# config покажет результат вместе с подстановкой полей расширений
 +docker-compose -f docker-compose.yml -f docker-compose-prod.yml config
 +</code>
 +Фрагмент вывода:
 +<code yaml>
 +  image-gallery:
 +    image: diamol/ch09-image-gallery
 +    labels:
 +      app-name: image-gallery
 +      public: web
 +    logging:
 +      options:
 +        max-file: '10'
 +        max-size: 100m
 +    networks:
 +      app-net: {}
 +    ports:
 +    - published: 80
 +      target: 80
 +</code>
 +Поля расширения помогают стандартизировать сервисы. Тем не менее, есть ограничение - поля расширения не применяются к нескольким файлам, т. е. нельзя задать поля в основном файле и потом использовать их в перекрывающем (это ограничение формата YAML).
 +
 +==== Процесс конфигурации с помощью Докера ====
 +Очень хорошо иметь всю конфигурацию для деплоя в гите - это позволяет развернуть любую версию приложения, взяв исходники и запустив скрипты развёртывания. Это также позволяет разработчикам быстро приступить к работе над исправлениями, запустив приложение локально и воспроизведя ошибку у себя.
 +
 +Между окружениями всегда существуют различия, и Докер-композ даёт возможность их настраивать. Эти различия сосредоточены в трёх ключевых областях:
 +  - **Композиция приложения:** не нужно запускать все компоненты приложения в каждом окружении. Положим, для разработчиков запускать панель мониторинга не нужно, в тестовом окружении БД работает в контейнере, а в проде БД лежит в облаке. Override-файлы хорошо помогают в настройке таких различий.
 +  - **Конфигурация контейнеров:** чтобы соответствовать требованиям и возможностям различных окружений, нужно настраивать свойства контейнеров. Публикуемые порты должны быть уникальными и непересекающимися, а пути томов могут вести на локальный диск в тестовом окружении и на общий каталог в проде. Override-файлы также позволяют это делать, создавая изолированные сети для каждого приложения, тем самым позволяя запускать несколько экземпляров приложения на одном сервере.
 +  - **Конфигурация приложения:** поведение приложения внутри контейнеров меняется от окружения к окружению. Уровень логирования (например, debug для разрабов, info для теста), размер кэша или включение/отключение каких-то функций. Здесь помогут переменные окружения, секреты или env-файлы и те же оverride-файлы.
 +
 +Важно, что для всех окружений используется одни и те же образы, которые собираются, проходят автоматические тесты и им проставляется версия. По мере дальнейшего тестирования они продвигаются по окружениям, и в конечном итоге то, что попадает в прод, является теми же образами, прошедшими все тесты.
 +
 +==== Лабораторная ====
 +Создать два окружения:
 +  - dev: локальная база (файл), публикация на порту 8089, образ v2. Запуск должен быть по команде без лишних ключей, настройки по умолчанию.
 +  - test: БД в отдельном контейнере и с томом для хранения данных, публикация на порту 8080, новейший образ.
 +
 +<WRAP group>
 +<WRAP half column>
 +<file yaml docker-compose.yml>
 +version: '3.7'
 +
 +services:
 +  todo-web:
 +    image: ${IMAGE_WEB}
 +    ports:
 +      - ${PORT_WEB}:80
 +    networks:
 +      - network
 +
 +networks:
 +  network:
 +    name: ${NETWORK}
 +</file>
 +<file bash .env>
 +IMAGE_WEB='diamol/ch06-todo-list:v2'
 +PORT_WEB=8089
 +COMPOSE_PROJECT_NAME=todo-dev
 +NETWORK=todo-dev
 +Database:Provider=Sqlite
 +</file>
 +<code bash>
 +# Запуск
 +docker-compose up -d
 +Creating network "todo-dev" with the default driver
 +Pulling todo-web (diamol/ch06-todo-list:v2)...
 +v2: Pulling from diamol/ch06-todo-list
 +68ced04f60ab: Already exists
 +e936bd534ffb: Already exists
 +caf64655bcbb: Already exists
 +d1927dbcbcab: Already exists
 +641667054481: Already exists
 +9d301c563cc9: Pull complete
 +92dc1ae7fce7: Pull complete
 +12c9a1dda02c: Pull complete
 +Digest: sha256:d2341450aaf2c48ed48e8607bd2271e5b89d38779487c746836d42ddafa5496c
 +Status: Downloaded newer image for diamol/ch06-todo-list:v2
 +Creating todo-dev_todo-web_1 ... done
 +</code>
 +</WRAP>
 +
 +<WRAP half column>
 +<file yaml docker-compose-test.yml>
 +services:
 +  todo-web:
 +    depends_on:
 +      - todo-db
 +  todo-db:
 +    image: ${IMAGE_DB}
 +    networks:
 +      - network
 +    volumes:
 +      - todo-db:${PGDATA}
 +volumes:
 +  todo-db:
 +</file>
 +<file bash test.env>
 +IMAGE_WEB='diamol/ch06-todo-list'
 +IMAGE_DB='diamol/postgres:11.5'
 +PORT_WEB=8080
 +COMPOSE_PROJECT_NAME=todo-test
 +NETWORK=todo-test
 +Database:Provider=Postgres
 +PGDATA=/var/lib/postgresql/data
 +</file>
 +<code bash>
 +# Запуск
 +docker-compose -f docker-compose.yml -f docker-compose-test.yml --env-file test.env up -d
 +Creating volume "todo-test_todo-db" with default driver
 +Creating todo-test_todo-db_1 ... done
 +Creating todo-test_todo-web_1 ... done
 +</code>
 +Имя тома можно хардкодить в композ-файле - при наличии имени проекта оно ставится спереди и получается ''todo-test_todo-db'', поэтому в разных окружениях будут создаваться разные тома.
 +
 +[[https://github.com/sixeyed/diamol/tree/master/ch10/lab|Решение автора]]
 +</WRAP>
 +</WRAP>
 +
 +Результат
 +<code bash>
 +docker ps
 +CONTAINER ID   IMAGE                      COMMAND                  CREATED          STATUS          PORTS                                   NAMES
 +91946dd92dd4   diamol/ch06-todo-list      "dotnet ToDoList.dll"    28 minutes ago   Up 28 minutes   0.0.0.0:8080->80/tcp, :::8080->80/tcp   todo-test_todo-web_1
 +4d834b9ae0a9   diamol/postgres:11.5       "docker-entrypoint.s…"   28 minutes ago   Up 28 minutes   5432/tcp                                todo-test_todo-db_1
 +69af7592fc08   diamol/ch06-todo-list:v2   "dotnet ToDoList.dll"    2 hours ago      Up 2 hours      0.0.0.0:8089->80/tcp, :::8089->80/tcp   todo-dev_todo-web_1
 +</code>
 +
 +===== 11. Сборка и тестирование приложений =====
 +Докер унифицирует CI-процесс, т. к. не нужно ставить зависимости, SDK и прочее на хост. Тем не менее, некоторые компоненты нужны в любом случае: Git, реестр образов и сервер автоматизации сборки. Есть масса вариантов - от Github + AzureDevOps + DockerHub или Gitlab как решения "всё в одном" до собственной инфраструктуры, развёрнутой в контейнерах. Мало кто хочет париться с собственной инфраструктурой когда есть бесплатные внешние решения, но о запуске системы сборки в Докере знать небесполезно. Помимо независимости от внешних сервисов и скорости доступа, хорошо иметь локальный резерв инфраструктуры на случай недоступности каких-либо внешних компонентов системы сборки или интернета в целом. Три вышеперечисленных компонента легко развёртываются - здесь это будут Gogs, Docker registry и Jenkins.
 +
 +Суть в том, что в контейнер с Дженкинсом ставится Docker CLI, который подключается к Docker API хоста для выполнения задач сборки.
 +<code yaml>
 +services:
 +  jenkins:
 +    volumes:
 +      - type: bind
 +      - source: /var/run/docker.sock
 +      - target: /var/run/docker.sock
 +</code>
 +
 +<WRAP group>
 +<WRAP half column>
 +Основной файл. Тут применяется замена переменной, если значение отсутствует, т. е. при локальной сборке имя образа будет ''docker.io/diamol/ch11-numbers-api:v3-build-local'', а при сборке в Дженкинсе, где переменные заданы, ''registry.local:5000/diamol/ch11-numbers-api:v3-build-2'' и дальше в конце будет прибавляться номер сборки.
 +<file yaml docker-compose.yaml>
 +version: "3.7"
 +
 +services:
 +  numbers-api:
 +    image: ${REGISTRY:-docker.io}/diamol/ch11-numbers-api:v3-build-${BUILD_NUMBER:-local}
 +    networks:
 +      - app-net
 +
 +  numbers-web:
 +    image: ${REGISTRY:-docker.io}/diamol/ch11-numbers-web:v3-build-${BUILD_NUMBER:-local}
 +    environment:
 +      - RngApi__Url=http://numbers-api/rng
 +    depends_on:
 +     - numbers-api
 +    networks:
 +      - app-net
 +</file>
 +</WRAP>
 +
 +<WRAP half column>
 +Перекрывающий файл: где искать докерфайлы.
 +<file yaml docker-compose-build.yaml>
 +x-args: &args
 +  args:
 +    BUILD_NUMBER: ${BUILD_NUMBER:-0}
 +    BUILD_TAG: ${BUILD_TAG:-local}
 +
 +services:
 +  numbers-api:
 +    build:
 +      context: numbers
 +      dockerfile: numbers-api/Dockerfile.v4
 +      <<: *args
 +      
 +  numbers-web:
 +    build:
 +      context: numbers
 +      dockerfile: numbers-web/Dockerfile.v4
 +      <<: *args
 +      
 +networks:
 +  app-net:
 +</file>
 +Context - относительный путь к рабочему каталогу сборки, dockerfile - относительный к контексту докерфайл, args - аргументы сборки (здесь используются поля расширения).
 +</WRAP>
 +</WRAP>
 +
 +<code bash>
 +# Сборка через докер-композ - соберутся все компоненты, перечисленные в файле
 +docker-compose -f docker-compose.yml -f docker-compose-build.yml build
 +# Проверка ярлыков образа api
 +docker image inspect -f '{{.Config.Labels}}' diamol/ch11-numbers-api:v3-build-local
 +map[build_number:0 build_tag:local version:3.0]
 +</code>
 +
 +Собирать через Композ - хорошая практика, потому что всем собранным образам присваиваются одинаковые тэги, которые удобны для опознания образов. Часть докерфайла api:
 +<code bash>
 +FROM diamol/dotnet-aspnet
 +
 +ARG BUILD_NUMBER=0
 +ARG BUILD_TAG=local
 +
 +LABEL version="3.0"
 +LABEL build_number=${BUILD_NUMBER}
 +LABEL build_tag=${BUILD_TAG}
 +
 +ENTRYPOINT ["dotnet", "Numbers.Api.dll"]
 +</code>
 +''ARG'' очень похож на ''ENV'', но он работает только во время сборки образа, а не во время работы контейнера. Это хороший способ передавать переменные для сборки, которые не нужны впоследствии. ''ARG'' - это значения по умолчанию, они перекрываются аргументами ''docker build''.
 +
 +Значения по умолчанию в нескольких местах здесь нужны, чтобы обеспечить корректную сборку и через CI, и локально. Сборка через обычный Докер:
 +<code bash>
 +# С указанием докерфайла и аргумента сборки (здесь --build-arg BUILD_TAG перекроет ARG в докерфайле)
 +docker image build -f numbers-api/Dockerfile.v4 --build-arg BUILD_TAG=ch11 -t numbers-api .
 +# Проверка ярлыков
 +docker image inspect -f '{{.Config.Labels}}' numbers-api
 +map[build_number:0 build_tag:ch11 version:3.0]
 +</code>
 +Тэги очень полезны: по ним можно выяснить, откуда взялся образ, посмотреть задачу в CI, которая собрала его, а оттуда перейти на версию кода, который запустил задачу. Это путь аудита от контейнера к исходному коду.
 +==== Создание задач CI без каких-либо зависимостей, кроме Докера ====
 +//Jenkins в репозитории устарел и сломан.//
 +
 +===== 12. Оркестраторы - Docker Swarm и k8s =====
 +<code bash>
 +# Включить режим Swarm
 +docker swarm init 
 +  Swarm initialized: current node (ti5tg544h7k6dsw0mnj6uepot) is now a manager.
 +  To add a worker to this swarm, run the following command:
 +      docker swarm join --token SWMTKN-1-5hlq0y8b4z34bsuuopr3uovtwliehdoiwhew 192.168.0.2:2377
 +  To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.
 +  
 +# Показать ссылку для добавления рабочего
 +docker swarm join-token worker
 +
 +# Показать ссылку для добавления менеджера
 +docker swarm join-token manager
 +
 +# Список узлов кластера
 +docker node ls
 +ID                            HOSTNAME    STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
 +ti5tg544h7k6dsw0mnj6uepot *   t-docker2   Ready     Active         Leader           20.10.21
 +</code>
 +
 +В кластере вы не запускаете контейнеры - вы разворачиваете сервисы, а контейнерами уже занимается сам кластер.
 +<code bash>
 +# Развернуть сервис
 +$ docker service create --name timecheck --replicas 1 diamol/ch12-timecheck:1.0
 +v02pz59txdtirqyqzr18s4w71
 +overall progress: 1 out of 1 tasks
 +1/1: running   [==================================================>]
 +verify: Service converged
 +
 +# Вывести список сервисов
 +$ docker service ls
 +ID             NAME        MODE         REPLICAS   IMAGE                       PORTS
 +v02pz59txdti   timecheck   replicated   1/       diamol/ch12-timecheck:1.0
 +
 +# Обновить образ у сервиса timecheck
 +docker service update --image diamol/ch12-timecheck:2.0 timecheck
 +
 +# Вывести все реплики сервиса timecheck
 +docker service ps timecheck
 +
 +# Откатиться к предыдущему состоянию
 +docker service update --rollback timecheck
 +
 +# Логи всех реплик за последние 30 сек
 +docker service logs --since 30s timecheck
 +
 +# Удалить сервис
 +docker service rm timecheck
 +</code>
 +
 +==== Управление сетью в кластере ====
 +Чтобы контейнеры могли обмениваться трафиком, находясь на разных узлах, в кластере создаётся своя абстрактная сеть (overlay network). Сервисы могут обращаться друг к другу внутри этой сети по именам сервисов или по DNS-именам. Разница в подходе между Композом и Свормом заключается в том, что Композ, когда у него несколько реплик контейнера, выдаёт на запрос все адреса и уже клиенту нужно выбирать, куда слать трафик. Сворм создаёт только один виртуальный IP для сервиса, за которым может крутиться множество контейнеров, и система сама решает вопрос балансировки.
 +
 +<code bash>
 +$ docker network create --driver overlay iotd-net
 +gpl2688gwl76xtmo67u1g4xvg
 +
 +$ docker service create --detach --replicas 3 --network iotd-net --name iotd diamol/ch09-image-of-the-day
 +if4bv9e96evlqm0d3hj35jx7c
 +
 +$ docker service create --detach --replicas 2 --network iotd-net --name accesslog diamol/ch09-access-log
 +d7pq960ki6gqj1da0f8e34byf
 +
 +$ docker service ls
 +ID             NAME        MODE         REPLICAS   IMAGE                                 PORTS
 +d7pq960ki6gq   accesslog   replicated   2/       diamol/ch09-access-log:latest
 +if4bv9e96evl   iotd        replicated   3/       diamol/ch09-image-of-the-day:latest
 +</code>
 +Балансировка запросов между узлами кластера, на которых запущены контейнеры сервиса и все ноды слушают один и тот же порт при публикации сервиса - это называется //ingress networking.// Это работает стандартно, когда сервис публикует порты.
 +<code bash>
 +# Запуск веб-части image gallery
 +$ docker service create --detach --replicas 2 --network iotd-net --name image-gallery --publish 8010:80 diamol/ch09-image-gallery
 +l9oju3hy9qi5qx4hz1ubinp4f
 +
 +$ docker service ls
 +ID             NAME            MODE         REPLICAS   IMAGE                                 PORTS
 +d7pq960ki6gq   accesslog       replicated   2/       diamol/ch09-access-log:latest
 +l9oju3hy9qi5   image-gallery   replicated   2/       diamol/ch09-image-gallery:latest      *:8010->80/tcp
 +if4bv9e96evl   iotd            replicated   3/       diamol/ch09-image-of-the-day:latest
 +</code>
 +Здесь видно один порт на несколько контейнеров, что невозможно в Композе.
 +
 +==== Выбор между Swarm и k8s ====
 +В датацентре удобнее и проще Сворм, в облаке - Кубер. Сворм в целом проще, хотя не имеет настолько больших возможностей, как Кубер, но в большинстве случаев они и не нужны. У Сворма yaml-файлы от 5 до 10 раз меньше по объёму.
 +
 +==== Лабораторная ====
 +Создать в Сворме приложение Numbers
 +<code bash>
 +docker network create --driver overlay numbers-net
 +docker service create --detach --replicas 2 --network numbers-net --name numbers-api diamol/ch08-numbers-api:v3
 +docker service create --detach --replicas 2 --network numbers-net --name numbers-web --publish 8010:80 diamol/ch08-numbers-web:v3
 +
 +docker service ls
 +ID             NAME          MODE         REPLICAS   IMAGE                        PORTS
 +vr1psfnkhxs8   numbers-api   replicated   2/       diamol/ch08-numbers-api:v3   
 +wtruhsgcjrhb   numbers-web   replicated   2/       diamol/ch08-numbers-web:v3   *:8010->80/tcp
 +
 +docker service rm numbers*
 +docker network rm numbers-net
 +</code>
 +
 +===== 13. Развёртывание приложений в кластере Docker Swarm =====
 +До этого команды запуска были императивными, но основной подход - это декларации, т. е. применение YAML-файлов, использующих синтаксис Композа. Тем не менее, имеются некоторые различия в подходе к Сворм-режиму и к обычному Композу.
 +
 +==== Композ для развёртывания в проде ====
 +К примеру, имеется композ-файл:
 +<file yaml v1.yml>
 +version: "3.7"
 +services:
 +  todo-web:
 +    image: diamol/ch06-todo-list
 +    ports:
 +      - 8080:80
 +</file>
 +Если развернуть его с помощью Композа на одном сервере, то будет один контейнер, опубликованный на порту 8080. Если развернуть через Сворм, то это будет сервис с одной репликой, использующей ингресс-сеть для опубликованного порта. В Сворм-режиме приложения разворачиваются как стэк, т. е., сущность, объединяющая сервисы, сети, тома и прочие ресурсы.
 +
 +<code bash>
 +# Развернуть стэк
 +docker stack deploy -c ./todo-list/v1.yml todo
 +Creating network todo_default
 +Creating service todo_todo-web
 +# Список стэков
 +docker stack ls
 +NAME      SERVICES   ORCHESTRATOR
 +todo      1          Swarm
 +# Список развёрнутых сервисов
 +docker service ls
 +ID             NAME            MODE         REPLICAS   IMAGE                          PORTS
 +pb9y6a0dyxbf   todo_todo-web   replicated   1/       diamol/ch06-todo-list:latest   *:8080->80/tcp
 +</code>
 +В этом примере используется стандартный композ-файл без каких-либо добавок, и когда он разворачивается через Сворм, где есть несколько узлов, то сервис получает высокую доступность - контейнер перезапускается на другом узле, если несущий хост становится недоступен.
 +
 +Специфичные для Сворма настройки размещаются в разделе deploy сервиса. Здесь указаны 2 реплики и ограничение в пол-ядра и 100 МБ памяти для каждой. Лимиты применяются во время запуска контейнера, поэтому после применения лимитов контейнеры будут пересозданы.
 +<code yaml>
 +services:
 +  todo-web:
 +    image: diamol/ch06-todo-list
 +    ports:
 +      - 8080:80
 +    deploy:
 +      replicas: 2
 +      resources:
 +        limits:
 +          cpus: "0.50"
 +          memory: 100M
 +</code>
 +Стэк - это организационная единица для управления приложением в кластере, используя команду stack в Docker CLI. Управлять ресурсами стэка можно без композ-файла, т. к. вся информация хранится у менеджеров кластера.
 +<code bash>
 +# список всех сервисов
 +docker stack services todo
 +# список всех реплик всех сервисов в стэке
 +docker stack ps todo
 +# удалить стэк
 +docker stack rm todo
 +</code>
 +
 +==== Управление конфигурацией приложения с помощью конфигурационных объектов ====
 +Ранее рассматривалась настройка приложения как с помощью настроек по умолчанию в среде разработки, так и с помощью переменных и локальных файлов в тестовой среде.  В проде, когда используется кластер, конфигурационные файлы хранятся на управляющих узлах. Это объекты ''docker config''.
 +<code bash>
 +# Создание конфига из локального json-файла
 +$ docker config create todo-list-config ./todo-list/configs/config.json
 +xyc2p4bngbacp4zib7a5qo3ah
 +# Список конфигов
 +$ docker config ls
 +ID                          NAME               CREATED         UPDATED
 +xyc2p4bngbacp4zib7a5qo3ah   todo-list-config   9 seconds ago   9 seconds ago
 +</code>
 +В примере файл JSON, но это может быть и XML, и бинарник. Сворм пробрасывает объект конфигурации в контейнер, который виден там как файл. С конфигами работают так же, как и с другими ресурсами, к примеру, ''inspect'' может показать содержимое файла. Необходимо помнить, что конфиги не предназначены для секретных данных, т. к. они не зашифрованы ни сами по себе, ни при передаче. В рабочем процессе девопса задачи управления объектами конфигурации и управление самими приложениями чаще всего разделены, особенно если инфраструктура более-менее крупная.
 +<code bash>
 +$ docker config inspect  --pretty todo-list-config
 +ID:                     xyc2p4bngbacp4zib7a5qo3ah
 +Name:                   todo-list-config
 +Created at:             2023-04-25 11:13:58.250292261 +0000 utc
 +Updated at:             2023-04-25 11:13:58.250292261 +0000 utc
 +Data:
 +{
 +  "Logging": {
 +    "LogLevel": {
 +      "Default": "Information",
 +      "Microsoft": "Warning",
 +      "Microsoft.Hosting.Lifetime": "Warning"
 +    }
 +  },
 +  "AllowedHosts": "*",
 +  "Database": {
 +    "Provider": "Postgres"
 +  }
 +}
 +</code>
 +Чтобы сервис использовал конфиг, нужно прописать это в композ-файле. ''External: true'' значит, что Композ не будет создавать конфиг в системе и подразумевается, что он будет использовать созданный до этого.
 +<code yaml>
 +services:
 +  todo-web:
 +    image: diamol/ch06-todo-list
 +    ports:
 +      - 8080:80
 +    configs:
 +      - source: todo-list-config
 +        target: /app/config/config.json
 +        
 +#...
 +
 +configs:
 +  todo-list-config:
 +    external: true
 +</code>
 +Обновить стэк можно, задеплоив новую версию с тем же именем, где сделаны необходимые изменения.
 +<code bash>
 +docker stack deploy -c ./todo-list/v3.yml todo
 +</code>
 +Но работать это не будет, т. к. в конфиге нет логина и пароля для подключения к базе, которые нужно передать в секрете, о чём ниже.
 +
 +==== Управление конфиденциальной информацией с помощью секретов ====
 +Секреты очень похожи на конфиги, но прочесть их содержимое можно только внутри контейнера, к которому они подключены. Во всех остальных случаях они зашифрованы.
 +<code bash>
 +$ docker secret create todo-list-secret ./todo-list/secrets/secrets.json
 +1hc6k5nt0hy8a6pru6qedonkk
 +
 +$ docker secret inspect --pretty todo-list-secret
 +ID:              1hc6k5nt0hy8a6pru6qedonkk
 +Name:              todo-list-secret
 +Driver:
 +Created at:        2023-04-25 11:49:38.860226816 +0000 utc
 +Updated at:        2023-04-25 11:49:38.860226816 +0000 utc
 +</code>
 +
 +Секрет в композ-файле:
 +<code yaml>
 +  secrets:
 +    - source: todo-list-secret
 +      target: /app/config/secrets.json
 +
 +#...
 +
 +secrets:
 +  todo-list-secret:
 +    external: true
 +</code>
 +Обновить стэк:
 +<code bash>
 +docker stack deploy -c ./todo-list/v4.yml todo
 +</code>
 +Теперь приложение работает.
 +
 +Ни конфиги, ни секреты нельзя обновить в Сворме, их можно только заменить. Делается так:
 +  - Создаётся новый конфиг/секрет
 +  - В композ-файле прописывается новый секрет
 +  - Стэк обновляется, используя новый композ-файл
 +
 +Кубер позволяет обновлять конфиги/секреты, но всё зависит от приложения - одно умеет обновлять конфиг на лету, другое - нет, так что во избежание проблем лучше их пересоздавать всегда.
 +
 +==== Хранение постоянных данных в Сворме (volumes) ====
 +В кластере контейнер может запуститься на разных нодах, поэтому локальный том имеет ограниченное применение. Самый простой случай, когда не учитывается возможность сбоя ноды и не нужно запускать несколько экземпляров контейнера - это присвоение ярлыка, который привязывает контейнер к ноде.
 +<code bash>
 +# Вывести идентификатор узла и прописать его как ярлык
 +docker node update --label-add storage=raid $(docker node ls -q)
 +</code>
 +Привязка в композ-файле:
 +<code yaml>
 +services:
 +  todo-db:
 +    image: diamol/postgres:11.5
 +    volumes:
 +      - todo-db-data:/var/lib/postgresql/data
 +    deploy:
 +      placement:
 +        constraints:
 +          - node.labels.storage == raid
 +
 +#...
 +
 +volumes:
 +  todo-db-data:
 +</code>
 +Это самый простой пример, но если требуется общее хранилище для всех нод, тут сложнее. У Докера есть плагины для разных систем хранения, и Сворм может быть настроен соответствующим образом. Настройка может быть различной для разных типов томов, но в любом случае тома подключаются к сервисам.
 +
 +==== Как кластер управляет стэками ====
 +Стэк - это группа ресурсов в кластере, которыми он управляет. Подход к управлению различается в зависимости от типа ресурса.
 +  * **Тома** могут создаваться и удаляться Свормом. Стэк создаст том по умолчанию, если он предусмотрен в образе, и этот том будет удалён при удалении стэка. Если том именованный, то он будет создан, но удалён вместе со стэком не будет.
 +  * **Секреты/конфиги** создаются при загрузке в кластер внешних файлов, хранятся в БД кластера и доставляются в контейнеры, которые в них нуждаются. Не могут быть изменены. Процессы хранения конфигураций и развёртывания приложений разделены.
 +  * **Сети** могут управляться отдельно от приложений, например, когда они создаются заранее, или Сворм может их создавать сам по необходимости. Каждый стэк запускается с сетью, к которой будут подключены его сервисы, даже если они не заданы в композ-файле.
 +  * **Сервисы** создаются и удаляются во время развёртывания стэка. Когда они работают, Сворм следит, чтобы они были в рабочем состоянии и в нужном количестве, сбойные перезапускаются.
 +
 +Между сервисами нельзя создать зависимость, т. е., чтобы один запускался только после другого, потому что это бы сильно замедлило бы развёртывание. Вместо этого нужно делать проверки здоровья и зависимостей в образах, тогда кластер сможет сам перезапускать или заменять контейнеры, делая приложение самовосстанавливающимся.
 +
 +===== 14. Автоматизация релизов - обновление и откат =====
 +Докер не поддерживает развёртывание стэка из нескольких (перекрывающих) композ-файлов, их надо сначала слепить.
 +<code bash>
 +# Слепить файлы, заодно проверить конфиг
 +docker-compose -f ./numbers/docker-compose.yml -f ./numbers/prod.yml config > stack.yml
 +# Развернуть
 +docker stack deploy -c stack.yml numbers
 +# Список сервисов стэка
 +docker stack services numbers
 +ID             NAME                  MODE         REPLICAS   IMAGE                            PORTS
 +htqpdr0tfvjw   numbers_numbers-api   replicated   6/       diamol/ch08-numbers-api:latest
 +w2veemvl0qxf   numbers_numbers-web   global       1/       diamol/ch08-numbers-web:latest
 +</code>
 +Режим global значит, что на каждом узле Сворма запускается по одному экземпляру контейнера, т. е. кол-во экземпляров контейнера будет равно кол-ву узлов кластера. При добавлении узлов в кластер на нём будет автоматически запущен этот контейнер. Фрагмент с настройкой:
 +<code yaml>
 +numbers-web:
 +  ports:
 +    - target: 80
 +      published: 80
 +      mode: host
 +  deploy:
 +    mode: global
 +</code>
 +''mode: host'' привязывает 80-й порт непосредственно на узле и не использует ingress-сеть.
 +
 +Обновим стэк, добавив проверки и заменив образ на v2:
 +<WRAP group>
 +<WRAP half column>
 +<file yaml docker-compose.yaml>
 +version: "3.7"
 +
 +services:
 +  numbers-api:
 +    image: diamol/ch08-numbers-api
 +    networks:
 +      - app-net
 +
 +  numbers-web:
 +    image: diamol/ch08-numbers-web
 +    environment:
 +      - RngApi__Url=http://numbers-api/rng
 +    networks:
 +      - app-net
 +</file>
 +<file yaml prod.yaml>
 +services:
 +  numbers-api:
 +    deploy:
 +      replicas: 6
 +      resources:
 +        limits:
 +          cpus: "0.50"
 +          memory: 75M
 +
 +  numbers-web:
 +    ports:
 +      - target: 80
 +        published: 80
 +        mode: host
 +    deploy:
 +      mode: global
 +      resources:
 +        limits:
 +          cpus: "0.75"
 +          memory: 150M
 +
 +networks:
 +  app-net:
 +    name: numbers-prod
 +</file>
 +</WRAP>
 +
 +<WRAP half column>
 +<file yaml prod-healthcheck.yaml>
 +services:
 +  numbers-api:
 +    healthcheck:
 +      test: ["CMD", "dotnet", "Utilities.HttpCheck.dll", "-u", "http://localhost/health", "-t", "500"]
 +      interval: 2s
 +      timeout: 3s
 +      retries: 2
 +      start_period: 5s
 +
 +  numbers-web:
 +    healthcheck:
 +      interval: 20s
 +      timeout: 10s
 +      retries: 3
 +      start_period: 30s
 +</file>
 +<file yaml v2.yaml>
 +services:
 +  numbers-api:
 +    image: diamol/ch08-numbers-api:v2
 +
 +  numbers-web:
 +    image: diamol/ch08-numbers-web:v2
 +</file>
 +</WRAP>
 +</WRAP>
 +Обновить стэк:
 +<code bash>
 +docker-compose -f ./numbers/docker-compose.yml -f ./numbers/prod.yml -f ./numbers/prod-healthcheck.yml -f ./numbers/v2.yml --log-level ERROR config > stack.yml
 +docker stack deploy -c stack.yml numbers
 +</code>
 +При таком обновлении контейнеры сначала останавливаются, потом запускаются новые. Это имеет смысл, если контейнер использует порт хоста напрямую, т. к. ещё один контейнер с такой настройкой не запустится. Стандартно Сворм останавливает контейнеры-реплики один за другим, потом запускает новые. Если обнаружены ошибки, то развёртывание останавливается. Это далеко не всегда подходящее поведение при развёртывании.
 +
 +<code yaml>
 +services:
 +  numbers-api:
 +    deploy:
 +      update_config:
 +        parallelism: 3 # Кол-во одновременных замен
 +        monitor: 60s # Период слежения за новыми репликами перед продолжением обновления
 +        failure_action: rollback # Действие при неудаче после истечения периода monitor
 +        order: start-first # Порядок замены реплик, здесь новые стартуют сначала, потом удаляются старые (стандартно stop-first)
 +</code>
 +
 +Есть команда, показывающая в удобном виде спецификацию сервиса, конфигурацию обновления и его последний статус. Здесь видно, что развёртывание, начатое 4 минуты назад, приостановлено, и названа причина.
 +<code bash>
 +# Сервис именуется {stack-name}_{service-name}
 +docker service inspect --pretty numbers_numbers-api
 +
 +ID:             htqpdr0tfvjwey55srp5njmph
 +Name:           numbers_numbers-api
 +Labels:
 + com.docker.stack.image=diamol/ch08-numbers-api:v2
 + com.docker.stack.namespace=numbers
 +Service Mode:   Replicated
 + Replicas:      6
 +UpdateStatus:
 + State:         rollback_paused
 + Started:       4 minutes ago
 + Message:       update paused due to failure or early termination of task j6qfwyekjvhlto6sh32tdxc45
 +Placement:
 +UpdateConfig:
 + Parallelism:   1
 + On failure:    pause
 + Monitoring Period: 5s
 + Max failure ratio: 0
 + Update order:      stop-first
 +RollbackConfig:
 + Parallelism:   1
 + On failure:    pause
 + Monitoring Period: 5s
 + Max failure ratio: 0
 + Rollback order:    stop-first
 +ContainerSpec:
 + Image:         diamol/ch08-numbers-api:v2@sha256:a14fa4b916118e7c7d3ee688193b28c0c00949a6bbc08353cf6e918bf47bb942
 +Resources:
 + Limits:
 +  CPU:          0.5
 +  Memory:       75MiB
 +Networks: numbers-prod
 +Endpoint Mode:  vip
 + Healthcheck:
 +  Interval = 2s
 +  Retries = 2
 +  StartPeriod = 5s
 +  Timeout =     3s
 +  Tests:
 +         Test = CMD
 +         Test = dotnet
 +         Test = Utilities.HttpCheck.dll
 +         Test = -u
 +         Test = http://localhost/health
 +         Test = -t
 +         Test = 500
 +</code>
 +
 +Нужно помнить, что так как были изменены стандартные настройки обновления, дальше нужно включать эти настройки в каждое последующее развёртывание, иначе поведение обновления станет как прежде.
 +
 +==== Откат обновления ====
 +Команды, которая откатывает обновление целого стэка, не существует. Откатить обратно можно только отдельные сервисы. Тем не менее, откатывать руками сервис не требуется, если только не произошло что-то уж совсем из ряда вон. Откат делается автоматически, если во время развёртывания обновления новые реплики не заработали в течение заданного периода мониторинга.
 +
 +Развёртывания всё равно являются основной причиной простоя, потому что даже если всё максимально автоматизировано, в любом случае возможны ошибки в yaml-файлах и каких-то упущенных из вида вещах.
 +
 +Агрессивная политика отката обновления
 +<code yaml>
 +services:
 +  numbers-api:
 +    deploy:
 +      rollback_config:
 +        parallelism: 6 # Здесь: все реплики сразу
 +        monitor: 0s # Note: Setting to 0 will use the default 5s.
 +        failure_action: continue # Если откат не удался, всё равно продолжить
 +        order: start-first # Сначала запустить старую версию, потом гасить новую
 +</code>
 +https://docs.docker.com/compose/compose-file/compose-file-v3/#rollback_config
 +
 +{{:learning:pasted:20230428-122002.png?600}}
 +
 +==== Обслуживание узлов кластера ====
 +Чтобы выгнать контейнеры с узла для его обновления или обслуживания, нужно ввести его в //drain mode.//
 +<code bash>
 +docker node update --availability drain node5
 +</code>
 +Управляющие узлы в дрейн-моде всё равно выполняют свои функции, т. е., убирается только рабочая нагрузка. Управляющие узлы работают в режиме //active-passive,// т. е., управляет кластером только один со статусом //leader.// Если лидер падает, среди оставшихся менеджеров проводятся выборы и лидером становится получивший наибольшее кол-во голосов. Поэтому в кластере нужно иметь нечётное число управляющих узлов - 3 для небольших и 5 для больших кластеров. Если управляющий узел сломался и больше доступен не будет, можно назначить управляющий узел из рабочих для восстановления нечётного числа этих управляющих узлов.
 +
 +<code bash>
 +# Повысить рабочий узел до менеджера
 +docker node promote node6
 +# Список узлов
 +docker node ls
 +</code>
 +
 +Есть 2 способа для ноды выйти из кластера:
 +  - С менеджера командой ''node rm''
 +  - С самой ноды командой ''swarm leave'' (на менеджере нода останется в списке узлов и будет выглядеть как выключенная)
 +
 +Понизить менеджера до рабочего: ''node demote''.
 +
 +Рассмотрим некоторые ситуации:
 +  * Все менеджеры недоступны - всё будет работать, но если какой-то контейнер упадёт, то он не перезапустится, т. к. состояние кластера никто не мониторит. Нужно поднять хотя бы одного менеджера.
 +  * Только один менеджер доступен, и он не лидер - выборы лидера не могут быть проведены, т. к. остался только один менеджер. Назначить лидера принудительно можно командой ''swarm init --force-new-cluster'', сохраняя всю информацию кластера и работающие задания. Затем нужно добавить менеджеров для восстановления отказоустойчивости.
 +  * Перераспределение существующих реплик по узлам - реплики автоматически не распространяются на новые добавленные узлы кластера, если сервисы не были обновлены. Пнуть перераспределение можно командой ''service update --force'', не меняя никаких других параметров.
 +
 +==== Отказоустойчивость в кластерах Сворма ====
 +Есть несколько слоёв внедрения приложения с точки зрения отказоустойчивости, которые были рассмотрены:
 +  * Проверки здоровья, сообщающие кластеру, работает ли приложение, и тот заменяет сбойные контейнеры;
 +  * Несколько рабочих узлов (перераспределение контейнеров);
 +  * Несколько менеджеров (передача обязанностей мониторинга и планировщика).
 +
 +Остался вопрос отказоустойчивости самого датацентра. Вариант разнесения датацентров кластера по разным геолокациям хорош с точки зрения простоты управления, но имеет проблемы, связанную с сетевой задержкой. Узлы кластера довольно много обмениваются информацией, поэтому задержки могут приводить к тому, что кластер подумает, что какие-то узлы выключились и начать перераспределять нагрузку на другие узлы. Также можно получить split-brain, когда группы менеджеров в разных локациях выберут своих лидеров.
 +
 +Лучше иметь несколько кластеров, что повлечёт увеличение сложности управления инфраструктурой, но зато не будет проблем с сетевой задержкой. Кластеры будут идентичными по конфигурации, а пользователи будут попадать на сервис с помощью внешнего DNS, направляющего запрос на ближайший расположенный к ним кластер.
 +
 +===== 15. Удалённый доступ и CI/CD =====
 +Стандартно Docker API открыт только для локальной машины, но можно открыть доступ извне.
 +
 +==== Незащищённый доступ ====
 +FIXME Незащищённый доступ использовать в реальной жизни крайне не рекомендуется!
 +<file yaml /etc/docker/daemon.json>
 +{
 +  "hosts": [
 +  # enable remote access on port 2375:
 +  "tcp://0.0.0.0:2375",
 +  # and keep listening on the local channel - Windows pipe:
 +  "npipe://"
 +  # OR Linux socket:
 +  "fd://"
 +  ],
 +  "insecure-registries": [
 +  "registry.local:5000"
 +  ]
 +}
 +</file>
 +//Это не работает, пробовал и [[https://docs.docker.com/config/daemon/remote-access/|пример из документации]].//
 +<code bash>
 +docker --host tcp://10.1.0.235:2375 container ls
 +Cannot connect to the Docker daemon at tcp://10.1.0.235:2375. Is the docker daemon running?
 +</code>
 +
 +==== Защищённый доступ, контекст ====
 +2 способа: TLS и SSH. SSH проще, т. к. не надо вносить изменения в конфиг Докера и нет возни с сертификатами.
 +
 +Чтобы в каждой команде не указывать сервер, можно создать //контекст,// содержащий всё, что нужно для подключения к удалённому Докер-движку.
 +<code bash>
 +# создание
 +docker context create remote --docker "host=ssh://user@server"
 +# список контекстов
 +docker context ls
 +NAME        DESCRIPTION                               DOCKER ENDPOINT               KUBERNETES ENDPOINT   ORCHESTRATOR
 +remote                                                ssh://user@server
 +default *   Current DOCKER_HOST based configuration   unix:///var/run/docker.sock                         swarm
 +</code>
 +Контекст можно указать временно для терминальной сессии, а можно сделать его работающим постоянно для всех сессий, пока не будет указано иначе.
 +<code bash>
 +# Временно
 +export DOCKER_CONTEXT='remote'
 +# Постоянно
 +docker context use remote
 +</code>
 +:!: Переменная ''DOCKER_CONTEXT'' имеет приоритет перед ''docker context use'', поэтому если переключиться на контекст с помощью переменной и попробовать отменить его командой ''docker context use'', то в текущей сессии останется контекст, заданный переменной. Если переключение между контекстами происходит часто, лучше использовать переменную и не трогать системную настройку.
 +
 +==== Модель доступа в Докере ====
 +Защита доступа состоит из 2-х частей: шифрование трафика между CLI и API и аутентификация. Авторизации нет: если клиент подключился к API, он может делать всё, что угодно. Docker Enterprise и k8s имеют ролевую модель, но есть подход GitOps, когда к кластеру никто не подключается, но он сам следит за появлением новых образов в реестре и развёртывает их.
 +
 +{{:learning:pasted:20230517-140318.png}}
 +
 +
 +===== 16. Сборка образов для разных архитектур (Linux, Windows, Intel, Arm) =====
 +
 +===== 17. Оптимизация образов по размеру, скорости и безопасности =====
 +✔ Докер не удаляет старые слои после скачивания новых - это нужно делать руками. Хорошей практикой является регулярная очистка.
 +<code bash>
 +# Оценка занимаемого места
 +docker system df
 +# Очистка старых слоёв и кэша
 +docker system prune
 +</code>
 +
 +✔ В образ нужно копировать только то, что необходимо для запуска приложения. Например, вместо ''COPY . .'' сделать ''COPY ./app ./app''. Удалять скопированные файлы, например, ''RUN rm -rf docs'' в Dockerfile бесполезно, т. к. из-за слоистой структуры образа файлы просто будут скрыты и образ останется того же размера.
 +
 +✔ Помимо избирательного копирования, хорошо использовать файл ''.dockerignore'', где перечислены пути или шаблоны, которые нужно исключить из контекста сборки. Контекст сборки - это каталог, где сборка запускается, и он может передаваться в том числе на удалённую машину.
 +<code bash>
 +# Запуск без .dockerignore
 +docker image build -t diamol/ch17-build-context:v3 -f ./Dockerfile.v3 .
 +Sending build context to Docker daemon  2.104MB
 +# После добавления .dockerignore с содержимым docs/
 +docker image build -t diamol/ch17-build-context:v3 -f ./Dockerfile.v3 .
 +Sending build context to Docker daemon  4.608kB
 +</code>
 +
 +✔ Надо выбирать базовый образ с минимальным набором необходимых функций, чтобы уменьшить размер образа и поверхность атаки. По возможности рассматривать варианты на базе Alpine/Debian Slim.
 +
 +✔ Контроль совместно устанавливаемых "рекомендуемых" пакетов + сокращение кол-ва слоёв.
 +<code bash>
 +FROM debian:stretch-slim
 +RUN apt-get update
 +RUN apt-get install -y curl=7.52.1-5+deb9u9
 +RUN apt-get install -y socat=1.7.3.1-2+deb9u1
 +
 +# -20 МБ
 +FROM debian:stretch-slim
 +RUN apt-get update \
 +&& apt-get install -y --no-install-recommends \
 +curl=7.52.1-5+deb9u9 \
 +socat=1.7.3.1-2+deb9u1 \
 +&& rm -rf /var/lib/apt/lists/*
 +</code>
 +
 +✔ Распаковка только тех файлов из внешних архивов, которые действительно нужны.
 +
 +<WRAP group>
 +<WRAP half column>
 +Большой размер образа
 +<code bash>
 +FROM diamol/base
 +
 +ARG DATASET_URL=https://archive.ics.uci.edu/ml/machine-learning-databases/url/url_svmlight.tar.gz
 +
 +WORKDIR /dataset
 +
 +RUN wget -O dataset.tar.gz ${DATASET_URL} && \
 +    tar xvzf dataset.tar.gz
 +
 +WORKDIR /dataset/url_svmlight
 +RUN cp Day1.svm Day1.bak && \
 +    rm -f *.svm && \
 +    mv Day1.bak Day1.svm
 +</code>
 +</WRAP>
 +
 +<WRAP half column>
 +Маленький размер образа
 +<code bash>
 +FROM diamol/base
 +
 +ARG DATASET_URL=https://archive.ics.uci.edu/ml/machine-learning-databases/url/url_svmlight.tar.gz
 +
 +WORKDIR /dataset
 +
 +RUN wget -O dataset.tar.gz ${DATASET_URL} && \
 +    tar -xf dataset.tar.gz url_svmlight/Day1.svm && \
 +    rm -f dataset.tar.gz
 +</code>
 +</WRAP>
 +</WRAP>
 +
 +Есть более удобный подход, когда для каждого этапа делается свой промежуточный контейнер.
 +<code bash>
 +FROM diamol/base AS download
 +ARG DATASET_URL=https://archive.ics.uci.edu/.../url_svmlight.tar.gz
 +RUN wget -O dataset.tar.gz ${DATASET_URL}
 +
 +FROM diamol/base AS expand
 +COPY --from=download dataset.tar.gz .
 +RUN tar xvzf dataset.tar.gz
 +
 +FROM diamol/base
 +WORKDIR /dataset/url_svmlight
 +COPY --from=expand url_svmlight/Day1.svm .
 +</code>
 +
 +Поэтапный докерфайл можно останавливать на любом этапе:
 +<code bash>
 +docker image build -t diamol/ch17-ml-dataset:v3 -f Dockerfile.v3 . # ~24 MB
 +docker image build -t diamol/ch17-ml-dataset:v3-expand -f Dockerfile.v3 --target expand . # ~2.4 GB
 +docker image build -t diamol/ch17-ml-dataset:v3-download -f Dockerfile.v3 --target download . # ~250 MB
 +
 +# Размеры образов
 +docker image ls -f reference=diamol/ch17-ml-dataset:v3*
 +</code>
 +Это даёт возможность запустить контейнер из образа нужного этапа и исправлять ошибки, глядя в файловую систему.
 +
 +Поэтапный докерфайл в этом случае - лучший выбор, т. к. на выходе имеется оптимизированный образ и используется более простой синтаксис - не надо, как во втором варианте, возиться с очисткой диска. Каждый этап кэшируется, поэтому при изменении этапа expand предыдущий уже не будет делаться заново.
 +
 +✔ Сокращение времени сборки. В поэтапном докерфайле самые редко меняющиеся части надо помещать в начало (опубликованные порты, переменные окружения, ENTRYPOINT), а самые часто меняющиеся - в конец (бинарники, конфиги).
 +
 +===== 18. Управление конфигурацией приложения в контейнерах =====
 +
 +==== Многоуровневый подход ====
 +
 +3 типа конфигурации:
 +  - По релизу: настройки одинаковые во всех окружениях
 +  - По окружению: настройки разнятся в зависимости от окружения
 +  - По функциям: изменение поведения приложения от релиза к релизу
 +
 +К примеру, настройки релиза зашиты в образ, настройки окружения - в файле, монтирующемся на томе, а функция включается из переменной.\\
 +Разрабы запускают базовый образ без метрик Прометея для экономии ресурсов, в тестовой среде метрики включаются и это регулируется переменной или конфигом.
 +<code bash>
 +# default config
 +docker container run -d -p 8080:80 diamol/ch18-access-log
 +# loading a local config file override
 +docker container run -d -p 8081:80 -v "$(pwd)/config/dev:/app/config-override" diamol/ch18-access-log
 +# override file + environment variable, которая должна быть в формате JSON (здесь: переменная не срабатывает)
 +docker container run -d -p 8082:80 -v "$(pwd)/config/dev:/app/config-override" -e NODE_CONFIG='{\"metrics\": {\"enabled\":\true\"}}' diamol/ch18-access-log
 +
 +# check the config APIs in each container:
 +curl http://localhost:8080/config
 +{"release":"19.12","environment":"UNKNOWN","metricsEnabled":true}
 +curl http://localhost:8081/config
 +{"release":"19.12","environment":"DEV","metricsEnabled":false}
 +curl http://localhost:8082/config
 +{"release":"19.12","environment":"DEV","metricsEnabled":true} # в реальности metricsEnabled остаётся false
 +</code>
 +Это базовый подход, но чреват ошибками, т. к. нужно учитывать много условий и правильно их выполнить для корректной настройки.
 +
 +==== Помещение всех конфигураций в образ ====
 +Можно зашивать в образ конфиги cразу для всех окружений, а потом переменной извне задавать, какой использовать. Здесь пример для .NET.
 +<code bash>
 +# Стандартно appsettings.json сливается с appsettings.Development.json (проверка - http://server:8083/diagnostics)
 +docker container run -d -p 8083:80 diamol/ch18-todo-list
 +# А здесь appsettings.json сливается с appsettings.Test.json (проверка - http://server:8084/diagnostics)
 +docker container run -d -p 8084:80 -e DOTNET_ENVIRONMENT=Test diamol/ch18-todo-list
 +# А здесь appsettings.json сливается с appsettings.Production.json, а ещё с файлом prod-local/local.json (проверка - http://server:8085/diagnostics)
 +# local.json указывает использовать локальный файл БД вместо сервера БД для диагностики ошибок
 +docker container run -d -p 8085:80 -e DOTNET_ENVIRONMENT=Production -v "$(pwd)/config/prod-local:/app/config-override" diamol/ch18-todo-list
 +# + имя релиза (проверка - http://server:8086/diagnostics)
 +docker container run -d -p 8086:80 -e DOTNET_ENVIRONMENT=Production -e release=CUSTOM -v "$(pwd)/config/prod-local:/app/config-override" diamol/ch18-todo-list
 +</code>
 +Тем не менее, зашивать всю конфигурацию невозможно, т. к. там могут быть секретные данные - логины/пароли и т. п., которым в образе не место, а с этим подходом высок риск именно так и сделать.
 +
 +==== Загрузка конфигурации из среды выполнения ====
 +Подобно .NET Core libraries или node-config, у Go есть модуль Viper, позволяющий управлять конфигурациями. Viper поддерживает JSON/YAML/TOML. TOML Наиболее удобен для конфигов.
 +
 +TOML, зашитый в образ
 +<code yaml>
 +release = "19.12"
 +environment = "UNKNOWN"
 +
 +[metrics]
 +enabled = true
 +
 +[apis]
 +  
 +  [apis.image]
 +  url = "http://iotd/image"
 +  
 +  [apis.access]
 +  url = "http://accesslog/access-log"
 +</code>
 +
 +Монтируемый TOML
 +<code yaml>
 +environment = "DEV"
 +
 +[metrics]
 +enabled = false
 +</code>
 +
 +<code bash>
 +# Значения по умолчанию, зашитые в образ (проверка - http://server:8086/config)
 +docker container run -d -p 8086:80 diamol/ch18-image-gallery
 +{"Release":"19.12","Environment":"UNKNOWN","Metrics":{"Enabled":true},"Apis":{"access":{"Url":"http://accesslog/access-log"},"image":{"Url":"http://iotd/image"}}}
 +# Монтирование доп. конфига (проверка - http://server:8087/config)
 +docker container run -d -p 8087:80 -v "$(pwd)/config/dev:/app/config-override" diamol/ch18-image-gallery
 +{"Release":"19.12","Environment":"DEV","Metrics":{"Enabled":false},"Apis":{"access":{"Url":"http://accesslog/access-log"},"image":{"Url":"http://iotd/image"}}}
 +</code>
 +
 +==== Настройка старых приложений ====
 +Чтобы заставить старое приложение, которое не умеет читать перекрывающие файлы и не учитывает переменные окружения, вести себя как новое, нужно предпринять дополнительные усилия.
 +  - Прочесть из специального файла перекрывающие настройки
 +  - Прочитать перекрывающие настройки из переменных окружения
 +  - Слить полученные перекрывающие настройки воедино с приоритетом переменных
 +  - Результат записать в файл в контейнере
 +
 +<code bash>
 +# Настройки по умолчанию
 +docker container run -d -p 8089:80 diamol/ch18-image-of-the-day
 +{"release":"19.12","environment":"UNKNOWN","managementEndpoints":"health,info,prometheus","apodUrl":"https://api.nasa.gov/planetary/apod?api_key="}
 +# Монтируется файл настройки и переменной указывается его расположение в контейнере
 +docker container run -d -p 8090:80 -v "$(pwd)/config/dev:/config-override" -e CONFIG_SOURCE_PATH="/config-override/application.properties" diamol/ch18-image-of-the-day
 +{"release":"19.12","environment":"DEV","managementEndpoints":"health","apodUrl":"https://api.nasa.gov/planetary/apod?api_key="}
 +</code>
 +В это время в Докерфайле задаётся переменная с путём:
 +<code bash>
 +FROM diamol/maven AS builder
 +# ...
 +RUN mvn package
 +# config util
 +FROM diamol/maven as utility-builder
 +WORKDIR /usr/src/utilities
 +COPY ./src/utilities/ConfigLoader.java .
 +RUN javac ConfigLoader.java
 +
 +# app
 +FROM diamol/openjdk
 +
 +ENV CONFIG_SOURCE_PATH="" \
 +    CONFIG_TARGET_PATH="/app/config/application.properties"
 +
 +CMD java ConfigLoader && \
 +    java -jar /app/iotd-service-0.1.0.jar
 +
 +WORKDIR /app
 +COPY --from=utility-builder /usr/src/utilities/ConfigLoader.class .
 +COPY --from=builder /usr/src/iotd/target/iotd-service-0.1.0.jar .
 +</code>
 +
  

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki