|
著者: ジョック WeChat公式アカウント:運営と開発のストーリー ジーフー:チャオおじさん 皆さんこんにちは。私は最前線の運用と保守の実務者である Qiao Ke です。 YAML エンジニアにとって、イメージは馴染み深いものであり、彼らはイメージの作成、構築、デプロイという反復的でありながら興味深いプロセスを毎日行っています。 イメージをビルドするためのDockerfileを一度書いたら、アプリケーションが正常に動作する限り、それを再度見直すことはほとんどありません(少なくとも私はそうです)。それが想像通り合理的かどうか、あるいはさらに最適化できるかどうかなど、深く考えることはありません。 この記事では、以下の側面からミラーリングをより深く理解できるようにします。 ミラーリングの基本概念何かを学ぶとき、最初に頭に浮かぶ疑問は「それって何?」です。Dockerイメージについて学ぶときも同じことが当てはまります。Dockerイメージとは何でしょうか? Docker イメージについて説明する前に、Linux ファイル システムについて簡単に説明しましょう。 一般的な Linux ファイル システムは、bootfs と rootfs で構成されます。bootfs はカーネルがメモリにロードした後にアンマウントされるため、システムに入ると表示されるのは、/etc、/prod、/bin などの標準ディレクトリなどの rootfs です。 Dockerイメージはルートファイルシステム(FS)と考えることができます。これにより、Dockerイメージとは何かをより明確に理解できます。例えば、公式のubuntu:21.10イメージには、カーネルは含まれていませんが、ubuntu:21.10の最小システムの完全なルートファイルシステムが含まれています。 Dockerイメージは、コンテナの実行に必要なプログラム、ライブラリ、リソース、設定、ランタイムパラメータを提供する特別なファイルシステムです。その最終的な目的は、コンテナ内でコードを実行できるようにすることです。 上記はDockerイメージとは何かというマクロレベルの視点を示しています。では、ミクロレベルの視点からさらに深く掘り下げてみましょう。例えば、`ubuntu:21.10`イメージしかないとします。`nginx`イメージが必要な場合、このイメージに`nginx`をインストールするだけで、`nginx`イメージに変換できるでしょうか? 答えは「はい」です。ここでは階層構造の概念が関わっています。最下層ではUbuntuイメージを使用し、その上にNginxイメージが階層化されて、Nginxイメージの構築が完了します。この場合、UbuntuイメージをNginxの親イメージと呼びます。 最初は少しわかりにくいかもしれませんが、次のミラー保存方法の説明を読めば理解しやすくなります。 ミラー保存方式ミラーの保存方法について説明する前に、UnionFS (Union File System) について簡単に紹介します。 UnionFSは、異なる物理的な場所にあるディレクトリを単一の仮想ファイルシステムに統合します。典型的な用途としては、CD/DVDとハードドライブ上のディレクトリを一緒にマウントし、ユーザーが読み取り専用のCD/DVDを変更できるようにすることが挙げられます。 DockerはUnionFSテクノロジーを最大限に活用して、階層化ストレージ用のイメージを設計します。現在使用されているファイルシステムはOverlayFSで、これはUnionFSの多くのファイルの一つです。 OverlayFS には、下位層と上位層の 2 つの層しかありません。名前の通り、上位層は最上位にあり、下位層はその下に位置し、上位層は下位層よりも優先度が高くなります。 mount を使用してオーバーレイ ファイル システムをマウントする場合は、次の規則に従ってください。
OverlayFS を例に挙げて、このファイルシステムの効果を直接体験してみましょう。 システム: CentOS 7.9 カーネル: 3.10.0 (1)lower、upper、merge、workの2つのディレクトリを作成します。
で:
(2)以下のように、下位ディレクトリと上位ディレクトリの両方にファイルを配置します。
下位ディレクトリと上位ディレクトリに同じ名前 (common-file) のファイルがありますが、その内容は異なることがわかります。 (3)以下のコマンドを使用して、これら2つのディレクトリをマウントします。
実装結果は以下の通りです。
ご覧のとおり、common-dir ディレクトリの内容がマージされ、重複したファイル common-file は uppderdir 内の common-file になっています。 (4)マージディレクトリにファイルを作成し、結果を確認します。
ご覧のとおり、下の層は変更されずに、新しく追加されたファイルが上の層に追加されます。 (5)マージレイヤーの下位ファイルを以下のように修正します。
ご覧のとおり、下層は変更されず、すべての変更は上層で行われます。 上記の実験は、興味深い点を明らかにしています。それは、上層がどのように変化しても、下層は同じままであるということです。 Dockerイメージはユニオンファイルシステムを採用しています。イメージをビルドする際、レイヤーは互いに積み重ねられます。各レイヤーは一度ビルドされると、変更されません。後続のレイヤーへの変更は、そのレイヤーのみに影響し、前のレイヤーには影響しません。 下の画像のように、例を使ってこれを説明しましょう。 具体的には次のようになります。
上記のイメージレイヤーは静的です。コンテナを実行すると、Dockerデーモンはコンテナ内のファイルを変更するための読み書きレイヤーも動的に生成します(下図参照)。 例えば、file2 を変更したい場合、コピーオンライト機構を使って file2 を読み書きレイヤーにコピーし、その後変更を加えます。同様に、コンテナの実行中はビューが存在します。コンテナを停止するとビューレイヤーは消えますが、読み書きレイヤーは保持されます。コンテナを再起動すると、以前の変更内容を確認できます。 ファイルを削除しても、実際には削除されるわけではなく、削除済みとしてマークされて非表示になるだけなので、ご注意ください。ファイルは見えなくなりますが、画像上には引き続き表示されます。 これで、階層化イメージストレージの基本を理解できました。この階層化ストレージは、記事の冒頭で紹介したUbuntuをベースにカスタマイズされたnginxイメージのように、イメージの再利用とカスタマイズを容易にします。 Dockerfileとイメージの関係アプリケーションコードにDockerfileを記述してイメージを作成することがよくありますが、Dockerfileとイメージの関係は一体何でしょうか?Dockerfileがなくてもイメージを作成することはできるのでしょうか? まず、単純な Dockerfile がどのようなものかを見てみましょう。
これらのコマンドを使用して新しいイメージを作成できますか? はい、これらのコマンドをファイルにまとめることで、Dockerはそれを使って新しいイメージを作成できます。これは、レモン、氷砂糖、スイカズラを用意すればレモンティーが作れるのと少し似ていると思いませんか? この接続を行うと、Dockerfile とイメージの関係が明確になります。 Dockerfileは原材料であり、イメージは私たちが求める成果物です。特定のイメージを作成したい場合は、Dockerfileを設定し、dockerコマンドを使って簡単に作成できます。 Dockerfile なしでイメージを作成することは可能ですか? 答えは「はい、可能です」です。この場合、まずベースイメージを起動し、「docker exec」コマンドを使ってコンテナに入り、必要なソフトウェアをインストールし、「docker commit」コマンドを使って新しいイメージを生成する必要があります。この方法はDockerfileほど明確でなく、操作も面倒です。 画像とコンテナの関係前述のように、Dockerfile はイメージの原材料です。ここでは、イメージがコンテナを実行するための基盤となります。 コンテナイメージは、私たちが普段目にするシステムイメージに似ています。CentOSイメージ(拡張子が.iso)のようなオペレーティングシステムイメージを取得する場合、通常、このCentOSオペレーティングシステムは直接サービスを提供できないため、インストールと設定を行う必要があります。 コンテナ イメージにも同じことが当てはまります。 Dockerfileを使ってイメージを作成すると、そのイメージは静的であり、必要なサービスを提供できません。Dockerを使ってイメージを実行し、イメージをコンテナに変換し、静的から動的に変換する必要があります。 簡単に言うと、イメージはファイルであり、コンテナはプロセスです。コンテナはイメージから作成されます。DockerイメージがなければDockerコンテナは作成できません。これはDockerの設計原則の一つです。 ミラーリング最適化技術前のセクションでは、イメージとは何か、イメージがどのように保存されるか、そしてDockerfile、イメージ、コンテナの関係について説明しました。このセクションでは、主にイメージ作成時に最適化するためのテクニックを紹介します。 Dockerイメージのビルドは、`docker build`コマンドによって開始されます。`docker build`は、Dockerfile内の指示に基づいてDockerイメージをビルドします。最終的なDockerイメージは、Dockerfile内のコマンドによって表現される一連のレイヤーです。そのため、Dockerfileの作成からイメージの作成までのプロセス全体に、最適化と注意が必要な領域があります。 ミラーの最適化は 2 つの方向に分けられます。
画像サイズを最適化するイメージ サイズの最適化では、主に Dockerfile を作成するときに考慮する点が重要になります。 前述の通り、イメージはレイヤーに保存されます。各イメージには親イメージがあり、以下のDockerfileに示すように、新しいイメージは親イメージの上に構築されます。
この Dockerfile は、ubuntu:latest を親イメージとして使用し、新しいイメージを作成するためのスクリプトを追加します。 したがって、ボリュームを最適化する際には、次の点を考慮することができます。 (1)できるだけ小さいベースイメージを選択するDocker Hubでは、同じベースイメージの複数のバージョンが存在する場合があります。可能であれば、Alpineバージョンの使用をお勧めします。Alpineバージョンは、多数の最適化が施され、不要なパッケージが削減され、容量が節約されています。ここでは、よく使われるOpenJDKイメージを例に、サイズの違いを簡単に確認してみましょう。 まず、Docker Hub で openjdk:17-jdk と openjdk:17-jdk-alpine のイメージ サイズを以下のように確認できます。 ご覧の通り、同じ Alpine バージョンのイメージは通常バージョンよりも 50MB ほど小さいため、この 2 つをベースイメージとしてビルドしたイメージサイズも異なります。 しかし、すべてのベースイメージに Alpine バージョンを選択する必要がありますか? いいえ、例えばアルプスの画像にも落とし穴はたくさんあります。
したがって、Alpineイメージの使用は慎重に検討する必要があります。実際のアプリケーションでは、Alpineイメージを使用する場合は、まず初期化を行い、必要な依存関係、ライブラリ、コマンドなどをカプセル化して新しいベースイメージを作成するのが最適です。その後、他のアプリケーションは、このベースイメージを親イメージとして操作に使用できます。 (2)ミラー層の数を最小限に抑える。前述の通り、画像はレイヤー構造で保存されます。上位レイヤーから下位レイヤーのファイルを変更する必要がある場合は、コピーオンライト機構が必要です。さらに、下位レイヤーのファイルは存在し続け、消えることはありません。レイヤー数が増えるほど、画像サイズは大きくなります。 たとえば、以下の Dockerfile です。
このDockerfileは動作しますか?もちろん、問題ありません。しかし、このように記述するとイメージレイヤーの数が非常に多くなってしまうのではないでしょうか? 親イメージ ubuntu:latest 自体のレイヤーを無視すると、上記の Dockerfile は 5 つのレイヤーを追加します。Dockerfile はコマンドのマージをサポートしているため、上記の Dockerfile を次のように変更できます。
この変更により、全体的なロジックは変更されずに、ミラー レイヤーの数が 5 から 3 に削減されます。 注: Docker 1.10以降では、RUN、COPY、ADD命令のみがレイヤーを作成します。その他の命令は一時的な中間イメージを作成するもので、ビルドイメージのサイズを直接増加させることはありません。 (3)不要なソフトウェアパッケージを削除する画像を作成する際は、常に次の点に留意してください。画像は可能な限りシンプルにしてください。これにより、画像の移植性も向上します。 たとえば、以下の Dockerfile です。
このイメージでは、外部ソースから圧縮ファイル a.tar.gz をコピーしました。解凍後、元のパッケージを削除していないため、まだスペースを占有しています。このDockerfileを以下のように修正できます。
この方法により、必要なファイルを取得できるだけでなく、不要なパッケージを保持することも回避できました。 (4)多段階ビルドを使用するこれは必須ではありません。 なぜそう言うのか?それは、マルチステージビルドは主にコンパイル環境から残った冗長ファイルを解決し、最終イメージを可能な限り小さくするためだからです。では、なぜ必須とは言わないのでしょうか?CI/CI実装では、コンパイルはコンパイルステップであり、ビルドはビルドステップであるため、マルチステージビルドは分離されていることが多いからです。そのため、必須ではないと述べます。 ただし、このアプローチは、以下に示すように、コンパイルとビルドの両方を 1 つの Dockerfile に含めることができるため優れています。
マルチステージは主に Dockerfile 内に複数の FROM ベースイメージを定義することで実現され、ステージはインデックスやエイリアスで参照できます。 画像サイズを最適化するための4つのポイントをご紹介しました。他にも良い方法や改善方法があれば、ぜひ教えてください。 ビルド速度を最適化するDockerfileを作成したら、イメージをビルドする必要があります。ビルド速度が遅いとイライラすることがよくあります。では、それを最適化するにはどうすればよいでしょうか?いくつかの提案をご紹介します。 (1)ネットワーク速度を最適化するネットワークは諸悪の根源です。例えば、多くの人がDocker Hubから直接ベースイメージをプルしています。マシンから初めてプルする場合、非常に遅くなります。このような場合、Docker Hubからのイメージをまずローカルのプライベートリポジトリに配置することができます。同じネットワーク環境であれば、プル速度はDocker Hubから直接プルする場合の10,000倍も速くなります。 Alibaba の Dragonfly などの別の画像配信テクノロジーは、P2P の概念を最大限に活用して、画像の検索と配信の速度を向上させます。 (2)コンテキストを最適化する`docker build` を使用してイメージをビルドすると、次のようにコンテキストが Docker デーモンに送信されることに気付いたかもしれません。
`docker build` を使用してイメージをビルドすると、Dockerfile と同じディレクトリ内のすべてのファイルが Docker デーモンに送信され、後続の操作はこのコンテキスト内で実行されます。 そのため、Dockerfile 内のディレクトリと同じディレクトリに不要なファイルが多数存在すると、メモリ消費量が増加するだけでなく、ビルドプロセス全体の速度も低下します。これを最適化する方法はありますか? ここでは 2 つの方法が提供されています。
(3)キャッシュを最大限に活用するDockerイメージはレイヤーに保存されます。`docker build`を使用してイメージをビルドする際、デフォルトでキャッシュが使用されます。Dockerは新しいイメージを作成する代わりに、まずキャッシュ内から使用するイメージを検索します。ルールは次のとおりです。ベースイメージから派生したすべての子イメージは、キャッシュ内に既に存在するイメージと比較され、そのうちのどれかが全く同じ命令でビルドされているかどうかが確認されます。もし異なる場合は、キャッシュは無効化され、イメージが再構築されます。 簡単に言えば、次の 3 つの要素にまとめることができます。
これら 3 つの要素が満たされている限りキャッシュが使用されるため、ビルド プロセスが高速化されます。 上記のセクションでは、Dockerイメージの最適化と、サイズと効率性に関する考慮事項について説明しました。このアプローチをイメージ設計に厳密に従えば、作成したイメージは長期間の使用に耐え、面接でも有利になるでしょう。 ミラーのセキュリティ管理画像に関連する多くのトピックについて説明してきましたが、最後に画像のセキュリティについて話しましょう。 イメージはコンテナの基盤であり、アプリケーションの担い手です。最終的には、イメージが直接的または間接的にビジネスにサービスを提供します。運用・保守担当者は、オペレーティングシステムのセキュリティ強化を実施しているはずですが、イメージにもセキュリティ強化が必要です。 このセクションでは、オペレーティング システムの強化については詳しく説明せず、コンテナーのみに焦点を当てます。 (1)鏡像はシンプルに合理化はセキュリティにつながるわけではありません。 しかし、簡素化されたオペレーティングシステムイメージは、ある程度のセキュリティ問題を軽減することができます。ご存知の通り、オペレーティングシステムには多数のソフトウェアプログラムが含まれており、日々様々な脆弱性が露呈し、悪意のある攻撃者の標的となります。オペレーティングシステムイメージは、いわば縮小版のようなものと考えることができます。同様に、イメージ内のソフトウェアの数が少なく、簡素化されているほど、脆弱性が露呈するリスクは低くなります。 (2)非ルートユーザーを使用するコンテナと仮想マシンの主な違いは、コンテナがホストカーネルを共有することです。デフォルトでは、Dockerコンテナはルートユーザーで実行されるため、データ漏洩につながる可能性があります。コンテナが侵害されると、ホストのルートアクセス権限も漏洩する可能性があります。 したがって、次の Java サービスのように、イメージを作成するときは非 root ユーザーを使用する必要があります。
(3)画像のセキュリティスキャンを実行します。コンテナレジストリでセキュリティスキャンを実行すると、さらなる価値が生まれます。イメージの保存に加え、イメージレジストリによる定期的なセキュリティスキャンは脆弱性の特定にも役立ちます。Dockerは、公式イメージとDocker Cloudでホストされているプライベートイメージの両方に対してセキュリティスキャンを提供しています。 もちろん、他のリポジトリにもセキュリティスキャンツールが統合されています。例えば、Harborの新バージョンでは、イメージスキャンルールをカスタマイズしたり、ブロックルールを定義したりできるため、イメージの脆弱性を効果的に検出できます。 (4)定期的にセキュリティ結果を確認する。他にもこんな風に感じたことはありませんか?「いろいろ加えたけど、違いが感じられない」 私も時々そう感じます。例えば、アプリケーションに監視機能を追加したものの、それを無視してしまい、監視がどのように機能するのか全く分からなくなったり、気にしなくなったりする時です。 イメージに対してセキュリティ スキャンを実行し、いくつかのツールをインストールする場合は、単にスキャンして終了するのではなく、それぞれのセキュリティ結果を確認する必要があります。 要約こんなに小さな鏡の中に、実に多くの精巧な仕組みが隠されています。実際に見るまでは信じられないでしょうが、実際に見れば衝撃を受けるでしょう。 この記事では、Docker イメージの概念から始め、いくつかの実用的なシナリオを使用して実装プロセスをより詳細に比較および分析し、Docker イメージの理解を深めます。 |