Рубрики
Без рубрики

Проектирование систем непрерывной сборки: Docker Swarm Orchestration

Строительный код иногда так же прост, как выполнение сценария. Но полнофункциональная система сборки требует … Tagged с помощью Python, Docker, CI, непрерывной передачи.

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

После нашей последней главы, обсуждающей события сборки, следующая итерация в Непрерывные сборки Серия охватывает, как развернуть контейнер внутри Docker Swarm, чтобы запустить сборку и протестировать его.

Что такое Docker Swarm

При запуске двигателя Docker в Режим роя ваше эффективное создание кластера. Docker будет управлять несколькими вычислительными узлами и их ресурсами, планируя работу через них, которые работают внутри контейнеров.

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

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

Настройка кластера

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

Один рой может иметь несколько менеджеров, а сами менеджеры также могут размещать контейнеры. Их задача состоит в том, чтобы отслеживать состояние кластера и контейнеры по вращанию по узлам по мере необходимости. Это обеспечивает избыточность по всему кластеру так, чтобы вы могли потерять одного или нескольких менеджеров или узлов и поддерживать базовые операции. Более подробная информация об этом доступна на официальном Docker Swarm документация Анкет

Чтобы создать рой, вам нужно запустить следующую команду на вашем первом менеджере (который также служит вашим первым узлом).

docker swarm init

Предыдущая команда расскажет вам, что запустить в каждом из ваших узлов, чтобы присоединиться к этому рому. Обычно это похоже на это:

docker swarm join --token SOME-TOKEN SOME_IP:SOME_PORT

Docker Daemon прослушивает Unix сокет расположен в /var/run/docker.sock по умолчанию. Это отлично подходит для локального доступа, но если вам нужен удаленный доступ, вам придется включить TCP гнезда явно.

Включение удаленного доступа

Docker Swarm предоставляет хороший API для управления услугами, но для нашего конкретного варианта использования нам нужна функция, которая недоступна на уровне роя и требует отдельного доступа к узлам. Частично причина в том, что мы используем Swarm для особого случая, для которого он не был создан: запуск одноразового недолгого контейнера-подробнее об этом позже.

Вам придется включить удаленный доступ на Daemons вашего узла, чтобы напрямую подключиться к ним. Это, кажется, немного различается между версиями Linux, распределением и местоположением файлов конфигурации Docker. Однако основная цель одинакова: вы должны добавить -H tcp://ip_address: 2375 Опция в выполнение службы Daemon, где Ip_address это интерфейс, на котором он слушает.

Вы обнаружите, что большинство примеров устанавливают его на 0.0.0.0 Чтобы любой мог подключиться к этому, но я бы порекомендовал ограничить его определенным адресом для лучшей безопасности – подробнее ниже.

Я был на изображении Ubuntu, которое использовало файл в /lib/systemd/system/docker.service Чтобы определить варианты демона. Вы просто найдете линию, которая начинается с Execstart = ... или имеет -H В нем и добавьте опцию дополнительного, упомянутое ранее.

Не забывайте, что вам нужно перезагрузить конфигурации Daemon и перезапустить Docker после внесения изменения:

sudo systemctl daemon-reload
sudo service docker restart

Я видел другие распределения, которые отслеживают эти настройки в рамках /etc/default/docker , и еще один, который использует файл в /etc/systemd/system/docker.service.d/ . Вы должны Google для Docker Daemon включить TCP или Docker Daemon включить удаленный API В паре с ароматом ОС, чтобы быть уверенным.

Последствия безопасности

Учитывая характер того, что вы можете сделать с Docker, важно отметить, что включение розеток TCP для удаленного доступа является очень серьезным риском безопасности. Он в основном открывает вашу систему для удаленного выполнения кода, потому что любой может подключиться к этому розетку и запустить или остановить контейнеры, просмотреть журналы, модифицировать сетевые ресурсы и т. Д.

Чтобы смягчить это, вы захотите включить проверку сертификата вместе с розетками TCP. Это заставляет демон подтвердить, что сертификат HTTPS, используемый потенциальными клиентами, подписан предварительно определенным органом сертификата (CA).

Вы создаете авторитет сертификата и подписываете любые клиентские сертификаты, прежде чем распространять их в вычислительные системы, которые будут выполнять оркестровку – обычно ваш сервис сборки.

Шаги о том, как генерировать сертификаты, выполнить подписание и включить, что параметры проверки доступны в документе Docker для Защита демона Анкет

Услуги, задачи и контейнеры

При запуске в режиме роя терминология Docker немного меняется. Вы больше не обеспокоены только контейнерами и изображениями, но также и задачи и Услуги Анкет

Служба определяет все части, которые объединяют приложение, работающее в кластере. Эти части являются задачами, и каждая задача является определением для контейнера.

Например, если у вас есть Application ABC, которая запускает API Flask, который вы хотите загрузить баланс по двум узлам, вы определяете одну службу ABC с двумя задачами. Рой заботится о том, чтобы держать их в двух узлах (даже если в кластере больше узлов), а также настраивает сеть, так что служба доступна по одному и тому же порту независимо от узла, к которому вы подключены.

Количество задач для выполнения является частью стратегии репликации, которую Swarm использует для определения того, сколько копий задач для продолжения работы в кластере. Мало того, что вы можете установить его на определенный номер, но также можете настроить его в глобальном режиме, который запускает копию задач в каждом узле роя.

Эти концепции в принципе просты, но могут стать сложными, когда вы пытаетесь сделать более сложные вещи позже. Поэтому я рекомендую вам проверить Сервисная документация Для получения дополнительной информации о том, как все это работает.

Используя эту терминологию для описания нашего варианта использования: для каждого нового запроса на сборку вы запускаете новую услугу в рое, который содержит один экземпляр задачи и один контейнер, который выполняет сборку. Планировщик роя позаботится о предоставлении его в любом узле. Контейнер должен удалить себя. Работа завершается.

Альтернативы

Как упоминалось ранее, Docker Swarm и его концепции предназначены для поддержания долгосрочных реплицированных услуг внутри кластера. Но у нас есть очень специфический случай эфимеровских услуг, выполненных для каждой сборки.

Мы не заботимся о высокой доступности или функциях балансировки нагрузки, мы хотим, чтобы это было для его возможностей планирования контейнеров.

Хотя это просто сделать, поскольку это не было создано для этого, может показаться, что мы собираем вещи вместе. Таким образом, другой вариант – создать наш собственный планировщик (или использовать существующий) и заставить его выполнять работу внутри контейнеров.

Это не сложно с существующими системами задач, аналогичных сельдерею или Драматик которые используют рабочие очереди, такие как RabbitMQ, для распространения задач управления контейнерами.

В том же направлении вы можете повторно использовать распределенные вычислительные системы для той же цели. Я развернул Dask успешно для этой цели. Это даже упростило некоторые рабочие процессы и позволило другим, что было бы невозможно в противном случае.

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

Оркестровка с питоном

Официальная библиотека Python, поддерживаемая Docker Retks, является Docker модуль. Он завершает все основные конструкции и является простым в использовании. Я уже давно использовал это.

Библиотека взаимодействует с Docker Daemon Rest API. Командные интерфейсы Daemon используют HTTP -глаголы на URL -адресах ресурсов для передачи данных JSON. Например: листинг контейнеров – это Get to /контейнеры/json , Создание тома – это пост в /тома/создание , так далее.

Если вам интересно, посетите Docker API Ссылка Больше подробностей.

Apiclient vs Dockerclient

Docker Сам модуль раскрывает два «слоя» общения с демона. Они проявляются как разные классы клиентов: Apiclient и Dockerclient Анкет Первый представляет собой обертку нижнего уровня вокруг конечных точек интерфейса напрямую, в то время как последний представляет собой объектно-ориентированный слой абстракции поверх этого клиента.

Для нашей цели сегодня мы сможем придерживаться экземпляров Dockerclient Чтобы выполнить все операции. Переход на более низкий уровень встречается редко, но иногда требуется. Оба интерфейса хорошо задокументированы в ссылке, совместно используемой ранее.

Создать клиента очень просто. После установки модуля с PIP установить Docker , вы можете импортировать класс клиента и создать его без каких -либо параметров. По умолчанию он подключается к сокету UNIX, упомянутому ранее.

from docker import DockerClient
dock = DockerClient()

Dockerclient Класс следует за общей архитектурой “client.resource.command”, которая делает ее интуитивно понятным для использования. Например: вы можете перечислить контейнеры с client.containers.list () , или просмотреть детали изображения с client.images.get ('python: 3-slim') Анкет

Каждый объект ресурса имеет методы для общих действий, таких как list () , create () или get () , а также те, специфичные для самого ресурса, как exec_run () для контейнеров.

Все атрибуты доступны с .attrs Собственность в форме словаря и reload () Метод получает последнюю информацию о ресурсе и обновляет экземпляр.

Для достижения наших целей вам нужно будет создать услугу для каждой сборки, найти узел, в котором выполняется его задача, внесите некоторые изменения в контейнер внутри этой задачи и запустите контейнер.

Сценарий выполнения

Настройка Swarm Service, которая выполняет сборку, составляет только пол битвы. Другая половина – это написание кода, который следует указаниям, которые мы определили в более ранняя глава Чтобы построить, проверить, записывать результаты и распространять артефакты. Это то, что я называю скриптом выполнения.

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

Я разработал и внедрил системы непрерывной интеграции, которые требуют выполнения Python, а также те, которые нет. Один из них добавляет сложность и ограничения для разработчиков, использующих систему, другую, а также сопровождающих системы.

Если это известный факт, что вы только когда -либо создадите код Python, то это не имеет значения, потому что изображения Docker, используемые в строительных репозиториях кода, уже установлены Python.

Если это не так, то вы можете увидеть значительное увеличение времени сборки, Complectiityy И, возможно, даже ограничение в поддерживаемых изображениях, потому что разработчики должны установить Python в контейнер как часть своей сборки.

Мой выбор в наши дни состоит в том, чтобы упаковать скрипт выполнения, чтобы он запустил внутри любого изображения Docker. Я задокументировал свои попытки в предыдущей статье о Упаковка модулей Python в качестве исполняемых файлов , где я заключил использование Pyinstaller для выполнения этой работы. Подробная информация о том, как создать пакет, включены там.

Строительство и выполнение тестов

Со всеми ингредиентами под рукой пришло время погрузиться в код, который делает новый сервис и запускает сборку. Как определено в предыдущих главах этой серии, следующие шаги предполагают, что:

  • Построенный репозиторий имеет файл YAML в своем корневом каталоге с директивами сборки.
  • Этот файл конфигурации содержит Изображение Директива, определяющая изображение Docker для использования со сборкой.
  • Есть среда Директива также, где пользователь может установить переменные среды.
  • Функции WebHook Обработка событий сборки получили информацию о конфигурации сборки и сохранили ее в словаре под названием конфигурация и предоставьте информацию о запросе вытягивания в премьер -министр диктат

Создание сервиса

При создании сервиса вам нужно рассмотреть, как его назвать, и как его найти при поиске роя.

Названия сервисов должны быть уникальными, и для того, чтобы упростить техническое обслуживание инфраструктуры, они должны быть описательными. Я предпочитаю использовать суффикс -{repo_owner}-{Repo_name}-{pr_number}-{timestamp} Анкет В этих именах также есть ограничения размера строкости, настолько осторожные, чтобы не стать слишком креативными.

Поскольку вам не нужны дублирующие сборки одного и того же запроса на притяжение, вам также нужна возможность программно искать рой для управления. Другими словами, если я выполняю сборку для данного запроса на привлечение, нет смысла разрешать сборку сборки, если я совершил новый код в том же PR. Вы не только тратите ресурсы, но даже если сборка успешно завершается, это бесполезно, потому что она уже устарела.

Чтобы справиться с этой ситуацией, я использую Ярлыки Анкет У сервиса может быть один или несколько ярлыков с метаданными о том, что она является и что он делает. Docker API также предоставляет методы фильтрации на основе этих метаданных.

Код, который следует за тем, как использует эти функции, чтобы определить, уже работает ли сборка, и останавливает ее перед созданием новой услуги.

import logging
import docker

...

def execute(pr, action=None, docker_node_port=None):
    ...

    # Get environment variables defined in the config
    environment = config['environment'] if 'environment' in config and isinstance(config['environment'], dict) else {}

    # Get network ports definition from the config
    ports = docker.types.EndpointSpec(ports=config['ports']) if 'ports' in config and isinstance(config['ports'], dict) else None

    environment.update({
        'FORGE_INSTALL_ID': forge.install_id,
        'FORGE_ACTION': 'execute' if action is None else action,
        'FORGE_PULL_REQUEST': pr['number'],
        'FORGE_OWNER': owner,
        'FORGE_REPO': repo,
        'FORGE_SHA': sha,
        'FORGE_STATUS_URL': pr['statuses_url'],
        'FORGE_COMMIT_COUNT': str(pr['commits'])
    })
    logging.debug(f'Container environment\n{environment}')

    # Connect to the Docker daemon
    dock = docker.DockerClient()

    # Stop any builds already running on the same pr
    for service in dock.services.list(filters={'label': [f"forge.repo={owner}/{repo}", f"forge.pull_request={pr['number']}"]}):
        logging.info(f"Found service {service.name} already running for this PR")

        # Remove the service
        service.remove()

    # Create the execution service
    service_name = f"forge-{owner}-{repo}-{pr['number']}-{datetime.now().strftime('%Y%m%dT%H%M%S')}"
    logging.info(f"Creating execution service {service_name}...")

    service = dock.services.create(
        config['image'],
        command=f"/forgexec",
        name=service_name,
        env=[f'{k}={v}' for k, v in environment.items()],
        restart_policy=docker.types.RestartPolicy('none'),
        labels={
            'forge.repo': f'{owner}/{repo}',
            'forge.pull_request': str(pr['number']),
        }
    )

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

Как видите, мы используем .Наименование услуг () Чтобы получить список услуг, которые в настоящее время работают в рой, фильтрованном с этикетками, которые мы описали ранее. Если существует услуга, звонит Service.remove () также избавится от его контейнеров.

Создание сервиса – это вызов .services.create () куда мы проходим:

  • Изображение контейнера, на котором основана служба – определено в нашей конфигурации сборки.
  • Команда для выполнения при запуске, что является именем нашего скрипта выполнения – Forgexec в таком случае.
  • Название службы.
  • Переменные среды определяются как список строк, отформатированных как Имя = значение Таким образом, мы конвертируем их из нашей среды DICT с использованием понимания списка.
  • Политика перезапуска – это термин, который Docker использует для определения того, что делать с контейнерами в случае перезапуска хоста. В нашем случае мы не хотим, чтобы они автоматически выходили в интернет, поэтому мы установили его на Нет Анкет
  • Ярлыки с метаданными, которые мы описали ранее.

Получение информации о задаче

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

    # Wait for service, task and container to initialize
    while 1:
        if len(service.tasks()) > 0:
            task = service.tasks()[0]
            if 'ContainerStatus' in task['Status'] and 'ContainerID' in task['Status']['ContainerStatus']:
                break
        time.sleep(1)

В службе сборки есть только одна задача, поэтому мы всегда можем предположить, что это первая в списке. Каждый Задача это словарь с атрибутами, описывающими контейнер, который его запускает.

Здесь есть две задержки: один до того, как задача будет назначена, и один до того, как контейнер разворачивается для задачи. Таким образом, необходимо подождать, пока информация о контейнере не будет доступна в данных задачи, прежде чем продолжить.

Копирование сценария выполнения в контейнер

Как обсуждалось ранее, вам необходимо скопировать наш сценарий упакованного выполнения в каждый контейнер сборки, чтобы он запустился – эквивалент Docker CP Анкет

Копирование данных в контейнер требует, чтобы файлы были TAR’D и сжаты, поэтому в самом начале нашего скрипта сервера событий мы создаем tar.gz Файл с использованием Tarfile Модуль Python:

if __name__ == '__main__':
    # Setup the execution script tarfile that copies into containers
    with tarfile.open('forgexec.tar.gz', 'w:gz') as tar:
        tar.add('forgexec.dist/forgexec', 'forgexec')

Это означает, что у нас есть forgexec.tar.gz Файл доступен для передачи с container.put_archive () функция, которую предоставляет модуль Docker. Делайте это каждый раз, когда сервер событий Webhook запускается и переопределяет любой существующий файл, чтобы убедиться, что вы не используете устаревший код.

Передача файлов в контейнер требует, чтобы мы напрямую подключались к узлу роя. На уровне сервиса Docker нет интерфейса, чтобы помочь нам сделать это. Вот почему мы должны были включить удаленный доступ раньше.

Сначала мы получаем информацию о узле Docker от задачи, а затем делаем новый Dockerclient экземпляр, который подключается к узлу:

    node = dock.nodes.get(task['NodeID'])
    nodeclient = docker.DockerClient(f"{node.attrs['Description']['Hostname']}:{docker_node_port}")

На этот раз в экземпляре используется порт TCP, в котором узлы слушают (настроены во время настройки кластера) и имя хоста узла. В зависимости от вашей сети и настройки DNS, вы можете использовать сокет Модуль, чтобы помочь с разрешением доменного имени. Что -то вроде socket.gethostbyname (node.attrs ['description'] ['hostname']) Может быть достаточно хорошо.

На этом этапе мы можем напрямую получить доступ к контейнеру, скопировать в него файл и запустить его:

    # Get container object
    container = nodeclient.containers.get(task['Status']['ContainerStatus']['ContainerID'])

    # Copy the file
    with open('forgexec.tar.gz', 'rb') as f:
        container.put_archive(path='/', data=f.read())

    # Start the container
    container.start()

Собрать это вместе

Вот наш новый exepute () Функция объединена с кодом из Обработка событий Webhook глава:

def execute(pr, action=None, docker_node_port=None):
    """Kick off .forge.yml test actions inside a docker container"""

    logging.info(f"Attempting to run {'' if action is None else action} tests for PR #{pr['number']}")

    owner = pr['head']['repo']['owner']['login']
    repo = pr['head']['repo']['name']
    sha = pr['head']['sha']

    # Select the forge for this user
    forge = forges[owner]

    # Get build info
    config = get_build_config(owner, repo, sha)

    if config is None or config.get('image') is None or config.get('execute') is None:
        logging.info('Unable to find or parse the .forge.yml configuration')
        return

    # Get environment variables defined in the config
    environment = config['environment'] if 'environment' in config and isinstance(config['environment'], dict) else {}

    # Get network ports definition from the config
    ports = docker.types.EndpointSpec(ports=config['ports']) if 'ports' in config and isinstance(config['ports'], dict) else None

    environment.update({
        'FORGE_INSTALL_ID': forge.install_id,
        'FORGE_ACTION': 'execute' if action is None else action,
        'FORGE_PULL_REQUEST': pr['number'],
        'FORGE_OWNER': owner,
        'FORGE_REPO': repo,
        'FORGE_SHA': sha,
        'FORGE_STATUS_URL': pr['statuses_url'],
        'FORGE_COMMIT_COUNT': str(pr['commits'])
    })
    logging.debug(f'Container environment\n{environment}')

    # Connect to the Docker daemon
    dock = docker.DockerClient(docker_host)

    # Stop any builds already running on the same pr
    for service in dock.services.list(filters={'label': [f"forge.repo={owner}/{repo}", f"forge.pull_request={pr['number']}"]}):
        logging.info(f"Found service {service.name} already running for this PR")

        # Remove the service
        service.remove()

    # Create the execution service
    service_name = f"forge-{owner}-{repo}-{pr['number']}-{datetime.now().strftime('%Y%m%dT%H%M%S')}"
    logging.info(f"Creating execution service {service_name}...")

    service = dock.services.create(
        config['image'],
        command=f"/forgexec",
        name=service_name,
        env=[f'{k}={v}' for k, v in environment.items()],
        restart_policy=docker.types.RestartPolicy('none'),
        # mounts=[],
        labels={
            'forge.repo': f'{owner}/{repo}',
            'forge.pull_request': str(pr['number']),
        }
    )

    # Wait for service, task and container to initialize
    while 1:
        if len(service.tasks()) > 0:
            task = service.tasks()[0]
            if 'ContainerStatus' in task['Status'] and 'ContainerID' in task['Status']['ContainerStatus']:
                break
        time.sleep(1)

    node = dock.nodes.get(task['NodeID'])
    nodeclient = docker.DockerClient(f"{node.attrs['Description']['Hostname']}:{docker_node_port}")
    container = nodeclient.containers.get(task['Status']['ContainerStatus']['ContainerID'])

    with open('forgexec.tar.gz', 'rb') as f:
        container.put_archive(path='/', data=f.read())

    container.start()

Что дальше?

Эта статья дала вам подробности, необходимые для использования Docker Swarm для обеспечения вычислительного вычисления, который строит код внутри кластера. Наряду с предыдущими главами по обработке событий репозитория и определению директив, необходимых для выполнения сборки, вы готовы перейти к следующей части, которая охватывает сам скрипт выполнения. Вам нужно будет настроить среду сборки, запустить команды и выполнить тесты для различных этапов в трубопроводе.

Учить больше!

Подписаться на tryexceppress.org Список рассылки Для получения дополнительного контента о Python, Docker, Open Source и нашем опыте работы с инженерией для предприятия.

Оригинал: “https://dev.to/tryexceptpass/designing-continuous-build-systems-docker-swarm-orchestration-5cbg”