Продолжение серии статей о 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)
Почему тогда раннер в контейнере?
Это решение оптимизировано для быстрого развёртывания тестового стенда, когда:
Нет опыта с Ansible/конфигурацией раннеров — новичок может потратить день на настройку runner.toml, разбираться с правами доступа к устройствам, путями к сокету Docker и т.д.
Разовое развёртывание — нужно быстро поднять стенд на Raspberry Pi или другом одноплатнике для тестирования конкретного устройства. Не планируется использование для множества проектов.
Изоляция экспериментов — можно быстро удалить контейнер и пересоздать, не затрагивая хост-систему. Для экспериментов с разными конфигурациями это удобнее, чем править конфиги раннера.
Документированное окружение — весь стек описан в 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-контейнера, который объединяет в себе:
CI Runner — self-hosted раннер для GitHub Actions или GitLab CI
Отладчики и программаторы — поддержка различных debug probe (Segger J-Link, Black Magic Probe и др.)
Интерфейсные адаптеры — работа с периферийными шинами (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"
Преимущества подхода
Автоматизация всего цикла — от сборки до прошивки реального железа
Раннее обнаружение проблем — тесты на реальном оборудовании при каждом коммите
Воспроизводимость — изолированное окружение в Docker
Масштабируемость — легко добавить несколько раннеров для параллельного тестирования
Гибкость — поддержка разных 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 runnerghcr.io/baremetaltestlab/baremetal-ci-docker:gitlab-latest— образ с GitLab Runner
Black Magic Probe — популярный опенсорсный debug probe
OpenOCD — универсальная поддержка различных программаторов (ST-Link, CMSIS-DAP и др.)
Дополнительные интерфейсные адаптеры — расширение возможностей тестирования периферии
Web-интерфейс мониторинга — визуализация состояния оборудования
Заключение
Описанный подход позволяет применять практики непрерывной интеграции к разработке встраиваемых систем, где традиционные облачные CI-системы не имеют доступа к физическому оборудованию. Автоматизация тестирования на реальных устройствах сокращает время обнаружения проблем и упрощает поддержку проектов в долгосрочной перспективе.
Примечание автора: Буду рад конструктивной критике, предложениям по улучшению архитектуры и обсуждению Docker best practices.
Ссылки:
GitHub репозиторий: BareMetalTestLab/baremetal-ci-docker