🚀

DinD vs DooD: CI/CD環境での選択と集中

に公開

はじめに

最近、会社でCI/CDに関する問題が発生した経験がありました。
GitLab CI/CD + ECR + AWS ECR Credential Helperを使用する環境で、コンテナ内でAWS ECR Credential Helperを使うためのdocker設定ファイルやAWS ECR Credential Helperファイルが見つからないという問題でした。

イメージをビルドする際に、そのバイナリやdocker設定ファイルを適用していなかったので当然のことでした。

関連内容をリサーチした結果、GitLabではこの問題を解決する2つの方法が提案されていました。[1]

  1. Docker-in-Docker(DinD) 方式
  2. Docker Socket Binding(DooD, Docker-outside-of-Docker) 方式

実際、DooD方式についてはセキュリティリスクのため推奨されていませんでした。[2]

しかし、すでにsocket通信がgitlab-runnerに適用されている状況だったため、影響範囲が分からないまま方式を変更するのはリスクが高く、結局EC2コンテナ内にインストールしておいたAWS ECR Credential Helperのバイナリとdockerの設定ファイルをvolumeで共有する方法で、まずはCI/CDが正常に動作するように処理しました(DooD方式を使用)。

本記事では、この過程で最も苦労したDinDとDooDの構造分析を行ってみようと思います。

実は私は無条件でDinD方式だけを考えていたので、socket通信をしてもDinD構成になると思い込んでいて、理解するのに少し苦労しました。

基本概念の理解

Dockerの基本構造

まずDockerの基本構造から理解してみましょう。

Dockerイメージから生成されるコンテナは、1つの仮想OSとして完全に分離されたOS環境です。この環境とデータをやり取りするには、基本的にネットワークを直接開くか、volumeを使って外部データを直接マウントして連携させる必要があります。

では、このコンテナを生成する主体は何でしょうか?私たちがよく「dockerデーモン(dockerエンジン)」と呼ぶものがコンテナを生成します。
正確にはdockerデーモンごとに1つのsocketを持ち、そのsocketでコンテナを生成します。簡単に言えばCPUのような役割だと考えると良いでしょう。

DinD vs DooD: 核心的な違い

DinDとDooDは、このコンテナを生成する主体によって概念が分かれます。

単純に考えると、socket通信を使ってsocket同士を接続して使うかどうかの話になりますが、socket通信をするということは、コンテナ内でhost OSのdockerデーモンを使うという意味と同じなので、DinDとDooDはこのコンテナを生成する主体によって分かれる概念だと言えます。

DinDとDooDの詳細比較

DinD (Docker-in-Docker): 独立実行方式

DinD方式は、その主体が各dockerコンテナ内に存在します。

図のようにHost OSにインストールされているDocker Daemonが1つのsocketを持っています。
そしてそのsocketからコンテナを生成します。

新しく生成されたコンテナでもsocketを持つことになります。docker:[version]-dinddocker:dind-rootlessを使うと考えて良いでしょう。これらのイメージは内部にdocker daemonを持つイメージです。

このような環境は依然として独立した環境を維持しているため、生成するたびに必要な要素をすべて新たに入れる必要があります。コンテナ内でさらにコンテナを生成すると、階段状にそのコンテナの下位に直接接続されると考えられます。直列接続と考えると分かりやすいでしょう。(これは、ホストOS → Jobコンテナ → DinDサービスコンテナ内のDockerデーモン → DinDデーモンが生成する子コンテナ、というように、コマンド実行やコンテナ生成の主体が段階的に移っていく階層構造を指しています。)

どのように動作するのか?

  • Host OSにインストールされたDocker Daemonから生成されたJobコンテナ(例:Docker CLIがインストールされたイメージ)は、同時に実行されるサービスコンテナ(例:docker:dindイメージ)のDockerデーモンと通信します。
  • この通信は通常TCPで行われ(DOCKER_HOST=tcp://docker:2375DOCKER_HOST=tcp://docker:2376環境変数設定)、Jobコンテナはこの内部Dockerデーモンにコマンドを送ります。
  • すべてのDocker作業(ビルド、プッシュ、実行など)は、この内部の独立したDockerデーモンによって分離された環境で実行されます。

長所

  • 高いセキュリティと分離性:各Jobが独自のDockerデーモンを持つため、ホストシステムや他のJobから完全に分離されます。これがDinDの最大の魅力ポイントです!
  • バージョンの柔軟性:各Jobごとに必要なDockerデーモンのバージョンを独立して使えます。
  • 一貫した環境:CI/CD環境とローカル開発環境でDockerの動作方式を同じようにできます。

短所

  • 設定の複雑さ増加:DooDに比べて初期設定がやや複雑です(サービスコンテナ設定、環境変数設定など)。
  • パフォーマンスオーバーヘッド:各JobごとにDockerデーモンを実行するため、若干のパフォーマンスオーバーヘッドやリソース使用量の増加があります(特にストレージドライバ関連)。
  • キャッシュ管理:基本的に各DinD環境は独立しているため、Job間でビルドキャッシュを効率的に共有するには別途設定(例:S3バケットを使ったキャッシュ同期)が必要です。

DinDとprivilegedモード

DinDを使うときによく出てくるオプションがprivileged = trueです。これは何でしょうか?

privileged = trueはなぜ必要なのか?
Dockerデーモンはコンテナを生成・管理するためにLinuxカーネルの特定機能(例:cgroups、特定ファイルシステムのマウントなど)にアクセスする必要があります。通常のコンテナはセキュリティ上これらのアクセスが制限されていますが、privileged = trueに設定されたコンテナはこれらの制限なくホストのほぼすべてのデバイスにアクセスし、カーネル機能を使うことができます。

privileged = falseとRootless DinD
ただしprivileged = trueはコンテナに強力な権限を与えるため、DooDほどではありませんが依然としてセキュリティ面での考慮が必要です。そこで登場したのがRootless DinDdocker:dind-rootlessイメージ)です。この方式はユーザーネームスペース(user namespaces)などのLinuxカーネル機能を活用し、root権限なしでもコンテナ内でDockerデーモンを実行できるようにします。

DooD (Docker-out-of-Docker): Socket共有方式

DooD方式の場合、主体はhost OSのみに存在します。

どのように動作するのか?

DooD方式はCI/CD Jobを実行するコンテナがホストマシン(GitLab RunnerがインストールされたEC2インスタンスなど)のDockerデーモンを共有する方式です。これは主にDockerソケットファイル(unix:///var/run/docker.sock)をコンテナにボリュームとしてマウントすることで実現されます。

  • Jobコンテナ内でdocker builddocker pushなどのコマンドを実行すると、このコマンドはマウントされたソケットを通じてホストのDockerデーモンに伝達されます。
  • 実際のイメージビルドやコンテナ実行などの作業はホストのDockerデーモンが実行します。
  • Jobコンテナは単にDocker CLIコマンドを伝えるクライアントの役割だけをします。

このようにsocketが共有されている状況でvolumeを生成すると、結局hostのvolumeを各コンテナでマウントする形になります。一方、DinD環境でJobコンテナが内部のDockerデーモン(サービスコンテナ)を介してボリュームを作成・使用する場合、そのボリュームはDinDサービスコンテナの管理スコープ内に置かれ、主にDinDデーモンが起動するコンテナによって利用されます。これは、ボリュームがDinD環境という一段深い階層で管理されるイメージです。

長所

  • 簡単な設定:既存のDocker環境がホストにすでにあれば、ソケットをマウントするだけで設定が比較的簡単です。
  • ビルドキャッシュの共有が容易:ホストのDockerデーモンを共有するため、複数のCI/CD Job間でDockerビルドキャッシュが自然に共有され、ビルド速度を上げることができます。
  • リソース効率:各コンテナごとにDockerデーモンを立ち上げないため、リソース使用面で若干の利点があります。

短所

  • セキュリティ脆弱性最大の短所です。コンテナがホストのDockerデーモンに直接アクセスできるということは、実質的にホストに対するroot権限を持つのと同じ状況を作り出すことになります。悪意のあるコードがコンテナ内で実行されると、ホストシステム全体が危険にさらされる可能性があります!
  • バージョン依存性:Jobコンテナ内のDocker CLIバージョンとホストDockerデーモンバージョン間で互換性の問題が発生することがあります。
  • 環境分離の不足:すべてのJobが同じDockerデーモンを使うため、1つのJobの作業が他のJobに予期せぬ影響を与えることがあります(例:ボリュームの衝突、ネットワーク設定の干渉など)。

そのため、実際DooDで処理するのは設定的には楽です。単にhost OSと接続してvolume指定だけしておけば、すべてのコンテナで内容が共有されます。しかし、気づいたかもしれませんが、この方式はそもそもdockerの設計思想と反します。コンテナの最も基本的な概念である独立した環境の維持が崩れる状況になってしまいます。

比較まとめ表

区分 DinD DooD
Dockerデーモンの位置 コンテナ内 ホストOS
接続方式 直列接続 並列接続
セキュリティ 高い(分離) 低い(ホストアクセス可能)
設定の複雑さ 複雑 簡単
リソース使用量 多い 少ない
キャッシュ共有 難しい 簡単
環境分離 完全分離 部分分離
推奨度 高い(GitLab推奨) 低い(セキュリティリスク)

※表中の「接続方式」について補足します。DinDの「直列接続」とは、前述の通り、ホストからJobコンテナ、さらに内部のDinDデーモンへと段階的に処理が連携される階層的なイメージを指します。一方、DooDの「並列接続」とは、複数のJobコンテナがホストの単一Dockerデーモンにそれぞれ独立してアクセスし、ホストデーモンがこれらのリクエストを並行して処理する様子や、生成されたコンテナ群がホストから見て同列に管理される状態を指しています。

実践活用:GitLab CI/CD設定

それではGitLab CI/CDでこの2つの方式をどのように設定するのか、gitlab-runner/config.tomlファイルの例を通じて簡単に見てみましょう。

DooD方式設定例

[[runners]]
  name = "dood-runner"
  url = "YOUR_GITLAB_URL"
  token = "RUNNER_TOKEN"
  executor = "docker"
  [runners.docker]
    tls_verify = false
    image = "docker:latest" # Docker CLIが含まれるイメージ
    privileged = false      # DooDはJobコンテナにprivilegedが不要
    # DockerソケットをJobコンテナにマウント
    volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]
    shm_size = 0

ポイントはvolumes = ["/var/run/docker.sock:/var/run/docker.sock"]部分です。このようにホストのDockerソケットを共有します。

DinD方式設定例

[[runners]]
  name = "dind-runner"
  url = "YOUR_GITLAB_URL"
  token = "RUNNER_TOKEN"
  executor = "docker"
  [runners.docker]
    tls_verify = false
    # 下で作るカスタムイメージを使うか、Docker CLIが含まれる一般的なイメージを使えます。
    image = "your-registry/my-ci-image-with-ecr-helper:latest" # または"docker:latest"
    # docker:dindサービスコンテナはprivilegedモードが必要な場合があります。
    # rootless dindを使うならfalseに設定可能です。
    privileged = true
    services = ["docker:dind"] # または"docker:dind-rootless"
    volumes = ["/cache"]
    shm_size = 0
    # DinDサービスと通信するための環境変数(自動で設定されることもあります)
    # DOCKER_HOST = "tcp://docker:2375"(通常のdind)
    # DOCKER_HOST = "tcp://docker:2376"(TLS dind)
    # DOCKER_TLS_CERTDIR = "/certs"(TLS使用時)

DinD方式ではservices = ["docker:dind"](またはdocker:dind-rootless)で別のDockerデーモンを実行します。JobコンテナはDOCKER_HOST環境変数を通じてこのサービスコンテナのDockerデーモンと通信します。

AWS ECR Credential Helper適用事例

理解を助けるために、私が経験した問題状況を当てはめてみます。

問題状況の分析

まずAWS ECR Credential Helperを使うには、Docker設定ファイル(/root/.docker/config.json)にそのHelperが登録されている必要があり、Helper実行ファイルにもアクセスできなければなりません。

ECR Credential Helperを使うための一般的な.docker/config.jsonファイル内容は次の通りです:

{
  "credHelpers": {
    "public.ecr.aws": "ecr-login",
    "YOUR_AWS_ACCOUNT_ID.dkr.ecr.YOUR_AWS_REGION.amazonaws.com": "ecr-login"
  }
}

方式別の解決策

DooD方式:ホスト設定の活用

DooDはホストのDockerデーモンを直接使うため、最も簡単な方法はホストマシンにAWS CLIとECR Credential Helperをきちんと設定しておくことです。

より明示的にJobコンテナにこの設定を提供したい場合は、GitLab Runnerのconfig.tomlでホストのDocker設定ディレクトリとHelperバイナリをボリュームマウントできます。

# gitlab-runner/config.toml例(DooD - ボリュームマウント)
[runners.docker]
  volumes = [
    "/var/run/docker.sock:/var/run/docker.sock",
    "/root/.docker/config.json:/root/.docker/config.json:ro", # ホストのconfig.json
    "/usr/local/bin/docker-credential-ecr-login:/usr/local/bin/docker-credential-ecr-login:ro" # Helperバイナリ
  ]

DinD方式:カスタムイメージ構成

DinD環境は完全に分離されているため、JobコンテナまたはDinDサービスコンテナ内にECR Credential Helperをインストールし、config.jsonファイルを適切な場所に作成する必要があります。

最も推奨される方法は、必要なすべてのツールと設定を含んだカスタムDockerイメージを事前に作成して使うことです。

AWS ECR Credential Helper用語整理

実際に実装を進める前に用語を明確にしておきます:

  • Amazon ECR Credential Helper = AWSが提供するDocker credential helperプロジェクトの公式名称
  • docker-credential-ecr-login = 実際の実行ファイル名およびパッケージ名
  • インストール後、Dockerが認識するcredential helper名はecr-loginです

つまり、Dockerのconfig.jsonでは次のように使います:

{
  "credHelpers": {
    "123456789012.dkr.ecr.us-west-2.amazonaws.com": "ecr-login"
  }
}

ここで重要なのは、AWS CLI v2をインストールしてもECR credential helperは自動で含まれないということです。別途インストールが必要です。

カスタムDockerイメージの作成

# Dockerfile

# Stage 1: Builder stage - AWS CLIインストール担当
# 推奨:'latest'ではなく特定バージョンを明記(例:alpine:3.19)
FROM alpine:3.22 AS builder

# AWS CLIインストールに必要なパッケージをインストール
RUN apk add --no-cache curl unzip bash

# AWS CLI v2ダウンロード&インストール
RUN echo "AWS CLI v2インストール中(Builder Stage)..." && \
    curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && \
    unzip awscliv2.zip && \
    ./aws/install && \
    rm -rf awscliv2.zip aws && \
    echo "AWS CLI v2インストール完了(Builder Stage)。"

# Stage 2: Final stage - 実際のCI Jobで使うイメージ
FROM alpine:3.22

# Docker CLIおよびCI Jobに必要なツールをインストール
RUN apk add --no-cache docker-cli git

# AWS ECR Credential Helperインストール
RUN echo "AWS ECR Credential Helperインストール中..." && \
    apk update && \
    apk add --no-cache docker-credential-ecr-login && \
    echo "AWS ECR Credential Helperインストール完了。"

# apkでインストールできない場合の直接ダウンロード方法(コメントアウト)
# RUN curl -LO https://amazon-ecr-credential-helper-releases.s3.us-east-2.amazonaws.com/0.9.1/linux-amd64/docker-credential-ecr-login && \
#     chmod +x docker-credential-ecr-login && \
#     mv docker-credential-ecr-login /usr/local/bin/

# Builder stageでインストールしたAWS CLI関連ファイルをFinal stageにコピー
COPY --from=builder /usr/local/aws-cli /usr/local/aws-cli
COPY --from=builder /usr/local/bin/aws /usr/local/bin/aws
COPY --from=builder /usr/local/bin/aws_completer /usr/local/bin/aws_completer

# Docker設定用ディレクトリ作成(rootユーザーで実行する場合)
RUN mkdir -p /root/.docker

# 事前に用意したDocker設定ファイル(config.json)をイメージ内にコピー
COPY config.json /root/.docker/config.json

# インストール確認
RUN echo "インストール確認中..." && \
    aws --version && \
    docker --version && \
    docker-credential-ecr-login version && \
    echo "すべてのツールのインストール完了!"

# 作業ディレクトリ設定
WORKDIR /app

GitLab CI/CDパイプライン例

# .gitlab-ci.yml
build_and_push_ecr_job:
  # カスタムイメージをCI Jobの基本イメージとして指定
  image: your-registry/my-ci-image-with-ecr-helper:latest
  services:
    - name: docker:dind # Dockerビルド/プッシュのためDinDサービスを使用
      alias: docker
  variables:
    DOCKER_HOST: tcp://docker:2375 # DinDサービスと通信するホスト
    DOCKER_TLS_CERTDIR: "" # TLSを使わない場合は空文字列
  before_script:
    # AWS認証情報設定(OIDC連携推奨)
    - echo "Configuring AWS credentials..."
    - aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID
    - aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY
    - aws configure set default.region $AWS_DEFAULT_REGION
    - echo "Verifying Docker setup..."
    - docker info # DinDサービス接続&Credential Helper認識確認
  script:
    - echo "Building Docker image..."
    - docker build -t my-application:latest .
    - echo "Pushing Docker image to ECR..."
    # Credential Helperのおかげで'docker login'コマンドなしでECRに直接push可能
    - docker push YOUR_AWS_ACCOUNT_ID.dkr.ecr.YOUR_AWS_REGION.amazonaws.com/my-application:latest
    - echo "Image pushed successfully!"

まとめ

DinDとDooDの構造的な違いを分析した結果、以下のような重要な知見が得られました:

  1. 構造的な本質の違い

    • DinDは各コンテナが独自のDockerデーモンを持ち、完全に分離された環境を構築
    • DooDはホストのDockerデーモンを共有하는 방식で、コンテナの独立性が損なわれる
  2. アーキテクチャの影響

    • DinDの直列接続構造は、セキュリティと分離性を高めるが、リソース使用量が増加
    • DooDの並列接続構造は、リソース効率は良いが、セキュリティリスクが高い
  3. 実装における考慮点

    • DinDは初期設定が複雑だが、長期的なセキュリティと安定性を提供
    • DooDは設定が簡単だが、セキュリティリスクを常に考慮する必要がある

推奨事項:可能であればDinD方式の使用を推奨します。特にRootless DinDを活用すればセキュリティをさらに高めることができます。

ただし、既存環境がDooDで構成されていてすぐに変更が難しい場合は、少なくとも次のようなセキュリティ対策を取るのが良いと思います:

  • Runnerが実行されるホストを分離された環境で運用
  • 信頼できるコードのみCI/CDで実行
  • 定期的なセキュリティチェックとモニタリング

この分析を通じて、DinDとDooDの選択は単なる設定の違いではなく、アーキテクチャの根本的な違いに基づく重要な決定であることが分かりました。セキュリティ、リソース効率、設定の複雑さなど、様々な要素を考慮した上で、プロジェクトの要件に最適な方式を選択することが重要です。


脚注
  1. https://docs.gitlab.com/17.11/ci/docker/using_docker_build ↩︎

  2. https://docs.gitlab.com/17.11/ci/docker/using_docker_build/#known-issues-with-docker-socket-binding ↩︎

Discussion