Подключение CI/CD к Django/React на примере CircleCI
Jul 7, 2018 11:55 · 1496 words · 8 minute read
Зачем мне это?
Ну куда ж в наше время без CI/CD - пришлось настраивать. Чтобы пуш в мастер обновлял прод, чтобы тесты прогонялись, и вообще спокойно жилось… У меня open source проект, ничего ставить на свои виртуалки я не хочу, так что подходили как минимум 2 сторонних сервиса: TravisCI и CircleCI. С первым я уже знакомился в рамках автоматического тестирования своей библиотеки для работы с Yandex.Disk, так что почему бы не попробовать другой для сравнения?!
CircleCI
Регистрация через GitHub занимает пару секунд, и вот уже появилась возможность подключить какой-нибудь проект. CircleCI даже услужливо предлагает как проще всего его настроить и даёт некоторый шаблон. Но мы пойдём другим путём - сначала настроим выполнение тестов локально! Всё равно пригодится для отладки - не буду же я каждый раз пушить, чтобы проверить корректность выполнения скрипта. Вот тут https://circleci.com/docs/2.0/local-cli/ описана настройка. Я пошёл по первому пути - скачал circleci через curl. И первая команда:
$ circleci config validate -c .circleci/config.yml
.circleci/config.yml is valid
Уже радует. Ладно, иду править конфиг. Если вы настраивали CI где-нибудь на GitLab или Bitbucket, то всё покажется очень скучным. Но на всякий случай распишу.
config.yml
1. Docker-образы
Определяемся с набором docker образов. Мне нужны python:3.6, postgres:9.6-postgis, redis:4.0.9, node:8.11. Кстати, в документации указаны далеко не все образы, которые есть на хабе, так что рекомендую заглянуть на их DockerHub и найти что-нибудь для себя.
2. Задачи
Пробуем составить свой первый джоб. Для упрощения я взял задачу сборки js бандлов: npm install
-> npm run build
-> сохранить куда-нибудь артефакты.
version: 2
jobs:
build:
docker:
- image: circleci/node:8.11
environment:
- NODE_ENV: "production"
working_directory: ~/repo
steps:
- checkout
- run:
name: install dependencies
command: npm install
- run:
name: make builds
command: npm run build
- store_artifacts:
path: static/js
destination: bundles
Обратите внимание, что весь ваш текущий каталог будет подмонтирован в working_directory. Да-да, вместе с .git и .idea. Для запуска одного конкретного джоба:
$ circleci local execute build
И оно даже запустилось, и даже собрало бандлы, но вот зааплоадить у меня не смогло - недоступно в локальном режиме.
Внимательный читатель мог заметить, что npm install
каждый раз будет заново выкачивать node_modules. Не сказал бы, что это оптимально, так что тут есть 2 решения:
-
использовать кеш CircleCI (save_cache/restore_cache)
-
собрать свои docker образы с предустановленными зависимостями (будет рассмотрен ниже)
К контейнеру для js у меня нет каких-либо особых требований. Его задача лишь собрать бандлы, так что для него я применю политику кеширования через save_cache/restore_cache - встроенные средства CircleCI. Его большой плюс в том, что не надо возиться с Docker, а также обновлять контейнер при изменении зависимостей. В результате джоб получился таким:
build:
docker:
- image: circleci/node:8.11
environment:
NODE_ENV: "production"
working_directory: ~/repo
steps:
- checkout
- restore_cache:
keys:
- bundles-{{ checksum "package.json" }}
- run:
name: Install frontend requirements
command: |
npm install --only=dev
npm install --only=prod
- run: npm run build
- save_cache:
key: bundles-{{ checksum "package.json" }}
paths:
- ~/repo/node_modules
- store_artifacts:
path: static/js
destination: bundles
К сожалению, локальный клиент circleci не умет ни кешировать, ни загружать артефакты, так что проверить можно исключительно пушем в какую-нибудь ветку.
3. Настройка окружений
Отлично, попробуем составить что-нибудь более сложное, например, тестирование серверного кода. Там помимо контейнера с самим приложением необходима база Postgres с установленным PostGIS, а также Redis для веб-сокетов. На GitLab настройка подобной конфигурации у меня отняла несколько часов, посмотрим как здесь…
Конфигурация у меня получилась вот такая:
test:
docker:
- image: circleci/python:3.6.1
environment:
DJANGO_SETTINGS_MODULE: "mercator.settings.circleci"
DB_HOST=postgres
DB_PORT=5432
DB_NAME=geopuzzle
DB_USER=geopuzzle
DB_USER_PASSWORD=geopuzzle
REDIS_HOST=redis
- image: circleci/postgres:9.6-postgis-ram
environment:
POSTGRES_USER: geopuzzle
POSTGRES_DB: geopuzzle
- image: circleci/redis:4.0.9
working_directory: ~/repo
steps:
- checkout
- run: sudo pip install -r requirements.txt
- run: python manage.py test
Однако, этот джоб так и не завёлся по причине отсутствия клиентской библиотеки GDAL для postgis. В принципе, эту проблему я и не думал решать - всё равно каждый раз прогонять установку зависимостей не камильфо. Так что…
Создание собственного образа
При работе с любой CI я предпочитаю создать свой собственный Docker образ, на базе которого и будет запускаться контейнер для тестов. Это позволяет более тонко настраивать окружение, а также уменьшить размер. Я не хочу здесь углубляться в тему создания образа, так что приведу для справки лишь финальную конфигурацию:
FROM python:3.6-alpine3.7
COPY requirements.txt /tmp/
RUN apk update --no-cache \
&& apk add --no-cache git \
&& apk add --no-cache --virtual .postgres-deps py3-psycopg2 postgresql-libs postgresql-dev \
&& apk add --no-cache --virtual .build-deps libffi-dev build-base zlib-dev jpeg-dev \
&& apk add --no-cache --repository https://dl-cdn.alpinelinux.org/alpine/edge/main libressl2.7-libcrypto \
&& apk add --no-cache --repository https://dl-cdn.alpinelinux.org/alpine/edge/testing geos gdal \
&& pip install -r /tmp/requirements.txt --no-cache-dir \
&& wget https://github.com/jwilder/dockerize/releases/download/v0.6.1/dockerize-linux-amd64-v0.6.1.tar.gz && tar -C /usr/local/bin -xzvf dockerize-linux-amd64-v0.6.1.tar.gz && rm dockerize-linux-amd64-v0.6.1.tar.gz \
&& apk del .build-deps && apk del .postgres-deps
CMD ["python3"]
Обратите внимание - я здесь ставлю утилиту dockerize для ожидания запуска подключённых контейнеров. Без её использования тесты могут начаться (и успеть свалиться) раньше, чем запустится Postgres. Создадим образ и отправим его на Docker Hub:
$ docker build -t tyvik/geopuzzle:app -f .circleci/Dockerfile.app
$ docker login
$ docker push tyvik/geopuzzle:app
Финальный джоб:
test:
docker:
- image: tyvik/geopuzzle:app
environment:
DJANGO_SETTINGS_MODULE: "mercator.settings.circleci"
DB_HOST: localhost
DB_PORT: 5432
DB_NAME: geopuzzle
DB_USER: geopuzzle
DB_USER_PASSWORD: geopuzzle
REDIS_HOST: localhost
- image: circleci/postgres:9.6-postgis-ram
environment:
POSTGRES_USER: geopuzzle
POSTGRES_DB: geopuzzle
POSTGRES_PASSWORD: geopuzzle
- image: circleci/redis:4.0.9
working_directory: ~/repo
steps:
- checkout
- run:
name: Wait for DB
command: dockerize -wait tcp://localhost:5432 -timeout 30s
- run:
name: Run tests
command: python manage.py test
Continuous deployment
Ну и конечно же CI не был бы CI без CD :) Я хочу чтобы на каждую ветку прогонялись тесты, а при пуше в production ещё и происходил деплой. За настройку такого поведения отвечает раздел workflows.
Деплой у меня слегка замороченный и состоит из кучи шагов:
npm run build
- собрать бандлыnpm run release
- залить source maps в Sentrypip install -r requirements.txt
- обновить зависимости./manage.py migrate
- обновить БД./manage.py collectstatic --noinpu
- собрать статику./manage.py deploystatic
- залить статику на S3./manage.py compilemessages
- обновить локализациюtouch reload
- перезапустить супервизор
Часть из них можно выполнять в docker-контейнерах, т.к. пункты, которые отвечают за статику (1, 2, 5, 6) фактически только заливают её на S3. Другие же (3, 4, 7, 8) отвечают непосредственно за обновление кода и сервера приложений - их я буду выполнять через SSH. А для доступа по SSH нужно зарегистрировать приватный ключ на стороне CircleCI, а публичный добавить в .ssh/authorized_keys на сервере. К тому же, как советуют в 12 факторах, моё приложение сильно зависит от переменных окружения. Все ключи и пароли вынесены из кода. Так что необходимо часть из них добавить в CircleCI (DJANGO_SETTINGS_MODULE, SECRET_KEY…) В итоге конфигурация получилась такой:
deploy:
docker:
- image: tyvik/geopuzzle:app
working_directory: ~/repo
steps:
- run:
name: Deploy release to Sentry
command: echo "Sentry"
- add_ssh_keys:
fngerprints:
- "c7:1f:fb:eb:c0:79:6b:c9:f7:71:62:d6:f5:c0:d5:e7"
- run:
name: Upload static
command: |
python manage.py collectstatic --noinput
python manage.py deploystatic
- run:
name: Update server
command: ssh $SSH_USER@$SSH_HOST "./circleci/update.sh"
Файл ./circleci/update.sh
отвечает непосредственно за обновление серверного кода:
#!/bin/bash
git pull
pip install -r requirements.txt
python manage.py compilemessages
python manage.py migrate
touch reload
Workflow
Осталось все джобы объединить в единый воркфлоу - определить последовательность и условия запусков. Мне хватило минимального:
workflows:
version: 2
build-deploy:
jobs:
- build
- test
- deploy:
requires:
- build
- test
filters:
branches:
only: production
Джобы build
и test
будут запускаться всегда, а вот deploy
только для ветки production
и только после того, как отработают первые 2. Кстати, задачи по умолчанию запускаются параллельно, что может иногда вызывать конфликты (допустим, в тестах понадобятся собранные на первом шаге бандлы). Для организации более сложного workflow лучше заглянуть в документацию.
Отладка
Очень странно, если бы такой сложный процесс запустился с первого раза. Я совсем забыл, что npm install
при NODE_ENV=production
не подтягивает dev-зависимости, среди которых находится и webpack. В итоге билд сфейлился, и пришлось искать способ отладки. И он есть! Это просто гениально - дать SSH доступ в контейнер! На странице билда справа вверху есть кнопка “Rerun job with SSH”. Нажимаем её, ждём пару минут и CircleCI говорит нам как подключиться в контейнер:
ssh -p 64539 -i .ssh/github 34.226.192.58
Так что проблема была решена очень быстро. Почему её не было при локальной сборке? Весь каталог с проектом монтировался в контейнер, включая и node_modules, в котором webpack уже и был установлен.
Шилды
И вот мы подобрались к самой важной части - шилдики для README.md. Тестирование, деплой… - да кому это нужно! Всех интересует красивые шилдики на главной странице Github репозитория (sarcasm!). Как вставить значёк от coveralls.io для визуализации покрытия кода тестами я описывал в своей статье про библиотеку для Yandex.Disk. Здесь ситуация похожая - определить переменную окружения COVERALLS_REPO_TOKEN и на последнем этапе вызвать coveralls.
Бейдж для circleci лежит в https://circleci.com/gh/TyVik/geopuzzle/edit#badges. Причём CircleCI предлагает его сразу в нескольких вариантах: markdown, rst, url…
Ура, теперь можно следить за статусом проекта прям с главной страницы репозитория!
Заключение
Работать с CircleCI мне понравилось, он хорошо подходит для сложных проектов, которые должны выполняться в стандартизованном окружении. Благодаря ему у меня нет больше проблем с деплоем, а применение новых изменений выполняется в пару кликов мышкой. Более того, тесты запускаются для каждого pull request'а, так что непосредственно на ревью я вижу и общее покрытие и статус тестирования, что позволяет избежать ошибочного мерджа. https://blog.github.com/2015-09-03-protected-branches-and-required-status-checks/ Ну и тесты, тесты… пойду писать тесты…