やや複雑な構造を持つディレクトリにおける compose+Dockerfile の書き方でハマった話

By: 壮大 増田
Posted: October 14, 2021

この記事の概要

Note

こういうフォルダ構成

grpc-services/
  - services-1/
	  - microservice-a/
	    - docker/Dockerfile
	    - (*.py)
	    - run-server.sh
	  - microservice-b/
      - docker/Dockerfile
	    - (*.py)
	    - run-server.sh

microservice-controller/
  - docker/Dockerfile
  - (*.py)
  - run-server.sh

protos/
  - ms-proto/
    - xxxx.proto
  - build.sh
  - clean.sh

docker-compose.yml

docker-compose.yml の build セクション書くときのヒント

docker-compose.ymlのbuild設定はとりあえずcontextもdockerfileも埋めとけって話 - Qiita
docker-composeを使った開発では以下の2つのディレクトリ構成になっていることが多いです。 Dockerの コンテキスト という概念を知っていないと、ディレクトリ構成が違うだけで何度も コンテキスト周りのエラーで悩まされることがあります(1敗)。 なので自分的結論を出してみました。 docker-compose を使った開発では のようにDockerfileがあるディレクトリを指定するだけでなく のようにコンテキストをルートディレクトリに指定して、Dockerfileの場所も直接指定しておけばおk docker build コマンドを実行したときの、カレントなワーキングディレクトリのことを ビルドコンテキスト(build context)と呼びます。 デフォルトで Dockerfile は、カレントなワーキングディレクトリにあるものとみなされます。 ただしファイルフラグ(-f)を使って別のディレクトリとすることもできます。 Dockerfile が実際にどこにあったとしても、カレントディレクトリ配下にあるファイルやディレクトリの内容がすべて、ビルドコンテキストとして Docker デーモンに送られることになります。 Dockerfile 記述のベストプラクティス これをさらに要約すれば 「docker buildコマンドを実行した場所」ってことですね。 docker build コマンドを実行した場所ってことなので、 docker build コマンドはDockerfileがあるディレクトリで実行すれば問題なさそうですね。 しかし、 docker-compose コマンドを使って開発している場合はどうでしょうか? Dockerfileがあるディレクトリでコマンドを実行することってほとんど無いと思います。その場合はコンテキストについてどう考えればいいのでしょうか? 例として「Laravel, Nginx」というよくあるプロジェクトの構成で考えてみます。 ディレクトリ構成は以下の様になります。 このディレクトリ構成の場合、 api/Dockerfileと nginx/Dockerfileのコンテキストはそれぞれどこになるか分かりますか? docker buildコマンドを実行する api/、 nginx/ ディレクトリ? nginx/ディレクトリがコンテキストだとすると nginx/Dockerfile は以下のようになります。 以下の1文に注目してください。コンテキストが nginx/なのに、 ../api/public で分かる通り、コンテキストのディレクトリから外れたファイルを参照していますね。 このまま実行すると のようなエラーが出ます。 Dockerはコンテキスト(カレントディレクトリ)の外のファイルにはアクセスできない仕様なのです。そこら辺に関しては以下の記事で詳しく説明されています。 ではどうやって nginx/のコンテキストから api/のファイルにアクセスすればいいのでしょうか? 答えは簡単です。 コンテキストをルートディレクトリにすればいいのです docker-compose.yml で「コンテキスト」と「Dockerfileのある場所」を直接指定してみましょう。 コンテキストはどちらのサービスも で docker-compose.yml があるルートディレクトリに設定。 Dockerfileの場所は でそれぞれ指定。 では次に docker というディレクトリを作って、その中に各サービスのDockerfileをまとめた構成を考えてみます。 先ほどと同様にコンテキストを docker/phpや docker/nginxと考えた場合、どうやってもうまくいきません。 このディレクトリ構成の場合は、そもそもDockerfileからコンテキスト外のサービスのファイル群が入っている api/と nginx/ に一切アクセスできません。 ここでも同様 docker-compose.yml でコンテキストをルートディレクトリに、Dockerfileの位置も直接指定する必要がありそうです。 ルートディレクトリをDockerのコンテキストにすることで、Dockerfileはどんなファイルにもアクセスできるようになりました。 一方で、 build 時はその分Dockerデーモンという奴にそれだけ多くのファイルを送ることになるので遅くなることがあるようです。

Dockerfileを記述するときは、 docker-compose.ym の services.service.build.context で記載した箇所がカレントディレクトリと考えてCOPYコマンドなどを記述する。

結論こうなった

version: "3"

services:
  microservice-controller:
    build:
      context: .
      dockerfile: ./microservice_controller/docker/Dockerfile

  microservice-a:
    build:
      context: .
      dockerfile: ./grpc-services/services-1/microservice-a/docker/Dockerfile

  microservice-b:
    build:
      context: .
      dockerfile: ./grpc-services/services-1/microservice-b/docker/Dockerfile
FROM python:3.9-buster as build
RUN apt-get -y update && apt-get -y install python3 python3-pip
RUN pip install grpcio-tools==1.41.0 python-json-logger==2.0.1

COPY ./protos /protos
WORKDIR /protos
RUN ./build.sh

WORKDIR /
COPY ./microservice_controller /app
RUN cp -r /protos/gen-py /app
WORKDIR /app
CMD ["./run-server.sh"]
FROM python:3.9-buster as build
RUN apt-get -y update && apt-get -y install python3 python3-pip
RUN pip install grpcio-tools==1.41.0 python-json-logger==2.0.1 datadog==0.41.0

COPY ./protos /protos
WORKDIR /protos
RUN ./build.sh

WORKDIR /
COPY ./grpc-services/services-1/microservice-a/ /app
RUN cp -r /protos/gen-py /app
WORKDIR /app
CMD ["/app/run-server.sh"]

あと、直接的には関係ないけど、改善した項目として ADD —> COPY に変えた

以下参考

DockerfileにてなぜADDよりCOPYが望ましいのか - Qiita
Docker公式のBest practices for writing Dockerfilesに ADDよりもCOPYが望ましい(ADDは使わない方が良い)との記述があるのを見つけたのでその理由を確認してみました。 理由は以下の2つに分けることができます。 ① Imageサイズの観点 ② セキュリティの観点 こちらの記事 にて端的にまとめられていたので日本語訳してしまいます。 COPY - 明示的なコピー元とコピー先のファイルまたはディレクトリを指定して、ローカルファイルを再帰的にコピーします。 COPYでは、場所を宣言する必要があります。 ADD - ローカルファイルを再帰的にコピーし、存在しない場合は暗黙的にコピー先ディレクトリを作成し、アーカイブをコピー元としてローカルURLまたはリモートURLとして受け入れます。アーカイブはそれぞれコピー先ディレクトリに展開またはダウンロードされます。 COPYはソース側のマシンにあるものをImageへコピーするだけというシンプルな機能であるのに対し、 ADD は対象が圧縮ファイルであればそれを展開してImageへ移し、リモート上のリソースも引っ張ってくる機能がついています。このCOPYに付与されたADDの機能自体が望ましくない原因になっています。 上記の公式ドキュメントに記載の内容です。 ADD を使ってしまっているアンチパターンが以下になります。 これの何がいけないかというと、 ADD https://...tar.xz /usr/src/things の記述により結果的に不要なダウンロードされたディレクトリを持つImageレイヤーができてしまっている点です。 そうではなく、このようにしましょうということです。 それぞれをビルドして docker imagesと docker history で比較してみましょう。 ダメな方では ADDで追加された圧縮ファイルを持ったレイヤー分サイズが大きくなってしまっています。改良版ではcurlした圧縮ファイルをパイプで tar へつなげているのでImageには残りません。 こちらの記事 の内容の紹介になります。 上記の ADDの機能を 利用者が知らずに使っている場合、 ADD はセキュリティのリスクが生じてしまいます。 リモート上のリソースを自動で取りに行くのでダウンロード中、中間者攻撃の対象となりえる 圧縮ファイルを自動で展開するので、ZIP爆弾やZIP SLIP脆弱性攻撃の対象となりえる ここで注意するべきは、上記のセキュリティリスクは COPYを使っている場合でも起こりえます。ただ、 COPY を使っていれば、 明示的にリモートリソースからダウンロード(wget, curlなど)をする 圧縮ファイルを展開(tarなど)する ので対象リソースが安全かを検証する仕組みを組み込みやすいということです。 これも上記公式ドキュメントに記載の話です。 Imageキャッシュをできるだけ利用できるようにすることを意識してDockerfileを記述しましょう。 2つの違いがImageキャッシュ利用にどう関わるか理解しておく必要があります。 RUN pip install --requirement /tmp/requirements.txtは requirements.txtの差分のみに影響されて実行されるべきです。 ダメな方はそれ以外の.以下の何かが変更されるたび、 COPY .

更に、今回の話とは直接関係ないけど、以下を参照して突貫工事で作ったDockerfileを見直したい

特に、不用意にイメージが肥大化していそうなところ、共通化すべきところを共通化できていないところなど

https://matsuand.github.io/docs.docker.jp.onthefly/develop/develop-images/dockerfile_best-practices/#ビルドコンテキストの理解