OS・実行環境

コンテナ(Docker)

アプリを隔離して動かす技術。エッジでも利用が広がる。

概要

コンテナとは、アプリケーションとその依存ライブラリ・設定ファイルをひとまとめにして、ホストOSとは隔離された環境で動かす技術だ。仮想マシン(VM)がハードウェアを仮想化するのとは異なり、コンテナはホストOSのカーネルを共有しながら、ユーザー空間だけを分離する。これにより、VMと比べてオーバーヘッドが少なく起動が速い。

Dockerは2013年に登場したコンテナランタイムで、コンテナ技術を一般に普及させたツールだ。Linuxカーネルのnamespace(プロセス・ネットワーク・ファイルシステムの隔離)とcgroups(CPU・メモリ等のリソース制限)を組み合わせることでコンテナを実現する。

組み込み・エッジコンピューティングの分野でも近年コンテナの採用が広がっている。Raspberry Pi、NVIDIA Jetson、SOMを搭載した産業用ゲートウェイなど、Linuxが動く組み込みデバイスであればDockerが動作し、クラウドで開発・テストしたアプリをそのままエッジに展開できる。YoctoBuildrootでフルカスタムのOSイメージを作る代わりに、ベースLinuxをシンプルに保ちアプリをコンテナで管理するアプローチが現実的な選択肢として定着してきた。

歴史・背景

コンテナの基礎技術はLinuxカーネルで段階的に整備された。2002年にnamespaceの一部(mount namespace)が、2006〜2008年にcgroupsが追加された。2013年にDockerがこれらを統合した使いやすいツールとして登場し、コンテナが爆発的に普及した。

2015年にはOpen Container Initiative(OCI)が設立され、コンテナイメージフォーマット(OCI Image Spec)とランタイム仕様(OCI Runtime Spec)が標準化された。これによりDockerだけでなくPodman、containerd、runCなど複数のランタイムが相互運用できる基盤が整った。

組み込み分野では、Balena(旧Resin.io)が2014年にDockerベースのエッジデバイス管理プラットフォームとして登場し、ARM向けコンテナのOTA(Over The Air)アップデートを実現した。その後、AWS IoT Greengrass v2、Azure IoT Edge、Google Cloud IoT Coreなど主要クラウドのエッジランタイムがいずれもコンテナベースのデプロイを採用している。

技術仕様

コンテナの構成要素

コンポーネント役割Linuxカーネル機能
プロセス隔離各コンテナが独立したPIDを持つPID namespace
ネットワーク隔離独立したネットワークスタックNet namespace
ファイルシステム隔離コンテナ専用のルートファイルシステムMount namespace + overlayfs
ユーザー隔離コンテナ内のroot≠ホストのrootUser namespace
リソース制限CPU・メモリ・I/Oの上限設定cgroups v1/v2
セキュリティシステムコールの制限seccomp, AppArmor/SELinux

コンテナイメージの階層構造

コンテナイメージはUnionFS(overlayfsなど)を使った読み取り専用レイヤーの積み重ねで構成される。

┌──────────────────────────┐ ← 書き込み可能レイヤー(コンテナ実行時)
├──────────────────────────┤ ← アプリレイヤー(COPY命令等)
├──────────────────────────┤ ← 依存ライブラリレイヤー(RUN apt install等)
├──────────────────────────┤ ← ベースイメージ(FROM ubuntu:22.04等)
└──────────────────────────┘ ← ホストOSのカーネル(共有)

各レイヤーはSHA256ハッシュで管理されており、同じレイヤーを複数のイメージで共有できる。これによりストレージ効率が高くなる。

ARM向けのマルチアーキテクチャ対応

コンテナイメージはCPUアーキテクチャに依存する。組み込みLinux環境(ARM32/ARM64)向けには対応アーキテクチャのイメージが必要だ。

# 利用可能なアーキテクチャ確認
docker buildx ls

# ARM64向けイメージのビルド
docker buildx build --platform linux/arm64 -t myapp:arm64 .

# マルチアーキテクチャイメージ(arm64/amd64両対応)のビルドとプッシュ
docker buildx build \
  --platform linux/amd64,linux/arm64,linux/arm/v7 \
  -t myregistry/myapp:latest \
  --push .

# 実行環境のアーキテクチャ確認
uname -m   # aarch64 = ARM64, armv7l = ARM32, x86_64 = x86-64

Dockerfileの基本(組み込みLinux向け)

# ARM64 Ubuntu 22.04をベースとした組み込みアプリ用コンテナ
FROM arm64v8/ubuntu:22.04

# 不要なパッケージをインストールしないようにする
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
    libgpiod2 \
    libi2c-dev \
    && rm -rf /var/lib/apt/lists/*

# アプリケーションバイナリをコピー
WORKDIR /app
COPY ./sensor_daemon /app/sensor_daemon
COPY ./config.json /app/config.json

# 最小権限で実行(rootではない)
RUN useradd -m -u 1000 appuser
USER appuser

# ヘルスチェック(コンテナが正常動作しているか確認)
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD /app/sensor_daemon --health-check || exit 1

ENTRYPOINT ["/app/sensor_daemon"]

docker-compose(マルチコンテナ構成)

エッジデバイスでよく使われる複数コンテナの構成例。

# docker-compose.yml(産業用IoTゲートウェイの例)
version: '3.8'

services:
  # センサーデータ収集コンテナ(デバイスアクセス権が必要)
  data-collector:
    image: myregistry/data-collector:arm64-latest
    privileged: false
    devices:
      - /dev/i2c-1:/dev/i2c-1    # I2Cバスへのアクセス
      - /dev/ttyUSB0:/dev/ttyUSB0  # UARTへのアクセス
    group_add:
      - dialout  # UARTアクセス用グループ
    volumes:
      - sensor-data:/data
    restart: always
    networks:
      - internal

  # データ処理・MQTT送信コンテナ
  mqtt-publisher:
    image: myregistry/mqtt-publisher:arm64-latest
    depends_on:
      - data-collector
    environment:
      - MQTT_BROKER=mqtt://broker.example.com:1883
      - DEVICE_ID=${DEVICE_ID}
    volumes:
      - sensor-data:/data:ro  # 読み取り専用マウント
    restart: always
    networks:
      - internal
      - external

  # ローカルウェブUI(設定・監視)
  webui:
    image: myregistry/webui:arm64-latest
    ports:
      - "8080:8080"
    restart: always
    networks:
      - external

volumes:
  sensor-data:

networks:
  internal:  # コンテナ間通信
  external:  # 外部ネットワーク
    driver: bridge

動作原理

namespace(名前空間)による隔離の詳細

Linuxのnamespaceはカーネル内でリソースの「見え方」を分離する仕組みだ。

# namespaceの確認(コンテナ内とホストで比較)
# ホスト側
ls -la /proc/self/ns/
# lrwxrwxrwx 1 root root 0 Jun 14 00:00 net -> 'net:[4026531992]'
# lrwxrwxrwx 1 root root 0 Jun 14 00:00 pid -> 'pid:[4026531836]'

# コンテナ内(異なるinode番号→別のnamespace)
# lrwxrwxrwx 1 root root 0 Jun 14 00:00 net -> 'net:[4026532456]'
# lrwxrwxrwx 1 root root 0 Jun 14 00:00 pid -> 'pid:[4026532399]'

cgroups(コントロールグループ)によるリソース制限

# コンテナのCPU・メモリ制限(docker runオプション)
docker run \
  --cpus="0.5" \          # CPUを0.5コア分に制限
  --memory="256m" \       # メモリを256MBに制限
  --memory-swap="256m" \  # スワップなし(組み込みではスワップ不使用が多い)
  --restart=always \      # 自動再起動
  myapp:arm64

# cgroupsの実際の設定ファイル(cgroups v2)
# /sys/fs/cgroup/docker/<container_id>/memory.max
# /sys/fs/cgroup/docker/<container_id>/cpu.max

コンテナとデバイスアクセス

組み込みLinuxでは、GPIOや各種バスデバイスへのアクセスが必要になる。

# GPIOへのアクセス(gpiodを使う方法)
docker run \
  --device /dev/gpiochip0 \   # GPIOデバイスファイル
  --group-add gpio \           # gpioグループに追加
  myapp:arm64

# SPI/I2Cデバイスへのアクセス
docker run \
  --device /dev/spidev0.0 \
  --device /dev/i2c-1 \
  myapp:arm64

# privilegedモード(全デバイスにアクセス可能、セキュリティ上非推奨)
docker run --privileged myapp:arm64
# コンテナ内からI2Cデバイスにアクセスする例(Python)
import smbus2

bus = smbus2.SMBus(1)  # /dev/i2c-1
BME280_ADDR = 0x76

# 温度データを読み取る
temp_raw = bus.read_word_data(BME280_ADDR, 0xFA)
print(f"Raw temperature: {temp_raw}")

用途・ユースケース

エッジAIアプリケーション

NVIDIA Jetsonや高性能SBCでのAI推論にコンテナが活用される。CUDAやTensorRTのライブラリ依存関係をコンテナにカプセル化することで、デプロイが大幅に簡素化される。

# NVIDIA Jetson向けの公式コンテナを使う例
# JetPackバージョンに合わせたコンテナが提供されている
docker run --runtime nvidia \
  nvcr.io/nvidia/l4t-pytorch:r35.3.1-pth2.0-py3 \
  python3 -c "import torch; print(torch.cuda.is_available())"

OTA(Over The Air)アップデート

Dockerイメージとしてアプリを管理すると、コンテナイメージのプルだけでアップデートが完結する。BalenaやAWS IoT Greengrassはこの仕組みを使ってエッジデバイスの遠隔管理を実現している。

# Balena CLIでのデプロイ例
balena push myfleet

# デバイス側では自動的に新しいコンテナイメージがプルされ起動される
# ロールバックも容易(古いイメージのタグを指定して戻す)

開発環境とエッジのパリティ

開発用PCのコンテナと本番エッジデバイスのコンテナが同じイメージを使うため「自分のPCでは動く」問題が解消される。クロスプラットフォームのマルチアーキテクチャビルドにより、x86_64の開発機でビルドしてarm64のデバイスにデプロイできる。

マイクロサービス分離

IoTゲートウェイで複数のプロトコル変換(Modbus→MQTT、CAN→HTTP等)を行う場合、機能ごとにコンテナを分割することで、個々の機能を独立してアップデート・再起動できる。

実装・開発のポイント

組み込みLinuxへのDockerインストール

# Raspberry Pi OS / Ubuntu (arm64) へのDockerインストール
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER

# armv7l(32ビットARM)の場合はraspbian向けの手順を確認
# Yoctoでのコンテナ対応:meta-virtualizationレイヤーを追加

イメージサイズの最小化

組み込みデバイスはストレージ容量・ネットワーク帯域が限られる。マルチステージビルドでイメージを小型化する。

# マルチステージビルドの例(C言語アプリケーション)
# ステージ1: ビルド環境(大きなイメージ)
FROM arm64v8/gcc:12 AS builder
WORKDIR /src
COPY . .
RUN gcc -O2 -o sensor_app main.c -lm

# ステージ2: 実行環境(最小イメージ)
FROM arm64v8/debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
    libgpiod2 \
    && rm -rf /var/lib/apt/lists/*

COPY --from=builder /src/sensor_app /usr/local/bin/sensor_app

# さらに小さくする場合はscratchやdistrolessを使う
# FROM gcr.io/distroless/base-debian12:arm64
ENTRYPOINT ["sensor_app"]

ヘルスチェックと自動復旧

# docker-compose.ymlでのヘルスチェックと再起動ポリシー
services:
  sensor-app:
    image: myapp:arm64
    healthcheck:
      test: ["CMD", "/usr/local/bin/sensor_app", "--health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 15s
    restart: unless-stopped  # 手動停止以外は自動再起動
    deploy:
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s

ストレージの永続化

コンテナは停止するとファイルシステムの変更が失われる(デフォルト)。設定ファイルや収集データはボリュームに永続化する。

# ボリュームによる永続化
docker volume create sensor-data
docker run -v sensor-data:/data myapp:arm64

# ホストディレクトリのバインドマウント(設定ファイル用)
docker run -v /etc/myapp/config.json:/app/config.json:ro myapp:arm64

# tmpfsマウント(再起動で消えるメモリ上のファイルシステム、一時データ用)
docker run --tmpfs /tmp:rw,noexec,nosuid,size=64m myapp:arm64

他技術との比較

比較項目Docker(コンテナ)仮想マシン(VM)ベアOSへの直接インストールYocto/Buildroot
起動時間秒以内数十秒〜分瞬時(プロセス起動)秒〜分(OS起動)
オーバーヘッド小(namespaceのみ)大(ハイパーバイザー)なしなし
隔離レベルプロセスレベルOSレベルなしなし
ポータビリティ高い高い低い低い
ストレージ消費中(レイヤー共有)最小
セキュリティ中(カーネル共有)高い設計依存設計依存
リアルタイム性制限あり制限あり最良良い
組み込み向け実績増加中少ない主流主流

YoctoBuildrootとの使い分け

YoctoやBuildrootはOSイメージ全体をカスタムビルドし、必要なパッケージだけを含んだ最小構成のLinuxを作れる。起動時間・メモリ・ストレージの最適化が極限まで可能だ。一方コンテナは既存のLinuxディストリビューション上で動作し、開発効率・デプロイ柔軟性が高い。最近はYoctoベースのホストOSにDockerを乗せてアプリをコンテナで管理するハイブリッド構成も普及している。

Embedded Linuxの要件

コンテナはLinuxカーネルのnamespaceとcgroupsに依存するため、RTOSには適用できない。FreeRTOSZephyrなどのRTOSベースのシステムではコンテナは使えず、Linuxが動くSBCSOMが前提となる。

関連用語

参考リンク