Продолжение серии статей о BareMetal CI. В первой части мы рассмотрели базовый подход к автоматизации тестирования микроконтроллеров с использованием J-Link и RTT. Эта статья посвящена масштабируемому решению на базе Docker, которое поддерживает различные типы оборудования и CI-платформы.

Введение

Разработка встраиваемых систем и микроконтроллеров традиционно сталкивается с проблемой тестирования реального железа в процессе непрерывной интеграции. Облачные CI-системы, такие как GitHub Actions или GitLab CI, прекрасно справляются с тестированием веб-приложений и серверного ПО, но не имеют доступа к физическим устройствам — программаторам, отладчикам и периферии.

BareMetal CI — это подход к организации CI/CD для встраиваемых систем, при котором тесты выполняются на реальном железе с использованием self-hosted раннеров. Это решение позволяет автоматически прошивать микроконтроллеры, выполнять отладку и тестировать работу с реальной периферией (CAN-шины, USB-устройства и др.) прямо в процессе CI/CD пайплайна.

Архитектура решения

Выбор архитектуры: раннер внутри контейнера

В отличие от традиционного подхода, где CI-раннер устанавливается на хост-систему и запускает джобы в отдельных контейнерах (Docker executor), в данном решении сам раннер работает внутри контейнера с executor=shell.

Важное уточнение о целевой аудитории:

Этот подход не является best practice для production CI-инфраструктуры. Более правильным решением было бы:

  • Установить раннер на хост-системе

  • Настроить Docker executor с монтированием необходимых устройств (GitLab, GitHub)

  • Использовать разные образы для разных проектов/задач

  • Управлять образами централизованно (например, через GHCR)

Почему тогда раннер в контейнере?

Это решение оптимизировано для быстрого развёртывания тестового стенда, когда:

  1. Нет опыта с Ansible/конфигурацией раннеров — новичок может потратить день на настройку runner.toml, разбираться с правами доступа к устройствам, путями к сокету Docker и т.д.

  2. Разовое развёртывание — нужно быстро поднять стенд на Raspberry Pi или другом одноплатнике для тестирования конкретного устройства. Не планируется использование для множества проектов.

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

  4. Документированное окружение — весь стек описан в Dockerfile и docker-compose.yml. Для человека, впервые настраивающего CI для embedded, это понятнее, чем конфиг раннера + отдельные скрипты установки.

Сценарий использования:

# Поставили чистую систему на Raspberry Pi
git clone <repo>
cp .env.example .env
# Отредактировали .env (2 переменных: RUNNER_PLATFORM и токен)
docker-compose up -d
# Готово — можно запускать тесты

Вместо:

  • Установка и настройка GitLab Runner на хосте

  • Разбор документации по Docker executor

  • Настройка монтирования устройств в runner.toml

  • Разбор прав доступа к /dev и Docker socket

  • Создание и регистрация образов с инструментами

Trade-offs (что теряем):

  • Гибкость — нельзя использовать разные образы для разных проектов

  • Изоляция — все джобы одного раннера делят окружение

  • Best practices — privileged контейнер с раннером внутри — это архитектурный антипаттерн

Когда НЕ стоит использовать этот подход:

  • ❌ У вас уже есть настроенная CI-инфраструктура

  • ❌ Нужно тестировать множество разных проектов на одном стенде

  • ❌ Требуется строгая изоляция между джобами

  • ❌ Есть специалист по DevOps в команде

Когда этот подход имеет смысл:

  • ✅ Первый опыт с CI для embedded

  • ✅ Нужно быстро поднять тестовый стенд

  • ✅ Один раннер = одно устройство/проект

  • ✅ Хочется "просто запустить и чтобы работало"

Этот подход — точка входа для тех, кто только начинает автоматизировать тестирование железа. После получения опыта логично мигрировать на правильную архитектуру с раннером на хосте и Docker executor.

Основные компоненты

Решение построено на базе Docker-контейнера, который объединяет в себе:

  1. CI Runner — self-hosted раннер для GitHub Actions или GitLab CI

  2. Отладчики и программаторы — поддержка различных debug probe (Segger J-Link, Black Magic Probe и др.)

  3. Интерфейсные адаптеры — работа с периферийными шинами (CAN через PEAK CAN/SocketCAN, USB, UART и др.)

Архитектурная схема

┌─────────────────────────────────────────────────────────┐
│                    GitHub / GitLab                      │
│                    (Cloud CI/CD)                        │
└────────────────────┬────────────────────────────────────┘
                     │ Webhooks / Job Assignment
                     ▼
┌─────────────────────────────────────────────────────────┐
│              Docker Container (Host Linux)              │
│  ┌────────────────────────────────────────────────────┐ │
│  │         GitHub/GitLab Self-Hosted Runner           │ │
│  └────────────────────────────────────────────────────┘ │
│  ┌────────────────────────────────────────────────────┐ │
│  │         Debug Probes & Programmers                 │ │
│  │   (J-Link, Black Magic Probe, OpenOCD, etc)        │ │
│  └────────────────────────────────────────────────────┘ │
│  ┌────────────────────────────────────────────────────┐ │
│  │         Interface Adapters & Protocols             │ │
│  │    (SocketCAN, USB, UART, SPI, I2C, etc)           │ │
│  └────────────────────────────────────────────────────┘ │
└──────────────┬──────────────────────────────────────────┘
               │ /dev mount (privileged mode)
               │ network_mode: host
               │
               ▼
      ┌─────────────────────────────────────┐
      │         Physical Hardware           │
      │  ┌──────────────┐  ┌──────────────┐ │
      │  │ Debug Probes │  │  Interface   │ │
      │  │ (J-Link,BMP) │  │   Adapters   │ │
      │  └──────┬───────┘  │ (CAN, USB..) │ │
      │         │          └──────┬───────┘ │
      │         ▼                 ▼         │
      │  ┌──────────────┐  ┌──────────────┐ │
      │  │  Target MCU  │  │ Peripheral   │ │
      │  │  (ARM/RISC-V)│  │   Devices    │ │
      │  └──────────────┘  └──────────────┘ │
      └─────────────────��───────────────────┘

Ключевые особенности

1. Универсальная поддержка CI-платформ

Контейнер поддерживает как GitHub Actions, так и GitLab CI через единую конфигурацию. Выбор платформы осуществляется через переменную окружения RUNNER_PLATFORM:

# В файле .env
RUNNER_PLATFORM=github    # или gitlab

Переключение между платформами происходит автоматически при старте контейнера. Единая точка входа (entrypoint.sh) определяет, какой раннер запускать:

if [ "${RUNNER_PLATFORM}" = "gitlab" ]; then
    source /home/runner/scripts/gitlab-runner.sh
    run_gitlab_runner
else
    source /home/runner/scripts/github-runner.sh
    run_github_runner
fi

2. Поддержка отладчиков и программаторов

Контейнер поддерживает различные типы debug probe для прошивки и отладки микроконтроллеров:

Segger J-Link

Отладчик от Segger с поддержкой широкого спектра MCU.

Black Magic Probe (планируется)

Опенсорсный debug probe с нативной поддержкой GDB:

  • Работает как USB CDC устройство

  • Не требует дополнительных драйверов

OpenOCD и другие адаптеры (планируется)

Поддержка популярных программаторов через OpenOCD:

  • ST-Link

  • CMSIS-DAP совместимые устройства

  • FT2232-based адаптеры

Установка компонентов выполняется на этапе сборки образа и может быть гибко настроена через переменные окружения. Например, для J-Link:

RUN if [ "${ENABLE_JLINK}" = "true" ]; then \
        wget --post-data "accept_license_agreement=accepted" \
        https://www.segger.com/downloads/jlink/JLink_Linux_${JLINK_VERSION}_x86_64.deb \
        -O JLink.deb && \
        dpkg --force-depends -i JLink.deb; \
    fi

3. Поддержка интерфейсных адаптеров

SocketCAN для CAN-шин

Многие встраиваемые системы используют CAN-шину для коммуникации. Контейнер поддерживает различные CAN-адаптеры через SocketCAN — стандартную подсистему Linux для работы с CAN.

Важно: Для корректной работы SocketCAN контейнер использует network_mode: host, что обеспечивает прямой доступ к сетевым интерфейсам хост-системы, включая CAN-интерфейсы.

На данный момент реализована поддержка PEAK CAN адаптеров:

setup_socketcan() {
    local BAUDRATE=${PCAN_BAUDRATE:-125000}
    
    # Загрузка модулей ядра
    sudo modprobe can
    sudo modprobe can_raw
    sudo modprobe peak_usb
    
    # Настройка интерфейса
    CAN_INTERFACE=$(ip link show | grep -o "can[0-9]*" | head -n 1)
    sudo ip link set ${CAN_INTERFACE} type can bitrate ${BAUDRATE}
    sudo ip link set ${CAN_INTERFACE} up
}

После настройки CAN-интерфейс доступен для тестирования:

# Отправка CAN-сообщения
cansend can0 123#DEADBEEF

# Мониторинг CAN-шины
candump can0

Другие интерфейсы (планируется)

Архитектура контейнера позволяет легко добавлять поддержку других интерфейсов:

  • USB — прямое взаимодействие с USB-устройствами

  • UART/Serial — тестирование последовательных интерфейсов

  • SPI/I2C — работа с периферией через адаптеры (например, FT232H)

  • Ethernet — все сетевые устройства уже доступны благодаря режиму network_mode: host

Важно: Технически уже реализован доступ к различным типам оборудования:

  • Локальные устройства через монтирование /dev — любое оборудование, драйвер которого есть в пакетах ОС (см. раздел ниже)

  • Сетевые устройства через network_mode: host — все устройства, подключенные к сети хоста, напрямую доступны из контейнера

В случае с PEAK CAN потребовалась дополнительная настройка с загрузкой модуля ядра peak_usb и конфигурацией SocketCAN-интерфейса, что усложнило процесс. Для большинства других устройств (UART, USB-адаптеры, сетевое оборудование) достаточно прямого доступа без дополнительной настройки.

4. USB в привилегированном режиме

Для работы с USB-устройствами (J-Link, PEAK CAN) контейнер запускается в привилегированном режиме с монтированием /dev:

services:
  ci-runner:
    privileged: true
    volumes:
      - /dev:/dev:rw

Это обеспечивает:

  • Динамическое обнаружение подключенных USB-устройств

  • Доступ к USB без предварительной настройки

  • Поддержку горячего подключения устройств

5. Автоматическая регистрация раннеров

При первом запуске контейнер автоматически регистрируется в GitHub/GitLab:

Для GitHub Actions:

./config.sh \
    --url "${RUNNER_URL}" \
    --token "${REGISTRATION_TOKEN}" \
    --name "${RUNNER_NAME}" \
    --labels "${RUNNER_LABELS}" \
    --unattended \
    --replace

Для GitLab CI:

gitlab-runner register \
    --non-interactive \
    --url "${GITLAB_URL}" \
    --token "${GITLAB_REGISTRATION_TOKEN}" \
    --executor "${RUNNER_EXECUTOR}"

6. Персистентное хранилище

Рабочие директории раннеров сохраняются между перезапусками контейнера через Docker volumes:

volumes:
  runner-work:      # GitHub Actions workspace
  runner-builds:    # GitLab Runner builds
  gitlab-runner-config:  # GitLab конфигурация

Практическое применение

Пример CI-пайплайна для прошивки MCU

GitHub Actions:

name: Build and Flash Firmware

on: [push, pull_request]

jobs:
  build-and-flash:
    runs-on: [self-hosted, baremetal, jlink]
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Build firmware
        run: |
          make clean
          make all
      
      - name: Flash to MCU via J-Link
        run: |
          JLinkExe -device STM32F407VG -if SWD -speed 4000 \
            -CommandFile flash.jlink
      
      - name: Run hardware tests
        run: |
          ./tests/hardware_test.sh

GitLab CI:

stages:
  - build
  - flash
  - test

build_firmware:
  stage: build
  tags:
    - baremetal
    - jlink
  script:
    - make clean
    - make all
  artifacts:
    paths:
      - build/firmware.bin

flash_mcu:
  stage: flash
  tags:
    - baremetal
    - jlink
  script:
    - JLinkExe -device STM32F407VG -if SWD -speed 4000 
        -CommandFile flash.jlink
  dependencies:
    - build_firmware

hardware_test:
  stage: test
  tags:
    - baremetal
    - socketcan
  script:
    - ./tests/can_bus_test.sh

Тестирование CAN-коммуникации

#!/bin/bash
# can_bus_test.sh

# Проверка доступности CAN-интерфейса
if ! ip link show can0 &>/dev/null; then
    echo "CAN interface not available"
    exit 1
fi

# Отправка тестового сообщения
cansend can0 123#1122334455667788

# Ожидание ответа
timeout 5 candump can0 | grep "456#" || {
    echo "No response from CAN device"
    exit 1
}

echo "CAN communication test passed"

Преимущества подхода

  1. Автоматизация всего цикла — от сборки до прошивки реального железа

  2. Раннее обнаружение проблем — тесты на реальном оборудовании при каждом коммите

  3. Воспроизводимость — изолированное окружение в Docker

  4. Масштабируемость — легко добавить несколько раннеров для параллельного тестирования

  5. Гибкость — поддержка разных CI-платформ и конфигураций

Безопасность

Контейнер запускается от непривилегированного пользователя runner с ограниченным набором sudo-прав:

echo "runner ALL=(ALL) NOPASSWD: /usr/sbin/ip" >> /etc/sudoers.d/runner
echo "runner ALL=(ALL) NOPASSWD: /usr/sbin/modprobe" >> /etc/sudoers.d/runner

Это минимизирует риски при выполнении недоверенного кода в CI-пайплайнах.

⚠️ Важн��е аспекты безопасности

Privileged режим и доступ к /dev представляют серьёзные риски безопасности. Данное решение предназначено только для доверенного кода в контролируемом окружении.

Критические ограничения:

  • ❌ Не подходит для Pull Requests от внешних contributors

  • ❌ Не должен быть доступен для непроверенного кода

  • ❌ Не предназначен для публично доступных раннеров

Рекомендуемые меры защиты:

  • ✅ Protected Runners (только для main/protected branches)

  • ✅ Manual Approval для hardware-джоб

  • ✅ CODEOWNERS для ревью изменений в CI конфигурации

  • ✅ Ограничение раннера специальными тегами (tags: [baremetal, trusted-only])

  • ✅ Ведение audit logs запусков на hardware

Для production окружения рассматриваются дополнительные меры:

  • Изоляция сети (firewall, отдельная VLAN)

  • Монтирование только необходимых устройств вместо всего /dev

  • Использование capabilities вместо полного privileged режима (где возможно)

  • Отдельный раннер для untrusted кода без доступа к оборудованию

Планы развития

  • Pre-built образы на GHCR — готовые образы с полной конфигурацией для GitHub и GitLab платформ, публикуемые на GitHub Container Registry. Это позволит быстро начать работу без необходимости локальной сборки образа:

    • ghcr.io/baremetaltestlab/baremetal-ci-docker:github-latest — образ с GitHub Actions runner

    • ghcr.io/baremetaltestlab/baremetal-ci-docker:gitlab-latest — образ с GitLab Runner

  • Black Magic Probe — популярный опенсорсный debug probe

  • OpenOCD — универсальная поддержка различных программаторов (ST-Link, CMSIS-DAP и др.)

  • Дополнительные интерфейсные адаптеры — расширение возможностей тестирования периферии

  • Web-интерфейс мониторинга — визуализация состояния оборудования

Заключение

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

Примечание автора: Буду рад конструктивной критике, предложениям по улучшению архитектуры и обсуждению Docker best practices.


Ссылки:

Комментарии (0)