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

При этом у нас около 10 проектов в разработке, к которым добавляются старые, которые мы поддерживаем. Когда мы проектировали весь этот описанный в статье «ролловер», мы позаботились о том, чтобы после каждого коммита на ветке master она автоматически расширялась. Так что, если хотите, можете самостоятельно развернуть другие ветки, развернуть на продакшн. В нашей новой картине мира нам не нужно было мучиться с разными версиями ПО, https можно было выдавать автоматически, а старые и ненужные ветки со временем удалялись. И все это для того, чтобы мы могли сосредоточиться на разработке и не тратить время на развертывание каждого проекта на тестовой площадке.
Как и многие, давным-давно мы все разворачивали вручную. Заходим на сервер, git pull, запускаем команды миграции. Потом вспомнили, что при миграции забыли запустить команду, что-то сломалось и ушли.
А в процессе можно было и растянуть при обновлении сайта, потому что, например, код уже можно было обновить, а миграцию в базе – нет. И не дай Бог, если у нас будет dev, stage, prod! Подойдите к каждому, переверните его руками. Но как-то захотелось продлевать несколько веток параллельно и тоже пришлось делать это вручную… Кошмар, вспоминать страшно, а ностальгия приятная.
Со временем это стало вызывать кучу проблем:
-
разные версии php, node.js;
-
некоторые приложения требовали установки утилит прямо в систему;
-
разница между местной средой и производством. То, что работало во время разработки, могло сломаться после развертывания в рабочей среде;
-
было сложно запускать старые проекты, у которых было много зависимостей
В любом случае, мы решили, что пора все это поместить в докер…

Что мы наделали?
В 2018 году мы обратились к Docker, потому что увидели в нем ключ к решению этих проблем. Мы решили вводить его постепенно, и только в новых проектах. Поэтому процесс затянулся. И когда практики Kubernetes и ci/cd начали набирать популярность, их решили использовать для тестовой среды.
Причины использования Kubernetes в наших проектах:
-
автоматическая выдача сертификатов;
-
масштабирование;
-
высокая доступность;
-
простота управления контейнером.
Kubernetes предоставил готовое решение для этих задач и позволил нам решить соответствующие проблемы быстро и эффективно.

Для ci/cd мы использовали GitLab Runner, так как мы храним проекты в собственном экземпляре GitLab. Kubernetes был разработан с использованием microk8s.
Развертывание происходило с помощью штанги. Это решение просуществовало долгое время, но породило много проблем:
-
Для надежной работы кластера Kubernetes требуется минимум 3 узла, у нас был только один.
-
Быстрое развитие Kubernetes и трудности, связанные с его обновлением.
-
Огромные ямлы, которые сложно читать, создавать и поддерживать.
-
Требуется много времени, чтобы учиться и решать всевозможные проблемы. У нас нет специальной команды devops.
В процессе мы обнаружили, что Kubernetes не подходит для всех наших задач и всех наших потребностей. Его поддержка требовала много дополнительных усилий и времени, а также значительного объема ИТ-ресурсов. Мы сняли его с производства и продолжили поиск оптимальных решений для каждого нашего проекта.
Однако опыт использования Kubernetes оказался очень полезным, и мы всегда помним, что при увеличении нагрузки на приложение мы можем обратиться к этому инструменту.
Чем мы закончили?
Устав от постоянных проблем и сложности Kubernetes, они начали искать альтернативу. Рассматривались Swarm и другие решения, но ничего лучше Docker Compose не нашлось. Они остановились там.
За что? Да потому, что каждый разработчик в компании знает, как работать с Docker Compose. С ним сложно выстрелить себе в ногу, его легко обслуживать и развертывать. А его недостатки практически не заметны на наших проектах.
Минусы считаю:
-
Ограничение ресурсов: Docker Compose не позволяет ограничивать потребление ресурсов в пределах одного контейнера.
-
Нет обновлений без простоев. При обновлении приложения в Docker Compose оно становится недоступным на несколько секунд.
Потом начали искать решение для развертывания в среде разработки. Главное, что нам нужно было, чтобы каждая ветка была доступна по своему адресу и получила сертификат для https.
Готового решения найти не удалось, поэтому мы реализовали собственный деплойер.
Что мы нашли для развертывания?
Наша диаграмма развертывания выглядит следующим образом:

Развертывание клиента и развертывание сервера — это два двоичных файла, написанных на golang. Клиент упакован в докер и помещен в наш реестр GitLab.
Все, что делает клиент, это берет все файлы в каталоге и отправляет их на сервер развертывания через http. Вместе с файлами он также отправляет переменные GitLab. В ci/cd это выглядит так:
review:
image: gitlab/company/ci-deployer/client:latest
stage: review
script:
- deploy
//прочий код
function deploy() {
mv ci/dev /deploys;
/golang/main up;
}
Сервер развертывания работает как демон и принимает файлы через http. Базовая файловая структура выглядит так:
config.json
Содержит конфигурацию. Пример:
{
"not_delete_old" : false, //проекты, в которые не пушили 28 дней удаляются
"cron": {
"enable": true, //настройка крон команд
"commands": [
{
"schedule": "* * * * *",
"task": "cron" //будет выполнена таска из Taskfile.yml
}
]
}
}
Taskfile.yml
Содержит задачи, которые будут выполняться во время развертывания. Утилита установлена в системе.
version: '3'
tasks:
up:
cmds:
- docker-compose pull
- docker-compose up -d --remove-orphans
- docker-compose exec -T back php artisan migrate --force
- docker-compose exec -T back php artisan search:index
down:
cmds:
- docker-compose down
cron:
cmds:
- docker-compose exec -T back php artisan schedule:run
tinker:
cmds:
- docker-compose exec back php artisan tinker
.env
Переменные среды для Docker Compose.
COMPOSE_PROJECT_NAME={{ .BaseName }}
VERSION={{ .Version }}
REGISTRY={{ .RegImage }}
docker-compose.yaml
version: "3.8"
services:
front:
networks:
- traefik
restart: always
image: ${REGISTRY}/front:${VERSION}
env_file: .env.fronted
labels:
- "traefik.enable=true"
- "traefik.http.routers.{{ .BaseName }}.rule=Host(`vendor.{{ .HOST }}`)"
- "traefik.http.routers.{{ .BaseName }}.entrypoints=websecure"
- "traefik.http.routers.{{ .BaseName }}.tls.certresolver=myresolver"
- "traefik.http.routers.{{ .BaseName }}.service={{ .BaseName }}"
- "traefik.http.services.{{ .BaseName }}.loadbalancer.server.port=3000"
networks:
traefik:
name: app_traefik
external: true
После принятия файлов сервер начинает свою работу. На основе переменных GitLab, которые включают имя ветки, имя проекта и т. д., создаются переменные развертывания. BaseName создается таким образом.
return fmt.Sprintf("%s_%s_%s",
receiver.EnvGit["CI_PROJECT_NAMESPACE"], //группа проекта
receiver.EnvGit["CI_PROJECT_NAME"], //название проекта
receiver.EnvGit["CI_COMMIT_BRANCH"], //название ветки
)
Все файлы рассматриваются как шаблоны golang. Поэтому, например, в docker-compose.yaml вместо {{.BaseName}} будет заменено уникальное имя, созданное для развертывания. {{.HOST}} также создается на основе имени ветки.
Затем для филиала создается база данных, если она еще не создана. Если ветка отличается от основной, то база данных создается не с нуля, а клонируется основная база данных. Это удобно, потому что миграции веток не влияют на main. Но основные данные в ветке могут быть полезны.
После этого готовые файлы помещаются в директорию, по пути
/группа проекта/название проекта/название ветки
И команда выполняется в системе задач, которая описана в файле Taskfile.yml. В нем уже описаны команды для конкретного проекта.
После этого деплой становится доступным в сети traefik, который автоматически запускает к нему прокси-трафик и выдает сертификат let’s encrypt.
Что мы имеем в итоге?
-
Такой подход значительно упростил разработку: стало проще использовать различные новые утилиты, которые легко добавить как дополнительный сервис в docker-compose.yaml. И теперь каждый разработчик понимает, как работает тестовая среда.
-
Эту систему легко настроить специально под наши нужды. Например, нам нужно было добавить возможность запуска cron. Решение очень простое. Мы создаем конфигурацию через config.json, разбираем ее в структуру golang и запускаем cron внутри сервера, который можно менять динамически.
-
Реализована еще одна идея: деактивировать неактивные проекты, которые не пушились более 28 дней. Для этого мы создали файл с данными о последнем развертывании.
"XXX_NAME": {
"user": "xxxx",
"slug": "xxxx",
"db": "xxx",
"branch": "main",
"db_default": "xxxxx",
"last_up": "2023-04-07T09:06:15.017236822Z",
"version": "cb2dd08b02fd29d57599d2ac14c4c26200e3c827",
"dir": "/projects/xxx/backend/main",
},
Также cron внутри server deployer проверяет этот файл раз в сутки, если видит там неактивный проект, переходит в dir и запускает команду down. Ну, он удаляет базу данных, если это не главная ветка. А для информативности сервер после выполнения работы отдает логи клиенту, который отображает их в GitLab.
Также cron внутри server deployer проверяет этот файл раз в сутки, если видит там неактивный проект, переходит в dir и запускает команду down. Ну, он удаляет базу данных, если это не главная ветка. А для информативности сервер после выполнения работы отдает логи клиенту, который отображает их в GitLab.
Так выглядит, когда все хорошо

Ну или так, когда что-то пошло не так 🙂

Было бы здорово, если бы вы поделились своим опытом в комментариях.
