この記事では、コンテナを操作しやすくするための一連のベスト プラクティスについて説明します。これらのプラクティスは、セキュリティからモニタリング、ロギングまで、幅広いトピックをカバーしています。その目的は、Google Kubernetes Engine とコンテナ全般でアプリケーションを実行しやすくすることです。ここで説明する方法の多くは、Twelve-Factor App の方法論から発想を得たものです。クラウドネイティブ アプリケーションを構築する際は、この方法論が非常に有用なリソースとなります。
ここで説明するおすすめの方法は、すべてが同じく重要であるというわけではありません。たとえば、本番環境ワークロードを正常に実行するために、必要のない方法もあれば、不可欠な方法もあります。特に、セキュリティに関連する方法の重要度は主観的なものです。実装するかどうかは、個々の環境と制約によって異なります。
この記事を最大限に活用するには、Docker と Kubernetes に関する知識が必要です。ここで説明されている一部の方法は Windows コンテナにも適用されますが、大部分の方法では、Linux コンテナで作業していることが前提となります。コンテナの構築に関するアドバイスについては、コンテナ構築のおすすめの方法をご覧ください。
コンテナのネイティブ ロギング メカニズムを使用する
重要度: 高
アプリケーションの管理に不可欠な部分として、ログにはアプリケーションで発生したイベントに関する貴重な情報が含まれています。Docker と Kubernetes は、ログ管理を容易にすることを目指しています。
従来のサーバーでは、おそらくログを別個のファイルに書き込んで、ディスクがいっぱいにならないようログ ローテーションを処理しなければなりません。高度なロギング システムを使用する場合は、ログをリモート サーバーに転送して一元管理することもあります。
コンテナでは、ログを stdout と stderr に書き込むことができるため、簡単かつ標準化された方法でログを処理できます。ログ行は Docker でキャプチャされるので、これらの行には docker logs
コマンドを使用してアクセスできます。アプリケーション デベロッパーとして、高度なロギング メカニズムを実装する必要はありません。代わりに、ネイティブ ロギング メカニズムを使用してください。
プラットフォーム オペレーターは、ログを一元管理して検索可能にするためのシステムを用意する必要があります。GKE では、Fluent Bit と Cloud Logging によってこのサービスが提供されます。GKE クラスタのマスター バージョンに基づいて、ログの収集に Fluentd または Fluent Bit が使用されます。GKE 1.17 以降では、Fluentbit ベースのエージェントを使用してログが収集されます。GKE 1.17 より前のバージョンを使用する GKE クラスタでは、Fluentd ベースのエージェントを使用します。他の Kubernetes ディストリビューションでは、他によく使われている方法としては、EFK(Elasticsearch、Fluentd、Kibana)スタックがあります。
JSON ログ
ほとんどのログ管理システムは、実際には時系列データベースであり、時刻でインデックス付けされたドキュメントを格納します。通常、これらのドキュメントは JSON 形式で作成されます。Cloud Logging と EFK ではどちらも、単一のログ行がメタデータ(Pod、コンテナ、ノードなどに関する情報)と併せて 1 つのドキュメントとして格納されます。
この動作を利用するには、各種のフィールドを使って直接 JSON 形式でログを記録します。このようにすれば、これらのフィールドに基づいて、より効率的にログを検索できます。
たとえば、次のログを JSON 形式に変換するとします。
[2018-01-01 01:01:01] foo - WARNING - foo.bar - There is something wrong.
変換後のログは次のようになります。
{ "date": "2018-01-01 01:01:01", "component": "foo", "subcomponent": "foo.bar", "level": "WARNING", "message": "There is something wrong." }
このように変換すると、WARNING
レベルのログやサブコンポーネント foo.bar
のログをすべて、ログの中から簡単に検索できます。
JSON 形式のログを書き込むことにした場合、各イベントをそれぞれ 1 行で書き込んで、正しく解析されるようにする必要があります。実際には、次のようになります。
{"date":"2018-01-01 01:01:01","component":"foo","subcomponent":"foo.bar","level": "WARNING","message": "There is something wrong."}
ご覧のように、通常のログ行よりもかなり読みにくくなります。この方法を使用する場合は、チームが手動によるログ検査に大きく依存することがないようにしてください。
ログ アグリゲータ サイドカー パターン
一部のアプリケーション(Tomcat など)では、stdout やstderr にログを書き出すよう構成するのは簡単ではありません。このようなアプリケーションはログをディスク上の別個のファイルに書き込むため、Kubernetes でログを処理するには、ロギングにサイドカー パターンを使用するのが最善の方法となります。サイドカーとは、アプリケーションと同じポッドで動作する小さなコンテナのことです。サイドカーについて詳しくは、公式の Kubernetes ドキュメントをご覧ください。
このソリューションでは、サイドカー コンテナに格納した Logging エージェントを(同じ Pod 内の)アプリケーションに追加して、これら 2 つのコンテナ間で emptyDir ボリュームを共有します(GitHub 上のこの YAML サンプルを参照)。次に、共有ボリュームにログを書き込むようにアプリケーションを構成し、必要に応じて Logging エージェントがログを読み取って転送するように構成します。
このパターンでは、Docker と Kubernetes のネイティブ ロギング メカニズムを使用していないため、ログ ローテーションに対処しなければなりません。Logging エージェントがログ ローテーションを処理しない場合、同じ Pod 内の別のサイドカー コンテナによってローテーションを処理できます。
コンテナがステートレスで不変であるようにする
重要度: 高
初めてコンテナを試している場合、従来のサーバーのようにコンテナを扱わないでください。たとえば、実行中のコンテナ内でアプリケーションを更新したり、脆弱性が発生したときに実行中のコンテナにパッチを適用したりしたくなるかもしれません。
コンテナは基本的に、このような処理に対応するように設計されていません。コンテナは、ステートレスかつ不変になるように設計されています。
ステートレス性
「ステートレス」とは、すべての状態(任意の種類の永続データ)がコンテナの外部に格納されることを意味します。この外部ストレージは、必要に応じてさまざまな形をとることができます。
- ファイルを格納するには、Cloud Storage などのオブジェクト ストアを使用することをおすすめします。
- ユーザー セッションなどの情報を格納するには、外部の低レイテンシの Key-Value ストア(Redis や Memcached)を使用することをおすすめします。
- ブロックレベルのストレージ(データベース用など)が必要な場合は、コンテナに接続された外部ディスクを使用できます。GKE の場合は、永続ディスクの使用をおすすめします。
上記の方法をとることで、コンテナそのものからデータを取り除きます。つまり、データの損失を心配せずに、いつでもコンテナを正常にシャットダウンしたり破棄したりできます。新しいコンテナを作成して古いコンテナを置き換える場合は、新しいコンテナを同じデータストアに接続するか、同じディスクにバインドするだけです。
不変性
「不変」とは、コンテナがその存続期間を通して変更されないことを意味します。つまり、更新されることも、パッチを適用されることも、構成が変更されることもありません。アプリケーション コードの更新やパッチの適用が必要な場合は、新しいイメージを作成して再デプロイします。不変性は、Deployment をより安全で、より繰り返しやすいものにします。ロールバックする必要がある場合は、単に古いイメージを再デプロイすればよいだけです。このアプローチでは、すべての環境に同じコンテナ イメージをデプロイして、可能な限り同一のデプロイにできます。
さまざまな環境で同じコンテナ イメージを使用できるようにするには、コンテナの構成(リスニング ポート、ランタイム オプションなど)を外部化することをおすすめします。通常、コンテナを構成するには、環境変数または特定のパスにマウントされた構成ファイルを使用します。Kubernetes では、環境変数またはファイルとしてコンテナ内に構成を注入する際に Secret と ConfigMaps の両方を使用できます。
構成を更新する必要がある場合は、更新した構成を使用して、(同じイメージに基づく)新しいコンテナをデプロイします。
ステートレス性と不変性の組み合わせは、コンテナベースのインフラストラクチャの強みの 1 つです。この組み合わせにより、デプロイを自動化して、デプロイを繰り返しやすくするとともに信頼性を向上させることができます。
特権付きコンテナを使わないようにする
重要度: 高
仮想マシンまたはベアメタル サーバーでは、root ユーザーとしてアプリケーションを実行しないでください。その理由は単純で、アプリケーションが不正侵入された場合、攻撃者がサーバーに対する完全アクセス権を入手することになるためです。同様の理由から、特権付きコンテナを使用することは避けてください。特権付きコンテナとは、ホストマシンのすべてのデバイスに対するアクセス権を持ち、コンテナのほぼすべてのセキュリティ機能をバイパスするコンテナのことです。
特権付きコンテナを使用する必要があると思われる場合は、次の代替手段を検討してください。
- Kubernetes の securityContext オプション、または Docker の
--cap-add
フラグを使用して、コンテナに固有の機能を与えます。Docker のドキュメントに、デフォルトで有効になっている機能と、明示的に有効にする必要がある機能の両方がリストされています。 - アプリケーションを実行するためにホストの設定を変更しなければならない場合は、サイドカー コンテナまたは初期コンテナ内で、ホストの設定を変更します。アプリケーションとは異なり、これらのコンテナは内部トラフィックにも外部トラフィックにも公開する必要がないため、分離を強化できます。
- Kubernetes 内の sysctls を変更する必要がある場合は、専用のアノテーションを使用します。
Kubernetes で特権付きコンテナを禁止するには、Policy Controller を使用します。Kubernetes クラスタでは、Policy Controller を使用して構成したポリシーに違反する Pod を作成することはできません。
アプリケーションをモニタリングしやすくする
重要度: 高
ロギングと同様に、モニタリングもアプリケーションの管理に不可欠な部分です。多くの点で、コンテナ化アプリケーションのモニタリングは、コンテナ化されていないアプリケーションをモニタリングする場合と同じ原則に従います。ただし、コンテナ化されたインフラストクチャは、コンテナが頻繁に作成または削除されることから極めて動的になる傾向があり、コンテナが作成、削除されるたびにモニタリング システムを再構成する余裕はありません。
モニタリングのクラスは、主に、ブラックボックス モニタリングとホワイトボックス モニタリングの 2 つに区別できます。ブラックボックス モニタリングとは、エンドユーザーであるかのように外部からアプリケーションを調べることを意味します。ブラックボックス モニタリングが役立つのは、最終的に提供しようとしているサービスが利用可能で、機能している場合です。ブラックボックス モニタリングはインフラストラクチャの外部から行うため、従来型のインフラストラクチャとコンテナ化されたインフラストラクチャを区別しません。
ホワイトボックス モニタリングとは、ある種の特権アクセスを使用してアプリケーションを調べ、エンドユーザーには見えないアプリケーションの動作に関する指標を収集することを意味します。ホワイトボックス モニタリングでは、インフラストラクチャの最も深くにあるレイヤを調べる必要があるため、従来型のインフラストラクチャとコンテナ化されたインフラストラクチャを明確に区別します。
Kubernetes コミュニティでホワイトボックス モニタリングによく使われているオプションは、モニタリング対象の Pod を自動的に検出できる、Prometheus というシステムです。Prometheus が Pod で収集する指標は、特定の形式でなければなりません。Google Cloud には Google Cloud Managed Service for Prometheus が用意されています。このサービスを使用すると、Prometheus を大規模に手動で管理、運用することなく、ワークロードのモニタリングとアラートの送信をグローバルに行うことができます。デフォルトでは、Google Cloud Managed Service for Prometheus は、GKE クラスタからシステム指標を収集して Cloud Monitoring に送信するように構成されています。詳細については、GKE のオブザーバビリティをご覧ください。
Prometheus または Monitoring を利用するには、アプリケーションで指標を公開する必要があります。以下に、そのための 2 つの方法について説明します。
指標 HTTP エンドポイント
指標 HTTP エンドポイントは、後述のアプリケーションの状態を公開するで取り上げるエンドポイントと同じように機能します。これはアプリケーションの内部指標を、通常は /metrics
URI で公開します。次のようなレスポンスが返されます。
http_requests_total{method="post",code="200"} 1027 http_requests_total{method="post",code="400"} 3 http_requests_total{method="get",code="200"} 10892 http_requests_total{method="get",code="400"} 97
上記の例では、http_requests_total
が指標、method
と code
がラベル、右端の数値がこれらのラベルに対応する指標の値です。この例では、アプリケーションが起動してから HTTP GET リクエストに 400 エラーコードで応答した回数は、97 回となっています。
多数の言語で用意されている Prometheus クライアント ライブラリを使用すると、この HTTP エンドポイントを簡単に生成できます。OpenCensus でも、この形式を使用して指標をエクスポートできます(他にも多くの機能があります)。このエンドポイントは、公共のインターネットには公開しないでください。
このトピックについては、公式の Prometheus ドキュメントで詳しく説明しています。また、ホワイトボックス(およびブラックボックス)モニタリングの詳細については、サイト信頼性エンジニアリングの第 6 章をご覧ください。
モニタリング用サイドカー パターン
すべてのアプリケーションを /metrics
HTTP エンドポイントでインストルメントできるわけではありません。標準化されたモニタリングを維持するには、正しい形式で指標を公開するためにサイドカー パターンを使用することをおすすめします。
ログ アグリゲータ サイドカー パターンのセクションで、サイドカー コンテナを使用してアプリケーションのログを管理する方法を説明しました。それと同じパターンをモニタリングにも使用できます。つまり、サイドカー コンテナでモニタリング エージェントをホストし、アプリケーションが指標を公開すると、サイドカー コンテナ内のモニタリング エージェントが、グローバル モニタリング システムで認識できる形式とプロトコルに指標を変換するという仕組みです。
具体的な例として、Java アプリケーションと Java Management Extensions(JMX)について見ていきましょう。多くの Java アプリケーションは、JMX を使用して指標を公開します。その場合、Prometheus 形式で指標を公開するようにアプリケーションを再作成するのではなく、jmx_exporter を利用できます。jmx_exporter は JMX を介してアプリケーションから指標を収集し、それらの指標を Prometheus で読み取り可能な /metrics
エンドポイントを介して公開します。このアプローチには、アプリケーション設定を変更するために使用できる JMX エンドポイントの公開が制限されるという利点もあります。
アプリケーションの状態を公開する
重要度: 中
本番環境でのアプリケーションの管理を容易にするためには、アプリケーションがその状態をシステム全体に伝える必要があります。つまり、アプリケーションが実行されているのか、正常な状態なのか、トラフィックを受信できる状態なのか、どのように動作しているのかといった情報です。
Kubernetes では、livenessProbe と readinessProbe という 2 つのタイプのヘルスチェックを使用できます。このセクションで説明するように、それぞれに特定の用途があります。いずれも、さまざまな実装方法があります(コンテナ内部でコマンドを実行する、TCP ポートをチェックするなど)。ただし、推奨される方法は、このベスト プラクティスで説明する HTTP エンドポイントを使用することです。このトピックについて詳しくは、Kubernetes のドキュメントをご覧ください。
livenessProbe
livenessProbe の実装に推奨される方法は、アプリケーションで /healthz
HTTP エンドポイントを公開することです。アプリケーションがこのエンドポイントでリクエストを受信して、それに対して「200 OK」レスポンスを送信すれば、アプリケーションは正常です。Kubernetes における「正常」の意味は、コンテナを強制終了したり再起動したりする必要がないということです。正常と見なされるための要素はアプリケーションによって異なりますが、通常は次のことを意味します。
- アプリケーションが実行中である
- 主な依存関係が満たされている(たとえば、アプリケーションがそのデータベースにアクセスできるなど)
readinessProbe
readinessProbe の実装に推奨される方法は、アプリケーションで /ready
HTTP エンドポイントを公開することです。アプリケーションがこのエンドポイントでリクエストを受信して、それに対して「200 OK」レスポンスを送信すれば、アプリケーションはトラフィックを受信できる状態です。トラフィックを受信できる状態とは、次のことを意味します。
- アプリケーションが正常な状態である
- 潜在的な初期化ステップが完了している
- アプリケーションに有効なリクエストを送信すると、エラーが発生しない
Kubernetes では、readinessProbe を使用してアプリケーションのデプロイをオーケストレートします。Deployment を更新すると、Kubernetes はその Deployment に属する Pod のローリング アップデートを行います。デフォルトの更新ポリシーでは、Pod は一度に 1 つずつ更新されます。つまり、Kubernetes は新しい Pod の準備ができるまで待ってから(準備ができると readinessProbe で示されます)、次の Pod の更新を行います。
root として実行しないようにする
重要度: 中
コンテナは分離を確保します。つまり、デフォルトの設定では、Docker コンテナ内のプロセスは、ホストマシンや他の連結コンテナからの情報にアクセスできません。ただし、コンテナはホストマシンのカーネルを共有するため、このブログで説明しているように、仮想マシンでの分離ほど完全な分離ではありません。したがって、攻撃者が(Docker コンテナ内または Linux カーネル自体で)脆弱性を見つけて、その脆弱性を悪用してコンテナからエスケープすることはできます。攻撃者が脆弱性を見つけた場合、プロセスがコンテナ内で root として実行されていると、攻撃者がホストマシンに root としてアクセスする恐れがあります。
この可能性を回避するためのベスト プラクティスは、コンテナ内で root としてプロセスを実行しないことです。Kubernetes でこの動作を強制するには、Policy Controller を使用します。Kubernetes で Pod を作成するときに、runAsUser オプションを使用して、プロセスを実行する Linux ユーザーを指定します。このアプローチは、Dockerfile の USER
命令をオーバーライドします。
実際には、いくつかの課題があります。よく知られている多くのソフトウェア パッケージでは、そのメインプロセスを root として実行します。root として実行しないようにするには、不明の非特権ユーザーで実行できるようにコンテナを設計します。つまり、多くの場合、さまざまなフォルダに対する権限を調整する必要が生じます。コンテナごとに 1 つのアプリケーションというベスト プラクティスに従って、コンテナで単一ユーザー(できれば root 以外)を使って 1 つのアプリケーションを実行している場合は、書き込みが必要なフォルダとファイルに対する書き込み権限をすべてのユーザーに付与し、他のすべてのフォルダとファイルに対する書き込み権限を root のみに与えることができます。
コンテナがこのベスト プラクティスを遵守しているかどうかを確認する簡単な方法は、次のようにランダムなユーザーを使用してローカルでコンテナを実行し、コンテナが正常に動作するかどうかをテストすることです。[YOUR_CONTAINER]
は、使用しているコンテナ名で置き換えてください。
docker run --user $((RANDOM+1)) [YOUR_CONTAINER]
コンテナに外部ボリュームが必要な場合は、fsGroup Kubernetes オプションを構成して、そのボリュームの所有権を特定の Linux グループに与えることができます。この構成により、外部ファイルの所有権の問題が解決されます。
非特権ユーザーによって実行されているプロセスを、1024 未満のポートにバインドすることはできません。通常、これが問題になることはありません。なぜなら、あるポートから別のポートにトラフィックをルーティングするように Kubernetes サービスを構成できるためです。たとえば、HTTP サーバーをポート 8080 にバインドし、Kubernetes サービスによってトラフィックをポート 80 からリダイレクトするように構成できます。
イメージのバージョンを慎重に選択する
重要度: 中
Docker イメージを Dockerfile 内のベースイメージとして使用する場合も、Kubernetes にデプロイされるイメージとして使用する場合も、使用するイメージのタグを選択する必要があります。
公開イメージと非公開イメージのほとんどは、コンテナ構築のベスト プラクティスで説明しているようなタグ付けシステムに従います。イメージがセマンティック バージョニングに近いシステムを使用している場合、考慮しなければならないタグ付けの詳細がいくつかあります。
最も重要な点として、「latest」タグはイメージの間で頻繁に移し替えられることがあります。そのため、このタグに頼って予測可能または再生可能なビルドを行うことはできません。たとえば、次の Dockerfile があるとします。
FROM debian:latest
RUN apt-get -y update && \ apt-get -y install nginx
この Dockerfile から、間隔を置いて 2 回イメージをビルドすると、2 つの異なるバージョンの Debian と NGINX が生成される可能性があります。代わりに、次のように改訂したバージョンを明記することを検討してください。
FROM debian:11.6
RUN apt-get -y update && \ apt-get -y install nginx
このように、より明確なタグを使用することで、常に特定のマイナー バージョンの Debian に基づくイメージがビルドされるようになります。特定の Debian バージョンには特定の NGINX バージョンも同梱されているため、ビルドされるイメージをより詳細に制御できます。
この結果は、ビルド時だけではなく、実行時にも当てはまります。Kubernetes マニフェスト内で「latest」タグを参照すると、Kubernetes が使用するバージョンが保証されません。この場合、クラスタ内の異なるノードが、さまざまな時点で同じ「latest」タグを pull する可能性があります。pull されたタグが、次に pull されるまでの間に更新されていると、ノードが実行するイメージが異なる可能性があります(これらすべてのイメージには、ある時点でいずれも「latest」タグが付けられます)。
理想的には、FROM 行では常に不変のタグを使用してください。不変のタグを使用すれば、常に再現可能なビルドになります。ただし、これにはセキュリティに関するトレードオフが伴います。それは、使用するバージョンを固定すると、イメージに対するセキュリティ パッチの適用を自動化しにくくなるという点です。使用するイメージが適切なセマンティック バージョニングを使用していれば、パッチ バージョン(つまり、「X.Y.Z」の「Z」)では下位互換性が維持された形で変更が行われるはずです。つまり、「X.Y」タグを使用して、自動的にバグ修正を行うことができます。
たとえば、「SuperSoft」という名前のソフトウェアがあるとします。SuperSoft のセキュリティ プロセスでは、新しいパッチ バージョンによって脆弱性が修正されます。SuperSoft をカスタマイズするために、次の Dockerfile ファイルを作成するとします。
FROM supersoft:1.2.3
RUN a-command
その後しばらくして、ベンダーが脆弱性を発見し、この問題に対処するためにバージョン 1.2.4 の SuperSoft をリリースしたとします。この場合、ユーザーは自分で SuperSoft のパッチに関する最新情報を入手し、それに応じて Dockerfile を更新する必要があります。代わりに Dockerfile で FROM supersoft:1.2
を使用すると、新しいバージョンが自動的に pull されます。
最終的には、使用する外部イメージごとのタグ付けシステムを慎重に調べ、これらのイメージの作成者をどれだけ信頼するか、そしてどのタグを使用するかを決定しなければなりません。
次のステップ
- コンテナ構築のおすすめの方法について学習する。
- Cloud Build で最初のコンテナを作成する。
- 初めての GKE クラスタをスピンアップする。
- Cloud Build のビルドを高速化する。
- Docker Inc. 独自のベスト プラクティスについて学習する。その一部はこのドキュメントで取り上げていますが、取り上げていないものもあります。
Google Cloud に関するリファレンス アーキテクチャ、図、ベスト プラクティスを確認する。Cloud アーキテクチャ センター をご覧ください。