Slide 1

Slide 1 text

© 2024 Wantedly, Inc. Dockerfileの考え方 開発を加速する Dec. 12 2024 - Masaki Hara

Slide 2

Slide 2 text

© 2024 Wantedly, Inc. なぜやるのか ● Dockerfileは、アプリケーション構成を保守可能な形で記録 できる強力なツールです ● しかし、その真価を十分に発揮するためには、Dockerfileを 書く人の理解がかかせません。 ● ウォンテッドリーではなるべくアプリ開発者が自分で構成を管 理する仕組みのため、皆さんにも知ってほしい。

Slide 3

Slide 3 text

© 2024 Wantedly, Inc. コンテナの基本

Slide 4

Slide 4 text

© 2024 Wantedly, Inc. コンテナのワークフロー Dockerfile イメージを作るための構 成手順 OCI image コンテナの状態が入った、 ディスクイメージのようなもの ビルド container ビルド用の 一時的なコンテナ container ベースイメージとして使用 container container コンテナ = 仮想コンピューター

Slide 5

Slide 5 text

© 2024 Wantedly, Inc. システムコール ● システムコール: アプリケーションがOSカーネルの機能を利用 するために呼び出すAPIのこと ○ わかりやすさのために「API」と呼んだが、正確にはABI部分と考えたほうがいい ● システムコールから先は、OSが処理する ○ たとえばファイルを開く・読むといった指示はシステムコールを通して実行されるが、そこ から先でどのような実装になっているかはアプリケーションは知らない

Slide 6

Slide 6 text

© 2024 Wantedly, Inc. コンテナ ● コンテナ: システムコールのレイヤーでコンピューターを仮想化 する仕組み ○ たとえば、コンテナ内で /etc/foo を読む命令を発行しても、実際には /var/container1/etc/foo のような別のファイルを返しているかもしれない ● アプリケーションの仮想化として「ちょうど良い」 ○ ライブラリの状態などはアプリケーションの動作に影響を与えるので再現する ○ しかし、カーネルのバージョンやドライバ、ファイルシステム実装まで再現する必要はない ○ CPUやSSDごと再現するより柔軟で効率的

Slide 7

Slide 7 text

© 2024 Wantedly, Inc. コンテナ ● 仮想化することで、コンピューター作り放題になる ○ 構成を切り替えたければ、新しいコンピューターを作って新しい構成をインストールし、古 いコンピューターは消してしまえばよい。 ○ リソース(CPU, メモリ)のやりくりがしたければ、ホストとなるコンピューターに仮想化され たコンピューターをいくつか作ってリソースを分配すればよい。 ● コンテナの仕組みで仮想化されたコンピューター自体も「コン テナ」と呼ぶ

Slide 8

Slide 8 text

© 2024 Wantedly, Inc. コンテナイメージ ● コンテナイメージ = コンテナの電源を切った状態の補助記憶 を取り出したもの ○ 主記憶 (メモリ) の状態は持たない ● これを複製して起動すれば、その状態から再開できる → コンテナを立ち上げるたびにセットアップするのではなく、 セットアップ済みのコンテナを複製して起動

Slide 9

Slide 9 text

© 2024 Wantedly, Inc. レイヤー ● ファイルシステムの状態を差分で表現 ○ 実行時はoverlayFSなどのプログラムを使ってunion mountingする ● 差分はチェーン状に繋げられる ○ 永続データ構造の一種であり、gitやブロックチェーンの仲間 ○ 他のバージョンとデータを部分的に共有できる利点がある Layer 1 Layer 2 Layer 3-A Layer 3-B Layer 1との差分のみを持つ Layer 2との差分のみを持つ

Slide 10

Slide 10 text

© 2024 Wantedly, Inc. DockerとOCI ● コンテナイメージとランタイムはOCIで標準化 ○ コンテナランタイム: イメージをもとにコンテナを作成して実行する処理 ○ 現在のDockerはruncというランタイムを同梱している ● DockerfileはDocker独自 ○ Podman/Buildahなど、Dockerfileに対応した他ツールもある ○ イメージは必ずDockerfileから作らなければいけないわけではない ■ とはいえDockerfile(Containerfile)が事実上の標準と考えてよさそう

Slide 11

Slide 11 text

© 2024 Wantedly, Inc. 3つの指針

Slide 12

Slide 12 text

© 2024 Wantedly, Inc. 3つの指針 Dockerfileを書くときは、3つの目標の最大化を目指す ● ビルドを決定論的にする ● ビルドを効率化する (←キャッシュ) ● 最終イメージを小さくする (→実行の効率化)

Slide 13

Slide 13 text

© 2024 Wantedly, Inc. 決定論

Slide 14

Slide 14 text

© 2024 Wantedly, Inc. 決定論 ● 決定論 = この世の出来事は初期状態からの帰結としてすで に決まっているという考え方のこと ● 転じて、コンピューターの世界では、初期パラメーターだけで 動作が一意に決まる場合を意味する。 ○ 決定論的 = 決定性 = deterministic ○ 非決定論的 = 非決定性 = non-deterministic

Slide 15

Slide 15 text

© 2024 Wantedly, Inc. 非決定論的な振舞い 非決定論的な振舞いは以下のような形で持ち込まれる: ● 現在時刻 ● 乱数 ● マルチスレッド処理のスケジュールの不確定性 ● OSカーネルやハードウェアの特性 ● ネットワークの外部にあるリソースの状態 ○ たとえばパッケージレジストリの状態など

Slide 16

Slide 16 text

© 2024 Wantedly, Inc. 決定論が重要な理由 ● 決定論とは、結局のところ、開発者の制御下にあるかどうかと いうこと ○ 本来の決定論とは立場が逆、神の視点で考えている ○ 開発者が制御できるものを決定論的と呼んでいるにすぎない ● アプリケーションの動作が開発者の制御下にあったほうがい いのは言うまでもない ○ 問題が発生したときに、開発者が明示的に行った変更のどれかを巻き戻せばいい ○ 問題の原因も、開発者が行った変更の中から探せばいい

Slide 17

Slide 17 text

© 2024 Wantedly, Inc. 同一性 決定論的かどうかで重要なのは、結果の同一性をどう定義する かということ。 ● 最終的にアプリケーションが望ましい動作をするのが目的 ○ たとえば、ルート証明書が入った ca-certificatesのバージョンを固定すればアプリケー ションの挙動は安定するかというと、むしろ逆効果になる場合もある なぜなら、通信相手の証明書の更新に追従できなければ動作が壊れるから ○ 逆に、システム上の動作が変わっても最終的な動作が同じならば実用的には困らない

Slide 18

Slide 18 text

© 2024 Wantedly, Inc. Dockerfileと決定論性 ● Dockerfileは、ビルドがある程度決定論的になるように作ら れている ● しかし、最後は開発者の意思に任されている ○ 特にネットワークアクセスは監視されていないため、常に最新の情報を取得する動作にす ることもできる (むしろデフォルトではそのような動作になる ) ○ たとえば、base imageとして ruby:latest を使うか、 ruby:3.4.0 を使うか、 ruby:3.4.0-bookworm を使うかは開発者に委ねられている ○ 挙動を固定する害のほうが大きければ、あえて最新の情報を取るという判断も可能な仕 組みになっている。

Slide 19

Slide 19 text

© 2024 Wantedly, Inc. アドバイス ● ここからは個人の意見 ○ 言語ごとのパッケージマネージャー内のパッケージバージョンは固定したほうがいい。こ れらはアプリケーションの挙動への影響が大きい。 ○ Debianなどのディストリのバージョンは、経験則としては固定せずに最新を参照してよい と思う。ビルドが壊れることはあるが本番で派手に壊れた事例は記憶にない。各パッケー ジのバージョンについても同様。

Slide 20

Slide 20 text

© 2024 Wantedly, Inc. ビルドの効率化

Slide 21

Slide 21 text

© 2024 Wantedly, Inc. キャッシュ ● 良いDockerfileは上手にキャッシュする ● キャッシュ以外の高速化も大事だが、ここでは触れない ○ 無駄な処理をしない、効率的なアルゴリズムを使う、高速な言語で書かれたツールに置き 換えるなど ○ これらはDocker特有の注意点が必要ないため

Slide 22

Slide 22 text

© 2024 Wantedly, Inc. キャッシュの定式化 ● キャッシュとは、計算結果を記憶しておいて、次に同じ計算が 来たときに再利用すること ○ ただし、ネットワークからの取得などもここでの「計算」に含まれる ● y = f(x) のfとxが過去と同じならキャッシュが使える ● 計算が決定論的であることを期待している ○ fが非決定論的であると、キャッシュを使うことで結果がより予測不可能になってしまう。

Slide 23

Slide 23 text

© 2024 Wantedly, Inc. キャッシュの粒度 Dockerには、粒度の異なる2つのキャッシュがある ● レイヤーキャッシュは、コマンドを1つの計算とみなしたキャッ シュのために使える。 ● キャッシュマウントは、コマンドよりも細かい単位での計算を キャッシュするために使える。

Slide 24

Slide 24 text

© 2024 Wantedly, Inc. キャッシュのコスト キャッシュにもコストがある ● キャッシュの取得にもネットワークコストがかかる ○ 特に、CI環境では毎回取得することになるので注意が必要 ○ パッケージレジストリからダウンロードする処理のキャッシュなどは利点が少なかったり、 デメリットが大きくなってしまう場合もある ● キャッシュの保管にもストレージコストがかかる ○ ストレージ上限にヒットしてビルドできなくなったり、別の有益なキャッシュが追い出されて しまうリスクも。

Slide 25

Slide 25 text

© 2024 Wantedly, Inc. ビルドの効率化: レイヤーキャッシュ

Slide 26

Slide 26 text

© 2024 Wantedly, Inc. レイヤーキャッシュ Dockerfileから作られたレイヤーには、コマンド情報が記録され ている Layer 1 Layer 2: Layer 1から COPY go.mod . で作った Layer 3-A: Layer 2から make で作った

Slide 27

Slide 27 text

© 2024 Wantedly, Inc. レイヤーキャッシュ 次のビルド時には、同じコマンドが記録されたレイヤーがあれば それを再利用 Layer 1 Layer 2: Layer 1から COPY go.mod . で作った Layer 3-A: Layer 2から make で作った 再利用 再利用

Slide 28

Slide 28 text

© 2024 Wantedly, Inc. レイヤーキャッシュ コマンドが異なる場合は、新しいレイヤーを作る Layer 1 Layer 2: Layer 1から COPY go.mod . で作った Layer 3-A: Layer 2から make で作った Layer 3-B: Layer 2から make server で 作った 再利用 新規作成

Slide 29

Slide 29 text

© 2024 Wantedly, Inc. レイヤーキャッシュ: キャッシュキー ● レイヤーのコマンド名に記録される ○ RUNのコマンドや主要なオプション ○ COPYの対象ファイル群とその内容 (をハッシュ化したもの?) ○ FROMの元イメージ ○ ENVの内容 ● レイヤーのコマンド名に記録されない ○ ネットワークアクセスの通信内容 ○ secret mountの内容 ○ cache mountの読み取り時点での内容

Slide 30

Slide 30 text

© 2024 Wantedly, Inc. キャッシュと決定論 ● Dockerfileは、同じ結果が得られるように書く ○ 「同じ結果」をどこまで求めるかは、書く人の責任で決める余地がある ● 異なる結果になる操作には、異なるキャッシュキーが割り当て られるように書く ○ たとえば、ネットワークから最新情報を取得するような操作は、キャッシュキーのユニーク 性を毀損しうる ○ ただしこれも、「同じ結果」をどこまで求めるか次第 ○ 何がキャッシュキーになるのか、何をもって同じ結果とするのかを意識しながら書く必要 がある

Slide 31

Slide 31 text

© 2024 Wantedly, Inc. ビルドの効率化: キャッシュマウント

Slide 32

Slide 32 text

© 2024 Wantedly, Inc. キャッシュマウント ● キャッシュ用の特別なファイルシステムをマウント ○ たとえば /var/cache にマウントしたら、 /var/cache 以下はDockerのキャッシュ ファイルシステムに管理される ● その中身はレイヤーに記録されない ○ キャッシュの中身がある場合でもない場合でも、同じ結果になるようにする必要がある ● キャッシュの中身はローカルで保存される ○ 別のビルドで同じマウントポイントを作ると、前の状態が復元される ○ コンピューターをまたいだ共有の仕組みは今のところない

Slide 33

Slide 33 text

© 2024 Wantedly, Inc. キャッシュマウントのメリット キャッシュマウントのメリット ● 過去の同じコマンドの結果を部分的に再利用できる ○ レイヤーキャッシュでは、1つのコマンドの結果を完全に再利用するか、全く再利用しない かのどちらかだった ● キャッシュはローカルのみ ○ ネットワーク由来のキャッシュの場合、ローカルでは有益だが CIでは有害な場合もあるた め、これが有利に働くケースもある ○ これは現時点での話

Slide 34

Slide 34 text

© 2024 Wantedly, Inc. キャッシュマウントのデメリット キャッシュマウントのデメリット ● キャッシュ処理は各コマンドに大いに委ねられている ○ レイヤーキャッシュであっても正当性は Dockerfileを書く人に委ねられているが、それよ りもさらに自由度が高く間違いやすい ● キャッシュはローカルのみ ○ CIでは今のところ役に立たない (現時点、独自にツールを組まない前提の話 ) ● キャッシュの肥大化 ○ 良いGC手法がない (現時点でローカルのみである理由のひとつと考えられる )

Slide 35

Slide 35 text

© 2024 Wantedly, Inc. キャッシュマウントの自由度 ● キャッシュマウントは、キャッシュに使える自由なストレージを 提供するだけ ● キャッシュ処理は個々のツールが正しく実装する必要がある ○ y = f(x) に対して、過去と同じfとxが使われたときはキャッシュを利用する。 ○ 普通、fとxをファイル名にして置いておくことが多い ○ 一般的には、Docker専用に組まれてなくてもだいたい上手くいくが、キャッシュキーに反 映されないパラメーターがないかは注意が必要 ■ 処理系バージョンや CPUアーキテクチャなど

Slide 36

Slide 36 text

© 2024 Wantedly, Inc. キャッシュマウントの例 キャッシュマウントを使う例 ● ネットワークキャッシュ ○ aptのダウンロードディレクトリ ○ go mod や Cargo のダウンロードディレクトリ ● ビルドキャッシュ ○ node_modules/.cache 以下にWebpack等が生成するキャッシュ ○ Cargoの target/ 以下

Slide 37

Slide 37 text

© 2024 Wantedly, Inc. キャッシュマウントのGC ● 必要ないキャッシュは消す必要がある ● これはレイヤーキャッシュ・キャッシュマウントの両方に当ては まるが、キャッシュマウントのほうが難しい ○ ファイルの利用時刻は正確に記録されていると限らない ● キャッシュマウントを独自ツールで永続化するなら、このあたり を考慮する必要がある ○ キャッシュマウントを使ってビルドした後の状態をさらに永続化しないほうが賢明かも

Slide 38

Slide 38 text

© 2024 Wantedly, Inc. 最終イメージを小さくする

Slide 39

Slide 39 text

© 2024 Wantedly, Inc. 最終イメージ 最終イメージは小さいほうが望ましい。 これは2つの視点がある ● ストレージサイズが小さければイメージの取得が高速になる。 ● 内容がスリムなほうが、攻撃経路が限定されセキュリティー上 有利になる。

Slide 40

Slide 40 text

© 2024 Wantedly, Inc. ビルド vs 実行 ● ビルドを高速化するには、なるべくキャッシュが残っているの が望ましい。 ○ 途中経過のデータを残した状態でアップロードするのが望ましい ● 実行を速くするには、なるべく不要なデータを削るのが望まし い。 ○ 途中経過は消してアップロードするのが望ましい このジレンマを解決する必要がある

Slide 41

Slide 41 text

© 2024 Wantedly, Inc. ビルド vs 実行 ビルド vs 実行のジレンマを解決する2つの道具 ● マルチステージビルド ○ Dockerfile内で複数の異なるイメージを生成する ○ 途中のイメージの結果の一部を、最終イメージにコピーする ○ 中間イメージ自体が無くても最終イメージは動作する ● キャッシュと最終成果物の分離 ○ キャッシュには中間イメージを含む全てのレイヤをアップロードする (max cache と呼ばれる) ○ 実際に実行するイメージには最終イメージだけを含める

Slide 42

Slide 42 text

© 2024 Wantedly, Inc. マルチステージビルドの基本構成 GoやJavaScriptなど、ビルドステップが必要な場合は2ステー ジ以上にする ● builder stageでは、ビルドに必要な依存を全て入手し、なる べく細かいステップでビルドを行う。 ● final stageでは、実行に必要な依存だけを入手する。 builder stageから最小限の成果物をコピーする。 なるべく少ないステップで構成する。

Slide 43

Slide 43 text

© 2024 Wantedly, Inc. まとめ

Slide 44

Slide 44 text

© 2024 Wantedly, Inc. まとめ Dockerfileを書くときは、3つの目標の最大化を目指す ● ビルドを決定論的にする ○ 入力が同じなら、最終成果物の動作も同等になるようにする ● ビルドを効率化する (←キャッシュ) ○ キャッシュを有効化。 ○ ただし、入力が異なるなら、キャッシュキーも異なるようにする ● 最終イメージを小さくする (→実行の効率化) ○ ビルドステージと最終ステージを分け、ビルドステージもキャッシュに含める ○ ビルドステージはレイヤーを分ける