Я слишком долго делал то, что технически мне было не нужно, поэтому теперь я собираюсь потратить больше времени на написание статьи об этом 😉. Когда-то давно я проводил тесты на CodeShip. Это было не очень. Он не был предназначен для пареллизации (в то время), он был медленным, как черт, я получал временные сбои, когда Elasticsearch выходил из себя, и результаты просто выплескивались в один огромный текстовый блок. Я перешел на CircleCI.
Переход с Codeship на CircleCI был относительно простым. Интерфейс CircleCI был более отзывчивым, он лучше разбирал результаты тестов RSpec, а распараллеливание было легко реализовать. Но мне все равно удалось найти вещи, которые мне не понравились: У параллельных бегунов было однобокое время, которое отказывалось обновляться, и, что хуже всего, не было способа уведомить его о том, что все параллельные работники либо преуспели, либо потерпели неудачу. Я мог получить только уведомление о прохождении или провале для каждого рабочего, что означало, что каждый push на GitHub приводил к 10 уведомлениям в Slack 😱. Я потратил несколько часов, пытаясь это исправить, но нашел только множество сообщений о том, что это невозможно. Я решил сменить сервис, когда найду время.
Когда был анонсирован GitHub Actions, он показался мне более привлекательным вариантом, поскольку я мог иметь и git-репо, и CI/CD инфру в одном месте. К тому же он был бесплатным. Но стоил ли этот переход того? Смогу ли я распараллелить GitHub Actions, насколько быстро это будет, сколько времени займет настройка, смогу ли я наконец перестать получать по 1 уведомлению из Slack на каждого параллельного работника? Так много вопросов и только один способ выяснить это.
- Старая установка
- Мечта
- Новая установка
- Проблем, с которыми пришлось столкнуться, было много
- 1 — Отсутствие обучающих ресурсов для данного конкретного случая использования.
- 2 — Отсутствие возможности настройки с помощью оператора container.
- 3 — Предоставление окружения docker без дублирования
- 4 — Команды просто не работали. Например, любая из них.
- 5 — Elasticsearch долго загружается
- 6 — Сбои в тестировании из-за очевидной проблемы с компиляцией активов
- Без лишних слов
- Выводы (на данный момент)
- Последние замечания!
Старая установка
Моя предыдущая настройка включала в себя много сборки образов и много ожидания.
- [Вручную] Создать образ локально, достичь некоторого рабочего состояния.
- [Необязательно] Отправка образа в реестр контейнеров Google (GCR), если произошли изменения в ОС или пакетах apk/apt
- [Manual] Push to GitHub
- [Автоматически] CircleCI извлекает последний образ из GCR, извлекает ветку или последний мастер-коммит из GitHub, повторно запускает bundle и yarn, предварительно компилирует активы, затем запускает RSpec.
- [Вручную] Если все 10 рабочих сообщили о прохождении тестов (глаз дергается), то я запускаю локальное развертывание, выполнив
git push hostname:master
, гдеhostname
— git remote моего производственного сервера, на котором настроен Dokku. Это позволит создать образ с нуля, а затем развернуть этот образ.
Если это звучит расточительно, то так оно и есть. В CircleCI была польза в извлечении из GCR и повторном выполнении шагов — мне нужно было вводить/выводить только тогда, когда происходили изменения под слоем пакета, а это были только ОС и зависимости библиотек APT/APK. Тем не менее, необходимость помнить, что для того, чтобы тесты CircleCI были действительны, нужно было проталкивать их в GCR, что несколько раз ставило меня в тупик.
Мечта
Я предварительно настроил GitHub Action для сборки образа и больше ничего с ним не делал. Эта простая проверка на вменяемость на самом деле очень полезна. В конце концов, можно отправить файл Docker, который не может собраться, и это помогло мне устранить проблему, когда сборка работала на моем Mac, на Windows с Ubuntu WSL, но не на Ubuntu (спасибо Docker), что требовалось для развертывания с помощью Dokku. Заставить это действие сборки работать было невероятно просто, но теперь было время посмотреть, смогу ли я достичь святой земли: один сервис репозитория и CI/CD, чтобы управлять ими всеми.
Не буду врать, запустить тесты на GH Actions было непросто. Потребовалось много догадок, проверок и ожиданий, поэтому я написал статью, чтобы избавить вас от моих мучений! Пожалуйста. «Build once use everywhere» — это мечта, но поскольку могут быть различия в зависимости от того, где мы собираем (кашель, спасибо еще раз Docker), я собираюсь изменить «мечту» на build once use everywhere, когда мы выкладываем на GitHub.
Новая установка
Новый рабочий процесс должен выглядеть следующим образом:
- [Manual] Собираем образ локально, достигаем некоторого рабочего состояния.
- [Manual] Push to GitHub
- [Автоматически] Запускается рабочий процесс GitHub Actions. Он создает образ, используя кэш докерного слоя GHA, и запускает rspec на этом образе с помощью docker-compose.
- [Автоматически] Если все прошло успешно, отправьте уже созданный образ на ghcr.io.
- [Автоматически] Условное развертывание в продакшн, дав команду Dokku извлечь образ с ghcr.io.
Это гораздо более автоматизированный способ, и в зависимости от того, насколько нагреты различные кэши, он может быть быстрее. В этой статье я не буду рассказывать о своей попытке распараллелить RSpec в GitHub Actions. Важной частью является то, что после отправки на GH мы можем автоматически перейти на продакшн, если все прошло хорошо.
Проблем, с которыми пришлось столкнуться, было много
1 — Отсутствие обучающих ресурсов для данного конкретного случая использования.
Существует множество ресурсов по GHA, но я не смог найти ни одного, который бы собирал образ, а затем запускал тесты на выходе сборки. Похоже, что все они довольствуются тестами на ubuntu-latest, затем собирают свой собственный Dockerfile и считают, что различия несущественны. Мое приложение требует многокомпонентной сборки Dockerfile для прохождения тестов.
Решение: Статья, которую вы сейчас читаете!
2 — Отсутствие возможности настройки с помощью оператора container
.
С помощью оператора container
можно запустить шаги на выбранном вами образе Docker, но удачи вам в запуске на динамическом теге, таком как pr-123
. Вывод тегов, который обычно доступен с помощью ${{ steps.meta.outputs.tags }}
, недоступен в операторе container. Также нет доступа к ${{ github.ref }}
или даже к env
. Это означает, что я не могу использовать оператор контейнера 🤷♂️.
Решение: Вместо того чтобы загружать образ в реестр контейнеров только для того, чтобы вытащить его и запустить на нем тесты, лучше загружать в реестр контейнеров только правильные образы и устранить узкое место в сети. Это означает использование load:true
на этапе сборки, а затем просто использование docker-compose для запуска на локально доступном образе.
3 — Предоставление окружения docker без дублирования
Предоставление окружения для слоя ubuntu-latest
было простым, благодаря использованию env
в yaml-файле рабочего процесса и ${{ secrets.EXAMPLE }}
, но предоставление env команде docker run
без дублирования оказалось сложной задачей. У нее есть очень простое решение, но оно сопровождается неприятностью (см. 4).
Решение: Просто сбросьте env в .env файл, который ожидает docker compose. Например:
-
name: Run tests
run: |
env > .env
docker-compose run web rails db:setup
docker-compose run web bundle exec rspec
4 — Команды просто не работали. Например, любая из них.
bundle, rails, rake, все они говорили /usr/bin/env: 'ruby': No such file or directory
.
Решение: Эта задача была сложной, и казалось, что GHA хочет меня подловить, но на самом деле я сделал это сам с помощью шага 3. Переменная $PATH в базовом окружении ubuntu-latest
не подходит для вашего окружения Dockerfile, и она была просто переопределена. Проверьте ваш $PATH
в локальном окружении с помощью чего-то вроде docker-compose run web echo $PATH
и убедитесь, что эти пути есть в вашем GH workflow yaml под $PATH
.
5 — Elasticsearch долго загружается
Неполная инициализация Elasticsearch перед запуском шага посева базы данных вызывала эту ошибку:
Faraday::Error::ConnectionFailed: Couldn't connect to server
Это никогда не было проблемой локально, но локально я обычно запускал docker-compose exec web rspec
после запуска docker-compose up
, что давало Elasticsearch достаточно времени для загрузки.
Решение: Наконец-то пришло время настроить правильную проверку работоспособности в docker-compose.yml. Вот мой сервис elasticsearch:
elasticsearch:
container_name: elasticsearch
image: docker.elastic.co/elasticsearch/elasticsearch:7.4.2
environment:
- discovery.type=single-node
- cluster.name=docker-cluster
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms1024m -Xmx1024m"
- "logger.org.elasticsearch=error"
healthcheck:
test: curl --fail elasticsearch:9200/_cat/health >/dev/null || exit 1
interval: 30s
timeout: 10s
retries: 5
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- esdata:/usr/share/elasticsearch/data
ports:
- 9200:9200
Вам может потребоваться изменить тест healthcheck в зависимости от вашей версии ES. Для этого потребуется соответствующий оператор depends_on
в веб-контейнере:
depends_on:
elasticsearch:
condition: service_healthy
postgres:
condition: service_started
redis:
condition: service_started
Мне кажется странным, что вы должны явно указывать это после определения утверждения healthcheck. Возможно, docker compose 4 будет делать это автоматически. Я столкнулся с той же проблемой с Redis, и решение этой проблемы находится в docker-ci.yml ниже.
6 — Сбои в тестировании из-за очевидной проблемы с компиляцией активов
На этом этапе у меня были запущены тесты, и некоторые из них прошли, что было огромным облегчением. Но все тесты запросов завершались следующим образом:
ActionView::Template::Error:
Webpacker can't find application.js in /app/public/packs-test/manifest.json. Possible causes:
1. You want to set webpacker.yml value of compile to true for your environment
unless you are using the `webpack -w` or the webpack-dev-server.
2. webpack has not yet re-run to reflect updates.
3. You have misconfigured Webpacker's config/webpacker.yml file.
4. Your webpack configuration is not creating a manifest.
Your manifest contains:
{
}
Это удивительно, поскольку мы только что запекли активы в изображение на этапе сборки.
Решение: Совет Даниэле Барон, здесь есть настоящий спасительный инструмент под названием tmate.
Вставьте его, и вы, по сути, получите отладочный отчет в рабочем процессе GH:
- # Creates a SSH tunnel!
name: Setup tmate session
uses: mxschmitt/action-tmate@v3
Просто дождитесь этого шага и скопируйте заявление ssh
в свой терминал.
Это была чертовски сложная проблема для устранения неполадок. Я всегда запекал производственные активы в изображение. У меня никогда не было ошибки отсутствия активов, если только я не нарушал конфигурацию webpack. Эта настройка всегда работала во всех окружениях, которые когда-либо полагались на нее — Codeship, CircleCI и 2 совершенно разных производственных окружения.
Каким-то образом, запуск этого показывает заполненный манифест:
docker run web cat public/packs/manifest.json
но это говорит об отсутствии такого файла:
docker-compose run web cat public/packs/manifest.json
… что? 🤔. Как могли активы явно присутствовать в изображении, а затем исчезнуть при использовании docker-compose
?
Как и во всем, что касается компьютеров, это был еще один случай, когда мы сами себя подставили под удар. Docker Compose монтирует тома, которые вы ему укажете с помощью такого заявления:
volumes:
- ".:/app"
Это берет текущий каталог (.
) и монтирует его в каталог /app
внутри образа. Это приводит к тому, что папка без скомпилированных активов записывается в папку с уже скомпилированными активами. Это не было заметно на Codeship или CircleCI, потому что у них был явный шаг предварительной компиляции активов. Я придумал 3 способа решения этой проблемы:
- Использовать addnab/docker-run-action и вообще отказаться от Docker Compose 😅.
- Просто перекомпилировать активы, несмотря на то, что они только что были скомпилированы. Легко, но расточительно.
- Написать docker-compose.yml специально для CI, который не монтирует локальные файлы.
Я попробовал 1, у меня возникли трудности с передачей env и проблемы с подключением к services
. Затем я попробовал 2, и это сработало, но было очень медленно. 3 кажется правильным подходом, и он сократил время выполнения с 31 м до 12 м!
Без лишних слов
Вот yamls, который вы ждали.
docker-ci.yml
version: "3.7"
services:
postgres:
image: "postgres:14-alpine"
environment:
POSTGRES_USER: "example"
POSTGRES_PASSWORD: "example"
ports:
- "5432:5432"
volumes:
- "postgres:/var/lib/postgresql/data"
redis:
image: "redis:5-alpine"
command: ["redis-server", "--requirepass", "yourpassword", "--appendonly", "yes"]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
ports:
- "6379:6379"
volumes:
- redis:/data
sysctls:
# https://github.com/docker-library/redis/issues/35
net.core.somaxconn: "511"
sidekiq:
depends_on:
- "postgres"
- "redis"
- "elasticsearch"
build:
context: .
args:
environment: development
image: you/yourapp
command: bundle exec sidekiq -C config/sidekiq.yml.erb
volumes:
- ".:/app"
# don"t mount tmp directory
- /app/tmp
env_file:
- ".env"
web:
build:
context: .
args:
environment: development
image: you/yourapp
command: bundle exec rspec
depends_on:
elasticsearch:
condition: service_healthy
postgres:
condition: service_started
redis:
condition: service_healthy
tty: true
stdin_open: true
ports:
- "3000:3000"
env_file:
- ".env"
elasticsearch:
container_name: elasticsearch
image: docker.elastic.co/elasticsearch/elasticsearch:7.4.2
environment:
- discovery.type=single-node
- cluster.name=docker-cluster
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms1024m -Xmx1024m"
- "logger.org.elasticsearch=error"
healthcheck:
test: curl --fail elasticsearch:9200/_cat/health >/dev/null || exit 1
interval: 30s
timeout: 10s
retries: 5
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- esdata:/usr/share/elasticsearch/data
ports:
- 9200:9200
volumes:
redis:
postgres:
esdata:
main.yml
name: Main
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
name: Build, Test, Push
runs-on: ubuntu-20.04
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
POSTGRES_USER: example
POSTGRES_PASSWORD: example
POSTGRES_HOST: postgres
# Humour me here, this needs to be production for the sake of baking assets into the image
RAILS_ENV: production
NODE_ENVIRONMENT: production
ACTION_MAILER_HOST: localhost:3000
REDIS_URL: redis://redis:yourpassword@redis:6379
DATABASE_URL: postgresql://example:example@postgres:5432/dbname?encoding=utf8&pool=5&timeout=5000
ELASTICSEARCH_URL: elasticsearch:9200
# Actually secret secrets
EXAMPLE_KEY: ${{ secrets.EXAMPLE_KEY}}
# Append our own PATHs because env > .env nukes it!
PATH: /usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/local/bundle/bin:/usr/local/bundle/gems/bin:/usr/lib/fullstaq-ruby/versions/3.0.4-jemalloc/bin:/app/bin
steps:
-
name: Checkout repo
uses: actions/checkout@v3
with:
fetch-depth: 1
- # Not required but recommend to be able to build multi-platform images, export cache, etc.
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
-
name: Log in to the Container registry
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
-
name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v3
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: ${{ steps.extract_branch.outputs.branch }}
-
name: Build with Docker
uses: docker/build-push-action@v3
with:
context: .
load: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: |
${{ steps.meta.outputs.tags }}
${{ github.repository }}:latest
# - # Creates a SSH tunnel!
# name: Setup tmate session
# uses: mxschmitt/action-tmate@v3
-
name: Run tests
run: |
env > .env
./run-tests.sh
- # NOTE Building in this step will use cache
name: Build and push Docker image
uses: docker/build-push-action@v3
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
run-tests.sh
# Uses docker-compose to run tests on GitHub Actions
# Exit if a step fails
set -e
echo "============== DB SETUP"
docker-compose -f docker-ci.yml run web rails db:reset RAILS_ENV=test
echo "============== RSPEC"
docker-compose -f docker-ci.yml up --abort-on-container-exit
Выводы (на данный момент)
Как вы можете видеть, GitHub Actions более свободен, чем большинство сервисов CI. Они в основном говорят: «Вот Ubuntu, развлекайтесь». Запуск созданного вами образа возможен, но они, конечно, не держат вас за руку. Если вы знаете GitHub Action, который действительно держит вас за руку в отношении запуска тестов на только что созданном образе, напишите об этом в комментариях!
Если у вас есть предложения по улучшению, я весь внимание, хотя я не могу сказать, что очень хочу вносить изменения в этот рабочий процесс в ближайшее время 😫.
Удалось ли мне распараллелить GitHub Actions?
Нет. Просто запуск на собранном образе был настоящим испытанием. Если вы знаете, как распараллелить GHA с помощью docker-compose, дайте мне знать в комментариях!
Сколько времени ушло на настройку этого GHA?
Слишком долго. Стыдно долго. Но я сэкономил вам время, верно?
… верно?
Насколько это быстро?
Это сравнение даже близко не справедливо. Локально и с помощью CircleCI, я использую уже созданный образ, а CircleCI имеет 10 параллельных рабочих.
CircleCI (теплый): 4m20s 🪴
Локально (тепло): 10м44с
GHA (теплый): 12м42с
Я действительно впечатлен временем, которое получает GHA, учитывая, что она собирает образ, собирает пакет (с теплым кэшем), прекомпилирует активы и отправляет образ на ghcr.io.
Стоил ли переход?
На данный момент — нет. Времени, потраченного на настройку, потребуется очень много, чтобы окупиться. 12 м против 4 м — это не улучшение, хотя это большое преимущество — не думать о том, когда я должен отправить свежий образ в наш реестр контейнеров, и это будет огромным преимуществом — возможность автоматического развертывания (без необходимости перестройки).
Смог ли я наконец-то перестать получать по 1 уведомлению Slack на каждого параллельного работника?
Да! Но я также вернулся к последовательному рабочему процессу с одним рабочим. Я мог бы легко указать одного работника в CircleCI и получить тот же результат.
Следите за deploy.yml для части Dokku, когда я доберусь до его написания!
Последние замечания!
- Возможно, вы не захотите запускать шаги развертывания при каждой отправке на master! Возможно, вы захотите выполнять их только при отправке нового тега.
- Если вы копируете и вставляете, вам нужно проверить ваш PATH и записать его в main.yml. Тот, который я предоставил, скорее всего, не будет работать для вас.
- Обратите внимание на отсутствие тома для веб-контейнера в docker-ci.yml. Это сделано специально, потому что мы не хотим перезаписывать активы, которые мы только что предварительно скомпилировали. Отсутствие предварительной компиляции активов во второй раз экономит много времени. Это также означает, что вы не сможете получить доступ к файлам, написанным во время тестирования (например, к файлу results.json), а затем получить к ним доступ на более позднем этапе.