開発・デバッグ・テスト

CI/CD

ビルド・テスト・配布を自動化する仕組み。

概要

CI/CD(Continuous Integration / Continuous Delivery:継続的インテグレーション / 継続的デリバリー)とは、ソフトウェア開発においてコードのビルド・テスト・デプロイを自動化する開発プラクティスおよびその仕組みの総称です。

CI(継続的インテグレーション):開発者がコードをリポジトリに頻繁にプッシュし、毎回自動でビルドとユニットテスト静的解析を実行して問題を早期発見するプラクティスです。

CD(継続的デリバリー/デプロイメント):CIが成功した後、自動または半自動でリリース可能な成果物(ファームウェアバイナリ等)を生成・配布するプラクティスです。

組み込み開発では、クロスコンパイル環境のDockerコンテナ化、ホストPC上でのユニットテスト自動実行、ファームウェアバイナリのアーティファクト管理、フラッシュサイズ監視などをCI/CDで自動化できます。

歴史・背景

1990年代後半:グレディ・ブーチが「継続的インテグレーション」の概念を提唱。しかしこの時代は手動ビルドが中心でした。

2001年:アジャイル宣言。継続的デリバリーがアジャイル開発の重要プラクティスとして位置づけられました。

2001年:CruiseControl(Javaプロジェクト向けCI)がオープンソースで登場。自動ビルドの先駆けとなりました。

2005年:Hudson(後のJenkins)がリリース。Webベースのダッシュボードで多くのチームに採用されました。

2011年:Travis CI・CircleCIがSaaSとして登場。設定ファイル(.travis.yml)によるコードベースのCI設定が普及しました。

2011年:Jez HumbleとDavid Farleyの著書「Continuous Delivery」が出版され、CDの実践方法が体系化されました。

2014年:GitLab CI/CDが統合リポジトリ+CI機能として普及。2019年にGitHub Actionsが登場し、GitHubリポジトリとCI/CDの統合が極めて容易になりました。

2020年代:DockerやKubernetesとCI/CDの統合が標準化。組み込み開発でもDockerコンテナ化されたクロスコンパイル環境によるCI/CDが普及しています。

技術仕様

主要なCI/CDプラットフォーム

プラットフォーム種別特徴組み込み対応
GitHub ActionsクラウドSaaSGitHub統合、無料枠あり、エコシステム豊富良好
GitLab CI/CDクラウド/セルフホストパイプライン可視化、セルフホスト可良好
Jenkinsセルフホスト高度にカスタマイズ可能、プラグイン豊富最も柔軟
CircleCIクラウドSaaS高速、Dockerネイティブ良好
Bitbucket PipelinesクラウドSaaSAtlassian製品との統合
Azure DevOpsクラウド/セルフホストMicrosoft製品との統合良好

組み込みCI/CDパイプラインの構成例

コードプッシュ(git push)


[ステージ1: ビルド]
  - クロスコンパイル(x86_64 → ARM)
  - ビルドサイズ確認(Flash/RAM使用量)
  - ビルドアーティファクト保存

        ▼ 成功
[ステージ2: 静的解析]
  - Cppcheck
  - MISRA C チェック(商用ツールの場合)
  - GCC警告ゼロ確認

        ▼ 成功
[ステージ3: ユニットテスト(ホスト)]
  - ホストGCCでネイティブビルド
  - Unity / Google Testでテスト実行
  - コードカバレッジ計測

        ▼ 成功
[ステージ4: 統合テスト(オプション)]
  - エミュレータ(QEMU等)でのテスト
  - HILテスト(自社サーバー)

        ▼ 成功(タグプッシュ時)
[ステージ5: リリース]
  - バージョンタグからリリースノート生成
  - ファームウェアバイナリ署名
  - GitHubリリースにバイナリ添付
  - OTA配布システムへアップロード

動作原理

GitHub Actions による組み込みCI実装

# .github/workflows/firmware-ci.yml
name: Firmware CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  TOOLCHAIN_VERSION: "10.3-2021.10"

jobs:
  # ジョブ1: クロスコンパイルビルド
  build:
    runs-on: ubuntu-22.04
    steps:
      - name: Checkout with submodules
        uses: actions/checkout@v4
        with:
          submodules: recursive

      - name: Install ARM toolchain
        run: |
          wget -q https://developer.arm.com/-/media/Files/downloads/gnu-rm/\
          ${TOOLCHAIN_VERSION}/gcc-arm-none-eabi-${TOOLCHAIN_VERSION}-x86_64-linux.tar.bz2
          tar xjf gcc-arm-none-eabi-*.tar.bz2
          echo "${PWD}/gcc-arm-none-eabi-${TOOLCHAIN_VERSION}/bin" >> $GITHUB_PATH

      - name: Build firmware
        run: |
          mkdir -p build && cd build
          cmake .. -DCMAKE_TOOLCHAIN_FILE=../toolchain/arm-cortex-m4.cmake \
                   -DCMAKE_BUILD_TYPE=Release
          make -j$(nproc)

      - name: Check binary size
        run: |
          arm-none-eabi-size build/firmware.elf
          # フラッシュ使用量が上限(512KB)の90%を超えたら警告
          FLASH_USED=$(arm-none-eabi-size build/firmware.elf | awk 'NR==2{print $1+$2}')
          FLASH_MAX=524288  # 512KB
          if [ "$FLASH_USED" -gt "$((FLASH_MAX * 90 / 100))" ]; then
            echo "::warning::Flash usage exceeds 90%: ${FLASH_USED}/${FLASH_MAX} bytes"
          fi

      - name: Upload firmware artifact
        uses: actions/upload-artifact@v4
        with:
          name: firmware-${{ github.sha }}
          path: |
            build/firmware.elf
            build/firmware.bin
            build/firmware.hex

  # ジョブ2: ユニットテスト(ホストPC上で実行)
  unit-test:
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4

      - name: Install dependencies
        run: sudo apt-get install -y gcc lcov

      - name: Build and run tests
        run: |
          mkdir -p build-test && cd build-test
          cmake .. -DCMAKE_BUILD_TYPE=Debug \
                   -DUNIT_TEST_BUILD=ON \
                   -DCMAKE_C_FLAGS="--coverage"
          make -j$(nproc)
          ctest --output-on-failure

      - name: Generate coverage report
        run: |
          lcov --capture --directory . --output-file coverage.info
          lcov --remove coverage.info '*/test/*' '*/unity/*' --output-file coverage.info
          genhtml coverage.info --output-directory coverage-html

      - name: Upload coverage report
        uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: coverage-html/

  # ジョブ3: 静的解析
  static-analysis:
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4

      - name: Install Cppcheck
        run: sudo apt-get install -y cppcheck

      - name: Run Cppcheck
        run: |
          cppcheck --enable=all \
            --error-exitcode=1 \
            --suppress=missingIncludeSystem \
            --std=c11 \
            --xml src/ 2> cppcheck-report.xml

      - name: Upload analysis report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: static-analysis-report
          path: cppcheck-report.xml

Dockerによるビルド環境の統一

# Dockerfile.build - 再現性のあるクロスコンパイル環境
FROM ubuntu:22.04

ARG TOOLCHAIN_VERSION=10.3-2021.10

RUN apt-get update && apt-get install -y \
    cmake ninja-build make \
    cppcheck lcov \
    python3 python3-pip \
    wget curl git \
    && rm -rf /var/lib/apt/lists/*

# ARM toolchain のインストール
RUN wget -q https://developer.arm.com/.../gcc-arm-none-eabi-${TOOLCHAIN_VERSION}-x86_64-linux.tar.bz2 \
    && tar xjf gcc-arm-none-eabi-*.tar.bz2 -C /opt \
    && rm gcc-arm-none-eabi-*.tar.bz2

ENV PATH="/opt/gcc-arm-none-eabi-${TOOLCHAIN_VERSION}/bin:${PATH}"

WORKDIR /workspace
# ローカルでもCI環境と同じビルドを実行
docker run --rm -v $(pwd):/workspace arm-build:latest \
    bash -c "cd /workspace && mkdir -p build && cd build && cmake .. && make"

フラッシュサイズの継続的な監視

# check_size.py - CI/CDでのサイズチェックスクリプト
import subprocess
import sys

LIMITS = {
    'text':  450 * 1024,   # .text + .rodata: 最大450KB(512KB中)
    'data':  100 * 1024,   # .data: 最大100KB(128KB中)
    'bss':   100 * 1024,   # .bss: 最大100KB
}

result = subprocess.run(
    ['arm-none-eabi-size', '--format=SysV', 'build/firmware.elf'],
    capture_output=True, text=True
)

for line in result.stdout.splitlines():
    for section, limit in LIMITS.items():
        if line.strip().startswith(f'.{section}'):
            size = int(line.split()[1])
            pct = size * 100 // limit
            print(f"{section}: {size:,} bytes ({pct}% of {limit//1024}KB)")
            if size > limit:
                print(f"ERROR: .{section} exceeds limit!", file=sys.stderr)
                sys.exit(1)
            elif pct > 90:
                print(f"WARNING: .{section} is at {pct}% of limit")

用途・ユースケース

自動リリースと OTA 配布

# タグプッシュ時に自動リリースとOTA配布
name: Release
on:
  push:
    tags:
      - 'v[0-9]+.[0-9]+.[0-9]+'

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive

      - name: Build release firmware
        run: make release

      - name: Sign firmware
        env:
          SIGNING_KEY: ${{ secrets.FIRMWARE_SIGNING_KEY }}
        run: |
          echo "$SIGNING_KEY" > /tmp/signing.key
          openssl dgst -sha256 -sign /tmp/signing.key \
            -out build/firmware.sig build/firmware.bin

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v1
        with:
          files: |
            build/firmware.bin
            build/firmware.hex
            build/firmware.sig
          generate_release_notes: true

      - name: Upload to OTA server
        env:
          OTA_API_KEY: ${{ secrets.OTA_API_KEY }}
        run: |
          curl -X POST https://ota.example.com/api/v1/upload \
            -H "Authorization: Bearer $OTA_API_KEY" \
            -F "firmware=@build/firmware.bin" \
            -F "version=${{ github.ref_name }}" \
            -F "signature=@build/firmware.sig"

複数ターゲット向けの並列ビルド

jobs:
  build-matrix:
    strategy:
      matrix:
        target:
          - { board: nucleo_f401re, mcu: STM32F401RE }
          - { board: nucleo_l476rg, mcu: STM32L476RG }
          - { board: esp32_devkit,  mcu: ESP32 }
    runs-on: ubuntu-latest
    name: Build for ${{ matrix.target.board }}
    steps:
      - uses: actions/checkout@v4
      - name: Build
        run: make BOARD=${{ matrix.target.board }}
      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: firmware-${{ matrix.target.board }}
          path: build/${{ matrix.target.board }}/firmware.bin

実装・開発のポイント

シークレット管理

# 署名キーやAPIキーはGitHub Secretsで管理
# → リポジトリ設定 → Secrets and variables → Actions で設定

# 使用例(YAMLで参照)
env:
  SIGNING_KEY: ${{ secrets.FIRMWARE_SIGNING_KEY }}
  OTA_API_KEY: ${{ secrets.OTA_SERVER_KEY }}

キャッシュによる高速化

- name: Cache ARM toolchain
  uses: actions/cache@v3
  with:
    path: /opt/gcc-arm-none-eabi
    key: arm-toolchain-${{ env.TOOLCHAIN_VERSION }}

- name: Cache CMake build directory
  uses: actions/cache@v3
  with:
    path: build
    key: cmake-${{ hashFiles('CMakeLists.txt', 'src/**/*.c') }}

セルフホストランナー(実機テスト用)

# 実機(ターゲットボード)に接続したサーバーでのテスト
jobs:
  hardware-test:
    runs-on: self-hosted  # 実機接続サーバーのランナーを指定
    steps:
      - uses: actions/checkout@v4
      - name: Flash and test
        run: |
          openocd -f interface/stlink.cfg -f target/stm32f4x.cfg \
            -c "program build/firmware.elf verify reset exit"
          python3 test/hardware_test.py --port /dev/ttyUSB0

他技術との比較

比較項目GitHub ActionsGitLab CI/CDJenkins
セットアップ難易度低(設定ファイルのみ)低〜中高(インストール設定)
セルフホスト可(GitHub Actions Runner)可(GitLab Runner)必須
無料枠2000分/月(publicは無料)400分/月完全無料
エコシステム最大(Marketplaceのアクション)豊富最大(プラグイン数)
組み込み対応良好良好最も柔軟
秘密情報管理SecretsCI/CD VariablesCredentials管理

バージョン管理との統合とDockerコンテナ化されたビルド環境が、現代の組み込みCI/CDの基盤です。

関連用語

参考リンク