DockerCon SF19 で発表の、基礎→マルチ・ステージ・ビルド→最新動向まで
Sakura Internet, Inc.
Masahito Zembutsu @zembutsu
Docker Meetup Kansai #3 #dockerkansai
May 24, 2019
Dockerfileを改善するための
Best Practice 2019年版
DockerCon SF19 での発表に基づく内容
• Dockerfile Best Practices
https://www.slideshare.net/Docker/dcsf19-dockerfile-best-practices
2
• 動画もご覧ください
https://www.docker.com/dockercon/2019-videos?watch=dockerfile-best-practices
オリジナルの発表は Tibor Vass 氏( @tiborvass) および Sebastian van Stijin 氏(@thajeztha)
による、 DockerCon 毎回恒例人気セッション"Dockerfile Best Practices" であり、両者に感謝します。
I thank you both for your excellent presentation of DockerCon. I also appreciate your permission to translate the content.
Please refer to this slides. And, this presentation video
will also be helpful.
このスライドは何?
3
⚫ DockerCon 19 で発表された人気シリーズ
“Best Practices” を日本訳+解説の追加。
⚫ Dockerfile の改善を通して、現在利用できる
マルチ・ステージ・ビルドや BuildKit 紹介。
以上の内容です。
※ "Docker Meetup Kansai #3" 発表時のスライドをベースに、
スライドそのままでは分かりづらい部分があるため、
一部で発表時と異なる表現・補足説明を用いている場合があります。
ゴール:
「Dockerfileの改善を具体的に理解」
Dockerfileのベストプラクティス
• "Best practices for writing Dockerfiles"
https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
4
最新の日本語訳を公開しました。
そもそもの基本となるのは、"Dockerfileを書くためのベストプラクティス"です。
Dockerfile解説スライドもご覧ください
• ベストプラクティス日本語版に解説・図解を加えたバージョンを作りました
5
https://www.slideshare.net/zembutsu/explaining-best-practices-for-writing-dockerfiles
もし、Dockerfileに慣れていないのであれば、前提として、こちらのスライドをご覧ください。
Dockerfile とは?
6
ド ッ カ ー フ ァ イ ル
Dockerfile
• Docker は Dockerfile から命令を読み込み、イメージを自動構築
• テキスト形式のドキュメント
• docker build / docker image build で Dockerfile を読み込み構築
• 「Docker イメージを作るための設計図」
• FROM、ADD、CMD など命令文で構成
• 例) 何のイメージをもとに、何を実行するか?
• 誰でも確実にイメージを構築できる
• イメージの構築過程を確認できる
• Dockerfileは広く使われている
• GitHub上に Dockerfile は100万以上
7
automate build
blueprint
Image by John Dortmunder from Pixabay
「Dockerfile」はDockerイメージを自動構築するために、必ず使うファイルです。
BuildKit: builder v2
• docker build は「イメージ・キャッシュ」があるため、素早く開発できる
• しかし、いくつかの制限があったため、buildKit プロジェクトで根本的に構築
• https://github.com/moby/buildkit
• "ゴールは Docker build のデフォルト"
• BuildKit: 特長
• 同時へいれt性(concurrency)
• 断片的なコンテキスト・アップロード (lazy context upload)
• キャッシュの改良
• 新しい Dockerfile 機能の追加
8
⚫並列性が無い
⚫docker run と同様に root で動作する必要性
⚫ボリューム機能が無い
高速にイメージを作れる BuildKit という汎用ツールが開発途上です。
Docker BuildKit を使う方法
• クライアントの環境変数
• Docker デーモン設定ファイル /etc/docker/daemon.json
• 現時点では Windows は対応していない
• Windows support coming soon!
9
{ "features": { "buildkit": true }}
export DOCKER_BUILDKIT=1
BiuldKit は Docker とは別のプロジェクト (https://github.com/moby/buildkit ) ですが、
現在の Docker CE v18.09 では、BuildKit の一部機能が既に利用できる状態です。
カイゼン Dockerfile
10
Dockerfile の改善領域
• 増大するbuild 時間
• イメージ容量
• 保守性
• 安全性
• 一貫性・反復性
11
(Incremental) build time
Image size
Maintainability
Security
Consistency/Repetability
ここからは、先日開催された DockerCon SF19での発表サンプルをベースにご紹介します。
サンプルのアプリケーションを使い、5つの視点で Dockerfile を改善する流れをみていきましょう。
サンプルの Dockerfile をカイゼンするぞ!
12
FROM debian
COPY . /app
RUN apt-get update
RUN apt-get –y install openjdk-8-jdk ssh emacs
CMD ["java", "-jar", "/app/target/app.jar"]
これは、Javaで"Hello world"を表示するための、サンプルプログラム用の Dockerfile です。
サンプルの Dockerfile をカイゼンするぞ!
13
FROM debian
COPY . /app
RUN apt-get update
RUN apt-get –y install openjdk-8-jdk ssh emacs
CMD ["java", "-jar", "/app/target/app.jar"]
vim
まずすべきは、emacs を消して、vim を入れましょう。もちろんジョークですが!
サンプルの Dockerfile をカイゼンするぞ!
14
FROM debian
COPY . /app
RUN apt-get update
RUN apt-get –y install openjdk-8-jdk ssh emacs
CMD ["java", "-jar", "/app/target/app.jar"]
vim
まずすべきは、emacs を消して、vim を入れましょう。もちろんジョークですが!
増大する構築(build)時間
ビルド・キャッシュと友達になろう
15
課題と対策
キャッシュする順番が重要
16
FROM debian
COPY . /app
RUN apt-get update
RUN apt-get –y install openjdk-8-jdk ssh vim
COPY . /app
CMD ["java", "-jar", "/app/target/app.jar"]
頻繁に変更するものを後ろへ
構築時のキャッシュとは、変更箇所があれば破棄されます。この例の COPY では「.」(カレント)にある
ファイルに変更があれば、毎回「apt-get update」と「install」が走るので、時間がかかってしまいます。
せっかくあrキャッシュを有効活用するには、頻繁に更新する可能性があるものを後ろにおきます。
キャッシュする順番が重要
17
FROM debian
COPY . /app
RUN apt-get update
RUN apt-get –y install openjdk-8-jdk ssh vim
COPY . /app
CMD ["java", "-jar", "/app/target/app.jar"]
頻繁に変更するものを後ろへ
構築時のキャッシュとは、変更箇所があれば破棄されます。この例の COPY では「.」(カレント)にある
ファイルに変更があれば、毎回「apt-get update」と「install」が走るので、時間がかかってしまいます。
せっかくあrキャッシュを有効活用するには、頻繁に更新する可能性があるものを後ろにおきます。
キャッシュ有効
キャッシュ破棄 … ホスト側「 ./ 」に変更時
キャッシュ破棄 … ホスト側「 ./ 」に変更時
時間がかかるので、キャッシュ活用すべし
時間かかる
キャッシュ破棄の影響を避けるため、範囲を狭く
18
FROM debian
RUN apt-get update
RUN apt-get –y install openjdk-8-jdk ssh vim
COPY . /app
COPY target/app.jar /app
CMD ["java", "-jar", "/app/target/app.jar"]
コピーに必要なものを明示する。
可能であれば "COPY ."を避ける
コピーすべき場所を絞っておけば、キャッシュは破棄されません。
この例では「app.jar」しか「COPY」しませんので、他のファイルに変更があってもキャッシュを保持。
キャッシュ破棄の影響を避けるため、範囲を狭く
19
FROM debian
RUN apt-get update
RUN apt-get –y install openjdk-8-jdk ssh vim
COPY . /app
COPY target/app.jar /app
CMD ["java", "-jar", "/app/target/app.jar"]
コピーすべき場所を絞っておけば、キャッシュは破棄されません。
この例では「app.jar」しか「COPY」しませんので、他のファイルに変更があってもキャッシュを保持。
キャッシュ有効
キャッシュ有効
キャッシュ有効
ちょっとキャッシュする範囲が拡がるかもね
キャッシュ対象の明確化
コピーに必要なものを明示する。
可能であれば "COPY ."を避ける
行をまとめる: apt-get update & install
20
FROM debian
RUN apt-get update
RUN apt-get –y install openjdk-8-jdk ssh vim
RUN apt-get update ¥
&& apt-get -y install ¥
openjdk-8-jdk ssh vim
COPY target/app.jar /app
CMD ["java", "-jar", "/app/target/app.jar"]
古いパッケージ情報のキャッシュ利用を避ける
パッケージ・マネージャを使う場合、「古い」パッケージ情報をキャッシュしがちです。
更新したいのに更新できないのを避けるには、情報の更新と、パッケージ追加・削除をまとめること。
行をまとめる: apt-get update & install
21
FROM debian
RUN apt-get update
RUN apt-get –y install openjdk-8-jdk ssh vim
RUN apt-get update ¥
&& apt-get -y install ¥
openjdk-8-jdk ssh vim
COPY target/app.jar /app
CMD ["java", "-jar", "/app/target/app.jar"]
古いパッケージ情報のキャッシュ利用を避ける
パッケージ・マネージャを使う場合、「古い」パッケージ情報をキャッシュしがちです。
更新したいのに更新できないのを避けるには、情報の更新と、パッケージ追加・削除をまとめること。
キャッシュ有効
古いものをキャッシュしてるかも!
時間かかる場合があっても、確実に処理
イメージ容量を減らす
より速くデプロイするには、イメージを小さく
22
課題と対策
不要な依存関係を削除
23
FROM debian
RUN apt-get update ¥
&& apt-get -y install --no-install-recommends ¥
openjdk-8-jdk ssh vim
COPY target/app.jar /app
CMD ["java", "-jar", "/app/target/app.jar"]
デプロイを素早くするためには、イメージ容量の削減が重要であり、必須課題。
推奨パッケージ(必須ではない)を避けるためには、「--no-install-recommends」フラグを付ける。
Javaの実行に不要なパッケージも入れないので「ssh」「vim」も消す。
不要なパッケージマネージャのキャッシュ削除
24
FROM debian
RUN apt-get update ¥
&& apt-get -y install –no-install-recommends ¥
openjdk-8-jdk ¥
&& rm –rf /var/lib/apt/lists/*
COPY target/app.jar /app
CMD ["java", "-jar", "/app/target/app.jar"]
パッケージ・マネージャのキャッシュ情報も、アプリケーションの実行に不要なので削除
保守性
できるだけシンプルに保つ
25
課題と対策
できるだけDocker公式(official)パッケージを使う
• メンテナンスにかける時間を減らす
(問題があるたびに、繰り返す更新)
• 容量を減らす(イメージ間でのレイヤ共有によって)
• コンテナとして使うために、予め設定済み
• 賢い人達が構築している
26パッケージ・マネージャのキャッシュ情報も、アプリケーションの実行に不要なので削除
27
FROM debian
RUN apt-get update ¥
&& apt-get -y install –no-install-recommends ¥
openjdk-8-jdk ¥
&& rm –rf /var/lib/apt/lists/*
FROM openjdk
COPY target/app.jar /app
CMD ["java", "-jar", "/app/target/app.jar"]
できるだけDocker公式(official)パッケージを使う
Javaの実行であれば、「openjdk」イメージがあるため、debianでセットアップする必要はない
タグを明示
28
FROM openjdk:latest
FROM openjdk:8
COPY target/app.jar /app
CMD ["java", "-jar", "/app/target/app.jar"]
"latest"タグは常に入れ替わる。
タグを指定しておけば、想定外の
ベース・イメージ変更発生を防止。
何も指定しなければ「latest」(最新)になるため、常にタグ(主にバージョン)指定を忘れずに
適切なタグを探す
29
Docker Hub のドキュメントを読もう
https://hub.docker.com/_/openjdk
どのようなタグがあるかは、Docker Hub上のドキュメントで確認
必要最小限のものを探す
30
REPOSITORY TAG SIZE
openjdk 8 624MB
openjdk 8-jre 443MB
openjdk 8-jre-slim 204MB
openjdk 8-jre-alpie 83MB
ベース・イメージの変更だけで
540MBも削減
どのイメージ(タグ)を選ぶかによって、容量がかなり異なる。
alpineタグは Alpine Linux という約 5MB の Linux ディストリビューションがベース
再利用性
Dockerfileは青写真であり、
ソースコードこそが本当の源(ソース)
31
課題と対策
32
一貫した環境を、ソースから構築する
• Dockerfile を設計図(青写真)にしよう:
• ビルド時するための構築環境を Dockerfile に記述
• 正しいバージョンのビルド・ツールをインストール
• 環境ごとの違いを発生させない
• システム依存はあるかもしれない
• "source of truth"(本当のソース)とは、
ソースコードである。ビルド成果物ではない。
33
Image by John Dortmunder from Pixabay
blue print
開発環境、テスト環境、実行環境で共通する Dockerfile を目指す
一貫した環境を、ソースから構築する
34
FROM openjdk:8-jre-alpine
FROM maven:3.6-jdk-8-alpine
WORKDIR /app
COPY app.jar/app
COPY pom.xml .
COPY src ./src
RUN mvn –e –B package
CMD ["java", "-jar", "/app/target/app.jar"]
openjdk にかわり、Javaプロジェクト管理ツールであるMavenを開発環境として入れる
一貫した環境を、ソースから構築する
35
FROM maven:3.6-jdk-8-alpine
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn –e –B package
CMD ["java", "-jar", "/app/target/app.jar"]
このように、開発環境向けの Dockerfile を整えていく。
依存関係の解決は、ステップを分ける
36
FROM maven:3.6-jdk-8-alpine
WORKDIR /app
COPY pom.xml .
RUN mvn –e –B dependency:resolve
COPY src ./src
RUN mvn –e –B package
CMD ["java", "-jar", "/app/target/app.jar"]
開発関係だけに必要な依存関係を追加。
構築時のみの依存関係が判明
37
FROM maven:3.6-jdk-8-alpine
WORKDIR /app
COPY pom.xml .
RUN mvn –e –B dependency:resolve
COPY src ./src
RUN mvn –e –B package
CMD ["java", "-jar", "/app/target/app.jar"]
しかし、この黄色い部分は「開発環境の構築」段階しか使わないものであり、本番稼働では無駄
マルチ・ステージ・ビルドで、構築時の部分を削除
38
FROM maven:3.6-jdk-8-alpine AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn –e –B dependency:resolve
COPY src ./src
RUN mvn –e –B package
CMD ["java", "-jar", "/app/target/app.jar"]
FROM openjdk:8-jre-alpine
COPY --from=builder /app/target/app.jar /
CMD ["java", "-jar", "/app/target/app.jar"]
そこで、マルチ・ステージ・ビルドで複数の「FROM」を使い、構築時(AS bilder)と実行時を分離
←「builder」ステージから
ファイルをコピー
←ステージ「builder」と名前を付ける
マルチ・ステージ・ビルドで、構築時の部分を削除
39
FROM maven:3.6-jdk-8-alpine AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn –e –B dependency:resolve
COPY src ./src
RUN mvn –e –B package
FROM openjdk:8-jre-alpine
COPY --from=builder /app/target/app.jar /
CMD ["java", "-jar", "/app/target/app.jar"]
マルチ・ステージ・ビルドでは、「docker build」は全てのビルドを実行しますし、
「docker build –target builder」と指定すると「AS builder」の「FROM」ステージしか処理しません。
マルチ・ステージ Dockerfile
単に容量を減らすためだけではない
40
応用
プロジェクトごとに多くのステージがある
• Moby: 16 ステージ
https://github.com/moby/moby/blob/master/Dockerfile
• BuildKit: 44 ステージ
https://github.com/moby/buildkit/blob/master/hack/dockerfiles
/test.buildkit.Dockerfile
41Docker 関連プロジェクトの Dockerfile にも、多くのステージ(FROM命令)がある
マルチ・ステージの利用例
• 実行環境と構築(ビルド)環境を分ける(イメージ容量の縮小)
• イメージに対する影響を最小限に(DRY)
• 構築・開発・テスト・構文チェック…のような環境を明示
• 依存関係を一直線に処理しない(並列化)
• プラットフォーム固有のステージ
42
一般的な目的は、最終成果物の容量を削減
構築ステージを --target で指定
43
FROM image_or_stage AS stage_name
…
$ docker build --target stage_name
「AS ステージ名」を FROM 命令に書いておけば、「docker build」時に「--target」でステージを指定
イメージの flavor を変える
44
FROM maven:3.6-jdk-8-alpine AS builder
...
FROM openjdk:8-jre-jessie AS release-jessie
COPY --from=builder /app/target/app.jar /
CMD ["java", "-jar", "/app.jar"]
FROM openjdk:8-jre-jessie AS release-alpine
COPY --from=builder /app/target/app.jar /
CMD ["java", "-jar", "/app.jar"]
$ docker build --target release-jessie .
Debian jessie (8.x) をベースとするイメージと、Alpine Linux をベースとするイメージ。
特色(風味)
45
FROM maven:3.6-jdk-8-alpine AS builder
...
FROM openjdk:8-jre-jessie AS release-jessie
COPY --from=builder /app/target/app.jar /
CMD ["java", "-jar", "/app.jar"]
FROM openjdk:8-jre-jessie AS release-alpine
COPY --from=builder /app/target/app.jar /
CMD ["java", "-jar", "/app.jar"]
$ docker build --target release-jessie .
イメージの flavor を変える
特色(風味)
しかし、 Dockerfile をよく見ると問題があり、
46
FROM maven:3.6-jdk-8-alpine AS builder
...
FROM openjdk:8-jre-jessie AS release-jessie
COPY --from=builder /app/target/app.jar /
CMD ["java", "-jar", "/app.jar"]
FROM openjdk:8-jre-jessie AS release-alpine
COPY --from=builder /app/target/app.jar /
CMD ["java", "-jar", "/app.jar"]
$ docker build --target release-jessie .
このように各ステージで、同じ命令が重複する箇所がある。
イメージの flavor を変える
特色(風味)
どちらも
同じ
47
ARG flavor=alpine
FROM maven:3.6-jdk-8-alpine AS builder
...
FROM openjdk:8-jre-$flavor AS release
COPY --from=builder /app/target/app.jar /
CMD ["java", "-jar", "/app.jar"]
$ docker build --target release
--build-arg flavor=jessie .
「ARG」を Dockerfile で指定しておくと、docker build 時に「--build-arg」を通して、変数のように展開
--build-arg で $flavor=xxx があれば、$flavor に xxx 代入
↓何もなければ「flavor=alpine」が適用
イメージの flavor を変える (DRY / 汎用的な ARG)
特色(風味)
※ DRY = “Don’t Repeat Yourself”(自分では繰り返さない)という
ソフトウェア開発の手法
引数
←ARG命令は、docker build時に指定できる変数を定義
書式は「変数名」または「変数名=デフォルト値」。
様々な環境:構築、開発、テスト、構文チェック(lint)…
• ステージとしての検討例:
⁃ builder: 依存関係すべてをビルド
⁃ build(または binary): builder + ビルド成果物
⁃ cross: 複数のプラットフォーム向けに構築
⁃ dev: build(er) + 開発/デバッグツール
⁃ lint: 最小限の構文チェック用依存関係
⁃ test: テストに関係する全ての依存関係 + テスト対象のビルド成果物
⁃ release: ビルド成果物を含む、最終的な最小イメージ
48様々な「ステージ」が検討できる。ここでは定型的な例
各ステージでは、依存関係が最小となるようにする
様々な環境:構築、開発、テスト、構文チェック(lint)…
49
FROM maven:3.6-jdk-8-alpine AS builder
...
FROM openjdk:8-jre-alpine AS lint
RUN wget https://github.com/checkstyle/checkstyle/releases/download/checkstyle-8.15/checkstyle-8.15-all.jar
COPY checks.xml .
COPY src /src
RUN java –jar checkstyle-8.15-all.jar –c checks.xml /src
これはシンプルな Java 構文チェック(リント)の確認用の Dockerfile を「AS lint」と指定
様々な環境:構築、開発、テスト、構文チェック(lint)…
50
FROM maven:3.6-jdk-8-alpine AS builder
...
FROM openjdk:8-jre-alpine AS release
COPY --from=builder /app/target/app.jar /
CMD ["java", "-jar", "/app.jar"]
FROM builder AS dev
RUN apk add --no-cache strace vim tcpdump
ENTRYPOINT ["ash"]
開発用デバッグ環境であれば、「builder」ステージをベースにしながら「AS dev」と明示し
シンプルにエディタ(vim)と tcpdump を入れている
様々な環境:構築、開発、テスト、構文チェック(lint)…
51
FROM maven:3.6-jdk-8-alpine AS builder
...
RUN mvn –e –B package –DskipTests
FROM builder AS unit-test
RUN mvn –e –B test
FROM release AS integration-test
RUN apk add --no-cache curl
RUN ./test/run.sh
他にも単体テスト、統合テスト用の環境も作れます。
並行性(Concurrency)
52
応用
一直線の Dockerfile ステージから …
• 全てのステージが順番(シーケンシャル)に実行
• この図では、上から下に一直線
• BuildKitがなければ、
不要なステージも無駄に実行し、破棄する
(無駄に時間がかかった)
53
s1
s2
s3
s4
s5
s6
デフォルトでは、全てのステージを順番に実行する
BuildKit のマルチ・ステージ graph へ
• BuildKit は下(--target のステージ名)から上に辿っていくような流れ
• 右図では s2, s3, s4 ステージを同時並行処理
• 不要なステージは無視できる
• 右図では s5 がビルド時に
不要であれば、何もしない
54
s1
s2 s3 s4
s5s6
Docker 17.05 からマルチ・ステージ・ビルドが利用可能になった。
18.06までは experimental 、18.09 は利用できるように組み込まれている
グラフ
※グラフは点と点とのつながり
(関係性)を表す
Multi-srage: 並行ビルド (build concurrently)
55
FROM maven:3.6-jdk-8-alpine AS builder
...
FROM tiborvass/whalesay AS assets
RUN whalesay "Hello DockerCon!" > /out/assets.html
FROM openjdk:8-jre-alpine AS release
COPY --from=builder /app/target/app.jar /
COPY --from=assets /out /assets
CMD ["java", "-jar", "/app.jar"]
「assets」は最終イメージ(release)に必要だが、別のステージとして処理できる
そして、この Dockerfile は「builder」と「assets」のステージからのコピーを並行処理
Multi-srage: 並行ビルド (build concurrently)
56
FROM maven:3.6-jdk-8-alpine AS builder-base
…
FROM gcc:8-alpine AS builder-someClib
…
RUN git clone … ¥
./configure --prefix=/out && make && make install
FROM g++:8-alpine AS builder-someCPPlib
…
RUN git clone … ¥
cmake …
FROM builder-base AS builder
COPY --from=builder-someClib /out /
COPY --from=builder-someCPPlib /out /
複数の COPY 命令を使う時には、特に効果を発揮し、時間を節約できる
並行処理が有用な典型的なパターンが
複数の COPY --from … の繰り返し
ベンチマーク
• github.com/moby/moby Dockerfile, master ブランチ
• 小さいほうが優れている
57
Dockerfile Best Practices
https://www.slideshare.net/Docker/dcsf19-dockerfile-best-practices/52
v18.03 の docker build と BuiltKit では2倍の速さ
ベンチマーク
• github.com/moby/moby Dockerfile, master ブランチ
• 小さいほうが優れている
58
Dockerfile Best Practices
https://www.slideshare.net/Docker/dcsf19-dockerfile-best-practices/52
キャッシュを有効にすると、7倍も速くなる
ベンチマーク
• github.com/moby/moby Dockerfile, master ブランチ
• 小さいほうが優れている
59
Dockerfile Best Practices
https://www.slideshare.net/Docker/dcsf19-dockerfile-best-practices/52
最も大事なのは、ソースコードの変更時。再構築の速度はマルチ・ステージ・ビルドで 2.5 倍に改善。
Dockerfileの最新機能
60
新機能
新機能を有効にするには?
61
# syntax=docker/dockerfile:1.0-experimental
FROM maven:3.6-jdk-8-alpine AS builder
WORKDIR /app
COPY . /app
RUN mvn -e –B package
FROM openjdk:8-jre-alpine
COPY --from=builder /app/target/app.jar /
CMD ["java", "-jar", "/app.jar"]
Dockerfile の新機能を有効にするには、この行を1行目に追加する必要がある
※experimental というのは、まだ
Dockerfile の正式な構文では
ないため
詳細
• https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/experimental.md
62構文の詳細は、こちらのドキュメントを参照
コンテキスト・マウント(v18.09+ w/ BuildKit)
63
# syntax=docker/dockerfile:1.0-experimental
FROM maven:3.6-jdk-8-alpine AS builder
WORKDIR /app
COPY . /app
RUN --mount=target=. mvn -e –B package -DoutputDirectory=/
FROM openjdk:8-jre-alpine
COPY --from=builder /app/app.jar /
CMD ["java", "-jar", "/app.jar"]
新機能を使って Dockerfile を書き換えていく。まずはマウント・オプション。
実行時に「.」(カレント・ディレクトリ内容)を Build Context として送信せず、マウントできる
※ Docker v18.09 以上かつ、BuildKit 有効化の状態で利用可能
contest mounts
Dockerfile
hello
コンテキスト・マウント(v18.09+ w/ BuildKit)
64
ディレクトリ内容をコンテナに COPY しなくても、
ビルド時に docker build コマンドを実行しているディレクトリ(ここでは 「target=.」で指定)を
rw(読み書き可能な状態)で直接コンテナにマウントし、
コンテナ内で「cat hello > /hello.txt」を実行
→ 構築したイメージの“/hello.txt” に “Hello world!” が書かれたファイルが作成される
# syntax=docker/dockerfile:1.0-experimental
FROM alpine
RUN --mount=type=bind,target=.,rw cat hello > /hello.txt
Hello world!
ソースコードなど、ビルド・コンテクストのコピーが不要となり、より早いビルドができる
contest mounts
• Dockerfile の mount 例:
ホスト側の docker build する場所に、
このファイルがあるとします
65
コンテキスト・マウント(v18.09+ w/ BuildKit)
contest mounts
従来 “COPY . /app” BuildKit
Dockerfile
app用ディレクトリ
Build用ディレクトリ
“docker build” 時、 「.」 以下の内容を
Docker イメージ用レイヤとして構築するため
全て Docker に対して送る必要があった
※結果として Docker イメージの不本意な増加
※あるいはマルチ・ステージ・ビルドで回避
Dockerfile
app用ディレクトリ
Build用ディレクトリ
コンテナ
コピー マウント
よいしょ…
みーてーるーだーけー
“docker build” の特定ステップのみ参照するため、
・ build 全体の時間を削減
・ イメージ・レイヤの肥大化を抑制
キャッシュの保持(BuildKitが無ければ)
66
FROM maven:3.6-jdk-8-alpine AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn -e –B dependency: resolve
COPY src ./src
RUN mvn –e –B package
CMD ["java", "-jar", "/app.jar"]
docker build 時にキャッシュが破棄されると、毎回依存関係の準備に時間がかかる
キャッシュの保持(v18.09+ w/ BuildKit)
67
# syntax=docker/dockerfile:1.0-experimental
FROM maven:3.6-jdk-8-alpine AS builder
WORKDIR /app
RUN --mount=target=. --mount=type=cache,target=/root/.m2 ¥
&& mvn package –DoutputDirectory=/
FROM openjdk:8-jre-alpine
COPY --from=builder /app.jar /
CMD ["java", "-jar", "/app.jar"]
apt: /var/lib/apt/lists
go: ~/.cache/go-build
go-modules: $GOPATH/pkg/mod
npm: ~/.npm
pip: ~/.cache/pip
キャッシュ用ディレクトリをマウントできるので、依存関係の準備にかかる時間を削減できる
キャッシュの保持
• --mount=type=cache 例:
68
Dockerfile
# syntax=docker/dockerfile:1.0-experimental
FROM ubuntu
RUN --mount=type=cache,target=/var/cache/apt ¥
--mount=type=cache,target=/var/lib/apt ¥
apt-get update && apt-get install -y wget curl
「type=cache」で指定した「target」のディレクトリは、docker build を実行したホスト上でキャッシュ
この例では apt-get install を含む「RUN」命令を書き換えたとしても、
一度キャッシュ(ビルド)済みであれば次回から高速なビルドが可能になる
キャッシュ情報をクリアするには「docker builder prune」コマンドを実行する
BuildKit
キャッシュはホスト側で保持
69
Dockerfile
app用ディレクトリ
Build用ディレクトリ
コンテナ
マウント
キャッシュしたあとは
みーてーるーだーけーDockerのキャッシュ用ディレクトリ
“docker build” の対象構築ステップ時のみ、
ホスト側のキャッシュ用ディレクトリを一時的にマウント
何度 build しても、キャッシュを有効活用できるので
高速にイメージを作りやすい
また、イメージ全体の容量も削減できる
docker build
※ docker builder prune で消せる
シークレット(これはダメな方法)
70
FROM baseimage
RUN …
ENV AWS_ACCESS_KEY_ID=…
ENV AWS_SECRET_ACCESS_KEY=…
RUN ./fetch-aseets-from-s3.sh
RUN ./build-scripts.sh
シークレット(secret)とは、パスワードやAPIキー、SSH 鍵などの認証情報(機微情報)
ENV に書くと docker history で丸見えですし、
シークレット(これもダメな方法)
71
FROM baseimage
RUN …
ARG AWS_ACCESS_KEY_ID
ARG AWS_SECRET_ACCESS_KEY
RUN ./fetch-aseets-from-s3.sh
RUN ./build-scripts.sh
$ docker build --build-arg ¥
AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID …
ARGでも途中のRUNで変数の情報をダンプできますし、
シェルのhistoryからも辿れるリスクが出てきます(記録しない方法もありますが、運用カバー系作業)
docker history
シークレット(v18.09+ w/ BiuldKit では、こうします)
72
# syntax=docker/dockerfile:1-experimental
FROM baseimage
RUN …
RUN --mount=type=secret,id=aws,target=/root/.aws/credentials,
required ./fetch-assets-from-s3.sh
RUN ./build-scripts.sh
$ docker build --secret id=aws,src=~/.aws/credentials .
最終イメージに混入させないための手法が「secret」としてのマウント。
対象ステップのみデータを参照できるよう一時的にマウントし、次のステップではマウントしません。
↓idは docker build 時の –secret オプションで識別するため
↑targetで、この構築ステップのみ、
コンテナ内のこの場所に
一時的にファイルを設置する指示
↑これはホスト側に存在するファイル。
~より、 $HOME や絶対パスのほうが安全かも
↑次のステップでは、先の id=aws でマウントしたシークレットは見えない( 0 byte のファイル残骸のみ)
BuildKit
73
Dockerfile
大事なディレクトリ
Build用ディレクトリ
コンテナ
id指定対象を
マウント
構築ステップの間だけ
みーてーるーだーけー
“docker build” の対象構築ステップ時のみ、
ホスト側のシークレット(機微情報)ファイルを一時的にマウント
環境変数などで取り込んで認証などに利用する
一時的にホスト側を参照するだけなので
イメージ内に大事な情報を残さない
docker
build
SECRET_FILE
シークレット・マウント(v18.09+ w/ BiuldKit)
secret mounts
id指定
• Dockerfile の mount secret 例:
Dockerfile
$HOME/secret
シークレット・マウント(v18.09+ w/ BiuldKit)
74
# syntax=docker/dockerfile:1.0-experimental
FROM alpine
RUN --mount=type=secret,id=check,target=/root/secret,required ¥
cat /root/secret > /data.txt
MYID=secretpass
Dockerfile で当該 RUN 命令行のビルド時のみ、一時的にホスト側ファイルをマウントできる
secret mounts
ホスト側にIDとパスワードのような、
いわゆるシークレット(機微情報)を記録するファイルを設置
もちろん、パーミッションは 600 にするなど、十分なセキュリティ配慮が必要
$ docker build --secret id=check,src=$HOME/.data/credentials -t myimage .
↑srcで指定したパスはホスト上のファイル
プライベート git リポジトリ(ダメぜったい)
75
FROM baseimage
COPY ./keys/private.pem /root/.ssh/private.pem
ARG REPO_REF=19ba8bcd9976ef8a9bd086187df19ba7bcd997f2
RUN git clone git@githubcom:org/repo /work && cd /work ¥
&& git checkout –b $REPO_REF
これもイメージ内に大事なファイルが残ってしまう NG 例
プライベート git リポジトリ(v18.09+ w/BuildKitの場合)
76
FROM alpine
RUN apk add --no-cache openssh-client
RUN mkdir –p –m 0700 ~/.ssh && ssh-keyscan github.com >>
~/.ssh/known_hosts
ARG REPO REF=19ba8bcd9976ef8a9bd086187df19ba7bcd997f2
RUN –-mount-type=ssh,required ¥
git clone git@github.com:org/repo /work && cd /work ¥
&& git checkout –b $REPO_REF
$ eval $(ssh-agent)
$ ssh-add ~/.ssh/id_rsa
$ docker build --ssh=default .
この RUN 命令実行時のみ、
ホスト側の SSH 認証情報を読み込む方法が
利用できる
BuildKit
77
SSH マウント(v18.09+ w/ BiuldKit)
mounts
Dockerfile
.ssh
Build用ディレクトリ
コンテナ
鍵情報
参照
このステップがある間だけ
みーてーるーだーけー
“docker build” の対象構築ステップ時のみ、
ホスト側のSSH agent 情報を一時的にマウント
シークレット・マウントに近いけれど
SSH の利用に特化
こんてなから GitHub や SSH 接続したい場合に有用
docker
build
id_rsa ssh-agent
--mount=type=ssh
SSH
リモート・ホストや
GitHub / GitLab 等
• Dockerfile の mount=SSH 例:
Dockerfile
SSH マウント(v18.09+ w/ BiuldKit)
78
# syntax=docker/dockerfile:1.0-experimental
FROM alpine
RUN apk update && apk add openssh
RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan <host_IP> > ~/.ssh/known_hosts
RUN --mount=type=ssh,required ¥
ssh <user>@<host_IP> ls -al / > /ls.txt
GitHubやGitLabの認証だけでなk、別のホストにこのようにしてログイン&操作も可能
mounts
$ eval $(ssh-agent)
$ ssh-add ~/.ssh/id_rsa
(パスフレーズを入力)
$ docker build --ssh default=$SSH_AUTH_SOCK -t <image_name> .
Dockerfile 改善のまとめ
• 従来
⁃ 構築・開発・テスト環境の矛盾
⁃ 膨れあがるイメージ容量
⁃ 構築時間がマシマシ(キャッシュ無効)になり時間がかかる
⁃ 構築が安全ではない
• これから( BuildKit の活用によって)
⁃ 構築・開発・テスト環境が一致
⁃ イメージ容量は最小
⁃ 構築が非常に速く、構築回数が増えていく
⁃ より安全に構築できる
79
export DOCKER_BUILDKIT=1重要なのは有効化に
新しいビルド時の一時マウント可能な bind、cache、tmpfs、secret、ssh
マルチ・ステージ・ビルドの活用によって、段階ごとの FROM
ステージ間のコピー機能により、最終成果物を小さく
並列ビルドの活用により、従来よりも素早い構築
参考資料
• Advanced multi-stage build patterns – Tõnis Tiigi – Medium
https://medium.com/@tonistiigi/advanced-multi-stage-build-patterns-6f741b852fae
• Build secrets and SSH forwarding in Docker 18.09 – Tõnis Tiigi – Medium
https://medium.com/@tonistiigi/build-secrets-and-ssh-forwarding-in-docker-18-09-
ae8161d066
• Introducing BuildKit – Moby Blog
https://blog.mobyproject.org/introducing-buildkit-17e056cc5317
• docker builder prune | Docker Documentation
https://docs.docker.com/engine/reference/commandline/builder_prune/
• Docker v18.09 新機能 (イメージビルド&セキュリティ) – nttlabs – Medium
https://medium.com/nttlabs/docker-v18-09-%E6%96%B0%E6%A9%9F%E8%83%BD-
%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8%E3%83%93%E3%83%AB%E3%83%89-
%E3%82%BB%E3%82%AD%E3%83%A5%E3%83%AA%E3%83%86%E3%82%A3-
9534714c26e2
80
参考資料
• BuildKitによる高速でセキュアなイメージビルド
https://www.slideshare.net/AkihiroSuda/buildkit
81
DockerCon での発表
• Dockerfile Best Practices
https://www.slideshare.net/Docker/dcsf19-dockerfile-best-practices
82
• 動画
https://www.docker.com/dockercon/2019-videos?watch=dockerfile-best-practices
Q&A
• 何か気になるところはありますか?
• Twitter: @zembutsu
• https://slideshare.net/zembutsu
• Dockerドキュメント日本語訳
http://docs.docker.jp
• Docker Composeドキュメント日本語訳
http://docs.docker.jp/compose/
• 公式ドキュメント
https://docs.docker.com
83

Dockerfileを改善するためのBest Practice 2019年版

  • 1.
    DockerCon SF19 で発表の、基礎→マルチ・ステージ・ビルド→最新動向まで SakuraInternet, Inc. Masahito Zembutsu @zembutsu Docker Meetup Kansai #3 #dockerkansai May 24, 2019 Dockerfileを改善するための Best Practice 2019年版
  • 2.
    DockerCon SF19 での発表に基づく内容 •Dockerfile Best Practices https://www.slideshare.net/Docker/dcsf19-dockerfile-best-practices 2 • 動画もご覧ください https://www.docker.com/dockercon/2019-videos?watch=dockerfile-best-practices オリジナルの発表は Tibor Vass 氏( @tiborvass) および Sebastian van Stijin 氏(@thajeztha) による、 DockerCon 毎回恒例人気セッション"Dockerfile Best Practices" であり、両者に感謝します。 I thank you both for your excellent presentation of DockerCon. I also appreciate your permission to translate the content. Please refer to this slides. And, this presentation video will also be helpful.
  • 3.
    このスライドは何? 3 ⚫ DockerCon 19で発表された人気シリーズ “Best Practices” を日本訳+解説の追加。 ⚫ Dockerfile の改善を通して、現在利用できる マルチ・ステージ・ビルドや BuildKit 紹介。 以上の内容です。 ※ "Docker Meetup Kansai #3" 発表時のスライドをベースに、 スライドそのままでは分かりづらい部分があるため、 一部で発表時と異なる表現・補足説明を用いている場合があります。 ゴール: 「Dockerfileの改善を具体的に理解」
  • 4.
    Dockerfileのベストプラクティス • "Best practicesfor writing Dockerfiles" https://docs.docker.com/develop/develop-images/dockerfile_best-practices/ 4 最新の日本語訳を公開しました。 そもそもの基本となるのは、"Dockerfileを書くためのベストプラクティス"です。
  • 5.
  • 6.
    Dockerfile とは? 6 ド ッカ ー フ ァ イ ル
  • 7.
    Dockerfile • Docker はDockerfile から命令を読み込み、イメージを自動構築 • テキスト形式のドキュメント • docker build / docker image build で Dockerfile を読み込み構築 • 「Docker イメージを作るための設計図」 • FROM、ADD、CMD など命令文で構成 • 例) 何のイメージをもとに、何を実行するか? • 誰でも確実にイメージを構築できる • イメージの構築過程を確認できる • Dockerfileは広く使われている • GitHub上に Dockerfile は100万以上 7 automate build blueprint Image by John Dortmunder from Pixabay 「Dockerfile」はDockerイメージを自動構築するために、必ず使うファイルです。
  • 8.
    BuildKit: builder v2 •docker build は「イメージ・キャッシュ」があるため、素早く開発できる • しかし、いくつかの制限があったため、buildKit プロジェクトで根本的に構築 • https://github.com/moby/buildkit • "ゴールは Docker build のデフォルト" • BuildKit: 特長 • 同時へいれt性(concurrency) • 断片的なコンテキスト・アップロード (lazy context upload) • キャッシュの改良 • 新しい Dockerfile 機能の追加 8 ⚫並列性が無い ⚫docker run と同様に root で動作する必要性 ⚫ボリューム機能が無い 高速にイメージを作れる BuildKit という汎用ツールが開発途上です。
  • 9.
    Docker BuildKit を使う方法 •クライアントの環境変数 • Docker デーモン設定ファイル /etc/docker/daemon.json • 現時点では Windows は対応していない • Windows support coming soon! 9 { "features": { "buildkit": true }} export DOCKER_BUILDKIT=1 BiuldKit は Docker とは別のプロジェクト (https://github.com/moby/buildkit ) ですが、 現在の Docker CE v18.09 では、BuildKit の一部機能が既に利用できる状態です。
  • 10.
  • 11.
    Dockerfile の改善領域 • 増大するbuild時間 • イメージ容量 • 保守性 • 安全性 • 一貫性・反復性 11 (Incremental) build time Image size Maintainability Security Consistency/Repetability ここからは、先日開催された DockerCon SF19での発表サンプルをベースにご紹介します。 サンプルのアプリケーションを使い、5つの視点で Dockerfile を改善する流れをみていきましょう。
  • 12.
    サンプルの Dockerfile をカイゼンするぞ! 12 FROMdebian COPY . /app RUN apt-get update RUN apt-get –y install openjdk-8-jdk ssh emacs CMD ["java", "-jar", "/app/target/app.jar"] これは、Javaで"Hello world"を表示するための、サンプルプログラム用の Dockerfile です。
  • 13.
    サンプルの Dockerfile をカイゼンするぞ! 13 FROMdebian COPY . /app RUN apt-get update RUN apt-get –y install openjdk-8-jdk ssh emacs CMD ["java", "-jar", "/app/target/app.jar"] vim まずすべきは、emacs を消して、vim を入れましょう。もちろんジョークですが!
  • 14.
    サンプルの Dockerfile をカイゼンするぞ! 14 FROMdebian COPY . /app RUN apt-get update RUN apt-get –y install openjdk-8-jdk ssh emacs CMD ["java", "-jar", "/app/target/app.jar"] vim まずすべきは、emacs を消して、vim を入れましょう。もちろんジョークですが!
  • 15.
  • 16.
    キャッシュする順番が重要 16 FROM debian COPY ./app RUN apt-get update RUN apt-get –y install openjdk-8-jdk ssh vim COPY . /app CMD ["java", "-jar", "/app/target/app.jar"] 頻繁に変更するものを後ろへ 構築時のキャッシュとは、変更箇所があれば破棄されます。この例の COPY では「.」(カレント)にある ファイルに変更があれば、毎回「apt-get update」と「install」が走るので、時間がかかってしまいます。 せっかくあrキャッシュを有効活用するには、頻繁に更新する可能性があるものを後ろにおきます。
  • 17.
    キャッシュする順番が重要 17 FROM debian COPY ./app RUN apt-get update RUN apt-get –y install openjdk-8-jdk ssh vim COPY . /app CMD ["java", "-jar", "/app/target/app.jar"] 頻繁に変更するものを後ろへ 構築時のキャッシュとは、変更箇所があれば破棄されます。この例の COPY では「.」(カレント)にある ファイルに変更があれば、毎回「apt-get update」と「install」が走るので、時間がかかってしまいます。 せっかくあrキャッシュを有効活用するには、頻繁に更新する可能性があるものを後ろにおきます。 キャッシュ有効 キャッシュ破棄 … ホスト側「 ./ 」に変更時 キャッシュ破棄 … ホスト側「 ./ 」に変更時 時間がかかるので、キャッシュ活用すべし 時間かかる
  • 18.
    キャッシュ破棄の影響を避けるため、範囲を狭く 18 FROM debian RUN apt-getupdate RUN apt-get –y install openjdk-8-jdk ssh vim COPY . /app COPY target/app.jar /app CMD ["java", "-jar", "/app/target/app.jar"] コピーに必要なものを明示する。 可能であれば "COPY ."を避ける コピーすべき場所を絞っておけば、キャッシュは破棄されません。 この例では「app.jar」しか「COPY」しませんので、他のファイルに変更があってもキャッシュを保持。
  • 19.
    キャッシュ破棄の影響を避けるため、範囲を狭く 19 FROM debian RUN apt-getupdate RUN apt-get –y install openjdk-8-jdk ssh vim COPY . /app COPY target/app.jar /app CMD ["java", "-jar", "/app/target/app.jar"] コピーすべき場所を絞っておけば、キャッシュは破棄されません。 この例では「app.jar」しか「COPY」しませんので、他のファイルに変更があってもキャッシュを保持。 キャッシュ有効 キャッシュ有効 キャッシュ有効 ちょっとキャッシュする範囲が拡がるかもね キャッシュ対象の明確化 コピーに必要なものを明示する。 可能であれば "COPY ."を避ける
  • 20.
    行をまとめる: apt-get update& install 20 FROM debian RUN apt-get update RUN apt-get –y install openjdk-8-jdk ssh vim RUN apt-get update ¥ && apt-get -y install ¥ openjdk-8-jdk ssh vim COPY target/app.jar /app CMD ["java", "-jar", "/app/target/app.jar"] 古いパッケージ情報のキャッシュ利用を避ける パッケージ・マネージャを使う場合、「古い」パッケージ情報をキャッシュしがちです。 更新したいのに更新できないのを避けるには、情報の更新と、パッケージ追加・削除をまとめること。
  • 21.
    行をまとめる: apt-get update& install 21 FROM debian RUN apt-get update RUN apt-get –y install openjdk-8-jdk ssh vim RUN apt-get update ¥ && apt-get -y install ¥ openjdk-8-jdk ssh vim COPY target/app.jar /app CMD ["java", "-jar", "/app/target/app.jar"] 古いパッケージ情報のキャッシュ利用を避ける パッケージ・マネージャを使う場合、「古い」パッケージ情報をキャッシュしがちです。 更新したいのに更新できないのを避けるには、情報の更新と、パッケージ追加・削除をまとめること。 キャッシュ有効 古いものをキャッシュしてるかも! 時間かかる場合があっても、確実に処理
  • 22.
  • 23.
    不要な依存関係を削除 23 FROM debian RUN apt-getupdate ¥ && apt-get -y install --no-install-recommends ¥ openjdk-8-jdk ssh vim COPY target/app.jar /app CMD ["java", "-jar", "/app/target/app.jar"] デプロイを素早くするためには、イメージ容量の削減が重要であり、必須課題。 推奨パッケージ(必須ではない)を避けるためには、「--no-install-recommends」フラグを付ける。 Javaの実行に不要なパッケージも入れないので「ssh」「vim」も消す。
  • 24.
    不要なパッケージマネージャのキャッシュ削除 24 FROM debian RUN apt-getupdate ¥ && apt-get -y install –no-install-recommends ¥ openjdk-8-jdk ¥ && rm –rf /var/lib/apt/lists/* COPY target/app.jar /app CMD ["java", "-jar", "/app/target/app.jar"] パッケージ・マネージャのキャッシュ情報も、アプリケーションの実行に不要なので削除
  • 25.
  • 26.
    できるだけDocker公式(official)パッケージを使う • メンテナンスにかける時間を減らす (問題があるたびに、繰り返す更新) • 容量を減らす(イメージ間でのレイヤ共有によって) •コンテナとして使うために、予め設定済み • 賢い人達が構築している 26パッケージ・マネージャのキャッシュ情報も、アプリケーションの実行に不要なので削除
  • 27.
    27 FROM debian RUN apt-getupdate ¥ && apt-get -y install –no-install-recommends ¥ openjdk-8-jdk ¥ && rm –rf /var/lib/apt/lists/* FROM openjdk COPY target/app.jar /app CMD ["java", "-jar", "/app/target/app.jar"] できるだけDocker公式(official)パッケージを使う Javaの実行であれば、「openjdk」イメージがあるため、debianでセットアップする必要はない
  • 28.
    タグを明示 28 FROM openjdk:latest FROM openjdk:8 COPYtarget/app.jar /app CMD ["java", "-jar", "/app/target/app.jar"] "latest"タグは常に入れ替わる。 タグを指定しておけば、想定外の ベース・イメージ変更発生を防止。 何も指定しなければ「latest」(最新)になるため、常にタグ(主にバージョン)指定を忘れずに
  • 29.
  • 30.
    必要最小限のものを探す 30 REPOSITORY TAG SIZE openjdk8 624MB openjdk 8-jre 443MB openjdk 8-jre-slim 204MB openjdk 8-jre-alpie 83MB ベース・イメージの変更だけで 540MBも削減 どのイメージ(タグ)を選ぶかによって、容量がかなり異なる。 alpineタグは Alpine Linux という約 5MB の Linux ディストリビューションがベース
  • 31.
  • 32.
  • 33.
    一貫した環境を、ソースから構築する • Dockerfile を設計図(青写真)にしよう: •ビルド時するための構築環境を Dockerfile に記述 • 正しいバージョンのビルド・ツールをインストール • 環境ごとの違いを発生させない • システム依存はあるかもしれない • "source of truth"(本当のソース)とは、 ソースコードである。ビルド成果物ではない。 33 Image by John Dortmunder from Pixabay blue print 開発環境、テスト環境、実行環境で共通する Dockerfile を目指す
  • 34.
    一貫した環境を、ソースから構築する 34 FROM openjdk:8-jre-alpine FROM maven:3.6-jdk-8-alpine WORKDIR/app COPY app.jar/app COPY pom.xml . COPY src ./src RUN mvn –e –B package CMD ["java", "-jar", "/app/target/app.jar"] openjdk にかわり、Javaプロジェクト管理ツールであるMavenを開発環境として入れる
  • 35.
    一貫した環境を、ソースから構築する 35 FROM maven:3.6-jdk-8-alpine WORKDIR /app COPYpom.xml . COPY src ./src RUN mvn –e –B package CMD ["java", "-jar", "/app/target/app.jar"] このように、開発環境向けの Dockerfile を整えていく。
  • 36.
    依存関係の解決は、ステップを分ける 36 FROM maven:3.6-jdk-8-alpine WORKDIR /app COPYpom.xml . RUN mvn –e –B dependency:resolve COPY src ./src RUN mvn –e –B package CMD ["java", "-jar", "/app/target/app.jar"] 開発関係だけに必要な依存関係を追加。
  • 37.
    構築時のみの依存関係が判明 37 FROM maven:3.6-jdk-8-alpine WORKDIR /app COPYpom.xml . RUN mvn –e –B dependency:resolve COPY src ./src RUN mvn –e –B package CMD ["java", "-jar", "/app/target/app.jar"] しかし、この黄色い部分は「開発環境の構築」段階しか使わないものであり、本番稼働では無駄
  • 38.
    マルチ・ステージ・ビルドで、構築時の部分を削除 38 FROM maven:3.6-jdk-8-alpine ASbuilder WORKDIR /app COPY pom.xml . RUN mvn –e –B dependency:resolve COPY src ./src RUN mvn –e –B package CMD ["java", "-jar", "/app/target/app.jar"] FROM openjdk:8-jre-alpine COPY --from=builder /app/target/app.jar / CMD ["java", "-jar", "/app/target/app.jar"] そこで、マルチ・ステージ・ビルドで複数の「FROM」を使い、構築時(AS bilder)と実行時を分離 ←「builder」ステージから ファイルをコピー ←ステージ「builder」と名前を付ける
  • 39.
    マルチ・ステージ・ビルドで、構築時の部分を削除 39 FROM maven:3.6-jdk-8-alpine ASbuilder WORKDIR /app COPY pom.xml . RUN mvn –e –B dependency:resolve COPY src ./src RUN mvn –e –B package FROM openjdk:8-jre-alpine COPY --from=builder /app/target/app.jar / CMD ["java", "-jar", "/app/target/app.jar"] マルチ・ステージ・ビルドでは、「docker build」は全てのビルドを実行しますし、 「docker build –target builder」と指定すると「AS builder」の「FROM」ステージしか処理しません。
  • 40.
  • 41.
    プロジェクトごとに多くのステージがある • Moby: 16ステージ https://github.com/moby/moby/blob/master/Dockerfile • BuildKit: 44 ステージ https://github.com/moby/buildkit/blob/master/hack/dockerfiles /test.buildkit.Dockerfile 41Docker 関連プロジェクトの Dockerfile にも、多くのステージ(FROM命令)がある
  • 42.
    マルチ・ステージの利用例 • 実行環境と構築(ビルド)環境を分ける(イメージ容量の縮小) • イメージに対する影響を最小限に(DRY) •構築・開発・テスト・構文チェック…のような環境を明示 • 依存関係を一直線に処理しない(並列化) • プラットフォーム固有のステージ 42 一般的な目的は、最終成果物の容量を削減
  • 43.
    構築ステージを --target で指定 43 FROMimage_or_stage AS stage_name … $ docker build --target stage_name 「AS ステージ名」を FROM 命令に書いておけば、「docker build」時に「--target」でステージを指定
  • 44.
    イメージの flavor を変える 44 FROMmaven:3.6-jdk-8-alpine AS builder ... FROM openjdk:8-jre-jessie AS release-jessie COPY --from=builder /app/target/app.jar / CMD ["java", "-jar", "/app.jar"] FROM openjdk:8-jre-jessie AS release-alpine COPY --from=builder /app/target/app.jar / CMD ["java", "-jar", "/app.jar"] $ docker build --target release-jessie . Debian jessie (8.x) をベースとするイメージと、Alpine Linux をベースとするイメージ。 特色(風味)
  • 45.
    45 FROM maven:3.6-jdk-8-alpine ASbuilder ... FROM openjdk:8-jre-jessie AS release-jessie COPY --from=builder /app/target/app.jar / CMD ["java", "-jar", "/app.jar"] FROM openjdk:8-jre-jessie AS release-alpine COPY --from=builder /app/target/app.jar / CMD ["java", "-jar", "/app.jar"] $ docker build --target release-jessie . イメージの flavor を変える 特色(風味) しかし、 Dockerfile をよく見ると問題があり、
  • 46.
    46 FROM maven:3.6-jdk-8-alpine ASbuilder ... FROM openjdk:8-jre-jessie AS release-jessie COPY --from=builder /app/target/app.jar / CMD ["java", "-jar", "/app.jar"] FROM openjdk:8-jre-jessie AS release-alpine COPY --from=builder /app/target/app.jar / CMD ["java", "-jar", "/app.jar"] $ docker build --target release-jessie . このように各ステージで、同じ命令が重複する箇所がある。 イメージの flavor を変える 特色(風味) どちらも 同じ
  • 47.
    47 ARG flavor=alpine FROM maven:3.6-jdk-8-alpineAS builder ... FROM openjdk:8-jre-$flavor AS release COPY --from=builder /app/target/app.jar / CMD ["java", "-jar", "/app.jar"] $ docker build --target release --build-arg flavor=jessie . 「ARG」を Dockerfile で指定しておくと、docker build 時に「--build-arg」を通して、変数のように展開 --build-arg で $flavor=xxx があれば、$flavor に xxx 代入 ↓何もなければ「flavor=alpine」が適用 イメージの flavor を変える (DRY / 汎用的な ARG) 特色(風味) ※ DRY = “Don’t Repeat Yourself”(自分では繰り返さない)という ソフトウェア開発の手法 引数 ←ARG命令は、docker build時に指定できる変数を定義 書式は「変数名」または「変数名=デフォルト値」。
  • 48.
    様々な環境:構築、開発、テスト、構文チェック(lint)… • ステージとしての検討例: ⁃ builder:依存関係すべてをビルド ⁃ build(または binary): builder + ビルド成果物 ⁃ cross: 複数のプラットフォーム向けに構築 ⁃ dev: build(er) + 開発/デバッグツール ⁃ lint: 最小限の構文チェック用依存関係 ⁃ test: テストに関係する全ての依存関係 + テスト対象のビルド成果物 ⁃ release: ビルド成果物を含む、最終的な最小イメージ 48様々な「ステージ」が検討できる。ここでは定型的な例 各ステージでは、依存関係が最小となるようにする
  • 49.
    様々な環境:構築、開発、テスト、構文チェック(lint)… 49 FROM maven:3.6-jdk-8-alpine ASbuilder ... FROM openjdk:8-jre-alpine AS lint RUN wget https://github.com/checkstyle/checkstyle/releases/download/checkstyle-8.15/checkstyle-8.15-all.jar COPY checks.xml . COPY src /src RUN java –jar checkstyle-8.15-all.jar –c checks.xml /src これはシンプルな Java 構文チェック(リント)の確認用の Dockerfile を「AS lint」と指定
  • 50.
    様々な環境:構築、開発、テスト、構文チェック(lint)… 50 FROM maven:3.6-jdk-8-alpine ASbuilder ... FROM openjdk:8-jre-alpine AS release COPY --from=builder /app/target/app.jar / CMD ["java", "-jar", "/app.jar"] FROM builder AS dev RUN apk add --no-cache strace vim tcpdump ENTRYPOINT ["ash"] 開発用デバッグ環境であれば、「builder」ステージをベースにしながら「AS dev」と明示し シンプルにエディタ(vim)と tcpdump を入れている
  • 51.
    様々な環境:構築、開発、テスト、構文チェック(lint)… 51 FROM maven:3.6-jdk-8-alpine ASbuilder ... RUN mvn –e –B package –DskipTests FROM builder AS unit-test RUN mvn –e –B test FROM release AS integration-test RUN apk add --no-cache curl RUN ./test/run.sh 他にも単体テスト、統合テスト用の環境も作れます。
  • 52.
  • 53.
    一直線の Dockerfile ステージから… • 全てのステージが順番(シーケンシャル)に実行 • この図では、上から下に一直線 • BuildKitがなければ、 不要なステージも無駄に実行し、破棄する (無駄に時間がかかった) 53 s1 s2 s3 s4 s5 s6 デフォルトでは、全てのステージを順番に実行する
  • 54.
    BuildKit のマルチ・ステージ graphへ • BuildKit は下(--target のステージ名)から上に辿っていくような流れ • 右図では s2, s3, s4 ステージを同時並行処理 • 不要なステージは無視できる • 右図では s5 がビルド時に 不要であれば、何もしない 54 s1 s2 s3 s4 s5s6 Docker 17.05 からマルチ・ステージ・ビルドが利用可能になった。 18.06までは experimental 、18.09 は利用できるように組み込まれている グラフ ※グラフは点と点とのつながり (関係性)を表す
  • 55.
    Multi-srage: 並行ビルド (buildconcurrently) 55 FROM maven:3.6-jdk-8-alpine AS builder ... FROM tiborvass/whalesay AS assets RUN whalesay "Hello DockerCon!" > /out/assets.html FROM openjdk:8-jre-alpine AS release COPY --from=builder /app/target/app.jar / COPY --from=assets /out /assets CMD ["java", "-jar", "/app.jar"] 「assets」は最終イメージ(release)に必要だが、別のステージとして処理できる そして、この Dockerfile は「builder」と「assets」のステージからのコピーを並行処理
  • 56.
    Multi-srage: 並行ビルド (buildconcurrently) 56 FROM maven:3.6-jdk-8-alpine AS builder-base … FROM gcc:8-alpine AS builder-someClib … RUN git clone … ¥ ./configure --prefix=/out && make && make install FROM g++:8-alpine AS builder-someCPPlib … RUN git clone … ¥ cmake … FROM builder-base AS builder COPY --from=builder-someClib /out / COPY --from=builder-someCPPlib /out / 複数の COPY 命令を使う時には、特に効果を発揮し、時間を節約できる 並行処理が有用な典型的なパターンが 複数の COPY --from … の繰り返し
  • 57.
    ベンチマーク • github.com/moby/moby Dockerfile,master ブランチ • 小さいほうが優れている 57 Dockerfile Best Practices https://www.slideshare.net/Docker/dcsf19-dockerfile-best-practices/52 v18.03 の docker build と BuiltKit では2倍の速さ
  • 58.
    ベンチマーク • github.com/moby/moby Dockerfile,master ブランチ • 小さいほうが優れている 58 Dockerfile Best Practices https://www.slideshare.net/Docker/dcsf19-dockerfile-best-practices/52 キャッシュを有効にすると、7倍も速くなる
  • 59.
    ベンチマーク • github.com/moby/moby Dockerfile,master ブランチ • 小さいほうが優れている 59 Dockerfile Best Practices https://www.slideshare.net/Docker/dcsf19-dockerfile-best-practices/52 最も大事なのは、ソースコードの変更時。再構築の速度はマルチ・ステージ・ビルドで 2.5 倍に改善。
  • 60.
  • 61.
    新機能を有効にするには? 61 # syntax=docker/dockerfile:1.0-experimental FROM maven:3.6-jdk-8-alpineAS builder WORKDIR /app COPY . /app RUN mvn -e –B package FROM openjdk:8-jre-alpine COPY --from=builder /app/target/app.jar / CMD ["java", "-jar", "/app.jar"] Dockerfile の新機能を有効にするには、この行を1行目に追加する必要がある ※experimental というのは、まだ Dockerfile の正式な構文では ないため
  • 62.
  • 63.
    コンテキスト・マウント(v18.09+ w/ BuildKit) 63 #syntax=docker/dockerfile:1.0-experimental FROM maven:3.6-jdk-8-alpine AS builder WORKDIR /app COPY . /app RUN --mount=target=. mvn -e –B package -DoutputDirectory=/ FROM openjdk:8-jre-alpine COPY --from=builder /app/app.jar / CMD ["java", "-jar", "/app.jar"] 新機能を使って Dockerfile を書き換えていく。まずはマウント・オプション。 実行時に「.」(カレント・ディレクトリ内容)を Build Context として送信せず、マウントできる ※ Docker v18.09 以上かつ、BuildKit 有効化の状態で利用可能 contest mounts
  • 64.
    Dockerfile hello コンテキスト・マウント(v18.09+ w/ BuildKit) 64 ディレクトリ内容をコンテナにCOPY しなくても、 ビルド時に docker build コマンドを実行しているディレクトリ(ここでは 「target=.」で指定)を rw(読み書き可能な状態)で直接コンテナにマウントし、 コンテナ内で「cat hello > /hello.txt」を実行 → 構築したイメージの“/hello.txt” に “Hello world!” が書かれたファイルが作成される # syntax=docker/dockerfile:1.0-experimental FROM alpine RUN --mount=type=bind,target=.,rw cat hello > /hello.txt Hello world! ソースコードなど、ビルド・コンテクストのコピーが不要となり、より早いビルドができる contest mounts • Dockerfile の mount 例: ホスト側の docker build する場所に、 このファイルがあるとします
  • 65.
    65 コンテキスト・マウント(v18.09+ w/ BuildKit) contestmounts 従来 “COPY . /app” BuildKit Dockerfile app用ディレクトリ Build用ディレクトリ “docker build” 時、 「.」 以下の内容を Docker イメージ用レイヤとして構築するため 全て Docker に対して送る必要があった ※結果として Docker イメージの不本意な増加 ※あるいはマルチ・ステージ・ビルドで回避 Dockerfile app用ディレクトリ Build用ディレクトリ コンテナ コピー マウント よいしょ… みーてーるーだーけー “docker build” の特定ステップのみ参照するため、 ・ build 全体の時間を削減 ・ イメージ・レイヤの肥大化を抑制
  • 66.
    キャッシュの保持(BuildKitが無ければ) 66 FROM maven:3.6-jdk-8-alpine ASbuilder WORKDIR /app COPY pom.xml . RUN mvn -e –B dependency: resolve COPY src ./src RUN mvn –e –B package CMD ["java", "-jar", "/app.jar"] docker build 時にキャッシュが破棄されると、毎回依存関係の準備に時間がかかる
  • 67.
    キャッシュの保持(v18.09+ w/ BuildKit) 67 #syntax=docker/dockerfile:1.0-experimental FROM maven:3.6-jdk-8-alpine AS builder WORKDIR /app RUN --mount=target=. --mount=type=cache,target=/root/.m2 ¥ && mvn package –DoutputDirectory=/ FROM openjdk:8-jre-alpine COPY --from=builder /app.jar / CMD ["java", "-jar", "/app.jar"] apt: /var/lib/apt/lists go: ~/.cache/go-build go-modules: $GOPATH/pkg/mod npm: ~/.npm pip: ~/.cache/pip キャッシュ用ディレクトリをマウントできるので、依存関係の準備にかかる時間を削減できる
  • 68.
    キャッシュの保持 • --mount=type=cache 例: 68 Dockerfile #syntax=docker/dockerfile:1.0-experimental FROM ubuntu RUN --mount=type=cache,target=/var/cache/apt ¥ --mount=type=cache,target=/var/lib/apt ¥ apt-get update && apt-get install -y wget curl 「type=cache」で指定した「target」のディレクトリは、docker build を実行したホスト上でキャッシュ この例では apt-get install を含む「RUN」命令を書き換えたとしても、 一度キャッシュ(ビルド)済みであれば次回から高速なビルドが可能になる キャッシュ情報をクリアするには「docker builder prune」コマンドを実行する
  • 69.
  • 70.
    シークレット(これはダメな方法) 70 FROM baseimage RUN … ENVAWS_ACCESS_KEY_ID=… ENV AWS_SECRET_ACCESS_KEY=… RUN ./fetch-aseets-from-s3.sh RUN ./build-scripts.sh シークレット(secret)とは、パスワードやAPIキー、SSH 鍵などの認証情報(機微情報) ENV に書くと docker history で丸見えですし、
  • 71.
    シークレット(これもダメな方法) 71 FROM baseimage RUN … ARGAWS_ACCESS_KEY_ID ARG AWS_SECRET_ACCESS_KEY RUN ./fetch-aseets-from-s3.sh RUN ./build-scripts.sh $ docker build --build-arg ¥ AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID … ARGでも途中のRUNで変数の情報をダンプできますし、 シェルのhistoryからも辿れるリスクが出てきます(記録しない方法もありますが、運用カバー系作業) docker history
  • 72.
    シークレット(v18.09+ w/ BiuldKitでは、こうします) 72 # syntax=docker/dockerfile:1-experimental FROM baseimage RUN … RUN --mount=type=secret,id=aws,target=/root/.aws/credentials, required ./fetch-assets-from-s3.sh RUN ./build-scripts.sh $ docker build --secret id=aws,src=~/.aws/credentials . 最終イメージに混入させないための手法が「secret」としてのマウント。 対象ステップのみデータを参照できるよう一時的にマウントし、次のステップではマウントしません。 ↓idは docker build 時の –secret オプションで識別するため ↑targetで、この構築ステップのみ、 コンテナ内のこの場所に 一時的にファイルを設置する指示 ↑これはホスト側に存在するファイル。 ~より、 $HOME や絶対パスのほうが安全かも ↑次のステップでは、先の id=aws でマウントしたシークレットは見えない( 0 byte のファイル残骸のみ)
  • 73.
  • 74.
    • Dockerfile のmount secret 例: Dockerfile $HOME/secret シークレット・マウント(v18.09+ w/ BiuldKit) 74 # syntax=docker/dockerfile:1.0-experimental FROM alpine RUN --mount=type=secret,id=check,target=/root/secret,required ¥ cat /root/secret > /data.txt MYID=secretpass Dockerfile で当該 RUN 命令行のビルド時のみ、一時的にホスト側ファイルをマウントできる secret mounts ホスト側にIDとパスワードのような、 いわゆるシークレット(機微情報)を記録するファイルを設置 もちろん、パーミッションは 600 にするなど、十分なセキュリティ配慮が必要 $ docker build --secret id=check,src=$HOME/.data/credentials -t myimage . ↑srcで指定したパスはホスト上のファイル
  • 75.
    プライベート git リポジトリ(ダメぜったい) 75 FROMbaseimage COPY ./keys/private.pem /root/.ssh/private.pem ARG REPO_REF=19ba8bcd9976ef8a9bd086187df19ba7bcd997f2 RUN git clone git@githubcom:org/repo /work && cd /work ¥ && git checkout –b $REPO_REF これもイメージ内に大事なファイルが残ってしまう NG 例
  • 76.
    プライベート git リポジトリ(v18.09+w/BuildKitの場合) 76 FROM alpine RUN apk add --no-cache openssh-client RUN mkdir –p –m 0700 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts ARG REPO REF=19ba8bcd9976ef8a9bd086187df19ba7bcd997f2 RUN –-mount-type=ssh,required ¥ git clone git@github.com:org/repo /work && cd /work ¥ && git checkout –b $REPO_REF $ eval $(ssh-agent) $ ssh-add ~/.ssh/id_rsa $ docker build --ssh=default . この RUN 命令実行時のみ、 ホスト側の SSH 認証情報を読み込む方法が 利用できる
  • 77.
    BuildKit 77 SSH マウント(v18.09+ w/BiuldKit) mounts Dockerfile .ssh Build用ディレクトリ コンテナ 鍵情報 参照 このステップがある間だけ みーてーるーだーけー “docker build” の対象構築ステップ時のみ、 ホスト側のSSH agent 情報を一時的にマウント シークレット・マウントに近いけれど SSH の利用に特化 こんてなから GitHub や SSH 接続したい場合に有用 docker build id_rsa ssh-agent --mount=type=ssh SSH リモート・ホストや GitHub / GitLab 等
  • 78.
    • Dockerfile のmount=SSH 例: Dockerfile SSH マウント(v18.09+ w/ BiuldKit) 78 # syntax=docker/dockerfile:1.0-experimental FROM alpine RUN apk update && apk add openssh RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan <host_IP> > ~/.ssh/known_hosts RUN --mount=type=ssh,required ¥ ssh <user>@<host_IP> ls -al / > /ls.txt GitHubやGitLabの認証だけでなk、別のホストにこのようにしてログイン&操作も可能 mounts $ eval $(ssh-agent) $ ssh-add ~/.ssh/id_rsa (パスフレーズを入力) $ docker build --ssh default=$SSH_AUTH_SOCK -t <image_name> .
  • 79.
    Dockerfile 改善のまとめ • 従来 ⁃構築・開発・テスト環境の矛盾 ⁃ 膨れあがるイメージ容量 ⁃ 構築時間がマシマシ(キャッシュ無効)になり時間がかかる ⁃ 構築が安全ではない • これから( BuildKit の活用によって) ⁃ 構築・開発・テスト環境が一致 ⁃ イメージ容量は最小 ⁃ 構築が非常に速く、構築回数が増えていく ⁃ より安全に構築できる 79 export DOCKER_BUILDKIT=1重要なのは有効化に 新しいビルド時の一時マウント可能な bind、cache、tmpfs、secret、ssh マルチ・ステージ・ビルドの活用によって、段階ごとの FROM ステージ間のコピー機能により、最終成果物を小さく 並列ビルドの活用により、従来よりも素早い構築
  • 80.
    参考資料 • Advanced multi-stagebuild patterns – Tõnis Tiigi – Medium https://medium.com/@tonistiigi/advanced-multi-stage-build-patterns-6f741b852fae • Build secrets and SSH forwarding in Docker 18.09 – Tõnis Tiigi – Medium https://medium.com/@tonistiigi/build-secrets-and-ssh-forwarding-in-docker-18-09- ae8161d066 • Introducing BuildKit – Moby Blog https://blog.mobyproject.org/introducing-buildkit-17e056cc5317 • docker builder prune | Docker Documentation https://docs.docker.com/engine/reference/commandline/builder_prune/ • Docker v18.09 新機能 (イメージビルド&セキュリティ) – nttlabs – Medium https://medium.com/nttlabs/docker-v18-09-%E6%96%B0%E6%A9%9F%E8%83%BD- %E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8%E3%83%93%E3%83%AB%E3%83%89- %E3%82%BB%E3%82%AD%E3%83%A5%E3%83%AA%E3%83%86%E3%82%A3- 9534714c26e2 80
  • 81.
  • 82.
    DockerCon での発表 • DockerfileBest Practices https://www.slideshare.net/Docker/dcsf19-dockerfile-best-practices 82 • 動画 https://www.docker.com/dockercon/2019-videos?watch=dockerfile-best-practices
  • 83.
    Q&A • 何か気になるところはありますか? • Twitter:@zembutsu • https://slideshare.net/zembutsu • Dockerドキュメント日本語訳 http://docs.docker.jp • Docker Composeドキュメント日本語訳 http://docs.docker.jp/compose/ • 公式ドキュメント https://docs.docker.com 83