>100 Views
December 12, 24
スライド概要
Dockerfileは、アプリケーション構成を保守可能な形で記録できる強力なツールです
しかし、その真価を十分に発揮するためには、Dockerfileを書く人の理解がかかせません。
本スライドは、Dockerfileを利用してアプリケーションをデプロイするアプリケーション開発者を対象に、優れたDockerfileを書くための原則として「決定論性」「ビルドキャッシュ」「イメージの最小化」の3つを提案し、それぞれの原則に対する具体的なアプローチを解説します。
開発を加速する Dockerfileの考え方 Dec. 12 2024 - Masaki Hara © 2024 Wantedly, Inc.
なぜやるのか ● Dockerfileは、アプリケーション構成を保守可能な形で記録 できる強力なツールです ● しかし、その真価を十分に発揮するためには、Dockerfileを 書く人の理解がかかせません。 ● ウォンテッドリーではなるべくアプリ開発者が自分で構成を管 理する仕組みのため、皆さんにも知ってほしい。 © 2024 Wantedly, Inc.
コンテナの基本 © 2024 Wantedly, Inc.
コンテナのワークフロー ベースイメージとして使用 Dockerfile OCI image ビルド コンテナの状態が入った、 ディスクイメージのようなもの イメージを作るための構 成手順 container ビルド用の 一時的なコンテナ © 2024 Wantedly, Inc. container container container コンテナ = 仮想コンピューター
システムコール ● システムコール: アプリケーションがOSカーネルの機能を利用 するために呼び出すAPIのこと ○ わかりやすさのために「API」と呼んだが、正確にはABI部分と考えたほうがいい ● システムコールから先は、OSが処理する ○ たとえばファイルを開く・読むといった指示はシステムコールを通して実行されるが、そこ から先でどのような実装になっているかはアプリケーションは知らない © 2024 Wantedly, Inc.
コンテナ ● コンテナ: システムコールのレイヤーでコンピューターを仮想化 する仕組み ○ たとえば、コンテナ内で /etc/foo を読む命令を発行しても、実際には /var/container1/etc/foo のような別のファイルを返しているかもしれない ● アプリケーションの仮想化として「ちょうど良い」 ○ ○ ○ ライブラリの状態などはアプリケーションの動作に影響を与えるので再現する しかし、カーネルのバージョンやドライバ、ファイルシステム実装まで再現する必要はない CPUやSSDごと再現するより柔軟で効率的 © 2024 Wantedly, Inc.
コンテナ ● 仮想化することで、コンピューター作り放題になる ○ ○ 構成を切り替えたければ、新しいコンピューターを作って新しい構成をインストールし、古 いコンピューターは消してしまえばよい。 リソース(CPU, メモリ)のやりくりがしたければ、ホストとなるコンピューターに仮想化され たコンピューターをいくつか作ってリソースを分配すればよい。 ● コンテナの仕組みで仮想化されたコンピューター自体も「コン テナ」と呼ぶ © 2024 Wantedly, Inc.
コンテナイメージ ● コンテナイメージ = コンテナの電源を切った状態の補助記憶 を取り出したもの ○ 主記憶 (メモリ) の状態は持たない ● これを複製して起動すれば、その状態から再開できる → コンテナを立ち上げるたびにセットアップするのではなく、 セットアップ済みのコンテナを複製して起動 © 2024 Wantedly, Inc.
レイヤー ● ファイルシステムの状態を差分で表現 ○ 実行時はoverlayFSなどのプログラムを使ってunion mountingする ● 差分はチェーン状に繋げられる ○ ○ 永続データ構造の一種であり、gitやブロックチェーンの仲間 他のバージョンとデータを部分的に共有できる利点がある Layer 3-A Layer 1 © 2024 Wantedly, Inc. Layer 2 Layer 2との差分のみを持つ Layer 1との差分のみを持つ Layer 3-B
DockerとOCI ● コンテナイメージとランタイムはOCIで標準化 ○ ○ コンテナランタイム: イメージをもとにコンテナを作成して実行する処理 現在のDockerはruncというランタイムを同梱している ● DockerfileはDocker独自 ○ ○ Podman/Buildahなど、Dockerfileに対応した他ツールもある イメージは必ずDockerfileから作らなければいけないわけではない ■ とはいえDockerfile(Containerfile)が事実上の標準と考えてよさそう © 2024 Wantedly, Inc.
3つの指針 © 2024 Wantedly, Inc.
3つの指針 Dockerfileを書くときは、3つの目標の最大化を目指す ● ビルドを決定論的にする ● ビルドを効率化する (←キャッシュ) ● 最終イメージを小さくする (→実行の効率化) © 2024 Wantedly, Inc.
決定論 © 2024 Wantedly, Inc.
決定論 ● 決定論 = この世の出来事は初期状態からの帰結としてすで に決まっているという考え方のこと ● 転じて、コンピューターの世界では、初期パラメーターだけで 動作が一意に決まる場合を意味する。 ○ ○ 決定論的 = 決定性 = deterministic 非決定論的 = 非決定性 = non-deterministic © 2024 Wantedly, Inc.
非決定論的な振舞い 非決定論的な振舞いは以下のような形で持ち込まれる: ● 現在時刻 ● 乱数 ● マルチスレッド処理のスケジュールの不確定性 ● OSカーネルやハードウェアの特性 ● ネットワークの外部にあるリソースの状態 ○ たとえばパッケージレジストリの状態など © 2024 Wantedly, Inc.
決定論が重要な理由 ● 決定論とは、結局のところ、開発者の制御下にあるかどうかと いうこと ○ ○ 本来の決定論とは立場が逆、神の視点で考えている 開発者が制御できるものを決定論的と呼んでいるにすぎない ● アプリケーションの動作が開発者の制御下にあったほうがい いのは言うまでもない ○ ○ 問題が発生したときに、開発者が明示的に行った変更のどれかを巻き戻せばいい 問題の原因も、開発者が行った変更の中から探せばいい © 2024 Wantedly, Inc.
同一性 決定論的かどうかで重要なのは、結果の同一性をどう定義する かということ。 ● 最終的にアプリケーションが望ましい動作をするのが目的 ○ ○ たとえば、ルート証明書が入ったca-certificatesのバージョンを固定すればアプリケー ションの挙動は安定するかというと、むしろ逆効果になる場合もある なぜなら、通信相手の証明書の更新に追従できなければ動作が壊れるから 逆に、システム上の動作が変わっても最終的な動作が同じならば実用的には困らない © 2024 Wantedly, Inc.
Dockerfileと決定論性 ● Dockerfileは、ビルドがある程度決定論的になるように作ら れている ● しかし、最後は開発者の意思に任されている ○ ○ ○ 特にネットワークアクセスは監視されていないため、常に最新の情報を取得する動作にす ることもできる (むしろデフォルトではそのような動作になる) たとえば、base imageとして ruby:latest を使うか、 ruby:3.4.0 を使うか、 ruby:3.4.0-bookworm を使うかは開発者に委ねられている 挙動を固定する害のほうが大きければ、あえて最新の情報を取るという判断も可能な仕 組みになっている。 © 2024 Wantedly, Inc.
アドバイス ● ここからは個人の意見 ○ ○ 言語ごとのパッケージマネージャー内のパッケージバージョンは固定したほうがいい。こ れらはアプリケーションの挙動への影響が大きい。 Debianなどのディストリのバージョンは、経験則としては固定せずに最新を参照してよい と思う。ビルドが壊れることはあるが本番で派手に壊れた事例は記憶にない。各パッケー ジのバージョンについても同様。 © 2024 Wantedly, Inc.
ビルドの効率化 © 2024 Wantedly, Inc.
キャッシュ ● 良いDockerfileは上手にキャッシュする ● キャッシュ以外の高速化も大事だが、ここでは触れない ○ ○ 無駄な処理をしない、効率的なアルゴリズムを使う、高速な言語で書かれたツールに置き 換えるなど これらはDocker特有の注意点が必要ないため © 2024 Wantedly, Inc.
キャッシュの定式化 ● キャッシュとは、計算結果を記憶しておいて、次に同じ計算が 来たときに再利用すること ○ ただし、ネットワークからの取得などもここでの「計算」に含まれる ● y = f(x) のfとxが過去と同じならキャッシュが使える ● 計算が決定論的であることを期待している ○ fが非決定論的であると、キャッシュを使うことで結果がより予測不可能になってしまう。 © 2024 Wantedly, Inc.
キャッシュの粒度 Dockerには、粒度の異なる2つのキャッシュがある ● レイヤーキャッシュは、コマンドを1つの計算とみなしたキャッ シュのために使える。 ● キャッシュマウントは、コマンドよりも細かい単位での計算を キャッシュするために使える。 © 2024 Wantedly, Inc.
キャッシュのコスト キャッシュにもコストがある ● キャッシュの取得にもネットワークコストがかかる ○ ○ 特に、CI環境では毎回取得することになるので注意が必要 パッケージレジストリからダウンロードする処理のキャッシュなどは利点が少なかったり、 デメリットが大きくなってしまう場合もある ● キャッシュの保管にもストレージコストがかかる ○ ストレージ上限にヒットしてビルドできなくなったり、別の有益なキャッシュが追い出されて しまうリスクも。 © 2024 Wantedly, Inc.
ビルドの効率化: レイヤーキャッシュ © 2024 Wantedly, Inc.
レイヤーキャッシュ Dockerfileから作られたレイヤーには、コマンド情報が記録され ている Layer 3-A: Layer 2から make で作った Layer 1 © 2024 Wantedly, Inc. Layer 2: Layer 1から COPY go.mod . で作った
レイヤーキャッシュ 次のビルド時には、同じコマンドが記録されたレイヤーがあれば それを再利用 用 再利 再利用 Layer 3-A: Layer 2から make で作った Layer 1 © 2024 Wantedly, Inc. Layer 2: Layer 1から COPY go.mod . で作った
レイヤーキャッシュ コマンドが異なる場合は、新しいレイヤーを作る Layer 3-A: 再利用 Layer 2から make で作った Layer 1 Layer 2: Layer 1から COPY go.mod . で作った Layer 3-B: 新規 作成 © 2024 Wantedly, Inc. Layer 2から make server で 作った
レイヤーキャッシュ: キャッシュキー ● レイヤーのコマンド名に記録される ○ ○ ○ ○ RUNのコマンドや主要なオプション COPYの対象ファイル群とその内容 (をハッシュ化したもの?) FROMの元イメージ ENVの内容 ● レイヤーのコマンド名に記録されない ○ ○ ○ ネットワークアクセスの通信内容 secret mountの内容 cache mountの読み取り時点での内容 © 2024 Wantedly, Inc.
キャッシュと決定論 ● Dockerfileは、同じ結果が得られるように書く ○ 「同じ結果」をどこまで求めるかは、書く人の責任で決める余地がある ● 異なる結果になる操作には、異なるキャッシュキーが割り当て られるように書く ○ ○ ○ たとえば、ネットワークから最新情報を取得するような操作は、キャッシュキーのユニーク 性を毀損しうる ただしこれも、「同じ結果」をどこまで求めるか次第 何がキャッシュキーになるのか、何をもって同じ結果とするのかを意識しながら書く必要 がある © 2024 Wantedly, Inc.
ビルドの効率化: キャッシュマウント © 2024 Wantedly, Inc.
キャッシュマウント ● キャッシュ用の特別なファイルシステムをマウント ○ たとえば /var/cache にマウントしたら、 /var/cache 以下はDockerのキャッシュ ファイルシステムに管理される ● その中身はレイヤーに記録されない ○ キャッシュの中身がある場合でもない場合でも、同じ結果になるようにする必要がある ● キャッシュの中身はローカルで保存される ○ ○ 別のビルドで同じマウントポイントを作ると、前の状態が復元される コンピューターをまたいだ共有の仕組みは今のところない © 2024 Wantedly, Inc.
キャッシュマウントのメリット キャッシュマウントのメリット ● 過去の同じコマンドの結果を部分的に再利用できる ○ レイヤーキャッシュでは、1つのコマンドの結果を完全に再利用するか、全く再利用しない かのどちらかだった ● キャッシュはローカルのみ ○ ○ ネットワーク由来のキャッシュの場合、ローカルでは有益だがCIでは有害な場合もあるた め、これが有利に働くケースもある これは現時点での話 © 2024 Wantedly, Inc.
キャッシュマウントのデメリット キャッシュマウントのデメリット ● キャッシュ処理は各コマンドに大いに委ねられている ○ レイヤーキャッシュであっても正当性はDockerfileを書く人に委ねられているが、それよ りもさらに自由度が高く間違いやすい ● キャッシュはローカルのみ ○ CIでは今のところ役に立たない (現時点、独自にツールを組まない前提の話) ● キャッシュの肥大化 ○ 良いGC手法がない (現時点でローカルのみである理由のひとつと考えられる) © 2024 Wantedly, Inc.
キャッシュマウントの自由度 ● キャッシュマウントは、キャッシュに使える自由なストレージを 提供するだけ ● キャッシュ処理は個々のツールが正しく実装する必要がある ○ ○ ○ y = f(x) に対して、過去と同じfとxが使われたときはキャッシュを利用する。 普通、fとxをファイル名にして置いておくことが多い 一般的には、Docker専用に組まれてなくてもだいたい上手くいくが、キャッシュキーに反 映されないパラメーターがないかは注意が必要 ■ 処理系バージョンや CPUアーキテクチャなど © 2024 Wantedly, Inc.
キャッシュマウントの例 キャッシュマウントを使う例 ● ネットワークキャッシュ ○ ○ aptのダウンロードディレクトリ go mod や Cargo のダウンロードディレクトリ ● ビルドキャッシュ ○ ○ node_modules/.cache 以下にWebpack等が生成するキャッシュ Cargoの target/ 以下 © 2024 Wantedly, Inc.
キャッシュマウントのGC ● 必要ないキャッシュは消す必要がある ● これはレイヤーキャッシュ・キャッシュマウントの両方に当ては まるが、キャッシュマウントのほうが難しい ○ ファイルの利用時刻は正確に記録されていると限らない ● キャッシュマウントを独自ツールで永続化するなら、このあたり を考慮する必要がある ○ キャッシュマウントを使ってビルドした後の状態をさらに永続化しないほうが賢明かも © 2024 Wantedly, Inc.
最終イメージを小さくする © 2024 Wantedly, Inc.
最終イメージ 最終イメージは小さいほうが望ましい。 これは2つの視点がある ● ストレージサイズが小さければイメージの取得が高速になる。 ● 内容がスリムなほうが、攻撃経路が限定されセキュリティー上 有利になる。 © 2024 Wantedly, Inc.
ビルド vs 実行 ● ビルドを高速化するには、なるべくキャッシュが残っているの が望ましい。 ○ 途中経過のデータを残した状態でアップロードするのが望ましい ● 実行を速くするには、なるべく不要なデータを削るのが望まし い。 ○ 途中経過は消してアップロードするのが望ましい このジレンマを解決する必要がある © 2024 Wantedly, Inc.
ビルド vs 実行 ビルド vs 実行のジレンマを解決する2つの道具 ● マルチステージビルド ○ ○ ○ Dockerfile内で複数の異なるイメージを生成する 途中のイメージの結果の一部を、最終イメージにコピーする 中間イメージ自体が無くても最終イメージは動作する ● キャッシュと最終成果物の分離 ○ ○ キャッシュには中間イメージを含む全てのレイヤをアップロードする (max cache と呼ばれる) 実際に実行するイメージには最終イメージだけを含める © 2024 Wantedly, Inc.
マルチステージビルドの基本構成 GoやJavaScriptなど、ビルドステップが必要な場合は2ステー ジ以上にする ● builder stageでは、ビルドに必要な依存を全て入手し、なる べく細かいステップでビルドを行う。 ● final stageでは、実行に必要な依存だけを入手する。 builder stageから最小限の成果物をコピーする。 なるべく少ないステップで構成する。 © 2024 Wantedly, Inc.
まとめ © 2024 Wantedly, Inc.
まとめ Dockerfileを書くときは、3つの目標の最大化を目指す ● ビルドを決定論的にする ○ 入力が同じなら、最終成果物の動作も同等になるようにする ● ビルドを効率化する (←キャッシュ) ○ ○ キャッシュを有効化。 ただし、入力が異なるなら、キャッシュキーも異なるようにする ● 最終イメージを小さくする (→実行の効率化) ○ ○ ビルドステージと最終ステージを分け、ビルドステージもキャッシュに含める ビルドステージはレイヤーを分ける © 2024 Wantedly, Inc.