これらの Dockerfile のベスト プラクティスで開発フローをスピードアップ

ドッカーファイルは、ドッカーイメージを作成するための開始点です。 ファイル形式には、ファイルやフォルダーのコピー、コマンドの実行、環境変数の設定、およびコンテナー イメージの作成に必要なその他のタスクを実行できる、明確に定義された一連のディレクティブが用意されています。 結果のイメージを安全かつ小さく、すばやく構築し、すばやく更新するために、Dockerfileを適切に作成することが非常に重要です。

この投稿では、開発フローを高速化し、ビルドの再現性を確保し、自信を持って運用環境にデプロイできるイメージを生成するための優れたDockerfileを作成する方法について説明します。

注:このブログ投稿では、Dockerfileの例を 、awesome-compose リポジトリの react-java-mysqlサンプル に基づいています。

開発の流れ

開発者として、開発環境をターゲットの運用コンテキストにできるだけ一致させて、ビルドしたものがデプロイ時に機能するようにしたいと考えています。


また、迅速に開発できるようにしたいので、ビルドを高速化し、デバッガーなどの開発者ツールを使用できるようにする必要があります。 コンテナは開発環境を体系化するための優れた方法ですが、コンテナとすばやく対話できるようにするには、Dockerfileを正しく定義する必要があります。

インクリメンタルビルド

Dockerfile は、コンテナー イメージをビルドするための手順の一覧です。 Docker ビルダーは各ステップの結果をイメージ レイヤーとしてキャッシュしますが、キャッシュを無効にすると、キャッシュを無効にしたステップと後続のすべてのステップを再実行し、対応するレイヤーを再生成する必要があります。


キャッシュは、COPY または ADD によって参照されるビルド コンテキスト内のファイルが変更されると無効になります。 したがって、ステップの順序は、パフォーマンスに大きな影響を与える可能性があります。

Dockerfile で NodeJs プロジェクトをビルドする例を見てみましょう。 このプロジェクトでは、npm ci コマンドの実行時にフェッチされる依存関係が package.json ファイルにあります。

最も単純なドッカーファイルは次のようになります。

FROM node:lts

ENV CI=true
ENV PORT=3000

WORKDIR /code
COPY . /code
RUN npm ci

CMD [ "npm", "start" ]

上記のように Dockerfile を構成すると、ビルド コンテキスト内のファイルが変更されるたびに COPY 行でキャッシュが無効になります。 これは、時間がかかる可能性のあるpackage.jsonファイルだけでなく、ファイルが変更されると依存関係がフェッチされ、node_modulesディレクトリがいっぱいになることを意味します。

これを回避し、依存関係が変更されたとき(つまり、package.jsonまたはpackage-lock.jsonが変更されたとき)にのみ依存関係をフェッチするには、依存関係のインストールをアプリケーションのビルドと実行から分離することを検討する必要があります。

より最適化されたドッカーファイルは次のようになります。

FROM node:lts

ENV CI=true
ENV PORT=3000

WORKDIR /code
COPY package.json package-lock.json /code/
RUN npm ci
COPY src /code/src

CMD [ "npm", "start" ]

この分離を使用すると、package.json または package-lock.json に変更がない場合、キャッシュは RUN npm ci 命令によって生成されたレイヤーに使用されます。 つまり、アプリケーションソースを編集して再構築するときに、依存関係が再ダウンロードされないため、時間が🎉節約されます。

また、 以前の投稿で 説明したように、 src 2番目 COPY をディレクトリに制限します。

ホストとコンテナーの間でライブ リロードをアクティブにしておく

このヒントはDockerfileとは直接関係ありませんが、コンテナでアプリを実行し、ホストマシン上のIDEからソースコードを変更している間、ライブリロードをアクティブにしておくにはどうすればよいですか?

この例では、プロジェクトディレクトリをコンテナにマウントし、環境変数を渡して、ホストからのNodeJSファイル変更イベントをラップする Chokidar を有効にする必要があります。

$ docker run -e CHOKIDAR_USEPOLLING=true  -v ${PWD}/src/:/code/src/ -p 3000:3000 repository/image_name

一貫性のあるビルド

Dockerfileで最も重要なことの1つは、同じビルドコンテキスト(ソース、依存関係など)からまったく同じイメージをビルドすることです。

前のセクションで定義した Dockerfile を引き続き改善していきます。

ソースから一貫して構築する

前のセクションで見たように、Dockerfile の説明にソース ファイルと依存関係を追加し、それらに対してコマンドを実行することで、アプリケーションを構築できます。


しかし、前の例では、Dockerビルドを実行するたびに生成されたイメージが同じであることを確認できません...なぜでしょうか。 NodeJSがリリースされるたびに、ltsタグがNodeJSイメージの最新のLTSバージョンを指していることが予想されますが、これは時間の経過とともに変更され、破壊的変更が発生する可能性があります。 ベースイメージにもっと具体的なタグを使用することで、これを簡単に修正できます(LTSまたは最新の安定バージョン😉から選択できます)

FROM node:13.12.0

ENV CI=true
ENV PORT=3000

WORKDIR /code
COPY package.json package-lock.json /code/
RUN npm ci
COPY src /code/src

CMD [ "npm", "start" ]

「最新のタグはありません」セクションでは、より具体的な基本画像タグを使用し、 最新のタグ を回避することには他にも利点があることがわかります。

適切な環境に合わせたマルチステージとターゲット

開発ビルドに一貫性を持たせましたが、本番アーティファクトに対してこれをどのように行うことができますか?

Docker 17.05以降、 マルチステージビルド を使用して、最終的なイメージを生成するためのステップを定義できます。 Dockerfile でこのメカニズムを使用すると、開発フローに使用するイメージを、アプリケーションのビルドに使用するイメージと運用環境で使用するイメージを分割できます。

FROM node:13.12.0 AS development

ENV CI=true
ENV PORT=3000

WORKDIR /code
COPY package.json package-lock.json /code/
RUN npm ci
COPY src /code/src

CMD [ "npm", "start" ]

FROM development AS builder

RUN npm run build

FROM nginx:1.17.9 AS production

COPY --from=builder /code/build /usr/share/nginx/html

あなたが見る FROM たびに...... AS それはビルド段階です。
これで、開発、ビルド、および本番の段階ができました。
フラグを使用して --target 特定の開発ステージイメージを構築することで、開発フローにコンテナを引き続き使用できます。

$ docker build --target development -t repository/image_name:development .

そしていつものようにそれを使用してください

$ docker run -e CHOKIDAR_USEPOLLING=true -v ${PWD}/src/:/code/src/ repository/image_name:development

フラグのない --target Docker ビルドでは、最終ステージ (この場合は運用イメージ) がビルドされます。 私たちのプロダクションイメージは、前の手順でビルドされたバイナリが提供される正しい場所に置かれた単なる nginx イメージです。

生産準備完了

本番イメージを可能な限り無駄なく安全に保つことは非常に重要です。 運用環境でコンテナーを実行する前に確認する必要があることがいくつかあります。

最新のイメージ バージョンはもうありません

ソースから一貫してビルドする」セクションで前述したように、ビルド ステップに特定のタグを使用すると、イメージのビルドを再現可能にするのに役立ちます。 画像に特定のタグを使用する理由は、他にも少なくとも2つあります。 

  • お気に入りのオーケストレーター (Swarm、Kubernetes...) のイメージ バージョンで実行されているすべてのコンテナーを簡単に見つけることができます。

# Search in Docker engine containers using our repository/image_name:development image

$ docker inspect $(docker ps -q) | jq -c '.[] | select(.Config.Image == "repository/image_name:development") |"\(.Id) \(.State) \(.Config)"'

"89bf376620b0da039715988fba42e78d42c239446d8cfd79e4fbc9fbcc4fd897 {\"Status\":\"running\",\"Running\":true,\"Paused\":false,\"Restarting\":false,\"OOMKilled\":false,\"Dead\":false,\"Pid\":25463,\"ExitCode\":0,\"Error\":\"\",\"StartedAt\":\"2020-04-20T09:38:31.600777983Z\",\"FinishedAt\":\"0001-01-01T00:00:00Z\"}
{\"Hostname\":\"89bf376620b0\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":true,\"AttachStderr\":true,\"ExposedPorts\":{\"3000/tcp\":{}},\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"CHOKIDAR_USEPOLLING=true\",\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"NODE_VERSION=12.16.2\",\"YARN_VERSION=1.22.4\",\"CI=true\",\"PORT=3000\"],\"Cmd\":[\"npm\",\"start\"],\"Image\":\"repository/image_name:development\",\"Volumes\":null,\"WorkingDir\":\"/code\",\"Entrypoint\":[\"docker-entrypoint.sh\"],\"OnBuild\":null,\"Labels\":{}}"

#Search in k8s pods running a container with our repository/image_name:development image (using jq cli)
$ kubectl get pods --all-namespaces -o json | jq -c '.items[] | select(.spec.containers[].image == "repository/image_name:development")| .metadata'

{"creationTimestamp":"2020-04-10T09:41:55Z","generateName":"image_name-78f95d4f8c-","labels":{"com.docker.default-service-type":"","com.docker.deploy-namespace":"docker","com.docker.fry":"image_name","com.docker.image-tag":"development","pod-template-hash":"78f95d4f8c"},"name":"image_name-78f95d4f8c-gmlrz","namespace":"docker","ownerReferences":[{"apiVersion":"apps/v1","blockOwnerDeletion":true,"controller":true,"kind":"ReplicaSet","name":"image_name-78f95d4f8c","uid":"5ad21a59-e691-4873-a6f0-8dc51563de8d"}],"resourceVersion":"532","selfLink":"/api/v1/namespaces/docker/pods/image_name-78f95d4f8c-gmlrz","uid":"5c70f340-05f1-418f-9a05-84d0abe7009d"}

  • CVE(一般的な脆弱性と露出)の場合、コンテナとイメージの説明にパッチを適用する必要があるかどうかをすばやく知ることができます。

この例から、開発イメージと運用イメージが高山バージョンであることを指定できます。

FROM node:13.12.0-alpine AS development

ENV CI=true
ENV PORT=3000

WORKDIR /code
COPY package.json package-lock.json /code/
RUN npm ci
COPY src /code/src

CMD [ "npm", "start" ]

FROM development AS builder

RUN npm run build

FROM nginx:1.17.9-alpine

COPY --from=builder /code/build /usr/share/nginx/html

公式画像を使用する

Docker Hub を使用して、Dockerfile で使用する基本イメージを検索できますが、そのうちのいくつかは公式にサポートされているものです。 これらの画像を次のように使用することを強くお勧めします。

  • それらのコンテンツは検証されています
  • CVEが修正されると、すぐに更新されます

開発をスピードアップ

image_filter要求クエリパラメータを追加して、公式画像のみを取得できます。

https://hub.docker.com/search?q=nginx&type=image&image_filter=official

この投稿の以前の例はすべて、NodeJSとNGINXの公式画像を使用していました。

ちょうど十分な権限!

コンテナで実行されているかどうかにかかわらず、すべてのアプリケーションは、アプリケーションが必要なリソースにのみアクセスする必要があることを意味する 最小特権の原則 に従う必要があります。 

悪意のある動作の場合、またはバグのために、あまりにも多くの特権で実行されているプロセスは、実行時にシステム全体に予期しない結果をもたらす可能性があります。

NodeJSの公式イメージは適切に セットアップされているため、 バックエンドのDockerfileに切り替えます。

特権のないユーザーとして実行するようにイメージを構成するのは非常に簡単です。

FROM maven:3.6.3-jdk-11 AS builder
WORKDIR /workdir/server
COPY pom.xml /workdir/server/pom.xml
RUN mvn dependency:go-offline

RUN mvn package

FROM openjdk:11-jre-slim
RUN addgroup -S java && adduser -S javauser -G java
USER javauser

EXPOSE 8080
COPY --from=builder /workdir/server/target/project-0.0.1-SNAPSHOT.jar /project-0.0.1-SNAPSHOT.jar

CMD ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/project-0.0.1-SNAPSHOT.jar"]

新しいグループを作成し、それにユーザーを追加し、USERディレクティブを使用するだけで、root以外のユーザーでコンテナを実行できます。

結論

このブログ投稿では、Dockerfileを慎重に作成することで、Dockerイメージを最適化および保護する多くの方法のいくつかを示しました。 さらに進んでみたい場合は、以下をご覧ください。