Подключение 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

интерфейс 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 решения:

  1. использовать кеш CircleCI (save_cache/restore_cache)

  2. собрать свои 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.

Деплой у меня слегка замороченный и состоит из кучи шагов:

  1. npm run build - собрать бандлы
  2. npm run release - залить source maps в Sentry
  3. pip install -r requirements.txt - обновить зависимости
  4. ./manage.py migrate - обновить БД
  5. ./manage.py collectstatic --noinpu - собрать статику
  6. ./manage.py deploystatic - залить статику на S3
  7. ./manage.py compilemessages - обновить локализацию
  8. 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…

Ура, теперь можно следить за статусом проекта прям с главной страницы репозитория! Github readme.md

Заключение

Работать с CircleCI мне понравилось, он хорошо подходит для сложных проектов, которые должны выполняться в стандартизованном окружении. Благодаря ему у меня нет больше проблем с деплоем, а применение новых изменений выполняется в пару кликов мышкой. Более того, тесты запускаются для каждого pull request'а, так что непосредственно на ревью я вижу и общее покрытие и статус тестирования, что позволяет избежать ошибочного мерджа. https://blog.github.com/2015-09-03-protected-branches-and-required-status-checks/ Ну и тесты, тесты… пойду писать тесты…