Read OSS

アーキテクチャとナビゲーションガイド:apple/container のコード構成を理解する

中級

前提知識

  • コンテナの基本概念(イメージ、namespace など)への馴染み
  • Swift コードの基本的な読み書きができること
  • macOS のプロセスモデルとプロセス間通信の概念的な理解

アーキテクチャとナビゲーションガイド:apple/container のコード構成を理解する

macOS 上で動作するほとんどのコンテナランタイムは、すべてのコンテナを1つの共有 Linux VM の中で動かします。apple/container はそこから根本的に発想を変えています。コンテナごとに独立した軽量 VM を用意するのです。これは単なる実装の細部ではありません。プロセスモデルからネットワークスタック、stdio のファイルディスクリプタをプロセス間でどう受け渡すかに至るまで、コードベースのあらゆる設計判断を規定する考え方です。このプロジェクトがどう動くかを理解したいなら、まずなぜこうなっているかを理解するところから始めましょう。

この記事はいわば地図です。読み終えるころには、リポジトリのどこに何があるかが明確にわかり、ターミナルで打ち込む CLI と VM 内部で起動する Linux カーネルをつなぐプロセス境界の全体像が見えてくるはずです。

apple/container が実際にやること

container ツールを使うと、Apple Silicon Mac 上で OCI 互換の Linux コンテナをビルド・実行・管理できます。Docker や Podman で使うのと同じ標準 OCI イメージをそのまま利用できるため、特定ツールへの依存もありません。最大の特徴は、そのアイソレーションモデルにあります。

macOS 向けの従来型コンテナランタイムは、Lima や Docker Desktop のバックエンドのような仕組みで1つの Linux VM を立ち上げ、その共有 VM の中でコンテナを Linux プロセスとして実行します。apple/container はこれとは異なり、Apple の Virtualization.framework を使ってコンテナごとに専用の VM を作成します。プロジェクトの technical overview にその理由が明快に説明されています。各コンテナは VM レベルの完全なアイソレーションを持ち、共有 VM のように「将来必要になるかもしれないもの」をすべてマウントするのではなく、そのコンテナが必要とするデータだけをマウントします。それでも起動時間は共有 VM 方式と遜色ありません。

VM の作成と管理という重い処理は、コンパニオンライブラリである apple/containerization が担っています。container ツールは、そのライブラリの上に構築されたユーザー向けのアプリケーション層として、CLI の操作受付、プロセスのオーケストレーション、イメージ管理、ネットワーク、永続化を担当しています。

ヒント: containerization ライブラリは Package.swift でほぼすべてのターゲットの依存関係として登場します。VZVirtualMachineManagerContentStorePlatform といった型を見かけたら、それはこのリポジトリではなくそのライブラリ由来です。

4層アーキテクチャ

apple/container は単一のプロセスではありません。5つの独立した実行ファイルが協調して動く、4層構造のシステムです。

flowchart TD
    CLI["container CLI<br/>(user-facing)"]
    API["container-apiserver<br/>(central coordinator)"]
    RT["container-runtime-linux<br/>(one per container)"]
    NET["container-network-vmnet<br/>(one per network)"]
    IMG["container-core-images<br/>(singleton)"]
    VF["Virtualization.framework"]
    VM["vmnet.framework"]
    XPC_FW["XPC / launchd"]

    CLI -->|XPC| API
    API -->|XPC| RT
    API -->|XPC| NET
    API -->|XPC| IMG
    RT --> VF
    NET --> VM
    API --> XPC_FW

    style CLI fill:#4A90D9,color:#fff
    style API fill:#D94A4A,color:#fff
    style RT fill:#7B68EE,color:#fff
    style NET fill:#2ECC71,color:#fff
    style IMG fill:#F39C12,color:#fff

第1層:CLI。 ユーザーが実際に叩く container バイナリです。引数を解析して XPC メッセージを組み立て、API サーバーに送信します。ビジネスロジックはほとんど持っていません。

第2層:API サーバー。 container-apiserver は常駐する launch agent です。コンテナの状態管理、ネットワーク割り当てのオーケストレーション、launchd へのランタイムプラグイン登録、2つの DNS サーバーの運用など、システム全体のコーディネーターとして機能します。コンテナ・ネットワーク・ボリューム・プラグインに関する XPC ルートはすべてここに登録されています。

第3層:ヘルパーデーモン。 3つのヘルパープロセスが特定のリソースを専任で管理します。

  • container-runtime-linux — 実行中のコンテナごとに1インスタンス起動し、VM のライフサイクルを管理する
  • container-network-vmnet — 仮想ネットワークごとに1インスタンス起動し、vmnet.framework を使って IP アドレスを割り当てる
  • container-core-images — OCI イメージのストレージとレジストリとのやり取りを管理するシングルトン

第4層:macOS フレームワーク。 Virtualization.framework、vmnet.framework、XPC、launchd がOS レベルのプリミティブを提供します。

API サーバーの起動シーケンスは APIServer+Start.swift で確認できます。run() メソッドがプラグインローダー、コンテナサービス、ネットワークサービス、ヘルスチェックハンドラー、2つの DNS サーバーを初期化し、それらを TaskGroup を使って並行起動する様子がよくわかります。

Swift Package の構成とターゲットの依存関係

Package.swift には5つの実行ファイルターゲットと約20のライブラリターゲットが定義されています。実行ファイルターゲットは先述の各プロセスと1対1で対応しています。

実行ファイルターゲット バイナリ名 パス
container container Sources/CLI
container-apiserver container-apiserver Sources/Helpers/APIServer
container-runtime-linux container-runtime-linux Sources/Helpers/RuntimeLinux
container-network-vmnet container-network-vmnet Sources/Helpers/NetworkVmnet
container-core-images container-core-images Sources/Helpers/Images

ライブラリターゲットはクライアント/サーバーの分割パターンに従っています。各サービスにはサーバー側のロジックと、クライアント側の XPC ラッパーとで別々のターゲットが用意されています。

graph LR
    subgraph "ContainerAPIService"
        APIS["Server<br/>ContainerAPIService"]
        APIC["Client<br/>ContainerAPIClient"]
    end
    subgraph "ContainerSandboxService"
        SS["Server<br/>ContainerSandboxService"]
        SC["Client<br/>ContainerSandboxServiceClient"]
    end
    subgraph "ContainerNetworkService"
        NS["Server<br/>ContainerNetworkService"]
        NC["Client<br/>ContainerNetworkServiceClient"]
    end
    subgraph "ContainerImagesService"
        IS["Server<br/>ContainerImagesService"]
        IC["Client<br/>ContainerImagesServiceClient"]
    end

    APIC --> SC
    APIC --> IC
    APIS --> NC
    APIS --> SC

注目すべき点があります。API サーバーのサーバーターゲットが、ネットワークとサンドボックスのクライアントターゲットに依存しているのです。これは API サーバーがヘルパーデーモンと通信するときにはクライアントとして振る舞うからです。ここが理解の肝です。すべてのプロセス境界はクライアント/サーバーのペアとしてモデル化されています。

共有基盤ライブラリ(ContainerXPCContainerPluginContainerPersistenceContainerResource)はほぼすべてのターゲットから使われています。特に ContainerXPC は重要で、すべてのプロセス境界で使われるメッセージフォーマットとトランスポートを提供しています。

ディレクトリ構成の案内

ソースツリーを素早く把握するためのチートシートです。

ディレクトリ 役割
Sources/CLI/ @main のエントリーポイント。すぐに Application に処理を委譲する
Sources/ContainerCommands/ CLI コマンド定義の全体(run、build、exec など)
Sources/ContainerBuild/ container build 向けの gRPC ベースビルドシステム
Sources/ContainerResource/ ContainerConfigurationNetworkConfiguration などの共有データ型
Sources/ContainerPersistence/ インメモリインデックスを持つ JSON ファイルベースのエンティティストア
Sources/ContainerPlugin/ プラグインの探索、設定スキーマ、launchd への登録
Sources/ContainerXPC/ XPC メッセージ型、サーバー、クライアント
Sources/Services/ API・Sandbox・Network・Images サービスのクライアント/サーバーペア
Sources/Helpers/ 4つのヘルパー実行ファイルのエントリーポイント
Sources/DNSServer/ SwiftNIO 上に構築されたカスタム UDP DNS サーバー
Sources/TerminalProgress/ ターミナルのプログレスバー描画
Sources/ContainerOS/ OS レベルのユーティリティ
config/ 組み込みランタイムとネットワークヘルパー向けのプラグイン config.json ファイル

CLI のエントリーポイントである ContainerCLI.swift はわずか19行という驚くほどシンプルな作りで、処理のすべてを Application に委譲しています。ApplicationApplication.swift でコマンドツリー全体を定義しており、Swift Argument Parser の groupedSubcommands を使ってコマンドを Container・Image・Volume・Other のグループに整理しています。DefaultCommand はプラグインのディスパッチを担う隠しデフォルトサブコマンドとして機能しています。

通信基盤としての XPC

apple/container のすべてのプロセス境界では XPC を使っています。XPC は Mach メッセージを基盤とする Apple ネイティブのプロセス間通信フレームワークです。macOS 側のプロセス間では、ソケットも共有メモリも HTTP も使いません。(gRPC は使われていますが、それは VM 内部の Linux プロセスとの通信に限られます。詳しくは第6回の記事で扱います。)

なぜ XPC なのか。理由は3つあります。

  1. 権限の分離。 XPC 接続は呼び出し元プロセスを識別する audit token を持ちます。サーバー側はクライアントの effective user ID が同一であることを検証し、異なるユーザーからのアクセスを防ぎます。
  2. launchd との統合。 XPC サービスは Mach サービスとして launchd に登録され、オンデマンドでのプロセス起動とライフサイクル管理が launchd に委ねられます。
  3. ファイルディスクリプタの受け渡し。 XPC はプロセス境界をまたいでファイルディスクリプタを送れます。CLI からAPI サーバーを経由してコンテナランタイムまで stdio パイプを届けるために不可欠な機能です。

このプロジェクトでは、生の xpc_object_t C API の上にカスタムの抽象化レイヤーを構築しています。XPCServer はルートベースのメッセージディスパッチを提供し、XPCClient はコールバックベースの送信 API を Swift の async/await にブリッジし、XPCMessage は内部のディクショナリに対して型安全なアクセサーを提供しています。これらについては次の記事で詳しく見ていきます。

sequenceDiagram
    participant CLI as container CLI
    participant API as container-apiserver
    participant RT as container-runtime-linux

    CLI->>API: XPC: containerCreate
    API->>API: Persist ContainerSnapshot
    API->>API: Register runtime with launchd
    CLI->>API: XPC: containerBootstrap
    API->>RT: XPC: createEndpoint
    RT-->>API: XPC endpoint
    API->>RT: XPC: bootstrap (via endpoint)
    RT->>RT: Boot Linux VM
    RT-->>API: OK
    API-->>CLI: OK

ヒント: コードを読む際は、そのファイルがどのプロセスで動くかを常に意識しましょう。Sources/Services/ContainerAPIService/Server/ にあるコードは container-apiserver の中で動きます。Sources/Services/ContainerAPIService/Client/ にあるコードは、API サーバーと通信する必要のあるプロセス(たいていは CLI)の中で動きます。この区別を間違えると、コードの読解が一気に混乱します。

次のステップ

全体の地図が頭に入ったところで、次の記事では XPC 通信レイヤーを深掘りします。プロセス間の連携を可能にするカスタム抽象化の仕組みを詳しく見ていきましょう。具体的には、XPCServer がルートごとにメッセージをディスパッチする仕組みを解説します。さらに XPCClient が XPC のコールバックモデルをタイムアウト設定つきの async/await へブリッジする方法や、container-runtime-linux の「2サーバーセキュリティパターン」も掘り下げます。