# Задания
## 1. Многоступенчатая сборка образов
В полученной вами директории `todo_app` находятся исходные коды Enterprise Quality™® системы для менеджмента напоминаний.
Она состоит из следующих частей:
- `TodoApi` - API-сервис, на платформе `.NET 7` ,
- `todo_ui` - веб-интерфейс к этому сервису, Single-Page `JavaScript` приложение(`SPA`), написанное с использованием `Vue.js 3` ,
- `PostgreSQL` - API-сервис использует `PostgreSQL` для хранения данных.
Необходимо будет контейнеризировать эту систему.
В этот раз, к сожалению, программисты не оставили никаких скриптов, но вы можете написать их сами, по ходу работы. Некоторые команды и их аргументы будут довольно длинные.
### 1.1 Контейнеризация приложения
#### 1.1.1 Подготовка образа для `TodoApi` и `todo_ui`
Используя подход к многоступенчатой сборке образов, напишите `Dockerfile` , из которого можно было бы создать образ,
и который отвечал бы следующим требованиям:
- Фаза сборки API-сервиса должна использовать образ `mcr.microsoft.com/dotnet/sdk:7.0`
- Фаза сборки `JavaScript` приложения должна использовать образ `node:21`
- Результирующий образ должен основываться на `mcr.microsoft.com/dotnet/aspnet:7.0`
- Готовое приложение API-сервиса вместе с зависимостями должно находиться в директории `/app`
- `JavaScript` приложение должно находиться в директории `/app/wwwroot`
- При старте контейнера из такого образа, должно запускаться приложение API-сервиса.
Документация по многоступенчатой сборке образов находится тут: https://docs.docker.com/build/building/multi-stage/.
Фаза сборки сервиса `TodoApi` состоит из следующих шагов:
- В о -первых, для е г о сборки необходим .NET 7 SDK
- Далее, из директории, в которой находятся исходные коды сервиса(см. `todo_app/TodoApi` ),
нужно сделать `dotnet restore TodoApi.csproj` - эта команда скачает зависимости сервиса.
- Следующим шагом идет непосредственно сборка сервиса.\
Она производится командой `dotnet build TodoApi.csproj -c Release -o <output_directory>` ,\
в которой вместо `<output_directory>` необходимо подставить имя директории, которое будет
содержать бинарные файлы сервиса, например `/app/build`
- Последняя вещь, которую необходимо сделать - это вызывать команду\
`dotnet publish TodoApi.csproj -c Release -o <publish_directory>` ,\
которая опубликует результирующие бинарные файлы, а также зависимости сервиса
в `<publish_directory>` (например в `/app/publish` ).
Для запуска процесса сервиса SDK не нужен, достаточно лишь соответствующего фреймворка `ASP.NET` - в данном случае версии `7.0` . Запуск осуществляется вызовом исполняемого файла `./TodoApi` из директории, которая хранит опубликованные бинарные файлы сервиса и е г о зависимостей.
Фаза сборки `JavaScript` приложения состоит из следующих шагов:
- Прежде всего, необходим `NodeJS` - в данном случае подойдет версия `21` .
- Далее, из директории, в которой находятся исходные коды приложения(см. `todo_app/todo_ui` ),
нужно вызывать `npm install` - это команда скачает зависимости приложения.
- После этого, в этой же директории необходимо выполнить команду `npm run build` , в результате чего,
в этой директории появится директория `wwwroot` , содержащая файл `index.html` ,
результирующий код на JS, а также е г о ресурсы, такие как картинки и файлы стилей(css). \
Перед выполнением этой команды, также можно установить переменную среды `BASE_PATH` , в значение, \
являющееся префиксом в пути URL, по которому приложение будет находится. По умолчанию, `BASE_PATH` \
трактуется как имеющее значение `/` , таким образом приложение должно раздаваться веб-сервером из корня сайта. \
Если же вы хотите, чтобы приложение находилось например по адресу `http://<домен>/todo/` , тогда необходимо \
установить `BASE_PATH` в значение `/todo/` соответственно. Для установки переменной среды, доступной во время \
сборки образа, используйте директиву `ARG` внутри докер-файла(и соответственно опцию `--build-arg` при вызове `docker build` ).
Для запуска `JavaScript` приложения `NodeJS` не нужен, достаточно лишь любого веб-сервера, и в
нашем случае, в API-сервисе такой имеется(он называется `Kestrel` ). Для хостинга JS-приложения в API-сервисе, необходимо всего лишь скопировать упомянутую выше директорию `wwwroot` внутрь результирующей директории API-сервиса(которая получается после выполнения `dotnet publish ...` ).
Создайте образ из полученного `Dockerfile` и назовите е г о `todo-bundle` .
#### 1.1.2 Подготовка образа PostgreSQL
Перед запуском самого приложения, необходимо запустить и подготовить `PostgreSQL` в сети `todo` .
Создайте именованную сеть типа `bridge` , в которой будет работать вся система, назовите е е `todo` .
Создайте том данных, в котором `PostgreSQL` будет хранить БД. Назовите е г о `todo_pgdata` .
Запустите именованный контейнер `todo_postgres` из образа `postgres:16` .
При этом:
- При запуске установите следующие переменные окружения контейнера в значение `todo` :
- `POSTGRES_DB` - имя базы данных, к которой будет обращаться API-сервис
- `POSTGRES_USER` - пользователь БД, через которого будет работать сервис
- `POSTGRES_PASSWORD` - пароль для пользователя
- Установите значение переменной среды `PGDATA` в значение `/var/lib/postgresql/data` - здесь `PostgreSQL` будет хранить данные.
- Примонтируйте том данных `todo_pgdata` в качестве директории `/var/lib/postgresql/data` .
- Примонтируйте директорию `initdb` из `todo_app` в качестве директории `/docker-entrypoint-initdb.d` внутри контейнера.
Там находится скрипт `init_db.sql` , который используется для инициализации базы данных при первом старте контейнера.
#### 1.1.3 Проверка работоспособности контейнеризованного приложения
Запустите контейнер `todo_bundle` в сети `todo` из созданного вами образа `todo-bundle` , при этом:
- Установите значение переменной среды `ConnectionStrings__PostgreSQL` в контейнере равной
`Host=todo_postgres;Port=5432;Database=todo;Username=todo;Password=todo` .
Можете выбрать другой `Host` в этой строке, если вы назвали контейнер с PostgreSQL по-другому.
Аналогично - и другие параметры.
- Пробросьте порт 80 на хост-систему(например на порт 8080).
API-сервис работает на 80м порту по умолчанию, но это можно изменить, установив значение переменной \
`ASPNETCORE_URLS` например в `http://*:5000` - тогда внутри контейнера сервис будет слушать порт 5000.
Убедитесь, что приложение работает и доступно на выбранном вами порту на локальной машине.
При возникновении сложностей и необходимости отладки, вы можете также установить значение переменной `ASPNETCORE_ENVIRONMENT` в `Development` - таким образом сервис будет выдавать больше логов.
### 1.2 Рефакторинг контейнеризованного приложения
#### 1.2.1 Декомпозиция образа `todo-bundle`
Несмотря на то что API-сервис поддерживает хостинг `JavaScript` приложения, в реальном(или скорее, идеальном) мире никто так не делает. Над интерфейсом и API часто работают разные команды, у них может быть разный график работы, разное версионирование приложений, и тем более разные репозитарии, и разный подход к разработке.
Кроме того, хотя `Kestrel` - хороший веб-сервер, все же раздача статических файлов - не основная е г о специализация.
Поэтому, руководствуясь принципом разделения ответственности, вам необходимо разделить контейнеризацию API-сервиса и `JavaScript` приложения.
Остановите контейнер `todo_bundle` , удалите е г о и образ `todo-bundle`
Модифицируйте `Dockerfile` для API-сервиса, таким образом, чтобы убрать из него все упоминание `JavaScript` приложения и е г о этапов сборки.
Создайте образ `todo-api` на основе нового докер-файла.
Напишите конфигурацию `Nginx` для использования е г о как в качестве обратного прокси для API-сервиса, так и для раздачи файлов из `wwwroot` , получаемой после сборки `JavaScript` приложения. При этом:
- Учтите, что все методы API-сервера имеют префикс `/api`
- Н е забудьте о том, что приложение на `JS` является `SPA` , и веб-сервер должен перенаправлять все нераспознанные пути
на `index.html` (используйте директиву `try_files` )
- Содержимое `wwwroot` должно лежать в `/var/www/todo`
Напишите `Dockerfile` для `JavaScript` приложения, взяв за основу образ `nginx` .
При сборке образа копируйте написанную вами конфигурацию Nginx в `/etc/nginx/nginx.conf` ,
а результат сборки `JavaScript` приложения в `/var/www/todo` .
Создайте образ `todo-ui` из этого докер-файла.
#### 1.2.2 Проверка работоспособности контейнеризованного приложения
Запустите в сети `todo` контейнер `todo_api` из образа `todo-api` , при этом:
- Н е забудьте про переменную среды `ConnectionStrings__PostgreSQL` .
- Убедитесь что контейнер недоступен из внешней сети.
Запустите в сети `todo` контейнер `todo_ui` из образа `todo-ui` , при этом:
- Пробросьте порт контейнера 80(или любой другой, который вы использовали при написании конфигурации Nginx) на локальную машину(например, опять же на порт 8080)
Убедитесь что приложение доступно извне и работает.
#### 1.2.3 Рефакторинг образа `todo-api`
Платформа `.NET` , на самом деле, позволяет собирать приложения, отвязанные от "внешней" предустановленной среды выполнения.
Хотя рекомендованный способ докеризации приложений на `.NET` - отталкиваться от образа с `mcr.microsoft.com` ,
иногда возникает необходимость именно в `self-contained` приложениях.
Модифицируйте `Dockerfile` для `todo-api` , следующим образом:
- Поменяйте образ SDK для сборки на `mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim`
- Замените команду публикации приложения на \
`dotnet publish TodoApi.csproj --self-contained -r linux-x64 -c Release -o /app/publish` \
(замените `/app/publish` на директорию которую использовали вы, если она у вас отличается)
- Поменяйте результирующий исходный образ на `debian:bullseye-slim`
- В образе `debian:bullseye-slim` не хватает `ICU` , необходимого для запуска полноценного `.NET` приложения. \
Установите `libicu` в этом образе при сборке: `apt-get update && apt-get install libicu67`
#### 1.2.4 Проверка работоспособности контейнеризованного приложения
Пересоберите образ `todo-api` , и перезапустите контейнер `todo_api` на е г о основе, проверьте что всё работает.
## 2. Политики перезапуска и базовый мониторинг
1. Запустите `todo_ui` , `todo_api` и `todo_postgres` так, чтобы они перезапускались при ошибке или же при перезапуске демона Docker.
Перезапустите демон Docker, или же вообще, сделайте перезагрузку машины, и проверьте что контейнеры запустились при старте Docker.
2. Перезапустите контейнер `todo_postges` , так чтобы healthcheck:
- Запускался командой `pg_isready -U todo -d todo`
- Имел интервал 10 секунд
- Имел timeout 5 секунд
- Проверка имела бы до 5 попыток старта в случае неудачи
- Начиналась бы после 10 секунд после старта контейнера
## 3. Ограничение ресурсов
1. Перезапустите контейнеры `todo_ui` , `todo_api` и `todo_postgres` , так они имели ограничение по памяти в 300, 500 и 800 мегабайт соответственно. \
Поэкспериментируйте с ограничениями и посмотрите что выходит, в случае их превышения.
2. Перезапустите контейнеры `todo_ui` , `todo_api` и `todo_postgres` , так они имели ограничение по ресурсам процессора в 1, 1 и 2 соответственно
## 4. Docker Compose
1. Убедитесь что docker compose установлен, проверьте версию.
2. Напишите `docker-compose.yml` для контейнера на основе `busybox` , который бы печатал `Hello, World` при запуске. Запустите.
3. Напишите конфигурацию compose для приложения `cats_app` . Сделайте так, чтобы образ `cats_app` собирался, если е г о еще не существует. Запустите приложение через docker compose, используя эту конфигурацию. Убедитесь в том, что приложение работает.
4. Напишите конфигурацию compose для вышеописанной системы todo. При этом:
- Добавьте туда как healthcheck, так и ограничения ресурсов описанные выше
- Сделайте так чтобы контейнер `todo_api` зависел от `todo_postgres` , а `todo_ui` - от `todo_api` .
- Сделайте так, чтобы образы `todo-ui` и `todo-api` собирались, если они еще не существуют.
Запустите систему через docker compose, и убедитесь что все работает.
5. Добавьте контейнер из образа `dpage/pgadmin4:7` в compose файл к вышеописанной системе, так чтобы он подключался к `PostgreSQL` и позволял смотреть данные приложения `todo` . При этом установите следующие переменные среды в контейнере в соответствующие значения:
- `PGADMIN_DEFAULT_EMAIL` = `todo@example.com`
- `PGADMIN_DEFAULT_PASSWORD` = `todo`
- `PGADMIN_CONFIG_SERVER_MODE` = `False`
Убедитесь что консоль администрирования недоступна из интернета, и чтобы проверить е е работоспособность, можно, например, \
прокинуть SSH-туннель от вашей машины, до машины с Docker, например таким образом: \
`ssh -L 5051:localhost:5050 stud@studN.myoffice.ru` . \
где вместо 5050 - подставьте порт, на котором pgadmin доступен изнутри машины `studN.myoffice.ru` . \
После выполнения этой команды, pgadmin будет доступен на вашей машине, на http://localhost:5051
Про SSH туннели подробнее можете почитать здесь: \
https://unix.stackexchange.com/questions/115897/whats-ssh-port-forwarding-and-whats-the-difference-between-ssh-local-and-remot
### 5. Docker Swarm
1. Инициализируйте Docker в режим Swarm
2. Выведите на экран список нод Docker Swarm
3. Выведите токен для присоединения к Swarm в качестве worker
4. Создайте сеть Docker типа `overlay` с именем `todo_swarm`
5. Создайте том данных `todo_swarm_pgdata` так, чтобы в него мог писать только 1 сервис одновременно
6. Запустите образ `postgres:16` как сервис Swarm с именем `todo_postgres` , подключенный к сети `todo_swarm` , с количеством реплик равному 1, при этом:
- При запуске установите следующие переменные окружения контейнера в значение `todo` :
- `POSTGRES_DB` - имя базы данных, к которой будет обращаться API-сервис
- `POSTGRES_USER` - пользователь БД, через которого будет работать сервис
- `POSTGRES_PASSWORD` - пароль для пользователя
- Установите значение переменной среды `PGDATA` в значение `/var/lib/postgresql/data` - здесь `PostgreSQL` будет хранить данные.
- Примонтируйте том данных `todo_swarm_pgdata` в качестве директории `/var/lib/postgresql/data` .
- Примонтируйте директорию `initdb` из `todo_app` в качестве директории `/docker-entrypoint-initdb.d` внутри контейнера.\
Там находится скрипт `init_db.sql` , который используется для инициализации базы данных при первом старте контейнера.
7. Запустите образ `todo-api` как сервис Swarm с именем `todo_api` , подключенный к сети `todo_swarm` , с количеством реплик равному 1, при этом:
- Установите переменную окружения `ConnectionStrings__PostgreSQL` \
в значение `Host=todo_postgres;Port=5432;Database=todo;Username=todo;Password=todo`
- Установите переменную окружения `ASPNETCORE_URLS` в `http://*:80`
8. Запустите образ `todo-ui` как сервис Swarm с именем `todo_ui` , подключенный к сети `todo_swarm` , с количеством реплик равному 1, при этом:
- Пробросьте порт 80 на внешний порт 8080
9. Убедитесь, что приложение доступно на порту 8080
10. Увеличьте количество реплик каждого из сервисов `todo_ui` и `todo_api` до 3. Убедитесь что приложение работает.
11. Выведите листинг реплик сервисов `todo_api` и `todo_ui`
12. Просмотрите логи сервиса `todo_api` . Убедитесь что запросы подхватываются разными репликами.
13. Удалите все три сервиса
14. Напишите compose-файл для этих сервисов, который бы также запускал по 3 реплики `todo_ui` и `todo_api` и 1 реплику `todo_postgres`
- Отталкивайтесь от ранее вами написанного файла для docker compose
15. Запустите docker stack, исходя из написанной вами конфигурации, и убедитесь в работоспособности системы