Развёртывание ПО, или деплой (deploy) — этап в разработке, в Devops в целом, это действия, которые делают ПО готовым к использованию. Если вы умеете в грамотный деплой, масштабирование и управление конвейерами (CI/CD), то ваш софт будет конкурентоспособным.
Далеко не все компании могут позволить себе нанять целую команду DevOps инженеров, чтобы управлять развёртыванием. Но здесь важно не количество разрабов, а качество их знаний. Есть инструменты, с которыми можно эффективно деплоить и без большой команды.
Мы в digital-агентстве успешно используем GitLab CI и Docker для развёртывания ПО в разных средах. Для чего нужны эти инструменты?
GitLab CI позволяет автоматизировать процессы сборки и доставки ПО. Docker — упаковать приложение и его зависимости в контейнеры, что упрощает развёртывание и масштабирование в разных средах. Используя их, вы сократите затраты на найм и оптимизируете деплой.
В этой статье расскажу о нашем опыте и покажу примеры настройки конвейеров CI/CD, как ими управлять с помощью GitLab CI и Docker. А также дам рекомендации, как масштабировать развертывание.
Вводные данные
Бэкенд на Python и фреймворков Django
Облачный сервер
База данных PostgreSQL
Отсутствие DevOps инженера и админов в команде, но желание не деплоить руками
Если вы не знаете, что такое CI/CD, можете почитать здесь.
Конфигурация Backend приложения (настройка контейнера, приложения, переменных окружения)
Для начала необходимо провести базовые конфигурации наших настроек на основе .env файла. Для этого мы используем пакеты python-decouple
и dj-database-url
Достаем переменные в нашем settings.py файле:
from dj_database_url import parse as db_url
from decouple import config
DEBUG = config("DEBUG", default=False, cast=bool)
SECRET_KEY = config("SECRET_KEY", default="")
DATABASES = {
"default": dict(
config("DATABASE_URL", cast=db_url),
)
}
Создаём файл с конфигурацией окружения .env
проекта в корне с содержимым:
DEBUG=true
SECRET_KEY=some_key
DATABASE_URL=postgresql://dev:dev@localhost:5432/supershtab
После нам необходимо настроить наше окружение для сервера. Для этого создадим в папке файл _CI/envs/dev.env
, каждое окружение мы будем рассматривать как нашу ветку.
Скопируем пока туда содержимое нашего .env файла в корне, а после заменим на последнем шаге.
Далее устанавливаем нужные пакеты для запуска приложения это:
uwsgi
psycopg2-binary
Также для запуска можно использовать gunicorn. Вообще это не так важно, какой вариант выбрать, все они работают одинаков. Для примера рассмотрим uwsgi.
После установки пакетов конфигурируем наш uwsgi, создав файл uwsgi.ini файл в корне проекта.
Наш конфиг выглядит таким образом, но ваш может отличаться. Здесь важно понимать, что наше бэкенд-приложение будет торчать наружу 9000 портом, и мы будем его ловить при дальнейшей конфигурации.
[uwsgi]
# Django-related settings
# Django's wsgi file
# process-related settings
# master
master = true
module = common.wsgi
enable-threads = true
die-on-term = true
single-interpreter = true
strict = true
need-app = true
# the socket (use the full path to be safe
http = 0.0.0.0:9000
# clear environment on exit
vacuum = true
# respawn processes taking more than 60 seconds
harakiri = 60
stats = /tmp/stats.socket
cheaper-algo = busyness
processes = 24 ; Maximum number of workers allowed
threads = 2
cheaper = 8 ; Minimum number of workers allowed
cheaper-initial = 16 ; Workers created at startup
cheaper-overload = 1 ; Length of a cycle in seconds
cheaper-step = 4 ; How many workers to spawn at a time
cheaper-busyness-multiplier = 30 ; How many cycles to wait before killing workers
cheaper-busyness-min = 20 ; Below this threshold, kill workers (if stable for multiplier cycles)
cheaper-busyness-max = 70 ; Above this threshold, spawn new workers
cheaper-busyness-backlog-alert = 16 ; Spawn emergency workers if more than this many requests are waiting in the queue
cheaper-busyness-backlog-step = 2 ; How many emergency workers to create if there are too many requests in the queue
max-requests = 1000 ; Restart workers after this many requests
max-worker-lifetime = 1800 ; Restart workers after this many seconds
reload-on-rss = 1024 ; Restart workers after this much resident memory
worker-reload-mercy = 60 ; How long to wait before forcefully killing workers
Далее мы можем запустить и проверить корректность конфигурации нашего приложения через команду:
uwsgi --ini uwsgi.ini
После запуска приложения в терминале мы должны увидеть, как выглядят логи запуска нашего uWSGi приложения и спавнинг воркеров. Далее проверим доступность по :9000 порту
При стандартной конфигурации мы должны увидеть стартовую страницу Django приложения. Если вы видите этот экран, то всё окей, и мы можем продолжать далее. Если нет, то копайте лог, ошибки могут быть разные и все предсказать нереально.
Следующий шаг — написание нашего Docker контейнера, который и будет использоваться на наших серверах для запуска.
Создаём папку _CI в корне проекта и кладём туда наш Dockerfile.
Выглядит он таким образом:
FROM python:3.11-slim as base
RUN apt-get update -y && apt-get install -y gettext \
build-essential \
libssl-dev \
git \
python3-dev \
gcc \
libpq-dev \
libffi-dev
# Устанавливаем локали для системы
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y locales
RUN sed -i -e 's/# ru_RU.UTF-8 UTF-8/ru_RU.UTF-8 UTF-8/' /etc/locale.gen && \
dpkg-reconfigure --frontend=noninteractive locales && \
update-locale LANG=ru_RU.UTF-8
# копируем и устанавливаем зависимости
COPY requirements.txt /app/requirements.txt
RUN pip install -r /app/requirements.txt
# копируем наш проект и устанавливаем рабочую папку
COPY . /app
WORKDIR /app
ENV PYTHONUNBUFFERED 1
# запускаем контейнер
ENTRYPOINT ["/bin/bash", "/app/_CI/runserver.sh"]
Далее нам необходимо написать runserver.sh файл, где мы и будем запускать наше приложение. Данный файл также должен находится в CI/runserver.sh в проекте. В данном файле есть переменные $ENVIRONMENT и $DATABASE_URL. Мы к ним ещё вернемся в последующих шагах.
#!/bin/bash
# Копируем файлы переменной окружения
cp -R /app/_CI/envs/$ENVIRONMENT.env /app/.env
echo "
DATABASE_URL=$DATABASE_URL
" >> /app/.env
# Экспортируем переменные окружения из файла
source /app/.env
cd /app
# Миграции и статика
python /app/manage.py collectstatic --no-input
python /app/manage.py migrate --no-input
# запуск
uwsgi --ini uwsgi.ini
Проверяем сборку нашего приложения командой:
docker build -t backend .
В ходе билда должны:
скачаться родительский image;
установиться системные пакеты;
сгенерироваться локали;
установиться пакеты из PyPi.
Далее мы можем запустить наш бэкенд, тут нам и понадобятся наши переменные:
docker run --restart=always --name=backend --env ENVIRONMENT=local --env DATABASE_URL=postgresql://dev:dev@localhost:5432/db_name -d backend -p 9000:9000
После чего нам необходимо проверить, всё ли хорошо с нашим контейнером, его логи сборки и запуска можно посмотреть через команду docker logs backend
и зайти на порт 9000.
В случае корректной конфигурации наш бэкенд станет доступен вне контейнера, и мы можем приступить к дальнейшей конфигурации нашего frontend приложения.
Конфигурация сервера и настройка смежных контейнеров
Далее необходимо подготовить сервер под развёртку нашего приложения и установить компоненты системы:
Nginx
Docker
PostgreSQL in Docker
Существует множество статей и способов установить данные компоненты, мы не будем останавливаться на них. Вы можете установить их самостоятельно:
Nginx можно поставить простой командой sudo apt install nginx
Конфигурация Gitlab CI/CD
После того как мы запушили наш проект в репозиторий в ветку master (будем считать, что он у нас был пустой и это одна ветка), нам необходимо создать ветку dev от ветки master.
После чего необходимо перейти к защите веток от нежелательных прямых вливаний. Сделать это можно в Gitlab в разделе Settings → Repository → Protected Branches.
Также это необходимо сделать, чтобы Gitlab Runner в последствии мог в этих ветках использовать защищённые переменные окружения.
Пример настройки защиты веток:
Далее нам необходимо создать переменные окружения, как и описывал выше это будет:
DATABASE_URL для примера, остальные переменные вы можете задать самостоятельно.
Для создания переменной нам необходимо будет перейти в раздел Settings → CI/CD →Variables и задать переменную
Здесь важно, чтобы у вас появилась возможность экранировать переменную в код через символ $
.
После сохранения необходимо перейти к автоматизации всего этого процесса, ручного труда нам и так хватает ?
Установка Gitlab Runner
Подробная установка Gitlab Runner на сервер описана в статье.
После установки раннера на сервер нам необходимо привязать его к нашему репозиторию. Для этого необходимо перейти в раздел Settings > CI/CD > Runners. В окне с инструкцией по настройке раннера нам будет выдан токен, его и нужно вбить при команде gitlab runner register. Также важно указать и запомнить наш тэг сервера — через него гитлаб и будет понимать, на каком сервере запускать наши команды.
При регистрации раннера важно указать executor → shell — тогда конфигурация будет совпадать с нашей, но ваша может отличаться, мы лишь показываем пример :)
После корректной установки раннера в списке раннеров должен появится тот, который мы создали. Например:
Это означает, что всё хорошо и гитлаб-раннер настроен корректно. Перейдем к настройке наших пайплайнов.
Так как билдить, тестить и запускать наш бэкенд мы будем на одном сервере, то возьмите мощностей немного с запасом, чтобы хватило на сборку проекта.
Пример .gitlab-ci.yml файла:
variables:
HOME: /home/gitlab-runner
BUILD_NAME: backend
stages:
- build
- deploy
- cleanup
# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# BUILD
# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
build:dev:
stage: build
script:
- cp _CI/Dockerfile .
- docker build -t $BUILD_NAME .
only:
- dev
tags:
- your_cool_tag
# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# DEPLOY
# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
deploy:dev:
tags:
- your_cool_tag
only:
- dev
stage: deploy
script:
- docker ps -q --filter "name=$BUILD_NAME" | grep -q . && docker rm -f $BUILD_NAME || true
- |
docker run \
--net=host \
--restart=always \
--name=${BUILD_NAME} \
--env DATABASE_URL=${DATABASE_URL} \
-d $BUILD_NAME
- |
# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# CLEANUP
# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
cleanup:
stage: cleanup
when: always
script:
- docker system prune --force
only:
- dev
tags:
- your_cool_tag