Домашний кластер разработчика

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

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

Я хочу попробовать иной метод работы, который, я надеюсь, должен помочь упростить использование нескольких проектов в Docker. Как я это делал, подробнее расскажу в статье. 

2b8ebf2fd5d72425bc1356389b0181ff.png

Какие задачи будем решать

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

  1. Вынести три часто используемых сервиса в отдельный контейнер docker-compose. Организовать сетевое соединение между ними, а также создать сеть для подключения приложения к ним.

  2. Научиться администрировать базы данных. Сами базы будем создавать вручную, используя язык SQL, а таблицы будут создаваться автоматически при использовании специализированных библиотек, таких как Eloquent, Doctrine и т. д.

Немного теории

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

Рис. 1. UML-диаграмма развертывания

Рис. 1. UML-диаграмма развертывания

Кластер будет написан в docker-compose файле. В нем будет три сервиса. Для более динамичной разработки многие значения вынесены в переменные окружения. Названия сервисов — это их назначение (db, s3, cache). Имя контейнера — это образ, который мы используем.

Переменные окружения

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

Образы

Если у меня стоит задача развернуть новый проект, я всегда создаю перемененную PROJECT_NAME. C ее помощью я могу писать скрипты, которые будут динамично подставлять нужное мне значение. Когда я открываю Docker Desktop, все контейнеры выглядят очень эстетично.

Рис. 2. Отображение контейнеров при использовании одинакового начала

Рис. 2. Отображение контейнеров при использовании одинакового начала

Так как у образов достаточно много версий, чтобы постоянно не исправлять docker-compose файл, я вынес их в переменные окружения. Это необходимо в случае, если нам нужно использовать какую-то определенную версию.

Порты

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

Настройки сети

В параметрах network необходимо создать сеть local. Это основная сеть, к которой будут подключаться приложения. Для нее необходимо прописать свойство external.

Используемое оборудование

Какие сервисы обычно используют при создании сайта или приложения? Однозначно — это база данных. На практике, когда выставляются требования к приложению, мы часто слышим, что просят использовать PostgreSQL. Сейчас это одна из самых распространенных баз данных. Я в работе использую MariaDB, по сути, это та же самая MySQL. Если у вас есть потребность использовать разные БД, их спокойно можно запустить одновременно. Они работают на разных портах (MariaDB:3306, PostgreSQL:5432).

Наша задача — проверить, насколько удобно будет использование данного стека. Поэтому я выберу БД, с которой приходится работать на моих проектах, — MariaDB.

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

Для хранения изображений часто применяют AWS S3. В качестве сервера мы будем использовать MinIO. У него удобный web-интерфейс и хорошее консольное приложение.

Связка наших сервисов будет работать на Docker. Я предпочитаю оригинальные образы, сначала проверяю, есть ли образ от самого Docker Inc., если нет — смотрю, что предлагают более известные авторы. Во-первых — это безопасность, а я работаю на ресурсах компании, во-вторых — это поддержка и минимальное количество ошибок. Официальные образы лучше поддерживаются и быстрее обновляются. Образы, которые нам понадобятся:

  • MariaDB,

  • Redis,

  • MinIO.

Для удобства в работе будем использовать IDE. Необходимо выбрать редактор, который хорошо адаптирован под Docker.

Перейдем к практике

Структура репозитория

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

cluster/
├── .env
├── README.md
└── docker-compose.yml

Если необходимо хранить файлы конфигурации для отдельных образов, их можно поместить в папку .docker/. Дампы и бэкапы можно хранить в директории db/ (database/). Также можно подключить дополнительные конфигурационные файлы, например, EditorConfig (для его работы необходимо убедиться в том, что ваша IDE поддерживает его).

Переменные окружения

Переменные были сгруппированы для удобной работы с ними. Если необходимо внести правки, то найти нужную переменную достаточно просто.

PROJECT_NAME = cluster
LOGIN = admin
PASSWORD = password

REDIS_PORT = 6379
REDIS_VERSION = alpine

S3_SERVER_PORT = 9000
S3_CLIENT_PORT = 9001
S3_VERSION = latest

DB_PORT = 3306
DB_VERSION = 10.7

Контейнеры

Хочу обратить внимание на названия контейнеров. На многих проектах, где я участвовал, не пользовались данной функциональностью. После реализации подобного подхода я получал положительную обратную связь.

services:
    db:
        container_name: ${PROJECT_NAME}-mariadb
        image: mariadb:${DB_VERSION}
        command: [mysqld, --character-set-server=utf8mb4, --collation-server=utf8mb4_unicode_ci, --innodb-file-format=Barracuda, --innodb-large-prefix=1, --innodb-file-per-table=1, --lower_case_table_names=1]
        ports:
            - ${DB_PORT}:3306
        environment:
            - MYSQL_ROOT_PASSWORD=${PASSWORD}
            - MYSQL_USER=${LOGIN}
            - MYSQL_PASSWORD=${PASSWORD}
        volumes:
            - database:/var/lib/mysql
        networks:
            - local

    s3:
        container_name: ${PROJECT_NAME}-minio
        image: minio/minio:${S3_VERSION}
        environment:
            MINIO_ROOT_USER: ${LOGIN}
            MINIO_ROOT_PASSWORD: ${PASSWORD}
        ports:
            - ${S3_SERVER_PORT}:9000

            - ${S3_CLIENT_PORT}:9001
        volumes:
            - minio:/data
        command: server /data --console-address ":9001"
        networks:
            local:

    cache:
        image: redis:${REDIS_VERSION}
        container_name: ${PROJECT_NAME}-redis
        command: [ redis-server, --maxmemory 128mb, --maxmemory-policy volatile-lru, --save "" ]
        ports:
            - ${REDIS_PORT}:6379
        volumes:
            - redis:/data
        networks:
            local:

volumes:
    database:
    minio:
    redis:

networks:
    local:
        external:
            name: local

Запуск контейнеров

При запуске контейнеров рекомендую всегда указывать параметр -p. С помощью этого параметра задается название стека. По умолчанию — это название корневой директории, где расположен файл docker-compose.yml.

docker network create local
docker compose -p "$PROJECT_NAME" up -d

Запустить контейнеры можно и отдельными командами, но я привык упрощать и приводить список команд в одну.

Рис. 3. Создание сети и сборка контейнеров

Рис. 3. Создание сети и сборка контейнеров

Когда контейнеры будут созданы и запущены, наш Docker Desktop будет выглядеть следующим образом. Прошу обратить внимание, что MinIO использует два порта. 9000 порт — порт сервера, куда приходят все запросы, 9001 порт — порт клиента, на нем отображается UI. Если открыть в браузере localhost:9000, то будет переадресация на localhost:9001.

Рис. 4. Результат сборки кластера

Рис. 4. Результат сборки кластера

Запуск приложения

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

docker build -t application $PWD

Рис. 5. Сборка приложения

Рис. 5. Сборка приложения

У меня эта часть сохранилась в кэше, поэтому этот процесс занял маленькое время. При выполнении в первый раз, происходит достаточно большой объем обработки кода и информации. Запуск самого контейнера выполняем следующей командой:

docker run -d -v $PWD:/var/www/html --name=project --network=local -p=80:80 application

Рис. 6. Запуск приложения

Рис. 6. Запуск приложения

Так выглядит одновременно запущенное приложение и стек сервисов.

Рис. 7. Отображение контейнеров кластера и отдельно запущенного приложения

Рис. 7. Отображение контейнеров кластера и отдельно запущенного приложения

Проверка сети

Воспользуемся расширением Portainer и зайдем во вкладку network, выберем нашу сеть local. Внизу, в разделе Containers in network мы увидим все подключенные контейнеры к сети.

Рис. 8. Детали сети local в расширении Portainer для Docker Desktop

Рис. 8. Детали сети local в расширении Portainer для Docker Desktop

Управление базой данных

По умолчанию, пользователь, которого мы указали при развертывании стека, бесполезен. Ему необходимо выдать права (разрешения). 

docker exec -ti cluster-mariadb mysql -uroot -ppassword -e 'GRANT ALL PRIVILEGES ON *.* TO `admin`@`%`;'

Теперь выполним авторизацию уже под нашим пользователем и создадим базу:

docker exec -ti cluster-mariadb mysql -uadmin -ppassword -e 'CREATE DATABASE project;'

Развертывание приложения

Так как наше приложение написано на PHP и использует пакетный менеджер Composer, нам необходимо выполнить команду composer install. Но выполнить ее нужно внутри контейнера, в который мы ранее установили composer. Выполним следующие команды:

docker exec -ti project  sh -c "composer install"

В приложении, которое я разворачиваю, в файле composer.json прописаны скрипты по работе с DoctrineORM. Поэтому все таблицы будут созданы самостоятельно, без моего участия.

Если есть необходимость загрузить определенный dump, то это можно спокойно делать.

Сложности в реализации

Долгое время я не мог понять, как правильно составить Dockerfile. С ним, пожалуй, у меня была самая большая проблема. В работе с проектами мы используем платформу, написанную на Symfony. Запустить эту платформу была очень большая проблема. Так как это приложение использует много различных расширений PHP, очень много времени понадобилось, чтобы разобраться в их установке. Результатов это не принесло.

Когда мои силы почти иссякли, я решил попробовать посмотреть в интернете содержимое docker-образа, который разработчики продукта предлагают использовать. Посмотрев их Dockerfile, я пришел к выводу, что простой copy-past решит мою проблему. Это частный случай, и связан он с тем, что платформа, с которой мы работаем, не очень известна среди разработчиков.

Следующая сложность, с которой я столкнулся, это вынести переменные из docker-compose в env-файл. Важное замечание, если в переменной необходимо указать url-адрес на контейнер, я указываю его как host.docker.internal.

DB_HOST = host.docker.internal
DB_PORT = 3306
DB_NAME = project
DB_USER = admin
DB_PASSWORD = password

S3_ENDPOINT = 'http://host.docker.internal:9000'
S3_BUCKET = project
S3_KEY = admin
S3_SECRET = password

REDIS_URL = 'redis://host.docker.internal:6379'

Результат проделанной работы

Примерно неделю я поработал в таком формате. Мне приходилось переключаться между двумя проектами.

Переключение между приложениями происходит намного быстрее. Это было связано с тем, что приходится тратить меньше времени на отключение всего стека.

Обычно в docker-compose.yml прописаны зависимости модулей. То есть один сервис не может запуститься, пока не запустится другой. Такое же правило работает при отключении стека. Сначала отключаются модули, которые зависят от других.

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

Появляется следующая проблема — для того, чтобы открыть приложение в браузере, необходимо постоянно прописывать порт. Я решил эту проблему добавлением страницы в закладки.

Рис. 9. Как сейчас выглядит мой Docker Desktop

Рис. 9. Как сейчас выглядит мой Docker Desktop

На текущий момент у меня есть возможность запустить все проекты одновременно. Теперь мне не приходится разбираться с портами, постоянно править конфигурации. Я перестал тратить время на ожидания, о которых писал выше. Лично для меня работать в таком формате стало приятнее.

Мой коллега Артемий вызвался на этот эксперимент, чтобы посмотреть, насколько такая идея окажется удобнее для него. Ниже его слова:  

»Опыт по использованию кластера положительный.

Преимущества в его использовании: простая и достаточно быстрая установка, удобство в развёртывании и разработке нескольких проектов, без постоянного переключения контейнеров, упрощенная конструкция в контейнерах Docker. Все переменные вынесены в отдельный файл».

Хочу выразить благодарность моим коллегам Евгению и Сергею за помощь в реализации этого исследования. Их опыт и знания помогли решить некоторые моменты, в которых я сомневался. Иногда не замечая, они давали подсказку, которая и была решением.

*Статья написана в рамках ХабраЧелленджа 0.1, который прошел в ЛАНИТ осенью 2023 года

© Habrahabr.ru