Статьи

Докер и Феникс: как сделать вашу непрерывную интеграцию более привлекательной

Хотя система сборки всегда имеет решающее значение для успеха программного проекта, поддерживать такую ​​систему не всегда весело. Следовательно, мы склонны исследовать множество различных способов уменьшить затраты на обслуживание. Благодаря  Docker есть возможность сделать сам агент сборки очень простым, потому что он ничего не делает, кроме вращения и запуска контейнера Docker.

ФениксПредставьте, что вы работаете в магазине Python и вдруг у вас появляется инженер, пытающийся поэкспериментировать с  Go  для нового сервера REST API. Безусловно, можно модернизировать вашу сборочную инфраструктуру, включив в нее инструменты и зависимости для разработки Go. Но что, если другая среда и другие структуры также необходимы? Невозможно масштабировать (с точки зрения процесса) постоянную ошибку ваших инженеров по сборке / выпуску и их (эти постоянные) требования.

В конфигурации, которая включает в себя настройку агента сервера (или в жаргоне Jenkins, master-slave), агент выполняет основную работу. В предыдущем сообщении в блоге «  Агент сборки: шаблон против инициализации» я уже рассказал о наиболее распространенных способах устранения необходимости присматривать за агентом сборки. Я сам большой поклонник подхода автоматического обеспечения. Вот что написал Мартин Фаулер о  Phoenix Server :

Сервер должен быть как феникс, регулярно восставший из пепла.

Когда агент сборки ведет себя неправильно из-за дрейфа конфигурации, мы не будем его устранять. Мы просто уничтожаем этого проблемного  феникса  и даем ему возможность восстановиться (благодаря механизму обеспечения). Для другого довольно философского аспекта этого подхода прочитайте также мой другой пост в блоге  «Модель зрелости для автоматизации сборки» .

Контейнер Феникс

Если многие ваши агенты сборки имеют одну и ту же черту, например, они в основном являются системой Linux (часто в ее виртуализированной форме, например, в экземпляре EC2) с различными инструментами (компиляторы, библиотеки, платформы, тестовые системы), тогда сценарий может быть дальше упрощается. Что, если агент по сборке — это  не  настоящий Феникс? Что, если агент сборки — это  только  область, где феникс живет (и умирает)?

В этой ситуации контейнер Docker становится  настоящим фениксом . Каждый проект должен будет предоставить некоторую дополнительную информацию (обязательно: в форме скрипта, декларативную: общую конфигурацию, понятную для инструмента сборки), необходимую агенту сборки: какой контейнер использовать и как инициировать эту сборку в контейнере.

Давайте возьмем простой проект и настроим сборку, используя этот подход Docker и Phoenix. Для этого примера мы  создадим инструмент обнаружения функций процессора  (реализован с использованием C ++). Если вы хотите следовать, просто клонируйте его git-репозиторий  bitbucket.org/ariya/cpu-detect  и обратите внимание на  подкаталог phoenix .

В подкаталоге phoenix есть два сценария оболочки:  init.sh и  build.sh.

Первый,  init.sh , является тем, который должен быть выполнен агентом сборки. Он вытягивает контейнер, используемый для выполнения фактического шага сборки. Поскольку это проект C ++, мы будем использовать  контейнер gcc . После этого он запускает контейнер с сопоставлением тома, так что  /source внутри контейнера сопоставляется каталог git checkout. Когда контейнер запускается, он также выполняет другой сценарий build.sh (так  /source/phoenix/build.sh как мы сейчас находимся внутри контейнера).

Если мы упростим это, все содержание  init.sh может быть суммировано как:

docker run -v $SOURCE_PATH:/source gcc:4.9 sh - c "/source/phoenix/build.sh"

Второй сценарий,  build.shне  выполняется агентом сборки напрямую. Он будет работать  внутри  указанного контейнера, как описано выше. Основная часть  build.sh состоит в том, чтобы выполнить фактический шаг сборки. Для этого проекта его нужно только вызвать  make (в реальном проекте батарея тестов должна быть частью этого). Перед этим сценарию необходимо подготовить каталог сборки и скопировать исходный код (помните, /source внутри контейнера соответствует git checkout). Как только сборка завершена, артефакт сборки должен быть возвращен обратно. В этом случае мы просто копируем сгенерированный  cpu-detectисполняемый файл.

Если какой-либо шаг во время этого процесса завершится неудачей, включая  make самого себя, тогда весь процесс будет помечен как сбой. Такое автоматическое распространение статуса устраняет необходимость настраиваемой обработки ошибок.

Чтобы проверить эту настройку, подготовьте коробку с Docker, а затем запустите  phoenix/init.sh. Если все работает правильно, вы увидите вывод, подобный следующему скриншоту.

incontainer

Если вы испытываете какой-то начальный момент, пытаясь выполнить шаги, пожалуйста, используйте следующую диаграмму. Это также полезное упражнение для адаптации этих двух сценариев феникса к вашему личному проекту.

диаграмма

Агент Демократии

In the above example, we pull and run a ready-to-use gcc container. In practice, you may want to come up with a set of customized containers to suit your need. Hence, it is highly recommended that you setup your own Docker registry to be used internally. This becomes a private registry and it should not be accessible by anyone outside your organization. Here is how your init.sh might look like incorporating the technique:

REGISTRY="docker.mycompany.com"
IMAGE="golang"
TAG="1.4"
CONTAINER="${REGISTRY}/${IMAGE}:${TAG}"
 
echo "Container to be used: $CONTAINER."
docker pull $CONTAINER
echo

Now that the build process only happens inside the container, you can trim down the build agent. For example, it does not need to have packages for all development environment, from Perl to Haskell. All it needs is Docker (and of course the client software to run as a build agent) and thereby massively reducing the provisioning and maintenance effort.

Let’s go back to the illustrative use case mentioned earlier. If an engineer in your team is inspired to evaluate Go, you do not need to modify your build infrastructure. Just ask them to provide a suitable Go development container (or reuse an existing once such as google/golang) and prepare that phoenix-like bootstrapper scripts. The same goes for the new intern who prefers to tinker with Rust instead. No change in the build agent is necessary! Everyone, regardless the project requirements, can utilize the same infrastructure.

In fact, if you think through this carefully, you will realize that all those Linux build agents are not unique at all. They all have the same installed packages and no agent is better or worse than the others. There is no second-class citizen. This is democracy at its best.

Parametrization and Resilience

Knowing the build number and other related build information is often essential to the build process. Fortunately, many continuous integration systems (BambooTeamCityJenkins, etc) can pass that information via environment variables. This is quite powerful since all we need to do is to continue to pass that to Docker. For example, if you use Bamboo, then the invocation of docker needs to be modified to look like (notice the use of -e option to denote an environment variable).

docker run -v $SOURCE_PATH:/source \
  -e bamboo_buildNumber=${bamboo_buildNumber}\
  $CONTAINER sh - c "/source/phoenix/build.sh"

Another side effect of this Docker-based build is the built-in error recovery. In many cases, a build may fail or it gets stuck in some process. Ideally, you want to terminate the build in this situation since it warrants a more thorough investigation. Armed with the useful Unix timeout command, we just need to modify our Docker invocation:

TIMEOUT=2m
echo "Triggering the build (with ${TIMEOUT} timeout)..."
timeout --signal=SIGKILL ${TIMEOUT} \
  docker run -v $SOURCE_PATH:/source \
  $CONTAINER sh - c "/source/phoenix/build.sh"

By the way, this is the reason why there is an explicit docker pull in init.sh. Technically it’s not needed, but we use it a mechanism to warm up the container cache. This way, the time it takes to initially pull the container will not be included in that 2-minute timeout.

With the use of timeout, if the Docker process would not complete in 2 minutes, it will be terminated with SIGKILL, effectively aborting the whole step at once. Since the offending application is isolated inside a container, this kind of clean-up also results in a really clean termination. There is no more server hanging out doing nothing because it was not killed properly. There is no stray zombie process eating the resources in the background.

Summary: Use Docker to modify the build agent to be a realm where your phoenix lives and dies. After that, turn every build process into a short-lived phoenix.