カスタム Docker イメージで価値を追加する

Beginner

This tutorial is from open-source community. Access the source code

はじめに

この実験では、コンテナを実行するために Docker コマンドを使用した実験 1 の知識を基にします。Dockerfile から構築されたカスタム Docker イメージを作成します。イメージを構築したら、それをセントラル レジストリにプッシュし、他の環境で展開するためにプルできるようにします。また、イメージ レイヤーと、Docker が「コピーオンライト」とユニオン ファイル システムをどのように組み込んでイメージを効率的に保存し、コンテナを実行するかについても簡単に説明します。

この実験ではいくつかの Docker コマンドを使用します。利用可能なコマンドの完全なドキュメントについては、公式ドキュメントを参照してください。

これは Guided Lab です。学習と実践を支援するためのステップバイステップの指示を提供します。各ステップを完了し、実践的な経験を積むために、指示に注意深く従ってください。過去のデータによると、この 初級 レベルの実験の完了率は 83%です。学習者から 100% の好評価を得ています。

Python アプリを作成する(Docker を使用せず)

次のコマンドを実行して、単純な Python プログラムが含まれる app.py という名前のファイルを作成します。(コード ブロック全体をコピーして貼り付けてください)

cd ~/project
echo 'from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "hello world!"

if __name__ == "__main__":
    app.run(host="0.0.0.0")' > app.py

これは、Flask を使用してポート 5000 で HTTP ウェブ サーバーを公開する単純な Python アプリです。(5000 は Flask の既定のポートです)。Python や Flask にあまり詳しくない場合は心配しないでください。これらの概念は、どんな言語で書かれたアプリケーションにも適用できます。

オプション:Python と pip がインストールされている場合は、このアプリをローカルで実行できます。そうでない場合は、次の手順に進んでください。

$ python3 --version
$ pip3 --version
$ pip3 install flask

$ python3 app.py
 * Serving Flask app "app" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

http://0.0.0.0:5000/ を使用して新しいブラウザ タブでアプリを開きます。

Flask app browser output

Docker イメージを作成して構築する

では、ローカルに Python がインストールされていない場合どうなるでしょうか?心配しないでください!コンテナを使用する利点の 1 つは、ホスト マシンに Python をインストールすることなく、コンテナ内で Python を構築できることです。

次のコマンドを実行して Dockerfile を作成します。(コード ブロック全体をコピーして貼り付けてください)

echo 'FROM python:3.8-alpine
RUN pip install flask
CMD ["python","app.py"]
COPY app.py /app.py' > Dockerfile

Dockerfile には、Docker イメージを構築するために必要な指示が記載されています。上記のファイルを 1 行ずつ見ていきましょう。

FROM python:3.8-alpine これは、Dockerfile の起点です。すべての Dockerfile は、レイヤーを構築するための起点イメージとなる FROM 行から始まる必要があります。

この場合、私たちは python:3.8-alpine ベース レイヤーを選択しています(python3.8/alpine3.12 の Dockerfile を参照)。なぜなら、このレイヤーには既にアプリケーションを実行するために必要な Python と pip のバージョンが含まれているからです。

alpine バージョンは、Alpine Linux ディストリビューションを使用しています。これは、他の多くの Linux のフレーバーよりも大幅に小さく、サイズが約 8MB です。一方、ディスクへの最小インストールサイズは約 130MB です。小さなイメージは、ダウンロード(展開)がはるかに速くなり、攻撃面が小さいためセキュリティ上の利点もあります。Alpine Linux は、musl と BusyBox に基づく Linux ディストリビューションです。

ここでは、Python イメージに対して "3.8-alpine" タグを使用しています。Docker Hub の公式 Python イメージの利用可能なタグを確認してください。親イメージを継承する際には、特定のタグを使用することがベスト プラクティスです。これにより、親依存関係の変更を制御できます。タグが指定されていない場合、"latest" タグが有効になります。これは、イメージの最新バージョンを指す動的ポインタとして機能します。

セキュリティ上の理由から、Docker イメージを構築するためのベースとなるレイヤーを理解することは非常に重要です。そのため、docker hub にある "公式" イメージ、または docker-store にある非コミュニティ イメージのみを使用することを強くお勧めします。これらのイメージは、特定のセキュリティ要件を満たすように検証されており、ユーザーが追うのに非常に良いドキュメントも備えています。docker hub では、この Python ベース イメージ や他に使用できるすべてのイメージに関する詳細情報を見つけることができます。

より複雑なアプリケーションの場合、チェーンの上位にある FROM イメージを使用する必要がある場合があります。たとえば、私たちの Python アプリの親 DockerfileFROM alpine から始まり、その後、イメージに対する一連の CMDRUN コマンドを指定しています。もっと細かい制御が必要な場合は、FROM alpine(または別のディストリビューション)から始めて、それらの手順を自分で実行することができます。最初は、必要に合わせた公式イメージを使用することをお勧めします。

RUN pip install flask RUN コマンドは、アプリケーション用にイメージをセットアップするために必要なコマンドを実行します。たとえば、パッケージのインストール、ファイルの編集、またはファイルのパーミッションの変更などです。この場合、私たちは flask をインストールしています。RUN コマンドはビルド時に実行され、イメージのレイヤーに追加されます。

CMD ["python","app.py"] CMD は、コンテナを起動したときに実行されるコマンドです。ここでは、CMD を使用して Python アプリを実行しています。

Dockerfile には 1 つの CMD のみが許されます。複数の CMD を指定した場合、最後の CMD が有効になります。親の python:3.8-alpine も CMD (CMD python3) を指定しています。公式 python:alpine イメージの Dockerfile は ここ にあります。

ホストに Python をインストールすることなく、公式の Python イメージを直接使用して Python スクリプトを実行することができます。しかし、今日は、ソースを含めたカスタム イメージを作成しています。これにより、アプリケーション付きのイメージを構築し、他の環境に配布することができます。

COPY app.py /app.py これは、ローカル ディレクトリ(docker image build を実行する場所)の app.py をイメージの新しいレイヤーにコピーします。この指示は、Dockerfile の最後の行です。頻繁に変更されるレイヤー、たとえばソース コードをイメージにコピーする場合、Docker レイヤー キャッシュの恩恵を最大限に活用するために、ファイルの下部に配置する必要があります。これにより、キャッシュされるはずのレイヤーを再構築することなく済みます。たとえば、FROM 指示に変更があった場合、このイメージのすべての後続レイヤーのキャッシュが無効になります。この実験の後半でこれを示します。

これを CMD ["python","app.py"] の行の後に置くのは直感的ではないように見えます。覚えておいてください。CMD の行は、コンテナが起動したときにのみ実行されるため、ここで file not found エラーは発生しません。

これで、非常にシンプルな Dockerfile が完成しました。Dockerfile に記載できるコマンドの完全なリストは ここ にあります。これで Dockerfile を定義したので、これを使ってカスタム Docker イメージを構築しましょう。

Docker イメージを構築します。

-t を指定して、イメージ名を python-hello-world にします。

docker image build -t python-hello-world.

イメージがイメージ一覧に表示されていることを確認します。

docker image ls

:ベース イメージ python:3.8-alpine も一覧に表示されます。

イメージの履歴とそのレイヤーを表示するには、履歴コマンドを実行します。

docker history python-hello-world
docker history python:3.8-alpine

Docker イメージを実行する

イメージを構築したので、それが機能することを確認するために実行しましょう。

Docker イメージを実行する

docker run -p 5001:5000 -d python-hello-world

-p フラグは、コンテナ内で実行されているポートをホストにマップします。この場合、コンテナ内のポート 5000 で実行されている Python アプリを、ホストのポート 5001 にマップしています。ホスト上の別のアプリケーションが既にポート 5001 を使用している場合は、5001 を別の値、たとえば 5002 に置き換える必要がある場合があります。

ターミナル ウィンドウの PORTS タブに移動し、リンクをクリックして新しいブラウザ タブでアプリを開きます。

Terminal ports tab link

ターミナルで curl localhost:5001 を実行すると、hello world! が返されます。

コンテナのログ出力を確認する

アプリケーションのログを表示したい場合は、docker container logs コマンドを使用できます。既定では、docker container logs はアプリケーションから標準出力に送信される内容を表示します。実行中のコンテナの ID を取得するには、docker container ls を使用します。

labex:project/ $ docker container ls
CONTAINER ID   IMAGE                COMMAND           CREATED         STATUS         PORTS                                       NAMES
52df977e5541   python-hello-world   "python app.py"   2 minutes ago   Up 2 minutes   0.0.0.0:5001->5000/tcp, :::5001->5000/tcp   heuristic_lamport
labex:project/ $ docker container logs 52df977e5541
 * Serving Flask app 'app'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:5000
 * Running on http://172.17.0.2:5000
Press CTRL+C to quit
172.17.0.1 - - [23/Jan/2024 02:43:10] "GET / HTTP/1.1" 200 -
172.17.0.1 - - [23/Jan/2024 02:43:10] "GET /favicon.ico HTTP/1.1" 404 -

Dockerfile は、アプリケーションの再現可能なビルドを作成する方法です。一般的なワークフローは、CI/CD 自動化がビルド プロセスの一部として docker image build を実行することです。イメージが構築されると、それらはセントラル レジストリに送信され、そのアプリケーションのインスタンスを実行する必要があるすべての環境(たとえばテスト環境)でアクセスできるようになります。次の手順では、カスタム イメージをパブリック Docker レジストリである Docker Hub にプッシュし、他の開発者やオペレーターが利用できるようにします。

セントラル レジストリにプッシュする

まだ持っていない場合は、Docker Hub に移動してアカウントを作成します。あるいは、例えば https://quay.io を使用することもできます。

この実験では、Docker Hub をセントラル レジストリとして使用します。Docker Hub は、公開可能なイメージを保存する無料サービスであり、プライベート イメージを保存する場合は有料です。Docker Hub ウェブサイトに移動して、無料アカウントを作成します。

Docker を大量に使用するほとんどの組織は、内部で独自のレジストリをセットアップします。簡単にするために、Docker Hub を使用しますが、以下の概念は任意のレジストリにも適用されます。

ログイン

ターミナルで docker login と入力するか、podman を使用している場合は podman login と入力することで、イメージ レジストリ アカウントにログインできます。

labex:project/ $ export DOCKERHUB_USERNAME=<your_docker_username>
labex:project/ $ docker login docker.io -u $DOCKERHUB_USERNAME
Password:
WARNING! Your password will be stored unencrypted in /home/labex/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded

イメージにユーザー名を付ける

Docker Hub の命名規則は、イメージに [dockerhub ユーザー名]/[イメージ名] を付けることです。これを行うために、先ほど作成したイメージ python-hello-world にこの形式に合うようにタグを付けます。

docker tag python-hello-world $DOCKERHUB_USERNAME/python-hello-world

イメージをレジストリにプッシュする

適切にタグ付けされたイメージがあれば、docker push コマンドを使用してイメージを Docker Hub レジストリにプッシュできます。

docker push $DOCKERHUB_USERNAME/python-hello-world

ブラウザで Docker Hub のイメージを確認する

Docker Hub に移動し、プロフィールに移動して、https://hub.docker.com/repository/docker/<dockerhub-username>/python-hello-world に新しくアップロードされたイメージを確認します。

イメージが Docker Hub にあるので、他の開発者やオペレーターは docker pull コマンドを使用して、イメージを他の環境に展開できます。

:Docker イメージには、イメージ内でアプリケーションを実行するために必要なすべての依存関係が含まれています。これは便利です。なぜなら、展開するすべての環境にインストールされている依存関係に依存する場合、環境のドリフト(バージョンの違い)を処理する必要がなくなるからです。また、これらの環境をプロビジョニングするための追加の手順も必要ありません。1 つの手順:Docker をインストールするだけで、準備完了です。

変更を展開する

「ハローワールド!」アプリケーションは評価が過大です。代わりに「ハロービューティフルワールド!」と表示するようにアプリを更新しましょう。

app.py を更新する

app.py の中で、文字列「Hello World」を「Hello Beautiful World!」に置き換えます。次のコマンドでファイルを更新できます。(コード ブロック全体をコピーして貼り付けてください)

echo 'from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "hello beautiful world!"

if __name__ == "__main__":
    app.run(host="0.0.0.0")' > app.py

イメージを再構築してプッシュする

アプリが更新されたので、上記の手順を繰り返してアプリを再構築し、Docker Hub レジストリにプッシュする必要があります。

まず再構築します。今回は、ビルド コマンドで Docker Hub のユーザー名を使用します。

docker image build -t $DOCKERHUB_USERNAME/python-hello-world.

ステップ 1 - 3 における「キャッシュの使用」に注目してください。Docker イメージのこれらのレイヤーは既に構築されており、docker image build は再構築する代わりにキャッシュからこれらのレイヤーを使用します。

docker push $DOCKERHUB_USERNAME/python-hello-world

レイヤーをプッシュするためのキャッシュ メカニズムもあります。Docker Hub には、以前のプッシュからのレイヤーのうち、1 つを除くすべてが既にあります。したがって、変更された 1 つのレイヤーのみがプッシュされます。

レイヤーを変更すると、その上に構築されたすべてのレイヤーを再構築する必要があります。Dockerfile の各行は、その前の行から作成されたレイヤーの上に構築される新しいレイヤーを作成します。これが、Dockerfile の行の順序が重要な理由です。私たちは Dockerfile を最適化して、最も変更される可能性のあるレイヤー(COPY app.py /app.py)が Dockerfile の最後の行になるようにしました。一般的にアプリケーションの場合、コードが最も頻繁に変更されます。この最適化は、自動化をできるだけ早く実行したい CI/CD プロセスにとって特に重要です。

イメージ レイヤーの理解

Docker の主な設計特性の 1 つは、ユニオン ファイル システムの使用です。

先ほど作成した Dockerfile を見てみましょう。

FROM python:3.8-alpine
RUN pip install flask
CMD ["python","app.py"]
COPY app.py /app.py

これらの各行は 1 つのレイヤーです。各レイヤーには、その前のレイヤーとの差分、diff または変更点のみが含まれています。これらのレイヤーを単一の実行中のコンテナにまとめるために、Docker は ユニオン ファイル システム を使用して、レイヤーを透明に重ねて単一のビューにします。

イメージの各レイヤーは 読み取り専用 ですが、実行中のコンテナ用に作成される最上位のレイヤーを除いています。読み取り/書き込み可能なコンテナ レイヤーは「書き込み時コピー」を実装しており、これは、下位のイメージ レイヤーに格納されているファイルが、それらのファイルに編集が加えられるときにのみ読み取り/書き込み可能なコンテナ レイヤーに引き上げられることを意味します。その後、これらの変更は実行中のコンテナ レイヤーに格納されます。「書き込み時コピー」機能は非常に高速で、ほとんどの場合、パフォーマンスに顕著な影響を与えません。docker diff コマンドを使用することで、コンテナ レベルに引き上げられたファイルを確認することができます。docker diff の使用方法に関する詳細は、ここ で見つけることができます。

understanding image layers

イメージ レイヤーは 読み取り専用 であるため、イメージと実行中のコンテナによって共有することができます。たとえば、同じベース レイヤーを持つ独自の Dockerfile で新しい Python アプリを作成すると、最初の Python アプリと共通のすべてのレイヤーを共有します。

FROM python:3.8-alpine
RUN pip install flask
CMD ["python","app2.py"]
COPY app2.py /app2.py

understanding image layers

同じイメージから複数のコンテナを起動する場合も、レイヤーの共有を体験することができます。コンテナは同じ読み取り専用レイヤーを使用するため、コンテナの起動は非常に高速で、ホスト上の占有領域も非常に小さいことが想像できます。

この Dockerfile と、この実験で先ほど作成した Dockerfile には重複する行があることに気付くかもしれません。これは非常に単純な例ですが、両方の Dockerfile の共通の行を「ベース」Dockerfile にまとめることができます。その後、各子 Dockerfile で FROM コマンドを使用してそれを指定することができます。

イメージのレイヤリングにより、ビルドとプッシュのための Docker キャッシュ メカニズムが可能になります。たとえば、最後の docker push の出力を見ると、イメージの一部のレイヤーが既に Docker Hub に存在することがわかります。

$ docker push $DOCKERHUB_USERNAME/python-hello-world

レイヤーをもっと詳細に見るには、作成した Python イメージの docker image history コマンドを使用することができます。

$ docker image history python-hello-world

各行はイメージの 1 つのレイヤーを表します。上位の行が先ほど作成した Dockerfile に一致し、その下の行は親の Python イメージから引き継がれていることに気付くでしょう。「<欠落>」タグは心配しないでください。これらはまだ通常のレイヤーです。ただし、docker システムによって ID が付けられていないだけです。

クリーンアップ

この実験を完了すると、ホスト上に多数の実行中のコンテナが残ります。これらをクリーンアップしましょう。

各実行中のコンテナに対して docker container stop [コンテナ ID] を実行する

まず、docker container ls を使用して実行中のコンテナの一覧を取得します。

$ docker container ls

次に、一覧に表示される各コンテナに対してコマンドを実行します。

$ docker container stop <コンテナID>

停止したコンテナを削除する

docker system prune は、システムをクリーンアップするための非常に便利なコマンドです。停止したコンテナ、未使用のボリュームとネットワーク、およびダングリング イメージを削除します。

$ docker system prune
WARNING! This will remove:
- all stopped containers
- all volumes not used by at least one container
- all networks not used by at least one container
- all dangling images
Are you sure you want to continue? [y/N] y
Deleted Containers:
0b2ba61df37fb4038d9ae5d145740c63c2c211ae2729fc27dc01b82b5aaafa26

Total reclaimed space: 300.3kB

まとめ

この実験では、独自のカスタム Docker コンテナを作成することで価値を追加することが始まりました。

要点:

  • Dockerfile は、アプリケーションの再現可能なビルドを作成する方法と、アプリケーションを Docker と統合して CI/CD パイプラインに組み込む方法です。
  • Docker イメージは、セントラル レジストリを介してすべての環境で利用可能にすることができます。Docker Hub はレジストリの 1 つの例ですが、コントロールするサーバー上に独自のレジストリを展開することもできます。
  • Docker イメージには、イメージ内でアプリケーションを実行するために必要なすべての依存関係が含まれています。これは便利です。なぜなら、展開するすべての環境にインストールされている依存関係に依存する場合、環境のドリフト(バージョンの違い)を処理する必要がなくなるからです。
  • Docker は、ユニオン ファイル システムと「書き込み時コピー」を利用してイメージのレイヤーを再利用します。これにより、イメージを保存するための占有領域が削減され、コンテナの起動性能が大幅に向上します。
  • イメージ レイヤーは、Docker ビルドとプッシュ システムによってキャッシュされます。対象のシステムに既に存在するイメージ レイヤーは、再構築または再プッシュする必要はありません。
  • Dockerfile の各行は新しいレイヤーを作成します。レイヤー キャッシュのため、より頻繁に変更される行(たとえば、イメージにソース コードを追加する場合)は、ファイルの下部付近に記載する必要があります。