これは、このセクションの複数ページの印刷可能なビューです。 印刷するには、ここをクリックしてください.

このページの通常のビューに戻る.

チュートリアル

本セクションにはチュートリアルが含まれています。チュートリアルでは、単一のタスクよりも大きな目標を達成する方法を示します。通常、チュートリアルにはいくつかのセクションがあり、各セクションには一連のステップがあります。各チュートリアルを進める前に、後で参照できるように標準化された用語集ページをブックマークしておくことをお勧めします。

基本

設定

ステートレスアプリケーション

ステートフルアプリケーション

サービス

セキュリティ

次の項目

チュートリアルのページタイプについての情報は、Content Page Typesを参照してください。

1 - Hello Minikube

このチュートリアルでは、minikubeとKatacodaを使用して、Kubernetes上でサンプルアプリケーションを動かす方法を紹介します。Katacodaはブラウザで無償のKubernetes環境を提供します。

目標

  • minikubeへのサンプルアプリケーションのデプロイ
  • アプリケーションの実行
  • アプリケーションログの確認

始める前に

このチュートリアルはNGINXを利用してすべての要求をエコーバックするコンテナイメージを提供します。

minikubeクラスターの作成

  1. Launch Terminal をクリックしてください

  1. ブラウザーでKubernetesダッシュボードを開いてください:

    minikube dashboard
    
  2. Katacoda環境のみ:ターミナルペーン上部の+ボタンをクリックしてから Select port to view on Host 1 をクリックしてください。

  3. Katacoda環境のみ:30000を入力し、Display Portをクリックしてください。

Deploymentの作成

KubernetesのPod は、コンテナの管理やネットワーキングの目的でまとめられた、1つ以上のコンテナのグループです。このチュートリアルのPodがもつコンテナは1つのみです。Kubernetesの Deployment はPodの状態を確認し、Podのコンテナが停止した場合には再起動します。DeploymentはPodの作成やスケールを管理するために推奨される方法(手段)です。

  1. kubectl create コマンドを使用してPodを管理するDeploymentを作成してください。Podは提供されたDockerイメージを元にコンテナを実行します。

    kubectl create deployment hello-node --image=registry.k8s.io/echoserver:1.4
    
  2. Deploymentを確認します:

    kubectl get deployments
    

    出力は下記のようになります:

    NAME         READY   UP-TO-DATE   AVAILABLE   AGE
    hello-node   1/1     1            1           1m
    
  3. Podを確認します:

    kubectl get pods
    

    出力は下記のようになります:

    NAME                          READY     STATUS    RESTARTS   AGE
    hello-node-5f76cf6ccf-br9b5   1/1       Running   0          1m
    
  4. クラスターイベントを確認します:

    kubectl get events
    
  5. kubectl で設定を確認します:

    kubectl config view
    

Serviceの作成

通常、PodはKubernetesクラスター内部のIPアドレスからのみアクセスすることができます。hello-nodeコンテナをKubernetesの仮想ネットワークの外部からアクセスするためには、KubernetesのServiceとしてPodを公開する必要があります。

  1. kubectl expose コマンドを使用してPodをインターネットに公開します:

    kubectl expose deployment hello-node --type=LoadBalancer --port=8080
    

    --type=LoadBalancerフラグはServiceをクラスター外部に公開したいことを示しています。

  2. 作成したServiceを確認します:

    kubectl get services
    

    出力は下記のようになります:

    NAME         TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
    hello-node   LoadBalancer   10.108.144.78   <pending>     8080:30369/TCP   21s
    kubernetes   ClusterIP      10.96.0.1       <none>        443/TCP          23m
    

    ロードバランサーをサポートするクラウドプロバイダーでは、Serviceにアクセスするための外部IPアドレスが提供されます。 minikube では、LoadBalancerタイプはminikube serviceコマンドを使用した接続可能なServiceを作成します。

  3. 次のコマンドを実行します:

    minikube service hello-node
    
  4. Katacoda環境のみ:ターミナル画面上部の+ボタンをクリックして Select port to view on Host 1 をクリックしてください。

  5. Katacoda環境のみ:8080の反対側のService出力に、5桁のポート番号が表示されます。このポート番号はランダムに生成されるため、ここで使用するポート番号と異なる場合があります。ポート番号テキストボックスに番号を入力し、ポートの表示をクリックしてください。前の例の場合は、30369と入力します。

    アプリケーションとその応答が表示されるブラウザーウィンドウが開きます。

アドオンの有効化

minikubeはビルトインのアドオンがあり、有効化、無効化、あるいはローカルのKubernetes環境に公開することができます。

  1. サポートされているアドオンをリストアップします:

    minikube addons list
    

    出力は下記のようになります:

    addon-manager: enabled
    dashboard: enabled
    default-storageclass: enabled
    efk: disabled
    freshpod: disabled
    gvisor: disabled
    helm-tiller: disabled
    ingress: disabled
    ingress-dns: disabled
    logviewer: disabled
    metrics-server: disabled
    nvidia-driver-installer: disabled
    nvidia-gpu-device-plugin: disabled
    registry: disabled
    registry-creds: disabled
    storage-provisioner: enabled
    storage-provisioner-gluster: disabled
    
  2. ここでは例としてmetrics-serverのアドオンを有効化します:

    minikube addons enable metrics-server
    

    出力は下記のようになります:

    metrics-server was successfully enabled
    
  3. 作成されたPodとサービスを確認します:

    kubectl get pod,svc -n kube-system
    

    出力:

    NAME                                        READY     STATUS    RESTARTS   AGE
    pod/coredns-5644d7b6d9-mh9ll                1/1       Running   0          34m
    pod/coredns-5644d7b6d9-pqd2t                1/1       Running   0          34m
    pod/metrics-server-67fb648c5                1/1       Running   0          26s
    pod/etcd-minikube                           1/1       Running   0          34m
    pod/influxdb-grafana-b29w8                  2/2       Running   0          26s
    pod/kube-addon-manager-minikube             1/1       Running   0          34m
    pod/kube-apiserver-minikube                 1/1       Running   0          34m
    pod/kube-controller-manager-minikube        1/1       Running   0          34m
    pod/kube-proxy-rnlps                        1/1       Running   0          34m
    pod/kube-scheduler-minikube                 1/1       Running   0          34m
    pod/storage-provisioner                     1/1       Running   0          34m
    
    NAME                           TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)             AGE
    service/metrics-server         ClusterIP   10.96.241.45    <none>        80/TCP              26s
    service/kube-dns               ClusterIP   10.96.0.10      <none>        53/UDP,53/TCP       34m
    service/monitoring-grafana     NodePort    10.99.24.54     <none>        80:30002/TCP        26s
    service/monitoring-influxdb    ClusterIP   10.111.169.94   <none>        8083/TCP,8086/TCP   26s
    
  4. metrics-serverを無効化します:

    minikube addons disable metrics-server
    

    出力は下記のようになります:

    metrics-server was successfully disabled
    

クリーンアップ

クラスターに作成したリソースをクリーンアップします:

kubectl delete service hello-node
kubectl delete deployment hello-node

(オプション)minikubeの仮想マシン(VM)を停止します:

minikube stop

(オプション)minikubeのVMを削除します:

minikube delete

次の項目

2 - Kubernetesの基本を学ぶ

Kubernetesの基本

このチュートリアルでは、Kubernetesクラスターオーケストレーションシステムの基本について学びます。各モジュールには、Kubernetesの主な機能と概念に関する背景情報と、インタラクティブなオンラインチュートリアルが含まれています。これらの対話型チュートリアルでは、簡単なクラスターとそのコンテナ化されたアプリケーションを自分で管理できます。

この対話型のチュートリアルでは、以下のことを学ぶことができます:

  • コンテナ化されたアプリケーションをクラスターにデプロイ
  • Deploymentのスケーリング
  • 新しいソフトウェアのバージョンでコンテナ化されたアプリケーションをアップデート
  • コンテナ化されたアプリケーションのデバッグ

このチュートリアルでは、Katacodaを使用して、Webブラウザ上の仮想ターミナルでMinikubeを実行します。Minikubeは、どこでも実行できるKubernetesの小規模なローカル環境です。ソフトウェアをインストールしたり、何かを設定したりする必要はありません。各対話型チュートリアルは、Webブラウザ自体の上で直接実行されます


Kubernetesはどんなことができるの?

モダンなWebサービスでは、ユーザはアプリケーションが24時間365日利用可能であることを期待しており、開発者はそれらのアプリケーションの新しいバージョンを1日に数回デプロイすることを期待しています。コンテナ化は、パッケージソフトウェアがこれらの目標を達成するのを助け、アプリケーションをダウンタイムなしで簡単かつ迅速にリリース、アップデートできるようにします。Kubernetesを使用すると、コンテナ化されたアプリケーションをいつでもどこでも好きなときに実行できるようになり、それらが機能するために必要なリソースとツールを見つけやすくなります。Kubernetesは、コンテナオーケストレーションにおけるGoogleのこれまでの経験と、コミュニティから得られた最善のアイデアを組み合わせて設計された、プロダクションレディなオープンソースプラットフォームです。

2.1 - クラスターの作成

2.1.1 - Minikubeを使ったクラスターの作成

目標

  • Kubernetesクラスターとは何かを学ぶ
  • Minikubeとは何かを学ぶ
  • Kubernetesクラスターを、オンラインのターミナルを使って動かす

Kubernetesクラスター

Kubernetesは、単一のユニットとして機能するように接続された、可用性の高いコンピューターのクラスターをまとめあげます。Kubernetesの抽象化により、コンテナ化されたアプリケーションを個々のマシンに特に結び付けることなくクラスターにデプロイできます。この新しいデプロイモデルを利用するには、アプリケーションを個々のホストから切り離す方法でアプリケーションをパッケージ化(つまり、コンテナ化)する必要があります。コンテナ化されたアプリケーションは、アプリケーションがホストに深く統合されたパッケージとして特定のマシンに直接インストールされていた従来のデプロイモデルよりも柔軟で、より迅速に利用可能です。Kubernetesはより効率的な方法で、クラスター全体のアプリケーションコンテナの配布とスケジューリングを自動化します。Kubernetesはオープンソースのプラットフォームであり、プロダクションレディです。

Kubernetesクラスターは以下の2種類のリソースで構成されています:

  • マスターがクラスターを管理する
  • ノードがアプリケーションを動かすワーカーとなる

まとめ:

  • Kubernetesクラスター
  • Minikube

Kubernetesは、コンピュータークラスター内およびコンピュータークラスター間でのアプリケーションコンテナの配置(スケジューリング)および実行を調整する、プロダクショングレードのオープンソースプラットフォームです。


クラスターダイアグラム


マスターはクラスターの管理を担当します。マスターは、アプリケーションのスケジューリング、望ましい状態の維持、アプリケーションのスケーリング、新しい更新のロールアウトなど、クラスター内のすべての動作をまとめあげます。

ノードは、Kubernetesクラスターのワーカーマシンとして機能するVMまたは物理マシンです。各ノードにはKubeletがあり、これはノードを管理し、Kubernetesマスターと通信するためのエージェントです。ノードにはDockerやrktなどのコンテナ操作を処理するためのツールもあるはずです。プロダクションのトラフィックを処理するKubernetesクラスターには、最低3つのノードが必要です。

マスターは実行中のアプリケーションをホストするために使用されるノードとクラスターを管理します。

Kubernetesにアプリケーションをデプロイするときは、マスターにアプリケーションコンテナを起動するように指示します。マスターはコンテナがクラスターのノードで実行されるようにスケジュールします。ノードは、マスターが公開しているKubernetes APIを使用してマスターと通信します。エンドユーザーは、Kubernetes APIを直接使用して対話することもできます。

Kubernetesクラスターは、物理マシンまたは仮想マシンのどちらにも配置できます。Kubernetes開発を始めるためにMinikubeを使うことができます。Minikubeは、ローカルマシン上にVMを作成し、1つのノードのみを含む単純なクラスターをデプロイする軽量なKubernetes実装です。Minikubeは、Linux、macOS、およびWindowsシステムで利用可能です。Minikube CLIは、起動、停止、ステータス、削除など、クラスターを操作するための基本的なブートストラップ操作を提供します。ただし、このチュートリアルでは、Minikubeがプリインストールされた状態で提供されているオンラインのターミナルを使用します。

Kubernetesが何であるかがわかったので、オンラインチュートリアルに行き、最初のクラスターを動かしましょう!


2.1.2 - 対話型チュートリアル - クラスターの作成

ターミナルを使うにはスクリーンが狭い場合は、PCまたはタブレットをお使いください

2.2 - アプリケーションのデプロイ

2.2.1 - kubectlを使ったDeploymentの作成

目標

  • アプリケーションのデプロイについて学ぶ
  • kubectlを使って、Kubernetes上にはじめてのアプリケーションをデプロイする

Kubernetes Deployments

実行中のKubernetesクラスターを入手すると、その上にコンテナ化アプリケーションをデプロイすることができます。そのためには、KubernetesのDeployment の設定を作成します。DeploymentはKubernetesにあなたのアプリケーションのインスタンスを作成し、更新する方法を指示します。Deploymentを作成すると、KubernetesマスターはDeployment内に含まれるアプリケーションインスタンスをクラスター内の個々のノードで実行するようにスケジュールします。

アプリケーションインスタンスが作成されると、Kubernetes Deploymentコントローラーは、それらのインスタンスを継続的に監視します。インスタンスをホストしているノードが停止、削除された場合、Deploymentコントローラーはそのインスタンスをクラスター内の別のノード上のインスタンスと置き換えます。これは、マシンの故障やメンテナンスに対処するためのセルフヒーリングの仕組みを提供しています。

オーケストレーションが入る前の世界では、インストールスクリプトを使用してアプリケーションを起動することはよくありましたが、マシン障害が発生した場合に復旧する事はできませんでした。アプリケーションのインスタンスを作成し、それらをノード間で実行し続けることで、Kubernetes Deploymentsはアプリケーションの管理に根本的に異なるアプローチを提供します。

まとめ:

  • Deployments
  • kubectl

Deploymentは、アプリケーションのインスタンスを作成および更新する責務があります。


Kubernetes上にはじめてのアプリケーションをデプロイする


Kubernetesのコマンドラインインターフェイスであるkubectlを使用して、Deploymentを作成、管理できます。kubectlはKubernetes APIを使用してクラスターと対話します。このモジュールでは、Kubernetesクラスターでアプリケーションを実行するDeploymentを作成するために必要な、最も一般的なkubectlコマンドについて学びます。

Deploymentを作成するときは、アプリケーションのコンテナイメージと実行するレプリカの数を指定する必要があります。Deploymentを更新することで、あとでその情報を変更できます。チュートリアルのモジュール56では、Deploymentをどのようにスケール、更新できるかについて説明します。

Kubernetesにデプロイするには、アプリケーションをサポートされているコンテナ形式のいずれかにパッケージ化する必要があります。

最初のDeploymentには、DockerコンテナにパッケージされたNode.jsアプリケーションを使用します。(まだNode.jsアプリケーションを作成してデプロイしていない場合、Hello Minikubeチュートリアルの通りにやってみましょう。)

Deploymentが何であるかがわかったので、オンラインチュートリアルに行き、最初のアプリケーションをデプロイしましょう!


2.2.2 - 対話型チュートリアル - アプリケーションのデプロイ

Podは、Kubernetesアプリケーションの基本的な実行単位です。各Podは、クラスターで実行されているワークロードの一部を表します。Podの詳細はこちらです


ターミナルを使うには、PCまたはタブレットをお使いください

2.3 - アプリケーションの探索

2.3.1 - Podとノードについて

目標

  • KubernetesのPodについて学ぶ
  • Kubernetesのノードについて学ぶ
  • デプロイされたアプリケーションのトラブルシューティング

Kubernetes Pod

モジュール2でDeploymentを作成したときに、KubernetesはアプリケーションインスタンスをホストするためのPodを作成しました。Podは、1つ以上のアプリケーションコンテナ(Dockerなど)のグループとそれらのコンテナの共有リソースを表すKubernetesの抽象概念です。 Podには以下のものが含まれます:

  • 共有ストレージ(ボリューム)
  • ネットワーキング(クラスターに固有のIPアドレス)
  • コンテナのイメージバージョンや使用するポートなどの、各コンテナをどう動かすかに関する情報

Podは、アプリケーション固有の「論理ホスト」をモデル化し、比較的密接に結合されたさまざまなアプリケーションコンテナを含むことができます。 たとえば、Podには、Node.jsアプリケーションを含むコンテナと、Node.js Webサーバによって公開されるデータを供給する別のコンテナの両方を含めることができます。Pod内のコンテナはIPアドレスとポートスペースを共有し、常に同じ場所に配置され、同じスケジュールに入れられ、同じノード上の共有コンテキストで実行されます。

Podは、Kubernetesプラットフォームの原子単位です。 Kubernetes上にDeploymentを作成すると、そのDeploymentはその中にコンテナを持つPodを作成します(コンテナを直接作成するのではなく)。 各Podは、スケジュールされているノードに関連付けられており、終了(再起動ポリシーに従って)または削除されるまでそこに残ります。 ノードに障害が発生した場合、同じPodがクラスター内の他の使用可能なノードにスケジュールされます。

まとめ:

  • Pod
  • ノード
  • kubectlの主要なコマンド

Podは1つ以上のアプリケーションコンテナ(Dockerなど)のグループであり、共有ストレージ(ボリューム)、IPアドレス、それらの実行方法に関する情報が含まれています。


Podの概要


ノード

Podは常にノード上で動作します。ノードはKubernetesではワーカーマシンであり、クラスターによって仮想、物理マシンのどちらであってもかまいません。各ノードはマスターによって管理されます。ノードは複数のPodを持つことができ、Kubernetesマスターはクラスター内のノード間でPodのスケジュールを自動的に処理します。マスターの自動スケジューリングは各ノードで利用可能なリソースを考慮に入れます。

すべてのKubernetesノードでは少なくとも以下のものが動作します。

  • Kubelet: Kubernetesマスターとノード間の通信を担当するプロセス。マシン上で実行されているPodとコンテナを管理します。
  • レジストリからコンテナイメージを取得し、コンテナを解凍し、アプリケーションを実行することを担当する、Dockerのようなコンテナランタイム。

コンテナ同士が密接に結合され、ディスクなどのリソースを共有する必要がある場合は、コンテナを1つのPodにまとめてスケジュールする必要があります。


ノードの概要


kubectlを使ったトラブルシューティング

モジュール2では、kubectlのコマンドラインインターフェースを使用しました。モジュール3でもこれを使用して、デプロイされたアプリケーションとその環境に関する情報を入手します。最も一般的な操作は、次のkubectlコマンドで実行できます。

  • kubectl get - リソースの一覧を表示
  • kubectl describe - 単一リソースに関する詳細情報を表示
  • kubectl logs - 単一Pod上の単一コンテナ内のログを表示
  • kubectl exec - 単一Pod上の単一コンテナ内でコマンドを実行

これらのコマンドを使用して、アプリケーションがいつデプロイされたか、それらの現在の状況、実行中の場所、および構成を確認することができます。

クラスターのコンポーネントとコマンドラインの詳細についてわかったので、次にデプロイしたアプリケーションを探索してみましょう。

ノードはKubernetesではワーカーマシンであり、クラスターに応じてVMまたは物理マシンになります。 複数のPodを1つのノードで実行できます。


2.3.2 - 対話型チュートリアル - デプロイしたアプリケーションの探索


ターミナルを使うには、PCまたはタブレットをお使いください

2.4 - アプリケーションの公開

2.4.1 - Serviceを使ったアプリケーションの公開

目標

  • KubernetesにおけるServiceについて理解する
  • ラベルとLabelSelectorオブジェクトがServiceにどう関係しているかを理解する
  • Serviceを使って、Kubernetesクラスターの外にアプリケーションを公開する

Kubernetes Serviceの概要

Kubernetes Podの寿命は永続的ではありません。実際、Podにはライフサイクルがあります。ワーカーのノードが停止すると、そのノードで実行されているPodも失われます。そうなると、ReplicaSetは、新しいPodを作成してアプリケーションを実行し続けるために、クラスターを動的に目的の状態に戻すことができます。別の例として、3つのレプリカを持つ画像処理バックエンドを考えます。それらのレプリカは交換可能です。フロントエンドシステムはバックエンドのレプリカを気にしたり、Podが失われて再作成されたとしても配慮すべきではありません。ただし、Kubernetesクラスター内の各Podは、同じノード上のPodであっても一意のIPアドレスを持っているため、アプリケーションが機能し続けるように、Pod間の変更を自動的に調整する方法が必要です。

KubernetesのServiceは、Podの論理セットと、それらにアクセスするためのポリシーを定義する抽象概念です。Serviceによって、依存Pod間の疎結合が可能になります。Serviceは、すべてのKubernetesオブジェクトのように、YAML(推奨)またはJSONを使って定義されます。Serviceが対象とするPodのセットは通常、LabelSelectorによって決定されます(なぜ仕様にセレクタを含めずにServiceが必要になるのかについては下記を参照してください)。

各Podには固有のIPアドレスがありますが、それらのIPは、Serviceなしではクラスターの外部に公開されません。Serviceによって、アプリケーションはトラフィックを受信できるようになります。ServiceSpecでtypeを指定することで、Serviceをさまざまな方法で公開することができます。

  • ClusterIP (既定値) - クラスター内の内部IPでServiceを公開します。この型では、Serviceはクラスター内からのみ到達可能になります。
  • NodePort - NATを使用して、クラスター内の選択された各ノードの同じポートにServiceを公開します。<NodeIP>:<NodePort>を使用してクラスターの外部からServiceにアクセスできるようにします。これはClusterIPのスーパーセットです。
  • LoadBalancer - 現在のクラウドに外部ロードバランサを作成し(サポートされている場合)、Serviceに固定の外部IPを割り当てます。これはNodePortのスーパーセットです。
  • ExternalName - 仕様のexternalNameで指定した名前のCNAMEレコードを返すことによって、任意の名前を使ってServiceを公開します。プロキシは使用されません。このタイプはv1.7以上のkube-dnsを必要とします。

さまざまな種類のServiceに関する詳細情報はUsing Source IP tutorialにあります。アプリケーションとServiceの接続も参照してください。

加えて、Serviceには、仕様にselectorを定義しないというユースケースがいくつかあります。selectorを指定せずに作成したServiceについて、対応するEndpointsオブジェクトは作成されません。これによって、ユーザーは手動でServiceを特定のエンドポイントにマッピングできます。セレクタがない可能性があるもう1つの可能性は、type:ExternalNameを厳密に使用していることです。

まとめ

  • Podを外部トラフィックに公開する
  • 複数のPod間でトラフィックを負荷分散する
  • ラベルを使う

Kubernetes Serviceは、Podの論理セットを定義し、それらのPodに対する外部トラフィックの公開、負荷分散、およびサービス検出を可能にする抽象化層です。


Serviceとラベル

Serviceは、一連のPodにトラフィックをルーティングします。Serviceは、アプリケーションに影響を与えることなく、KubernetesでPodが死んだり複製したりすることを可能にする抽象概念です。(アプリケーションのフロントエンドおよびバックエンドコンポーネントなどの)依存Pod間の検出とルーティングは、Kubernetes Serviceによって処理されます。

Serviceは、ラベルとセレクタを使用して一連のPodを照合します。これは、Kubernetes内のオブジェクトに対する論理操作を可能にするグループ化のプリミティブです。ラベルはオブジェクトに付けられたkey/valueのペアであり、さまざまな方法で使用できます。

  • 開発、テスト、および本番用のオブジェクトを指定する
  • バージョンタグを埋め込む
  • タグを使用してオブジェクトを分類する


ラベルは、作成時またはそれ以降にオブジェクトにアタッチでき、いつでも変更可能です。Serviceを使用してアプリケーションを公開し、いくつかのラベルを適用してみましょう。


2.4.2 - 対話型チュートリアル - アプリケーションの公開

ターミナルを使うには、PCまたはタブレットをお使いください

2.5 - アプリケーションのスケーリング

2.5.1 - アプリケーションの複数インスタンスを実行

目標

  • kubectlを使用してアプリケーションをスケールする

アプリケーションのスケーリング

前回のモジュールでは、Deploymentを作成し、それをService経由で公開しました。該当のDeploymentでは、アプリケーションを実行するためのPodを1つだけ作成しました。トラフィックが増加した場合、ユーザーの需要に対応するためにアプリケーションをスケールする必要があります。

スケーリングは、Deploymentのレプリカの数を変更することによって実現可能です。

まとめ

  • Deploymentのスケーリング

kubectl create deploymentコマンドの--replicasパラメーターを使用することで、最初から複数のインスタンスを含むDeploymentを作成できます。


スケーリングの概要


Deploymentをスケールアウトすると、新しいPodが作成され、使用可能なリソースを持つノードにスケジュールされます。スケールすると、Podの数が増えて新たな望ましい状態になります。KubernetesはPodのオートスケーリングもサポートしていますが、このチュートリアルでは範囲外です。スケーリングを0に設定することも可能で、指定された配置のすべてのPodを終了させます。

アプリケーションの複数インスタンスを実行するには、それらすべてにトラフィックを分散する方法が必要になります。Serviceには、公開されたDeploymentのすべてのPodにネットワークトラフィックを分散する統合ロードバランサがあります。Serviceは、エンドポイントを使用して実行中のPodを継続的に監視し、トラフィックが使用可能なPodにのみ送信されるようにします。

スケーリングは、Deploymentのレプリカの数を変更することによって実現可能です。


アプリケーションの複数のインスタンスを実行すると、ダウンタイムなしでローリングアップデートを実行できます。それについては、次のモジュールで学習します。それでは、オンラインのターミナルを使って、アプリケーションをデプロイしてみましょう。


2.5.2 - 対話型チュートリアル - アプリケーションのスケーリング

ターミナルを使うには、PCまたはタブレットをお使いください

2.6 - アプリケーションのアップデート

2.6.1 - ローリングアップデートの実行

目標

  • kubectlを使ってローリングアップデートを実行する

アプリケーションのアップデート

ユーザーはアプリケーションが常に利用可能であることを期待し、開発者はそれらの新しいバージョンを1日に数回デプロイすることが期待されます。Kubernetesでは、アプリケーションのアップデートをローリングアップデートで行います。ローリングアップデートでは、Podインスタンスを新しいインスタンスで段階的にアップデートすることで、ダウンタイムなしでDeploymentをアップデートできます。新しいPodは、利用可能なリソースを持つノードにスケジュールされます。

前回のモジュールでは、複数のインスタンスを実行するようにアプリケーションをデプロイしました。これは、アプリケーションの可用性に影響を与えずにアップデートを行うための要件です。デフォルトでは、アップデート中に使用できなくなる可能性があるPodの最大数と作成できる新しいPodの最大数は1です。どちらのオプションも、Podの数または全体数に対する割合(%)のいずれかに設定できます。Kubernetesでは、アップデートはバージョン管理されており、Deploymentにおけるアップデートは以前の(stable)バージョンに戻すことができます。

まとめ

  • アプリケーションのアップデート

ローリングアップデートでは、Podを新しいインスタンスで段階的にアップデートすることで、ダウンタイムなしDeploymentをアップデートできます。


ローリングアップデートの概要


アプリケーションのスケーリングと同様に、Deploymentがパブリックに公開されている場合、Serviceはアップデート中に利用可能なPodのみにトラフィックを負荷分散します。 利用可能なPodは、アプリケーションのユーザーが利用できるインスタンスです。

ローリングアップデートでは、次の操作が可能です。

  • コンテナイメージのアップデートを介した、ある環境から別の環境へのアプリケーションの昇格
  • 以前のバージョンへのロールバック
  • ダウンタイムなしでのアプリケーションのCI/CD

Deploymentがパブリックに公開されている場合、Serviceはアップデート中に利用可能なPodにのみトラフィックを負荷分散します。


次の対話型チュートリアルでは、アプリケーションを新しいバージョンにアップデートし、ロールバックも実行します。


2.6.2 - 対話型チュートリアル - アプリケーションのアップデート

ターミナルを使うには、PCまたはタブレットをお使いください

3 - 設定

3.1 - ConfigMapを使ったRedisの設定

本ページでは、ConfigMapを使ったコンテナの設定に基づき、ConfigMapを使ってRedisの設定を行う実践的な例を提供します。

目標

  • 以下の要素を含むkustomization.yamlファイルを作成する:
    • ConfigMapGenerator
    • ConfigMapを使ったPodリソースの設定
  • kubectl apply -k ./コマンドにてディレクトリ全体を適用する
  • 設定が正しく反映されていることを確認する

始める前に

Kubernetesクラスターが必要、かつそのクラスターと通信するためにkubectlコマンドラインツールが設定されている必要があります。 このチュートリアルは、コントロールプレーンのホストとして動作していない少なくとも2つのノードを持つクラスターで実行することをおすすめします。 まだクラスターがない場合、minikubeを使って作成するか、 以下のいずれかのKubernetesプレイグラウンドも使用できます:

バージョンを確認するには次のコマンドを実行してください: kubectl version.

実践例: ConfigMapを使ったRedisの設定

以下の手順に従って、ConfigMapに保存されているデータを使用してRedisキャッシュを設定できます。

最初に、redis-configファイルからConfigMapを含むkustomization.yamlを作成します:

maxmemory 2mb
maxmemory-policy allkeys-lru
curl -OL https://k8s.io/examples/pods/config/redis-config

cat <<EOF >./kustomization.yaml
configMapGenerator:
- name: example-redis-config
  files:
  - redis-config
EOF

Podリソースの設定をkustomization.yamlに入れます:

apiVersion: v1
kind: Pod
metadata:
  name: redis
spec:
  containers:
  - name: redis
    image: kubernetes/redis:v1
    env:
    - name: MASTER
      value: "true"
    ports:
    - containerPort: 6379
    resources:
      limits:
        cpu: "0.1"
    volumeMounts:
    - mountPath: /redis-master-data
      name: data
    - mountPath: /redis-master
      name: config
  volumes:
    - name: data
      emptyDir: {}
    - name: config
      configMap:
        name: example-redis-config
        items:
        - key: redis-config
          path: redis.conf
curl -OL https://raw.githubusercontent.com/kubernetes/website/master/content/en/examples/pods/config/redis-pod.yaml

cat <<EOF >>./kustomization.yaml
resources:
- redis-pod.yaml
EOF

kustomizationディレクトリを反映して、ConfigMapオブジェクトとPodオブジェクトの両方を作成します:

kubectl apply -k .

作成されたオブジェクトを確認します

> kubectl get -k .
NAME                                        DATA   AGE
configmap/example-redis-config-dgh9dg555m   1      52s

NAME        READY   STATUS    RESTARTS   AGE
pod/redis   1/1     Running   0          52s

この例では、設定ファイルのボリュームは/redis-masterにマウントされています。 pathを使ってredis-configキーをredis.confという名前のファイルに追加します。 したがって、redisコンフィグのファイルパスは/redis-master/redis.confです。 ここが、コンテナイメージがredisマスターの設定ファイルを探す場所です。

kubectl execを使ってPodに入り、redis-cliツールを実行して設定が正しく適用されたことを確認してください:

kubectl exec -it redis -- redis-cli
127.0.0.1:6379> CONFIG GET maxmemory
1) "maxmemory"
2) "2097152"
127.0.0.1:6379> CONFIG GET maxmemory-policy
1) "maxmemory-policy"
2) "allkeys-lru"

作成したPodを削除します:

kubectl delete pod redis

次の項目

4 - ステートレスアプリケーション

4.1 - クラスター内のアプリケーションにアクセスするために外部IPアドレスを公開する

このページでは、外部IPアドレスを公開するKubernetesのServiceオブジェクトを作成する方法を示します。

始める前に

  • kubectlをインストールしてください。

  • Kubernetesクラスターを作成する際に、Google Kubernetes EngineやAmazon Web Servicesのようなクラウドプロバイダーを使用します。このチュートリアルでは、クラウドプロバイダーを必要とする外部ロードバランサーを作成します。

  • Kubernetes APIサーバーと通信するために、kubectlを設定してください。手順については、各クラウドプロバイダーのドキュメントを参照してください。

目標

  • 5つのインスタンスで実際のアプリケーションを起動します。
  • 外部IPアドレスを公開するServiceオブジェクトを作成します。
  • 起動中のアプリケーションにアクセスするためにServiceオブジェクトを使用します。

5つのPodで起動しているアプリケーションへのServiceの作成

  1. クラスターにてHello Worldアプリケーションを実行してください。
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/name: load-balancer-example
  name: hello-world
spec:
  replicas: 5
  selector:
    matchLabels:
      app.kubernetes.io/name: load-balancer-example
  template:
    metadata:
      labels:
        app.kubernetes.io/name: load-balancer-example
    spec:
      containers:
      - image: gcr.io/google-samples/node-hello:1.0
        name: hello-world
        ports:
        - containerPort: 8080
kubectl apply -f https://k8s.io/examples/service/load-balancer-example.yaml

上記のコマンドにより、 Deploymentを作成し、ReplicaSetを関連づけます。ReplicaSetには5つのPodがあり、それぞれHello Worldアプリケーションが起動しています。

  1. Deploymentに関する情報を表示します:

     kubectl get deployments hello-world
     kubectl describe deployments hello-world
    
  2. ReplicaSetオブジェクトに関する情報を表示します:

     kubectl get replicasets
     kubectl describe replicasets
    
  3. Deploymentを公開するServiceオブジェクトを作成します。

     kubectl expose deployment hello-world --type=LoadBalancer --name=my-service
    
  4. Serviceに関する情報を表示します:

     kubectl get services my-service
    

    出力は次のようになります:

     NAME         TYPE           CLUSTER-IP     EXTERNAL-IP      PORT(S)    AGE
     my-service   LoadBalancer   10.3.245.137   104.198.205.71   8080/TCP   54s
    
  5. Serviceに関する詳細な情報を表示します:

     kubectl describe services my-service
    

    出力は次のようになります:

     Name:           my-service
     Namespace:      default
     Labels:         app.kubernetes.io/name=load-balancer-example
     Annotations:    <none>
     Selector:       app.kubernetes.io/name=load-balancer-example
     Type:           LoadBalancer
     IP:             10.3.245.137
     LoadBalancer Ingress:   104.198.205.71
     Port:           <unset> 8080/TCP
     NodePort:       <unset> 32377/TCP
     Endpoints:      10.0.0.6:8080,10.0.1.6:8080,10.0.1.7:8080 + 2 more...
     Session Affinity:   None
     Events:         <none>
    

    Serviceによって公開された外部IPアドレス(LoadBalancer Ingress)を記録しておいてください。 この例では、外部IPアドレスは104.198.205.71です。 また、PortおよびNodePortの値も控えてください。 この例では、Portは8080、NodePortは32377です。

  6. 先ほどの出力にて、Serviceにはいくつかのエンドポイントがあることを確認できます: 10.0.0.6:8080、 10.0.1.6:8080、10.0.1.7:8080、その他2つです。 これらはHello Worldアプリケーションが動作しているPodの内部IPアドレスです。 これらのPodのアドレスを確認するには、次のコマンドを実行します:

     kubectl get pods --output=wide
    

    出力は次のようになります:

     NAME                         ...  IP         NODE
     hello-world-2895499144-1jaz9 ...  10.0.1.6   gke-cluster-1-default-pool-e0b8d269-1afc
     hello-world-2895499144-2e5uh ...  10.0.1.8   gke-cluster-1-default-pool-e0b8d269-1afc
     hello-world-2895499144-9m4h1 ...  10.0.0.6   gke-cluster-1-default-pool-e0b8d269-5v7a
     hello-world-2895499144-o4z13 ...  10.0.1.7   gke-cluster-1-default-pool-e0b8d269-1afc
     hello-world-2895499144-segjf ...  10.0.2.5   gke-cluster-1-default-pool-e0b8d269-cpuc
    
  7. Hello Worldアプリケーションにアクセスするために、外部IPアドレス(LoadBalancer Ingress)を使用します:

     curl http://<external-ip>:<port>
    

    ここで、<external-ip>はServiceの外部IPアドレス(LoadBalancer Ingress)で、 <port>はServiceの詳細出力におけるPortです。minikubeを使用している場合、minikube service my-serviceを実行することでHello Worldアプリケーションをブラウザで自動的に 開かれます。

    正常なリクエストに対するレスポンスは、helloメッセージです:

     Hello Kubernetes!
    

クリーンアップ

Serviceを削除する場合、次のコマンドを実行します:

kubectl delete services my-service

Deployment、ReplicaSet、およびHello Worldアプリケーションが動作しているPodを削除する場合、次のコマンドを実行します:

kubectl delete deployment hello-world

次の項目

connecting applications with servicesにて詳細を学ぶことができます。

4.2 - 例: Redisを使用したPHPのゲストブックアプリケーションのデプロイ

このチュートリアルでは、KubernetesとDockerを使用した、シンプルなマルチティアのウェブアプリケーションのビルドとデプロイの方法を紹介します。この例は、以下のコンポーネントから構成されています。

  • ゲストブックのエントリーを保存するための、シングルインスタンスのRedisマスター
  • 読み込みデータ配信用の、複数のレプリケーションされたRedisインスタンス
  • 複数のウェブフロントエンドのインスタンス

目標

  • Redisのマスターを起動する。
  • Redisのスレーブを起動する。
  • ゲストブックのフロントエンドを起動する。
  • フロントエンドのServiceを公開して表示を確認する。
  • クリーンアップする。

始める前に

Kubernetesクラスターが必要、かつそのクラスターと通信するためにkubectlコマンドラインツールが設定されている必要があります。 このチュートリアルは、コントロールプレーンのホストとして動作していない少なくとも2つのノードを持つクラスターで実行することをおすすめします。 まだクラスターがない場合、minikubeを使って作成するか、 以下のいずれかのKubernetesプレイグラウンドも使用できます:

バージョンを確認するには次のコマンドを実行してください: kubectl version.

Redisのマスターを起動する

ゲストブックアプリケーションでは、データを保存するためにRedisを使用します。ゲストブックはRedisのマスターインスタンスにデータを書き込み、複数のRedisのスレーブインスタンスからデータを読み込みます。

RedisのマスターのDeploymentを作成する

以下のマニフェストファイルは、シングルレプリカのRedisのマスターPodを実行するDeploymentコントローラーを指定しています。

apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
  name: redis-master
  labels:
    app: redis
spec:
  selector:
    matchLabels:
      app: redis
      role: master
      tier: backend
  replicas: 1
  template:
    metadata:
      labels:
        app: redis
        role: master
        tier: backend
    spec:
      containers:
      - name: master
        image: registry.k8s.io/redis:e2e  # or just image: redis
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
        ports:
        - containerPort: 6379
  1. マニフェストファイルをダウンロードしたディレクトリ内で、ターミナルウィンドウを起動します。

  2. redis-master-deployment.yamlファイルから、RedisのマスターのDeploymentを適用します。

    kubectl apply -f https://k8s.io/examples/application/guestbook/redis-master-deployment.yaml
    
  3. Podのリストを問い合わせて、RedisのマスターのPodが実行中になっていることを確認します。

    kubectl get pods
    

    結果は次のようになるはずです。

    NAME                            READY     STATUS    RESTARTS   AGE
    redis-master-1068406935-3lswp   1/1       Running   0          28s
    
  4. 次のコマンドを実行して、RedisのマスターのPodからログを表示します。

    kubectl logs -f POD-NAME
    

RedisのマスターのServiceを作成する

ゲストブックアプリケーションは、データを書き込むためにRedisのマスターと通信する必要があります。そのためには、Serviceを適用して、トラフィックをRedisのマスターのPodへプロキシーしなければなりません。Serviceは、Podにアクセスするためのポリシーを指定します。

apiVersion: v1
kind: Service
metadata:
  name: redis-master
  labels:
    app: redis
    role: master
    tier: backend
spec:
  ports:
  - port: 6379
    targetPort: 6379
  selector:
    app: redis
    role: master
    tier: backend
  1. 次のredis-master-service.yamlから、RedisのマスターのServiceを適用します。

    kubectl apply -f https://k8s.io/examples/application/guestbook/redis-master-service.yaml
    
  2. Serviceのリストを問い合わせて、RedisのマスターのServiceが実行中になっていることを確認します。

    kubectl get service
    

    The response should be similar to this:

    NAME           TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
    kubernetes     ClusterIP   10.0.0.1     <none>        443/TCP    1m
    redis-master   ClusterIP   10.0.0.151   <none>        6379/TCP   8s
    

Redisのスレーブを起動する

Redisのマスターは1つのPodですが、レプリカのRedisのスレーブを追加することで、トラフィックの需要を満たすための高い可用性を持たせることができます。

RedisのスレーブのDeploymentを作成する

Deploymentはマニフェストファイル内に書かれた設定に基づいてスケールします。ここでは、Deploymentオブジェクトは2つのレプリカを指定しています。

もし1つもレプリカが実行されていなければ、このDeploymentは2つのレプリカをコンテナクラスター上で起動します。逆に、もしすでに2つ以上のレプリカが実行されていれば、実行中のレプリカが2つになるようにスケールダウンします。

apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
  name: redis-slave
  labels:
    app: redis
spec:
  selector:
    matchLabels:
      app: redis
      role: slave
      tier: backend
  replicas: 2
  template:
    metadata:
      labels:
        app: redis
        role: slave
        tier: backend
    spec:
      containers:
      - name: slave
        image: gcr.io/google_samples/gb-redisslave:v3
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
        env:
        - name: GET_HOSTS_FROM
          value: dns
          # Using `GET_HOSTS_FROM=dns` requires your cluster to
          # provide a dns service. As of Kubernetes 1.3, DNS is a built-in
          # service launched automatically. However, if the cluster you are using
          # does not have a built-in DNS service, you can instead
          # access an environment variable to find the master
          # service's host. To do so, comment out the 'value: dns' line above, and
          # uncomment the line below:
          # value: env
        ports:
        - containerPort: 6379
  1. redis-slave-deployment.yamlファイルから、RedisのスレーブのDeploymentを適用します。

    kubectl apply -f https://k8s.io/examples/application/guestbook/redis-slave-deployment.yaml
    
  2. Podのリストを問い合わせて、RedisのスレーブのPodが実行中になっていることを確認します。

    kubectl get pods
    

    結果は次のようになるはずです。

    NAME                            READY     STATUS              RESTARTS   AGE
    redis-master-1068406935-3lswp   1/1       Running             0          1m
    redis-slave-2005841000-fpvqc    0/1       ContainerCreating   0          6s
    redis-slave-2005841000-phfv9    0/1       ContainerCreating   0          6s
    

RedisのスレーブのServiceを作成する

ゲストブックアプリケーションは、データを読み込むためにRedisのスレーブと通信する必要があります。Redisのスレーブが発見できるようにするためには、Serviceをセットアップする必要があります。Serviceは一連のPodに対する透過的なロードバランシングを提供します。

apiVersion: v1
kind: Service
metadata:
  name: redis-slave
  labels:
    app: redis
    role: slave
    tier: backend
spec:
  ports:
  - port: 6379
  selector:
    app: redis
    role: slave
    tier: backend
  1. 次のredis-slave-service.yamlファイルから、RedisのスレーブのServiceを適用します。

    kubectl apply -f https://k8s.io/examples/application/guestbook/redis-slave-service.yaml
    
  2. Serviceのリストを問い合わせて、RedisのスレーブのServiceが実行中になっていることを確認します。

    kubectl get services
    

    結果は次のようになるはずです。

    NAME           TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
    kubernetes     ClusterIP   10.0.0.1     <none>        443/TCP    2m
    redis-master   ClusterIP   10.0.0.151   <none>        6379/TCP   1m
    redis-slave    ClusterIP   10.0.0.223   <none>        6379/TCP   6s
    

ゲストブックのフロントエンドをセットアップして公開する

ゲストブックアプリケーションには、HTTPリクエストをサーブするPHPで書かれたウェブフロントエンドがあります。このアプリケーションは、書き込みリクエストに対してはredis-master Serviceに、読み込みリクエストに対してはredis-slave Serviceに接続するように設定されています。

ゲストブックのフロントエンドのDeploymentを作成する

apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
  name: frontend
  labels:
    app: guestbook
spec:
  selector:
    matchLabels:
      app: guestbook
      tier: frontend
  replicas: 3
  template:
    metadata:
      labels:
        app: guestbook
        tier: frontend
    spec:
      containers:
      - name: php-redis
        image: gcr.io/google-samples/gb-frontend:v4
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
        env:
        - name: GET_HOSTS_FROM
          value: dns
          # Using `GET_HOSTS_FROM=dns` requires your cluster to
          # provide a dns service. As of Kubernetes 1.3, DNS is a built-in
          # service launched automatically. However, if the cluster you are using
          # does not have a built-in DNS service, you can instead
          # access an environment variable to find the master
          # service's host. To do so, comment out the 'value: dns' line above, and
          # uncomment the line below:
          # value: env
        ports:
        - containerPort: 80
  1. frontend-deployment.yamlファイルから、フロントエンドのDeploymentを適用します。

    kubectl apply -f https://k8s.io/examples/application/guestbook/frontend-deployment.yaml
    
  2. Podのリストを問い合わせて、3つのフロントエンドのレプリカが実行中になっていることを確認します。

    kubectl get pods -l app=guestbook -l tier=frontend
    

    結果は次のようになるはずです。

    NAME                        READY     STATUS    RESTARTS   AGE
    frontend-3823415956-dsvc5   1/1       Running   0          54s
    frontend-3823415956-k22zn   1/1       Running   0          54s
    frontend-3823415956-w9gbt   1/1       Running   0          54s
    

フロントエンドのServiceを作成する

適用したredis-slaveおよびredis-master Serviceは、コンテナクラスター内部からのみアクセス可能です。これは、デフォルトのServiceのtypeがClusterIPであるためです。ClusterIPは、Serviceが指している一連のPodに対して1つのIPアドレスを提供します。このIPアドレスはクラスター内部からのみアクセスできます。

もしゲストの人にゲストブックにアクセスしてほしいのなら、フロントエンドServiceを外部から見えるように設定しなければなりません。そうすれば、クライアントはコンテナクラスターの外部からServiceにリクエストを送れるようになります。Minikubeでは、ServiceをNodePortでのみ公開できます。

apiVersion: v1
kind: Service
metadata:
  name: frontend
  labels:
    app: guestbook
    tier: frontend
spec:
  # comment or delete the following line if you want to use a LoadBalancer
  type: NodePort 
  # if your cluster supports it, uncomment the following to automatically create
  # an external load-balanced IP for the frontend service.
  # type: LoadBalancer
  ports:
  - port: 80
  selector:
    app: guestbook
    tier: frontend
  1. frontend-service.yamlファイルから、フロントエンドのServiceを提供します。

    kubectl apply -f https://k8s.io/examples/application/guestbook/frontend-service.yaml
    
  2. Serviceのリストを問い合わせて、フロントエンドのServiceが実行中であることを確認します。

    kubectl get services
    

    結果は次のようになるはずです。

    NAME           TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)        AGE
    frontend       NodePort    10.0.0.112   <none>       80:31323/TCP   6s
    kubernetes     ClusterIP   10.0.0.1     <none>        443/TCP        4m
    redis-master   ClusterIP   10.0.0.151   <none>        6379/TCP       2m
    redis-slave    ClusterIP   10.0.0.223   <none>        6379/TCP       1m
    

フロントエンドのServiceをNodePort経由で表示する

このアプリケーションをMinikubeやローカルのクラスターにデプロイした場合、ゲストブックを表示するためのIPアドレスを見つける必要があります。

  1. 次のコマンドを実行すると、フロントエンドServiceに対するIPアドレスを取得できます。

    minikube service frontend --url
    

    結果は次のようになるはずです。

    http://192.168.99.100:31323
    
  2. IPアドレスをコピーして、ブラウザー上でページを読み込み、ゲストブックを表示しましょう。

フロントエンドのServiceをLoadBalancer経由で表示する

もしfrontend-service.yamlマニフェストをtype: LoadBalancerでデプロイした場合、ゲストブックを表示するためのIPアドレスを見つける必要があります。

  1. 次のコマンドを実行すると、フロントエンドServiceに対するIPアドレスを取得できます。

    kubectl get service frontend
    

    結果は次のようになるはずです。

    NAME       TYPE        CLUSTER-IP      EXTERNAL-IP        PORT(S)        AGE
    frontend   ClusterIP   10.51.242.136   109.197.92.229     80:32372/TCP   1m
    
  2. 外部IPアドレス(EXTERNAL-IP)をコピーして、ブラウザー上でページを読み込み、ゲストブックを表示しましょう。

ウェブフロントエンドをスケールする

サーバーがDeploymentコントローラーを使用するServiceとして定義されているため、スケールアップやスケールダウンは簡単です。

  1. 次のコマンドを実行すると、フロントエンドのPodの数をスケールアップできます。

    kubectl scale deployment frontend --replicas=5
    
  2. Podのリストを問い合わせて、実行中のフロントエンドのPodの数を確認します。

    kubectl get pods
    

    結果は次のようになるはずです。

    NAME                            READY     STATUS    RESTARTS   AGE
    frontend-3823415956-70qj5       1/1       Running   0          5s
    frontend-3823415956-dsvc5       1/1       Running   0          54m
    frontend-3823415956-k22zn       1/1       Running   0          54m
    frontend-3823415956-w9gbt       1/1       Running   0          54m
    frontend-3823415956-x2pld       1/1       Running   0          5s
    redis-master-1068406935-3lswp   1/1       Running   0          56m
    redis-slave-2005841000-fpvqc    1/1       Running   0          55m
    redis-slave-2005841000-phfv9    1/1       Running   0          55m
    
  3. 次のコマンドを実行すると、フロントエンドのPodの数をスケールダウンできます。

    kubectl scale deployment frontend --replicas=2
    
  4. Podのリストを問い合わせて、実行中のフロントエンドのPodの数を確認します。

    kubectl get pods
    

    結果は次のようになるはずです。

    NAME                            READY     STATUS    RESTARTS   AGE
    frontend-3823415956-k22zn       1/1       Running   0          1h
    frontend-3823415956-w9gbt       1/1       Running   0          1h
    redis-master-1068406935-3lswp   1/1       Running   0          1h
    redis-slave-2005841000-fpvqc    1/1       Running   0          1h
    redis-slave-2005841000-phfv9    1/1       Running   0          1h
    

クリーンアップ

DeploymentとServiceを削除すると、実行中のPodも削除されます。ラベルを使用すると、複数のリソースを1つのコマンドで削除できます。

  1. 次のコマンドを実行すると、すべてのPod、Deployment、Serviceが削除されます。

    kubectl delete deployment -l app=redis
    kubectl delete service -l app=redis
    kubectl delete deployment -l app=guestbook
    kubectl delete service -l app=guestbook
    

    結果は次のようになるはずです。

    deployment.apps "redis-master" deleted
    deployment.apps "redis-slave" deleted
    service "redis-master" deleted
    service "redis-slave" deleted
    deployment.apps "frontend" deleted
    service "frontend" deleted
    
  2. Podのリストを問い合わせて、実行中のPodが存在しないことを確認します。

    kubectl get pods
    

    結果は次のようになるはずです。

    No resources found.
    

次の項目

4.3 - 例: PHP / Redisを使用したゲストブックの例にロギングとメトリクスを追加する

このチュートリアルは、Redisを使用したPHPのゲストブックのチュートリアルを前提に作られています。Elasticが開発したログ、メトリクス、ネットワークデータを転送するオープンソースの軽量データシッパーであるBeatsを、ゲストブックと同じKubernetesクラスターにデプロイします。BeatsはElasticsearchに対してデータの収集、分析、インデックス作成を行うため、結果の運用情報をKibana上で表示・分析できるようになります。この例は、以下のコンポーネントから構成されます。

目標

  • Redisを使用したPHPのゲストブックを起動する。
  • kube-state-metricsをインストールする。
  • KubernetesのSecretを作成する。
  • Beatsをデプロイする。
  • ログとメトリクスのダッシュボードを表示する。

始める前に

Kubernetesクラスターが必要、かつそのクラスターと通信するためにkubectlコマンドラインツールが設定されている必要があります。 このチュートリアルは、コントロールプレーンのホストとして動作していない少なくとも2つのノードを持つクラスターで実行することをおすすめします。 まだクラスターがない場合、minikubeを使って作成するか、 以下のいずれかのKubernetesプレイグラウンドも使用できます:

バージョンを確認するには次のコマンドを実行してください: kubectl version.

追加で以下の作業が必要です。

Redisを使用したPHPのゲストブックを起動する

このチュートリアルは、Redisを使用したPHPのゲストブックのチュートリアルを前提に作られています。もしゲストブックアプリケーションが実行中なら、そのアプリケーションを監視できます。もしまだ実行中のアプリケーションがなければ、ゲストブックのデプロイの手順を行い、クリーンアップのステップは実行しないでください。ゲストブックが起動したら、このページに戻ってきてください。

Cluster role bindingを追加する

クラスターレベルのrole bindingを作成して、kube-state-metricsとBeatsをクラスターレベルで(kube-system内に)デプロイできるようにします。

kubectl create clusterrolebinding cluster-admin-binding \
 --clusterrole=cluster-admin --user=<k8sのプロバイダーアカウントと紐付いたあなたのメールアドレス>

kube-state-metricsをインストールする

Kubernetesのkube-state-metricsは、Kubernetes APIサーバーをlistenして、オブジェクトの状態に関するメトリクスを生成する単純なサービスです。Metricbeatはこれらのメトリクスを報告します。kube-state-metricsをゲストブックが実行されているKubernetesクラスターに追加しましょう。

kube-state-metricsが起動しているか確認する

kubectl get pods --namespace=kube-system | grep kube-state

必要に応じてkube-state-metricsをインストールする

git clone https://github.com/kubernetes/kube-state-metrics.git kube-state-metrics
kubectl apply -f kube-state-metrics/examples/standard
kubectl get pods --namespace=kube-system | grep kube-state-metrics

kube-state-metricsがRunningかつreadyの状態になっていることを確認します。

kubectl get pods -n kube-system -l app.kubernetes.io/name=kube-state-metrics

結果は次のようになります。

NAME                                 READY   STATUS    RESTARTS   AGE
kube-state-metrics-89d656bf8-vdthm   1/1     Running     0          21s

GitHubリポジトリのElasticの例をクローンする

git clone https://github.com/elastic/examples.git

これ以降のコマンドはexamples/beats-k8s-send-anywhereディレクトリ内のファイルを参照するため、カレントディレクトリを変更します。

cd examples/beats-k8s-send-anywhere

KubernetesのSecretを作成する

KubernetesのSecretとは、パスワード、トークン、または鍵などの小さなサイズの機密データを含んだオブジェクトのことです。このような機密情報はPodのspecやイメージの中に置くことも不可能ではありませんが、Secretオブジェクトの中に置くことで、情報の使用方法を適切に制御したり、誤って公開してしまうリスクを減らすことができます。

セルフマネージド

Elastic Cloud上のElasticsearch Serviceに接続する場合は、マネージドサービスタブに切り替えてください。

クレデンシャルを設定する

セルフマネージドのElasticsearchとKibanaへ接続する場合、KubernetesのSecretを作成するために編集するべきファイルは4つあります(セルフマネージドとは、事実上Elastic Cloud以外で実行されているElasticsearch Serviceを指します)。ファイルは次の4つです。

  1. ELASTICSEARCH_HOSTS
  2. ELASTICSEARCH_PASSWORD
  3. ELASTICSEARCH_USERNAME
  4. KIBANA_HOST

これらのファイルにElasticsearchクラスターとKibanaホストの情報を設定してください。ここでは例をいくつか示します(こちらの設定も参照してください)。

ELASTICSEARCH_HOSTS

  1. Elastic Elasticsearch Helm Chartで作成したnodeGroupの場合。

    ["http://elasticsearch-master.default.svc.cluster.local:9200"]
    
  2. Mac上で単一のElasticsearchノードが実行されており、BeatsがDocker for Macで実行中の場合。

    ["http://host.docker.internal:9200"]
    
  3. 2ノードのElasticsearchがVM上または物理ハードウェア上で実行中の場合。

    ["http://host1.example.com:9200", "http://host2.example.com:9200"]
    

ELASTICSEARCH_HOSTSを編集します。

vi ELASTICSEARCH_HOSTS

ELASTICSEARCH_PASSWORD

パスワードだけを書きます。空白、クォート、<>などの文字は書かないでください。

<yoursecretpassword>

ELASTICSEARCH_PASSWORDを編集します。

vi ELASTICSEARCH_PASSWORD

ELASTICSEARCH_USERNAME

ユーザー名だけを書きます。空白、クォート、<>などの文字は書かないでください。

<Elasticsearchに追加するユーザー名>

ELASTICSEARCH_USERNAMEを編集します。

vi ELASTICSEARCH_USERNAME

KIBANA_HOST

  1. Elastic Kibana Helm Chartで作成したKibanaインスタンスが実行中の場合。defaultというサブドメインは、default Namespaceを指します。もしHelm Chartを別のNamespaceにデプロイした場合、サブドメインは異なります。

    "kibana-kibana.default.svc.cluster.local:5601"
    
  2. Mac上でKibanaインスタンスが実行中で、BeatsがDocker for Macで実行中の場合。

    "host.docker.internal:5601"
    
  3. 2つのElasticsearchノードが、VMまたは物理ハードウェア上で実行中の場合。

    "host1.example.com:5601"
    

KIBANA_HOSTを編集します。

vi KIBANA_HOST

KubernetesのSecretを作成する

次のコマンドを実行すると、KubernetesのシステムレベルのNamespace(kube-system)に、たった今編集したファイルを元にSecretが作成されます。

kubectl create secret generic dynamic-logging \
  --from-file=./ELASTICSEARCH_HOSTS \
  --from-file=./ELASTICSEARCH_PASSWORD \
  --from-file=./ELASTICSEARCH_USERNAME \
  --from-file=./KIBANA_HOST \
  --namespace=kube-system

マネージドサービス

このタブは、Elastic Cloud上のElasticsearch Serviceの場合のみ必要です。もしセルフマネージドのElasticsearchとKibanaのDeployment向けにSecretをすでに作成した場合、Beatsをデプロイするに進んでください。

クレデンシャルを設定する

Elastic Cloud上のマネージドElasticsearch Serviceに接続する場合、KubernetesのSecretを作成するために編集する必要があるのは、次の2つのファイルです。

  1. ELASTIC_CLOUD_AUTH
  2. ELASTIC_CLOUD_ID

Deploymentを作成するときに、Elasticsearch Serviceのコンソールから提供された情報を設定してください。以下に例を示します。

ELASTIC_CLOUD_ID

devk8s:ABC123def456ghi789jkl123mno456pqr789stu123vwx456yza789bcd012efg345hijj678klm901nop345zEwOTJjMTc5YWQ0YzQ5OThlN2U5MjAwYTg4NTIzZQ==

ELASTIC_CLOUD_AUTH

ユーザー名、コロン(:)、パスワードだけを書きます。空白やクォートは書かないでください。

elastic:VFxJJf9Tjwer90wnfTghsn8w

必要なファイルを編集する

vi ELASTIC_CLOUD_ID
vi ELASTIC_CLOUD_AUTH

KubernetesのSecretを作成する

次のコマンドを実行すると、KubernetesのシステムレベルのNamespace(kube-system)に、たった今編集したファイルを元にSecretが作成されます。

kubectl create secret generic dynamic-logging \
  --from-file=./ELASTIC_CLOUD_ID \
  --from-file=./ELASTIC_CLOUD_AUTH \
  --namespace=kube-system

Beatsをデプロイする

マニフェストファイルはBeatごとに提供されます。これらのマニフェストファイルは、上で作成したSecretを使用して、BeatsをElasticsearchおよびKibanaサーバーに接続するように設定します。

Filebeatについて

Filebeatは、Kubernetesのノードと、ノード上で実行している各Pod内のコンテナから、ログを収集します。FilebeatはDaemonSetとしてデプロイされます。FilebeatはKubernetesクラスター上で実行されているアプリケーションを自動検出することもできます。起動時にFilebeatは既存のコンテナをスキャンし、それらに対して適切な設定を立ち上げ、その後、新しいstart/stopイベントを監視します。

Filebeatが、ゲストブックアプリケーションでデプロイしたRedisコンテナからRedisのログを特定・解析できるように自動検出を設定する例を示します。この設定はfilebeat-kubernetes.yamlファイル内にあります。

- condition.contains:
    kubernetes.labels.app: redis
  config:
    - module: redis
      log:
        input:
          type: docker
          containers.ids:
            - ${data.kubernetes.container.id}
      slowlog:
        enabled: true
        var.hosts: ["${data.host}:${data.port}"]

この設定により、Filebeatは、appラベルにredisという文字列が含まれるコンテナを検出したときにredis Filebeatモジュールを適用するようになります。redisモジュールには、input typeとしてdockerを使用することで(このRedisコンテナの標準出力のストリームと関連付けられた、Kubernetesノード上のファイルを読み取ることで)コンテナからlogストリームを収集する機能があります。さらに、このモジュールには、コンテナのメタデータとして提供された適切なPodのホストとポートと接続することにより、Redisのslowlogエントリーを収集する機能もあります。

Filebeatをデプロイする

kubectl create -f filebeat-kubernetes.yaml

検証する

kubectl get pods -n kube-system -l k8s-app=filebeat-dynamic

Metricbeatについて

Metricbeatの自動検出はFilebeatと同じ方法で設定します。以下にMetricbeatにおけるRedisコンテナの自動検出の設定を示します。この設定はmetricbeat-kubernetes.yamlファイル内にあります。

- condition.equals:
    kubernetes.labels.tier: backend
  config:
    - module: redis
      metricsets: ["info", "keyspace"]
      period: 10s

      # Redis hosts
      hosts: ["${data.host}:${data.port}"]

この設定により、Metricbeatは、tierラベルにbackendという文字列が含まれるコンテナを検出したときにredis Metricbeatモジュールを適用するようになります。redisモジュールには、コンテナのメタデータとして提供された適切なPodのホストとポートと接続することにより、コンテナからinfoおよびkeyspaceメトリクスを収集する機能があります。

Metricbeatをデプロイする

kubectl create -f metricbeat-kubernetes.yaml

検証する

kubectl get pods -n kube-system -l k8s-app=metricbeat

Packetbeatについて

Packetbeatの設定は、FilebeatやMetricbeatとは異なります。コンテナのラベルに対するパターンマッチを指定する代わりに、関連するプロトコルとポート番号に基づいた設定を書きます。以下に示すのは、ポート番号のサブセットです。

packetbeat.interfaces.device: any

packetbeat.protocols:
- type: dns
  ports: [53]
  include_authorities: true
  include_additionals: true

- type: http
  ports: [80, 8000, 8080, 9200]

- type: mysql
  ports: [3306]

- type: redis
  ports: [6379]

packetbeat.flows:
  timeout: 30s
  period: 10s

Packetbeatをデプロイする

kubectl create -f packetbeat-kubernetes.yaml

検証する

kubectl get pods -n kube-system -l k8s-app=packetbeat-dynamic

Kibanaで表示する

ブラウザでKibanaを開き、Dashboardアプリケーションを開きます。検索バーでKubernetesと入力して、KubernetesのためのMetricbeatダッシュボードを開きます。このダッシュボードでは、NodeやDeploymentなどの状態のレポートが表示されます。

DashboardページでPacketbeatと検索し、Packetbeat overviewを表示します。

同様に、ApacheおよびRedisのためのDashboardを表示します。それぞれに対してログとメトリクスのDashboardが表示されます。Apache Metricbeat dashboardには何も表示されていないはずです。Apache Filebeat dashboardを表示して、ページの最下部までスクロールしてApacheのエラーログを確認します。ログを読むと、Apacheのメトリクスが表示されない理由が分かります。

Metricbeatを有効にしてApacheのメトリクスを取得するには、mod-status設定ファイルを含んだConfigMapを追加してゲストブックを再デプロイすることで、server-statusを有効にします。

Deploymentをスケールして新しいPodが監視されるのを確認する

存在するDeploymentを一覧します。

kubectl get deployments

出力は次のようになります。

NAME            READY   UP-TO-DATE   AVAILABLE   AGE
frontend        3/3     3            3           3h27m
redis-master    1/1     1            1           3h27m
redis-slave     2/2     2            2           3h27m

frontendのPodを2つにスケールダウンします。

kubectl scale --replicas=2 deployment/frontend

出力は次のようになります。

deployment.extensions/frontend scaled

frontendのPodを再び3つにスケールアップします。

kubectl scale --replicas=3 deployment/frontend

Kibana上で変更を表示する

スクリーンショットを確認し、指定されたフィルターを追加して、ビューにカラムを追加します。赤い枠の右下を見ると、ScalingReplicaSetというエントリーが確認できます。そこからリストを上に見てゆくと、イメージのpull、ボリュームのマウント、Podのスタートなどのイベントが確認できます。

Kibana Discover

クリーンアップ

DeploymentとServiceを削除すると、実行中のすべてのPodも削除されます。ラベルを使って複数のリソースを1つのコマンドで削除します。

  1. 次のコマンドを実行して、すべてのPod、Deployment、Serviceを削除します。

    kubectl delete deployment -l app=redis
    kubectl delete service -l app=redis
    kubectl delete deployment -l app=guestbook
    kubectl delete service -l app=guestbook
    kubectl delete -f filebeat-kubernetes.yaml
    kubectl delete -f metricbeat-kubernetes.yaml
    kubectl delete -f packetbeat-kubernetes.yaml
    kubectl delete secret dynamic-logging -n kube-system
    
  2. Podの一覧を問い合わせて、実行中のPodがなくなったことを確認します。

    kubectl get pods
    

    結果は次のようになるはずです。

    No resources found.
    

次の項目

5 - セキュリティ

5.1 - クラスターレベルでのPodセキュリティの標準の適用

Podセキュリティアドミッション(PSA)は、ベータへ進み、v1.23以降でデフォルトで有効になっています。 Podセキュリティアドミッションは、Podが作成される際に、Podセキュリティの標準の適用の認可を制御するものです。 このチュートリアルでは、クラスター内の全ての名前空間に標準設定を適用することで、クラスターレベルでbaseline Podセキュリティの標準を強制する方法を示します。

Podセキュリティの標準を特定の名前空間に適用するには、名前空間レベルでのPodセキュリティの標準の適用を参照してください。

v1.29以外のKubernetesバージョンを実行している場合は、そのバージョンのドキュメントを確認してください。

始める前に

ワークステーションに以下をインストールしてください:

このチュートリアルでは、完全な制御下にあるKubernetesクラスターの何を設定できるかをデモンストレーションします。 コントロールプレーンを設定できない管理されたクラスターのPodセキュリティアドミッションに対しての設定方法を知りたいのであれば、名前空間レベルでのPodセキュリティの標準の適用を参照してください。

適用する正しいPodセキュリティの標準の選択

Podのセキュリティアドミッションは、以下のモードでビルトインのPodセキュリティの標準の適用を促します: enforceauditwarn。 設定に最適なPodセキュリティの標準を選択するにあたって助けになる情報を収集するために、以下を行ってください:

  1. Podセキュリティの標準を適用していないクラスターを作成します:

    kind create cluster --name psa-wo-cluster-pss
    

    出力は次のようになります:

    Creating cluster "psa-wo-cluster-pss" ...
    ✓ Ensuring node image (kindest/node:v1.29.0) 🖼
    ✓ Preparing nodes 📦
    ✓ Writing configuration 📜
    ✓ Starting control-plane 🕹️
    ✓ Installing CNI 🔌
    ✓ Installing StorageClass 💾
    Set kubectl context to "kind-psa-wo-cluster-pss"
    You can now use your cluster with:
    
    kubectl cluster-info --context kind-psa-wo-cluster-pss
    
    Thanks for using kind! 😊
    
  2. kubectl contextを新しいクラスターにセットします:

    kubectl cluster-info --context kind-psa-wo-cluster-pss
    

    出力は次のようになります:

    Kubernetes control plane is running at https://127.0.0.1:61350
    
    CoreDNS is running at https://127.0.0.1:61350/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
    
    To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
    
  3. クラスター内の名前空間の一覧を取得します:

    kubectl get ns
    

    出力は次のようになります:

    NAME                 STATUS   AGE
    default              Active   9m30s
    kube-node-lease      Active   9m32s
    kube-public          Active   9m32s
    kube-system          Active   9m32s
    local-path-storage   Active   9m26s
    
  4. 異なるPodセキュリティの標準が適用されたときに何が起きるかを理解するために、-dry-run=serverを使います:

    1. privileged

      kubectl label --dry-run=server --overwrite ns --all \
      pod-security.kubernetes.io/enforce=privileged
      

      出力は次のようになります:

      namespace/default labeled
      namespace/kube-node-lease labeled
      namespace/kube-public labeled
      namespace/kube-system labeled
      namespace/local-path-storage labeled
      
    2. baseline

      kubectl label --dry-run=server --overwrite ns --all \
      pod-security.kubernetes.io/enforce=baseline
      

      出力は次のようになります:

      namespace/default labeled
      namespace/kube-node-lease labeled
      namespace/kube-public labeled
      Warning: existing pods in namespace "kube-system" violate the new PodSecurity enforce level "baseline:latest"
      Warning: etcd-psa-wo-cluster-pss-control-plane (and 3 other pods): host namespaces, hostPath volumes
      Warning: kindnet-vzj42: non-default capabilities, host namespaces, hostPath volumes
      Warning: kube-proxy-m6hwf: host namespaces, hostPath volumes, privileged
      namespace/kube-system labeled
      namespace/local-path-storage labeled
      
    3. restricted

      kubectl label --dry-run=server --overwrite ns --all \
      pod-security.kubernetes.io/enforce=restricted
      

      出力は次のようになります:

      namespace/default labeled
      namespace/kube-node-lease labeled
      namespace/kube-public labeled
      Warning: existing pods in namespace "kube-system" violate the new PodSecurity enforce level "restricted:latest"
      Warning: coredns-7bb9c7b568-hsptc (and 1 other pod): unrestricted capabilities, runAsNonRoot != true, seccompProfile
      Warning: etcd-psa-wo-cluster-pss-control-plane (and 3 other pods): host namespaces, hostPath volumes, allowPrivilegeEscalation != false, unrestricted capabilities, restricted volume types, runAsNonRoot != true
      Warning: kindnet-vzj42: non-default capabilities, host namespaces, hostPath volumes, allowPrivilegeEscalation != false, unrestricted capabilities, restricted volume types, runAsNonRoot != true, seccompProfile
      Warning: kube-proxy-m6hwf: host namespaces, hostPath volumes, privileged, allowPrivilegeEscalation != false, unrestricted capabilities, restricted volume types, runAsNonRoot != true, seccompProfile
      namespace/kube-system labeled
      Warning: existing pods in namespace "local-path-storage" violate the new PodSecurity enforce level "restricted:latest"
      Warning: local-path-provisioner-d6d9f7ffc-lw9lh: allowPrivilegeEscalation != false, unrestricted capabilities, runAsNonRoot != true, seccompProfile
      namespace/local-path-storage labeled
      

この出力から、privileged Podセキュリティの標準を適用すると、名前空間のどれにも警告が示されないことに気付くでしょう。 これに対し、baselinerestrictの標準ではどちらも、とりわけkube-system名前空間に対して警告が示されています。

モード、バージョン、標準のセット

このセクションでは、latestバージョンに以下のPodセキュリティの標準を適用します:

  • enforceモードでbaseline標準。
  • warnおよびauditモードでrestricted標準。

baseline Podセキュリティの標準は、免除リストを短く保てて、かつ既知の特権昇格を防ぐような、利便性のある中庸を提供します。

加えて、kube-system内の失敗からPodを守るために、適用されるPodセキュリティの標準の対象から名前空間を免除します。

環境にPodセキュリティアドミッションを実装する際には、以下の点を考慮してください:

  1. クラスターに適用されるリスク状況に基づくと、restrictedのようにより厳格なPodセキュリティの標準のほうが、より良い選択肢かもしれません。

  2. kube-ssytem名前空間の免除は、Podがその名前空間でprivilegedとして実行するのを許容することになります。 実世界で使うにあたっては、以下の最小権限の原則に従ってkube-systemへのアクセスを制限する厳格なRBACポリシーを適用することを、Kubernetesプロジェクトは強く推奨します。 上記の標準を実装するには、次のようにします:

  3. 目的のPodセキュリティの標準を実装するために、Podセキュリティアドミッションコントローラーで利用可能な設定ファイルを作成します:

    mkdir -p /tmp/pss
    cat <<EOF > /tmp/pss/cluster-level-pss.yaml
    apiVersion: apiserver.config.k8s.io/v1
    kind: AdmissionConfiguration
    plugins:
    - name: PodSecurity
      configuration:
        apiVersion: pod-security.admission.config.k8s.io/v1
        kind: PodSecurityConfiguration
        defaults:
          enforce: "baseline"
          enforce-version: "latest"
          audit: "restricted"
          audit-version: "latest"
          warn: "restricted"
          warn-version: "latest"
        exemptions:
          usernames: []
          runtimeClasses: []
          namespaces: [kube-system]
    EOF
    
  4. クラスターの作成中にこのファイルを取り込むAPIサーバーを設定します:

    cat <<EOF > /tmp/pss/cluster-config.yaml
    kind: Cluster
    apiVersion: kind.x-k8s.io/v1alpha4
    nodes:
    - role: control-plane
      kubeadmConfigPatches:
      - |
        kind: ClusterConfiguration
        apiServer:
            extraArgs:
              admission-control-config-file: /etc/config/cluster-level-pss.yaml
            extraVolumes:
              - name: accf
                hostPath: /etc/config
                mountPath: /etc/config
                readOnly: false
                pathType: "DirectoryOrCreate"
      extraMounts:
      - hostPath: /tmp/pss
        containerPath: /etc/config
        # optional: if set, the mount is read-only.
        # default false
        readOnly: false
        # optional: if set, the mount needs SELinux relabeling.
        # default false
        selinuxRelabel: false
        # optional: set propagation mode (None, HostToContainer or Bidirectional)
        # see https://kubernetes.io/docs/concepts/storage/volumes/#mount-propagation
        # default None
        propagation: None
    EOF
    
  5. 目的のPodセキュリティの標準を適用するために、Podセキュリティアドミッションを使うクラスターを作成します:

    kind create cluster --name psa-with-cluster-pss --config /tmp/pss/cluster-config.yaml
    

    出力は次のようになります:

    Creating cluster "psa-with-cluster-pss" ...
     ✓ Ensuring node image (kindest/node:v1.29.0) 🖼
     ✓ Preparing nodes 📦
     ✓ Writing configuration 📜
     ✓ Starting control-plane 🕹️
     ✓ Installing CNI 🔌
     ✓ Installing StorageClass 💾
    Set kubectl context to "kind-psa-with-cluster-pss"
    You can now use your cluster with:
    
    kubectl cluster-info --context kind-psa-with-cluster-pss
    
    Have a question, bug, or feature request? Let us know! https://kind.sigs.k8s.io/#community 🙂
    
  6. kubectlをこのクラスターに向けます:

    kubectl cluster-info --context kind-psa-with-cluster-pss
    

    出力は次のようになります:

    Kubernetes control plane is running at https://127.0.0.1:63855
    CoreDNS is running at https://127.0.0.1:63855/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
    
    To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
    
  7. デフォルトの名前空間にPodを作成します:

    kubectl apply -f https://k8s.io/examples/security/example-baseline-pod.yaml
    

    Podは正常に開始されますが、出力には警告が含まれます:

    Warning: would violate PodSecurity "restricted:latest": allowPrivilegeEscalation != false (container "nginx" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "nginx" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "nginx" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "nginx" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")
    pod/nginx created
    

後片付け

では、上記で作成したクラスターを、以下のコマンドを実行して削除します:

kind delete cluster --name psa-with-cluster-pss
kind delete cluster --name psa-wo-cluster-pss

次の項目

5.2 - 名前空間レベルでのPodセキュリティの標準の適用

Podセキュリティアドミッション(PSA)は、ベータへ進み、v1.23以降でデフォルトで有効になっています。 Podセキュリティアドミッションは、Podが作成される際に、Podセキュリティの標準の適用の認可を制御するものです。 このチュートリアルでは、一度に1つの名前空間でbaseline Podセキュリティ標準を強制します。

Podセキュリティの標準を複数の名前空間に一度にクラスターレベルで適用することもできます。やり方についてはクラスターレベルでのPodセキュリティの標準の適用を参照してください。

始める前に

ワークステーションに以下をインストールしてください:

クラスターの作成

  1. 以下のようにKinDクラスターを作成します。

    kind create cluster --name psa-ns-level
    

    出力は次のようになります:

    Creating cluster "psa-ns-level" ...
     ✓ Ensuring node image (kindest/node:v1.29.2) 🖼 
     ✓ Preparing nodes 📦  
     ✓ Writing configuration 📜 
     ✓ Starting control-plane 🕹️ 
     ✓ Installing CNI 🔌 
     ✓ Installing StorageClass 💾 
    Set kubectl context to "kind-psa-ns-level"
    You can now use your cluster with:
    
    kubectl cluster-info --context kind-psa-ns-level
    
    Not sure what to do next? 😅  Check out https://kind.sigs.k8s.io/docs/user/quick-start/
    
  2. kubectl のコンテキストを新しいクラスターにセットします:

    kubectl cluster-info --context kind-psa-ns-level
    

    出力は次のようになります:

    Kubernetes control plane is running at https://127.0.0.1:50996
    CoreDNS is running at https://127.0.0.1:50996/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
    
    To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
    

名前空間の作成

exampleと呼ぶ新しい名前空間を作成します:

kubectl create ns example

出力は次のようになります:

namespace/example created

名前空間へのPodセキュリティの標準チェックの有効化

  1. ビルトインのPod Security Admissionでサポートされているラベルを使って、この名前空間のPodセキュリティの標準を有効にします。 このステップでは、baseline Podセキュリティの標準の最新バージョンに合わないPodについて警告するチェックを設定します。

    kubectl label --overwrite ns example \
       pod-security.kubernetes.io/warn=baseline \
       pod-security.kubernetes.io/warn-version=latest
    
  2. ラベルを使って、任意の名前空間に対して複数のPodセキュリティの標準チェックを設定できます。 以下のコマンドは、baseline Podセキュリティの標準をenforce(強制)としますが、restricted Podセキュリティの標準には最新バージョンに準じてwarn(警告)およびaudit(監査)とします(デフォルト値)。

    kubectl label --overwrite ns example \
      pod-security.kubernetes.io/enforce=baseline \
      pod-security.kubernetes.io/enforce-version=latest \
      pod-security.kubernetes.io/warn=restricted \
      pod-security.kubernetes.io/warn-version=latest \
      pod-security.kubernetes.io/audit=restricted \
      pod-security.kubernetes.io/audit-version=latest
    

Podセキュリティの標準の強制の実証

  1. example名前空間内にbaseline Podを作成します:

    kubectl apply -n example -f https://k8s.io/examples/security/example-baseline-pod.yaml
    

    Podは正常に起動しますが、出力には警告が含まれています。例えば:

    Warning: would violate PodSecurity "restricted:latest": allowPrivilegeEscalation != false (container "nginx" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "nginx" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "nginx" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "nginx" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")
    pod/nginx created
    
  2. default名前空間内にbaseline Podを作成します:

    kubectl apply -n default -f https://k8s.io/examples/security/example-baseline-pod.yaml
    

    出力は次のようになります:

    pod/nginx created
    

example名前空間にだけ、Podセキュリティの標準のenforceと警告の設定が適用されました。 default名前空間内では、警告なしに同じPodを作成できました。

後片付け

では、上記で作成したクラスターを、以下のコマンドを実行して削除します:

kind delete cluster --name psa-ns-level

次の項目

6 - ステートフルアプリケーション

6.1 - StatefulSetの基本

このチュートリアルでは、StatefulSetを使用したアプリケーションを管理するための基本を説明します。StatefulSetのPodを作成、削除、スケール、そして更新する方法について紹介します。

始める前に

このチュートリアルを始める前に、以下のKubernetesの概念について理解しておく必要があります。

目標

StatefulSetはステートフルアプリケーションや分散システムで使用するために存在します。しかし、Kubernetes上のステートフルアプリケーションや分散システムは、広範で複雑なトピックです。StatefulSetの基本的な機能を示すという目的のため、また、ステートフルアプリケーションを分散システムと混同しないようにするために、ここでは、Statefulsetを使用する単純なウェブアプリケーションのデプロイを行います。

このチュートリアルを終えると、以下のことが理解できるようになります。

  • StatefulSetの作成方法
  • StatefulSetがどのようにPodを管理するのか
  • StatefulSetの削除方法
  • StatefulSetのスケール方法
  • StatefulSetが管理するPodの更新方法

StatefulSetを作成する

はじめに、以下の例を使ってStatefulSetを作成しましょう。これは、コンセプトのStatefulSetのページで使ったものと同じような例です。nginxというheadless Serviceを作成し、webというStatefulSet内のPodのIPアドレスを公開します。

apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: registry.k8s.io/nginx-slim:0.8
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi

上の例をダウンロードして、web.yamlという名前で保存します。

ここでは、ターミナルウィンドウを2つ使う必要があります。1つ目のターミナルでは、kubectl getを使って、StatefulSetのPodの作成を監視します。

kubectl get pods -w -l app=nginx

2つ目のターミナルでは、kubectl applyを使って、web.yamlに定義されたheadless ServiceとStatefulSetを作成します。

kubectl apply -f web.yaml
service/nginx created
statefulset.apps/web created

上のコマンドを実行すると、2つのPodが作成され、それぞれのPodでNGINXウェブサーバーが実行されます。nginxServiceを取得してみましょう。

kubectl get service nginx
NAME      TYPE         CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
nginx     ClusterIP    None         <none>        80/TCP    12s

そして、webStatefulSetを取得して、2つのリソースの作成が成功したことも確認します。

kubectl get statefulset web
NAME      DESIRED   CURRENT   AGE
web       2         1         20s

順序付きPodの作成

n 個のレプリカを持つStatefulSetは、Podをデプロイするとき、1つずつ順番に作成し、 {0..n-1} という順序付けを行います。1つ目のターミナルでkubectl getコマンドの出力を確認しましょう。最終的に、以下の例のような出力が表示されるはずです。

kubectl get pods -w -l app=nginx
NAME      READY     STATUS    RESTARTS   AGE
web-0     0/1       Pending   0          0s
web-0     0/1       Pending   0         0s
web-0     0/1       ContainerCreating   0         0s
web-0     1/1       Running   0         19s
web-1     0/1       Pending   0         0s
web-1     0/1       Pending   0         0s
web-1     0/1       ContainerCreating   0         0s
web-1     1/1       Running   0         18s

web-0Podが Running (Pod Phaseを参照)かつ Ready (Pod Conditionstypeを参照)の状態になるまでは、web-1Podが起動していないことに注目してください。

StatefulSet内のPod

StatefulSet内のPodは、ユニークな順序インデックスと安定したネットワーク識別子を持ちます。

Podの順序インデックスを確かめる

StatefulSetのPodを取得します。

kubectl get pods -l app=nginx
NAME      READY     STATUS    RESTARTS   AGE
web-0     1/1       Running   0          1m
web-1     1/1       Running   0          1m

StatefulSetのコンセプトで説明したように、StatefulSet内のPodは安定したユニークな識別子を持ちます。この識別子は、StatefulSetコントローラーによって各Podに割り当てられる、ユニークな順序インデックスに基づいて付けられます。Podの名前は、<statefulsetの名前>-<順序インデックス>という形式です。webStatefulSetは2つのレプリカを持つため、web-0web-1という2つのPodを作成します。

安定したネットワーク識別子の使用

各Podは、順序インデックスに基づいた安定したホスト名を持ちます。kubectl execを使用して、各Pod内でhostnameコマンドを実行してみましょう。

for i in 0 1; do kubectl exec "web-$i" -- sh -c 'hostname'; done
web-0
web-1

kubectl runを使用して、dnsutilsパッケージのnslookupコマンドを提供するコンテナを実行します。Podのホスト名に対してnslookupを実行すると、クラスター内のDNSアドレスが確認できます。

kubectl run -i --tty --image busybox:1.28 dns-test --restart=Never --rm

これにより、新しいシェルが起動します。新しいシェルで、次のコマンドを実行します。

# このコマンドは、dns-testコンテナのシェルで実行してください
nslookup web-0.nginx

出力は次のようになります。

Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-0.nginx
Address 1: 10.244.1.6

nslookup web-1.nginx
Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-1.nginx
Address 1: 10.244.2.6

(コンテナのシェルを終了するために、exitコマンドを実行してください。)

headless serviceのCNAMEは、SRVレコードを指しています(1つのレコードがRunningかつReadyのPodに対応します)。SRVレコードは、PodのIPアドレスを含むAレコードを指します。

1つ目のターミナルで、StatefulSetのPodを監視します。

kubectl get pod -w -l app=nginx

2つ目のターミナルで、kubectl deleteを使用して、StatefulSetのすべてのPodを削除します。

kubectl delete pod -l app=nginx
pod "web-0" deleted
pod "web-1" deleted

StatefulSetがPodを再起動して、2つのPodがRunningかつReadyの状態に移行するのを待ちます。

kubectl get pod -w -l app=nginx
NAME      READY     STATUS              RESTARTS   AGE
web-0     0/1       ContainerCreating   0          0s
NAME      READY     STATUS    RESTARTS   AGE
web-0     1/1       Running   0          2s
web-1     0/1       Pending   0         0s
web-1     0/1       Pending   0         0s
web-1     0/1       ContainerCreating   0         0s
web-1     1/1       Running   0         34s

kubectl execkubectl runコマンドを使用して、Podのホスト名とクラスター内DNSエントリーを確認します。まず、Podのホスト名を見てみましょう。

for i in 0 1; do kubectl exec web-$i -- sh -c 'hostname'; done
web-0
web-1

その後、次のコマンドを実行します。

kubectl run -i --tty --image busybox:1.28 dns-test --restart=Never --rm

これにより、新しいシェルが起動します。新しいシェルで、次のコマンドを実行します。

# このコマンドは、dns-testコンテナのシェルで実行してください
nslookup web-0.nginx

出力は次のようになります。

Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-0.nginx
Address 1: 10.244.1.7

nslookup web-1.nginx
Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-1.nginx
Address 1: 10.244.2.8

(コンテナのシェルを終了するために、exitコマンドを実行してください。)

Podの順序インデックス、ホスト名、SRVレコード、そしてAレコード名は変化していませんが、Podに紐付けられたIPアドレスは変化する可能性があります。このチュートリアルで使用しているクラスターでは、IPアドレスは変わりました。このようなことがあるため、他のアプリケーションがStatefulSet内のPodに接続するときには、IPアドレスで指定しないことが重要です。

StatefulSetの有効なメンバーを探して接続する必要がある場合は、headless ServiceのCNAME(nginx.default.svc.cluster.local)をクエリしなければなりません。CNAMEに紐付けられたSRVレコードには、StatefulSet内のRunnningかつReadyなPodだけが含まれます。

アプリケーションがlivenessとreadinessをテストするコネクションのロジックをすでに実装している場合、PodのSRVレコード(web-0.nginx.default.svc.cluster.localweb-1.nginx.default.svc.cluster.local)をPodが安定しているものとして使用できます。PodがRunning and Readyな状態に移行すれば、アプリケーションはPodのアドレスを発見できるようになります。

安定したストレージへの書き込み

web-0およびweb-1のためのPersistentVolumeClaimを取得しましょう。

kubectl get pvc -l app=nginx

出力は次のようになります。

NAME        STATUS    VOLUME                                     CAPACITY   ACCESSMODES   AGE
www-web-0   Bound     pvc-15c268c7-b507-11e6-932f-42010a800002   1Gi        RWO           48s
www-web-1   Bound     pvc-15c79307-b507-11e6-932f-42010a800002   1Gi        RWO           48s

StatefulSetコントローラーは、2つのPersistentVolumeにバインドされた2つのPersistentVolumeClaimを作成しています。

このチュートリアルで使用しているクラスターでは、PersistentVolumeの動的なプロビジョニングが設定されているため、PersistentVolumeが自動的に作成されてバインドされています。

デフォルトでは、NGINXウェブサーバーは/usr/share/nginx/html/index.htmlに置かれたindexファイルを配信します。StatefulSetのspec内のvolumeMountsフィールドによって、/usr/share/nginx/htmlディレクトリがPersistentVolume上にあることが保証されます。

Podのホスト名をindex.htmlファイルに書き込むことで、NGINXウェブサーバーがホスト名を配信することを検証しましょう。

for i in 0 1; do kubectl exec "web-$i" -- sh -c 'echo "$(hostname)" > /usr/share/nginx/html/index.html'; done

for i in 0 1; do kubectl exec -i -t "web-$i" -- curl http://localhost/; done
web-0
web-1

1つ目のターミナルで、StatefulSetのPodを監視します。

kubectl get pod -w -l app=nginx

2つ目のターミナルで、StatefulSetのすべてのPodを削除します。

kubectl delete pod -l app=nginx
pod "web-0" deleted
pod "web-1" deleted

1つ目のターミナルでkubectl getコマンドの出力を確認して、すべてのPodがRunningかつReadyの状態に変わるまで待ちます。

kubectl get pod -w -l app=nginx
NAME      READY     STATUS              RESTARTS   AGE
web-0     0/1       ContainerCreating   0          0s
NAME      READY     STATUS    RESTARTS   AGE
web-0     1/1       Running   0          2s
web-1     0/1       Pending   0         0s
web-1     0/1       Pending   0         0s
web-1     0/1       ContainerCreating   0         0s
web-1     1/1       Running   0         34s

ウェブサーバーがホスト名を配信し続けていることを確認します。

for i in 0 1; do kubectl exec -i -t "web-$i" -- curl http://localhost/; done
web-0
web-1

もしweb-0およびweb-1が再スケジュールされたとしても、Podは同じホスト名を配信し続けます。これは、PodのPersistentVolumeClaimに紐付けられたPersistentVolumeが、PodのvolumeMountsに再マウントされるためです。web-0web-1がどんなノードにスケジュールされたとしても、PodのPersistentVolumeは適切なマウントポイントにマウントされます。

StatefulSetをスケールする

StatefulSetのスケールとは、レプリカ数を増減することを意味します。これは、replicasフィールドを更新することによって実現できます。StatefulSetのスケールには、kubectl scalekubectl patchのどちらも使用できます。

スケールアップ

1つ目のターミナルで、StatefulSet内のPodを監視します。

kubectl get pods -w -l app=nginx

2つ目のターミナルで、kubectl scaleを使って、レプリカ数を5にスケールします。

kubectl scale sts web --replicas=5
statefulset.apps/web scaled

1つ目のターミナルのkubectl getコマンドの出力を確認して、3つの追加のPodがRunningかつReadyの状態に変わるまで待ちます。

kubectl get pods -w -l app=nginx
NAME      READY     STATUS    RESTARTS   AGE
web-0     1/1       Running   0          2h
web-1     1/1       Running   0          2h
NAME      READY     STATUS    RESTARTS   AGE
web-2     0/1       Pending   0          0s
web-2     0/1       Pending   0         0s
web-2     0/1       ContainerCreating   0         0s
web-2     1/1       Running   0         19s
web-3     0/1       Pending   0         0s
web-3     0/1       Pending   0         0s
web-3     0/1       ContainerCreating   0         0s
web-3     1/1       Running   0         18s
web-4     0/1       Pending   0         0s
web-4     0/1       Pending   0         0s
web-4     0/1       ContainerCreating   0         0s
web-4     1/1       Running   0         19s

StatefulSetコントローラーはレプリカ数をスケールします。 StatefulSetを作成するで説明したように、StatefulSetコントローラーは各Podを順序インデックスに従って1つずつ作成し、次のPodを起動する前に、1つ前のPodがRunningかつReadyの状態になるまで待ちます。

スケールダウン

1つ目のターミナルで、StatefulSetのPodを監視します。

kubectl get pods -w -l app=nginx

2つ目のターミナルで、kubectl patchコマンドを使用して、StatefulSetを3つのレプリカにスケールダウンします。

kubectl patch sts web -p '{"spec":{"replicas":3}}'
statefulset.apps/web patched

web-4およびweb-3がTerminatingの状態になるまで待ちます。

kubectl get pods -w -l app=nginx
NAME      READY     STATUS              RESTARTS   AGE
web-0     1/1       Running             0          3h
web-1     1/1       Running             0          3h
web-2     1/1       Running             0          55s
web-3     1/1       Running             0          36s
web-4     0/1       ContainerCreating   0          18s
NAME      READY     STATUS    RESTARTS   AGE
web-4     1/1       Running   0          19s
web-4     1/1       Terminating   0         24s
web-4     1/1       Terminating   0         24s
web-3     1/1       Terminating   0         42s
web-3     1/1       Terminating   0         42s

順序付きPodを削除する

コントローラーは、順序インデックスの逆順に1度に1つのPodを削除し、次のPodを削除する前には、各Podが完全にシャットダウンするまで待機しています。

StatefulSetのPersistentVolumeClaimを取得しましょう。

kubectl get pvc -l app=nginx
NAME        STATUS    VOLUME                                     CAPACITY   ACCESSMODES   AGE
www-web-0   Bound     pvc-15c268c7-b507-11e6-932f-42010a800002   1Gi        RWO           13h
www-web-1   Bound     pvc-15c79307-b507-11e6-932f-42010a800002   1Gi        RWO           13h
www-web-2   Bound     pvc-e1125b27-b508-11e6-932f-42010a800002   1Gi        RWO           13h
www-web-3   Bound     pvc-e1176df6-b508-11e6-932f-42010a800002   1Gi        RWO           13h
www-web-4   Bound     pvc-e11bb5f8-b508-11e6-932f-42010a800002   1Gi        RWO           13h

まだ、5つのPersistentVolumeClaimと5つのPersistentVolumeが残っています。安定したストレージへの書き込みを読むと、StatefulSetのPodが削除されても、StatefulSetのPodにマウントされたPersistentVolumeは削除されないと書かれています。このことは、StatefulSetのスケールダウンによってPodが削除された場合にも当てはまります。

StatefulSetsを更新する

Kubernetes 1.7以降では、StatefulSetコントローラーは自動アップデートをサポートしています。使われる戦略は、StatefulSet APIオブジェクトのspec.updateStrategyフィールドによって決まります。この機能はコンテナイメージのアップグレード、リソースのrequestsやlimits、ラベル、StatefulSet内のPodのアノテーションの更新時に利用できます。有効なアップデートの戦略は、RollingUpdateOnDeleteの2種類です。

RollingUpdateは、StatefulSetのデフォルトのアップデート戦略です。

RollingUpdate

RollingUpdateアップデート戦略は、StatefulSetの保証を尊重しながら、順序インデックスの逆順にStatefulSet内のすべてのPodをアップデートします。

webStatefulSetにpatchを当てて、RollingUpdateアップデート戦略を適用しましょう。

kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"type":"RollingUpdate"}}}'
statefulset.apps/web patched

1つ目のターミナルで、webStatefulSetに再度patchを当てて、コンテナイメージを変更します。

kubectl patch statefulset web --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value":"gcr.io/google_containers/nginx-slim:0.8"}]'
statefulset.apps/web patched

2つ目のターミナルで、StatefulSet内のPodを監視します。

kubectl get pod -l app=nginx -w

出力は次のようになります。

NAME      READY     STATUS    RESTARTS   AGE
web-0     1/1       Running   0          7m
web-1     1/1       Running   0          7m
web-2     1/1       Running   0          8m
web-2     1/1       Terminating   0         8m
web-2     1/1       Terminating   0         8m
web-2     0/1       Terminating   0         8m
web-2     0/1       Terminating   0         8m
web-2     0/1       Terminating   0         8m
web-2     0/1       Terminating   0         8m
web-2     0/1       Pending   0         0s
web-2     0/1       Pending   0         0s
web-2     0/1       ContainerCreating   0         0s
web-2     1/1       Running   0         19s
web-1     1/1       Terminating   0         8m
web-1     0/1       Terminating   0         8m
web-1     0/1       Terminating   0         8m
web-1     0/1       Terminating   0         8m
web-1     0/1       Pending   0         0s
web-1     0/1       Pending   0         0s
web-1     0/1       ContainerCreating   0         0s
web-1     1/1       Running   0         6s
web-0     1/1       Terminating   0         7m
web-0     1/1       Terminating   0         7m
web-0     0/1       Terminating   0         7m
web-0     0/1       Terminating   0         7m
web-0     0/1       Terminating   0         7m
web-0     0/1       Terminating   0         7m
web-0     0/1       Pending   0         0s
web-0     0/1       Pending   0         0s
web-0     0/1       ContainerCreating   0         0s
web-0     1/1       Running   0         10s

StatefulSet内のPodは、順序インデックスの逆順に更新されました。StatefulSetコントローラーは各Podを終了させ、次のPodを更新する前に、新しいPodがRunningかつReadyの状態に変わるまで待機します。ここで、StatefulSetコントローラーは順序インデックスの前のPodがRunningかつReadyの状態になるまで次のPodの更新を始めず、現在の状態へのアップデートに失敗したPodがあった場合、そのPodをリストアすることに注意してください。

すでにアップデートを受け取ったPodは、アップデートされたバージョンにリストアされます。まだアップデートを受け取っていないPodは、前のバージョンにリストアされます。このような方法により、もし途中で失敗が起こっても、コントローラーはアプリケーションが健全な状態を保ち続けられるようにし、更新が一貫したものになるようにします。

Podを取得して、コンテナイメージを確認してみましょう。

for p in 0 1 2; do kubectl get pod "web-$p" --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'; echo; done
registry.k8s.io/nginx-slim:0.8
registry.k8s.io/nginx-slim:0.8
registry.k8s.io/nginx-slim:0.8

現在、StatefulSet内のすべてのPodは、前のコンテナイメージを実行しています。

ステージングアップデート

RollingUpdateアップデート戦略にpartitionパラメーターを使用すると、StatefulSetへのアップデートをステージングすることができます。ステージングアップデートを利用すれば、StatefulSet内のすべてのPodを現在のバージョンにしたまま、StatefulSetの.spec.templateを変更することが可能になります。

webStatefulSetにpatchを当てて、updateStrategyフィールドにpartitionを追加しましょう。

kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":3}}}}'
statefulset.apps/web patched

StatefulSetに再度patchを当てて、コンテナイメージを変更します。

kubectl patch statefulset web --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value":"registry.k8s.io/nginx-slim:0.7"}]'
statefulset.apps/web patched

StatefulSet内のPodを削除します。

kubectl delete pod web-2
pod "web-2" deleted

PodがRunningかつReadyになるまで待ちます。

kubectl get pod -l app=nginx -w
NAME      READY     STATUS              RESTARTS   AGE
web-0     1/1       Running             0          4m
web-1     1/1       Running             0          4m
web-2     0/1       ContainerCreating   0          11s
web-2     1/1       Running   0         18s

Podのコンテナイメージを取得します。

kubectl get pod web-2 --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'
registry.k8s.io/nginx-slim:0.8

アップデート戦略がRollingUpdateであっても、StatefulSetが元のコンテナを持つPodをリストアしたことがわかります。これは、Podの順序インデックスがupdateStrategyで指定したpartitionより小さいためです。

カナリア版をロールアウトする

ステージングアップデートのときに指定したpartitionを小さくすることで、変更をテストするためのカナリア版をロールアウトできます。

StatefulSetにpatchを当てて、partitionを小さくします。

kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":2}}}}'
statefulset.apps/web patched

web-2がRunningかつReadyの状態になるまで待ちます。

kubectl get pod -l app=nginx -w
NAME      READY     STATUS              RESTARTS   AGE
web-0     1/1       Running             0          4m
web-1     1/1       Running             0          4m
web-2     0/1       ContainerCreating   0          11s
web-2     1/1       Running   0         18s

Podのコンテナを取得します。

kubectl get pod web-2 --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'
registry.k8s.io/nginx-slim:0.7

partitionを変更すると、StatefulSetコントローラーはPodを自動的に更新します。Podの順序インデックスがpartition以上の値であるためです。

web-1Podを削除します。

kubectl delete pod web-1
pod "web-1" deleted

web-1PodがRunningかつReadyになるまで待ちます。

kubectl get pod -l app=nginx -w

出力は次のようになります。

NAME      READY     STATUS        RESTARTS   AGE
web-0     1/1       Running       0          6m
web-1     0/1       Terminating   0          6m
web-2     1/1       Running       0          2m
web-1     0/1       Terminating   0         6m
web-1     0/1       Terminating   0         6m
web-1     0/1       Terminating   0         6m
web-1     0/1       Pending   0         0s
web-1     0/1       Pending   0         0s
web-1     0/1       ContainerCreating   0         0s
web-1     1/1       Running   0         18s

web-1Podのコンテナイメージを取得します。

kubectl get pod web-1 --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'
registry.k8s.io/nginx-slim:0.8

Podの順序インデックスがpartitionよりも小さいため、web-1は元の設定のコンテナイメージにリストアされました。partitionを指定すると、StatefulSetの.spec.templateが更新されたときに、順序インデックスがそれ以上の値を持つすべてのPodがアップデートされます。partitionよりも小さな順序インデックスを持つPodが削除されたり終了されたりすると、元の設定のPodにリストアされます。

フェーズロールアウト

カナリア版をロールアウトするのと同じような方法でパーティションされたローリングアップデートを使用すると、フェーズロールアウト(例: 線形、幾何級数的、指数関数的ロールアウト)を実行できます。フェーズロールアウトを実行するには、コントローラーがアップデートを途中で止めてほしい順序インデックスをpartitionに設定します。

現在、partitionは2に設定されています。partitionを0に設定します。

kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":0}}}}'
statefulset.apps/web patched

StatefulSet内のすべてのPodがRunningかつReadyの状態になるまで待ちます。

kubectl get pod -l app=nginx -w

出力は次のようになります。

NAME      READY     STATUS              RESTARTS   AGE
web-0     1/1       Running             0          3m
web-1     0/1       ContainerCreating   0          11s
web-2     1/1       Running             0          2m
web-1     1/1       Running   0         18s
web-0     1/1       Terminating   0         3m
web-0     1/1       Terminating   0         3m
web-0     0/1       Terminating   0         3m
web-0     0/1       Terminating   0         3m
web-0     0/1       Terminating   0         3m
web-0     0/1       Terminating   0         3m
web-0     0/1       Pending   0         0s
web-0     0/1       Pending   0         0s
web-0     0/1       ContainerCreating   0         0s
web-0     1/1       Running   0         3s

StatefulSet内のPodのコンテナイメージの詳細を取得します。

for p in 0 1 2; do kubectl get pod "web-$p" --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'; echo; done
registry.k8s.io/nginx-slim:0.7
registry.k8s.io/nginx-slim:0.7
registry.k8s.io/nginx-slim:0.7

partition0に移動することで、StatefulSetがアップデート処理を続けられるようにできます。

OnDelete

OnDeleteアップデート戦略は、(1.6以前の)レガシーな動作を実装しています。このアップデート戦略を選択すると、StatefulSetの.spec.templateフィールドへ変更を加えても、StatefulSetコントローラーが自動的にPodを更新しなくなります。この戦略を選択するには、.spec.template.updateStrategy.typeOnDeleteを設定します。

StatefulSetを削除する

StatefulSetは、非カスケードな削除とカスケードな削除の両方をサポートしています。非カスケードな削除では、StatefulSetが削除されても、StatefulSet内のPodは削除されません。カスケードな削除では、StatefulSetとPodが一緒に削除されます。

非カスケードな削除

1つ目のターミナルで、StatefulSet内のPodを監視します

kubectl get pods -w -l app=nginx

kubectl deleteを使用して、StatefulSetを削除します。このとき、--cascade=orphanパラメーターをコマンドに与えてください。このパラメーターは、Kubernetesに対して、StatefulSetだけを削除して配下のPodは削除しないように指示します。

kubectl delete statefulset web --cascade=orphan
statefulset.apps "web" deleted

Podを取得して、ステータスを確認します。

kubectl get pods -l app=nginx
NAME      READY     STATUS    RESTARTS   AGE
web-0     1/1       Running   0          6m
web-1     1/1       Running   0          7m
web-2     1/1       Running   0          5m

webが削除されても、すべてのPodはまだRunningかつReadyの状態のままです。web-0を削除します。

kubectl delete pod web-0
pod "web-0" deleted

StatefulSetのPodを取得します。

kubectl get pods -l app=nginx
NAME      READY     STATUS    RESTARTS   AGE
web-1     1/1       Running   0          10m
web-2     1/1       Running   0          7m

webStatefulSetはすでに削除されているため、web-0は再起動しません。

1つ目のターミナルで、StatefulSetのPodを監視します。

kubectl get pods -w -l app=nginx

2つ目のターミナルで、StatefulSetを再作成します。もしnginxServiceを削除しなかった場合(この場合は削除するべきではありませんでした)、Serviceがすでに存在することを示すエラーが表示されます。

kubectl apply -f web.yaml
statefulset.apps/web created
service/nginx unchanged

このエラーは無視してください。このメッセージは、すでに存在する nginx というheadless Serviceを作成しようと試みたということを示しているだけです。

1つ目のターミナルで、kubectl getコマンドの出力を確認します。

kubectl get pods -w -l app=nginx
NAME      READY     STATUS    RESTARTS   AGE
web-1     1/1       Running   0          16m
web-2     1/1       Running   0          2m
NAME      READY     STATUS    RESTARTS   AGE
web-0     0/1       Pending   0          0s
web-0     0/1       Pending   0         0s
web-0     0/1       ContainerCreating   0         0s
web-0     1/1       Running   0         18s
web-2     1/1       Terminating   0         3m
web-2     0/1       Terminating   0         3m
web-2     0/1       Terminating   0         3m
web-2     0/1       Terminating   0         3m

webStatefulSetが再作成されると、最初にweb-0を再実行します。web-1はすでにRunningかつReadyの状態であるため、web-0がRunningかつReadyの状態に移行すると、StatefulSetは単純にこのPodを選びます。StatefulSetをreplicasを2にして再作成したため、一度web-0が再作成されて、web-1がすでにRunningかつReadyの状態であることが判明したら、web-2は停止されます。

Podのウェブサーバーが配信しているindex.htmlファイルのコンテンツをもう一度見てみましょう。

for i in 0 1; do kubectl exec -i -t "web-$i" -- curl http://localhost/; done
web-0
web-1

たとえStatefulSetとweb-0Podの両方が削除されても、Podは最初にindex.htmlファイルに書き込んだホスト名をまだ配信しています。これは、StatefulSetがPodに紐付けられたPersistentVolumeを削除しないためです。StatefulSetを再作成してweb-0を再実行すると、元のPersistentVolumeが再マウントされます。

カスケードな削除

1つ目のターミナルで、StatefulSet内のPodを監視します。

kubectl get pods -w -l app=nginx

2つ目のターミナルで、StatefulSetをもう一度削除します。今回は、--cascade=orphanパラメーターを省略します。

kubectl delete statefulset web
statefulset.apps "web" deleted

1つ目のターミナルで実行しているkubectl getコマンドの出力を確認し、すべてのPodがTerminatingの状態に変わるまで待ちます。

kubectl get pods -w -l app=nginx
NAME      READY     STATUS    RESTARTS   AGE
web-0     1/1       Running   0          11m
web-1     1/1       Running   0          27m
NAME      READY     STATUS        RESTARTS   AGE
web-0     1/1       Terminating   0          12m
web-1     1/1       Terminating   0         29m
web-0     0/1       Terminating   0         12m
web-0     0/1       Terminating   0         12m
web-0     0/1       Terminating   0         12m
web-1     0/1       Terminating   0         29m
web-1     0/1       Terminating   0         29m
web-1     0/1       Terminating   0         29m

スケールダウンのセクションで見たように、順序インデックスの逆順に従って、Podは一度に1つずつ終了します。StatefulSetコントローラーは、次のPodを終了する前に、前のPodが完全に終了するまで待ちます。

kubectl delete service nginx
service "nginx" deleted

さらにもう一度、StatefulSetとheadless Serviceを再作成します。

kubectl apply -f web.yaml
service/nginx created
statefulset.apps/web created

StatefulSet上のすべてのPodがRunningかつReadyの状態に変わったら、Pod上のindex.htmlファイルのコンテンツを取得します。

for i in 0 1; do kubectl exec -i -t "web-$i" -- curl http://localhost/; done
web-0
web-1

StatefulSetを完全に削除して、すべてのPodが削除されたとしても、PersistentVolumeがマウントされたPodが再生成されて、web-0web-1はホスト名の配信を続けます。

最後に、webStatefulSetを削除します。

kubectl delete service nginx
service "nginx" deleted

そして、nginxServiceも削除します。

kubectl delete statefulset web
statefulset "web" deleted

Pod管理ポリシー

分散システムによっては、StatefulSetの順序の保証が不必要であったり望ましくない場合もあります。こうしたシステムでは、一意性と同一性だけが求められます。この問題に対処するために、Kubernetes 1.7でStatefulSet APIオブジェクトに.spec.podManagementPolicyが導入されました。

OrderedReadyのPod管理

OrderedReadyのPod管理はStatefulSetのデフォルトの設定です。StatefulSetコントローラーに対して、これまでに紹介したような順序の保証を尊重するように指示します。

ParallelのPod管理

ParallelのPod管理では、StatefulSetコントローラーに対して、PodがRunningかつReadyの状態や完全に停止するまで待たないように指示し、すべてのPodを並列に起動または停止させるようにします。

apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"
  podManagementPolicy: "Parallel"
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: registry.k8s.io/nginx-slim:0.8
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi

上の例をダウンロードして、web-parallel.yamlという名前でファイルに保存してください。

このマニフェストは、.spec.podManagementPolicyParallelに設定されている以外は、前にダウンロードしたwebStatefulSetと同一です。

1つ目のターミナルで、StatefulSet内のPodを監視します。

kubectl get pod -l app=nginx -w

2つ目のターミナルで、マニフェスト内のStatefulSetとServiceを作成します。

kubectl apply -f web-parallel.yaml
service/nginx created
statefulset.apps/web created

1つ目のターミナルで実行したkubectl getコマンドの出力を確認します。

kubectl get pod -l app=nginx -w
NAME      READY     STATUS    RESTARTS   AGE
web-0     0/1       Pending   0          0s
web-0     0/1       Pending   0         0s
web-1     0/1       Pending   0         0s
web-1     0/1       Pending   0         0s
web-0     0/1       ContainerCreating   0         0s
web-1     0/1       ContainerCreating   0         0s
web-0     1/1       Running   0         10s
web-1     1/1       Running   0         10s

StatefulSetコントローラーはweb-0web-1を同時に起動しています。

2つ目のターミナルで、StatefulSetをスケールしてみます。

kubectl scale statefulset/web --replicas=4
statefulset.apps/web scaled

kubectl getコマンドを実行しているターミナルの出力を確認します。

web-3     0/1       Pending   0         0s
web-3     0/1       Pending   0         0s
web-3     0/1       Pending   0         7s
web-3     0/1       ContainerCreating   0         7s
web-2     1/1       Running   0         10s
web-3     1/1       Running   0         26s

StatefulSetが2つのPodを実行し、1つ目のPodがRunningかつReadyの状態になるのを待たずに2つ目のPodを実行しているのがわかります。

クリーンアップ

2つのターミナルが開かれているはずなので、クリーンアップの一部としてkubectlコマンドを実行する準備ができています。

kubectl delete sts web
# stsは、statefulsetの略です。

kubectl getを監視すると、Podが削除されていく様子を確認できます。

kubectl get pod -l app=nginx -w
web-3     1/1       Terminating   0         9m
web-2     1/1       Terminating   0         9m
web-3     1/1       Terminating   0         9m
web-2     1/1       Terminating   0         9m
web-1     1/1       Terminating   0         44m
web-0     1/1       Terminating   0         44m
web-0     0/1       Terminating   0         44m
web-3     0/1       Terminating   0         9m
web-2     0/1       Terminating   0         9m
web-1     0/1       Terminating   0         44m
web-0     0/1       Terminating   0         44m
web-2     0/1       Terminating   0         9m
web-2     0/1       Terminating   0         9m
web-2     0/1       Terminating   0         9m
web-1     0/1       Terminating   0         44m
web-1     0/1       Terminating   0         44m
web-1     0/1       Terminating   0         44m
web-0     0/1       Terminating   0         44m
web-0     0/1       Terminating   0         44m
web-0     0/1       Terminating   0         44m
web-3     0/1       Terminating   0         9m
web-3     0/1       Terminating   0         9m
web-3     0/1       Terminating   0         9m

削除の間、StatefulSetはすべてのPodを並列に削除し、順序インデックスが1つ前のPodが停止するのを待つことはありません。

kubectl getコマンドを実行しているターミナルを閉じて、nginxServiceを削除します。

kubectl delete svc nginx

6.2 - 例: Persistent Volumeを使用したWordpressとMySQLをデプロイする

このチュートリアルでは、WordPressのサイトとMySQLデータベースをMinikubeを使ってデプロイする方法を紹介します。2つのアプリケーションとも、データを保存するためにPersistentVolumeとPersistentVolumeClaimを使用します。

PersistentVolume(PV)とは、管理者が手動でプロビジョニングを行うか、StorageClassを使ってKubernetesによって動的にプロビジョニングされた、クラスター内のストレージの一部です。PersistentVolumeClaim(PVC)は、PVによって満たすことができる、ユーザーによるストレージへのリクエストのことです。PersistentVolumeとPersistentVolumeClaimは、Podのライフサイクルからは独立していて、Podの再起動、Podの再スケジューリング、さらにはPodの削除が行われたとしても、その中のデータは削除されずに残ります。

目標

  • PersistentVolumeClaimとPersistentVolumeを作成する
  • 以下を含むkustomization.yamlを作成する
    • Secret generator
    • MySQLリソースの設定
    • WordPressリソースの設定
  • kustomizationディレクトリをkubectl apply -k ./で適用する
  • クリーンアップする

始める前に

Kubernetesクラスターが必要、かつそのクラスターと通信するためにkubectlコマンドラインツールが設定されている必要があります。 このチュートリアルは、コントロールプレーンのホストとして動作していない少なくとも2つのノードを持つクラスターで実行することをおすすめします。 まだクラスターがない場合、minikubeを使って作成するか、 以下のいずれかのKubernetesプレイグラウンドも使用できます:

バージョンを確認するには次のコマンドを実行してください: kubectl version. このページで示された例は、kubectl 1.14以降で動作します。

以下の設定ファイルをダウンロードします。

  1. mysql-deployment.yaml

  2. wordpress-deployment.yaml

PersistentVolumeClaimとPersistentVolumeを作成する

MySQLとWordpressはそれぞれ、データを保存するためのPersistentVolumeを必要とします。各PersistentVolumeClaimはデプロイの段階で作成されます。

多くのクラスター環境では、デフォルトのStorageClassがインストールされています。StorageClassがPersistentVolumeClaim中で指定されていなかった場合、クラスターのデフォルトのStorageClassが代わりに使われます。

PersistentVolumeClaimが作成されるとき、StorageClassの設定に基づいてPersistentVolumeが動的にプロビジョニングされます。

kustomization.yamlを作成する

Secret generatorを追加する

Secretとは、パスワードやキーのような機密性の高いデータ片を保存するためのオブジェクトです。バージョン1.14からは、kubectlがkustomizationファイルを使用したKubernetesオブジェクトの管理をサポートしています。kustomization.yaml内のgeneratorによってSecretを作成することができます。

以下のコマンドを実行して、kustomization.yamlの中にSecret generatorを追加します。YOUR_PASSWORDの部分を使いたいパスワードに置換してください。

cat <<EOF >./kustomization.yaml
secretGenerator:
- name: mysql-pass
  literals:
  - password=YOUR_PASSWORD
EOF

MySQLとWordPressのためのリソースの設定を追加する

以下のマニフェストには、シングルインスタンスのMySQLのDeploymentが書かれています。MySQLコンテナはPersistentVolumeを/var/lib/mysqlにマウントします。MYSQL_ROOT_PASSWORD環境変数には、Secretから得られたデータベースのパスワードが設定されます。

apiVersion: v1
kind: Service
metadata:
  name: wordpress-mysql
  labels:
    app: wordpress
spec:
  ports:
    - port: 3306
  selector:
    app: wordpress
    tier: mysql
  clusterIP: None
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-pv-claim
  labels:
    app: wordpress
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi
---
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
  name: wordpress-mysql
  labels:
    app: wordpress
spec:
  selector:
    matchLabels:
      app: wordpress
      tier: mysql
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: wordpress
        tier: mysql
    spec:
      containers:
      - image: mysql:5.6
        name: mysql
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-pass
              key: password
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: mysql-persistent-storage
          mountPath: /var/lib/mysql
      volumes:
      - name: mysql-persistent-storage
        persistentVolumeClaim:
          claimName: mysql-pv-claim

以下のマニフェストには、シングルインスタンスのWordPressのDeploymentが書かれています。WordPressコンテナはPersistentVolumeをウェブサイトのデータファイルのために/var/www/htmlにマウントします。WORDPRESS_DB_HOST環境変数に上で定義したMySQLのServiceの名前を設定すると、WordPressはServiceによってデータベースにアクセスします。WORDPRESS_DB_PASSWORD環境変数には、kustomizeが生成したSecretから得たデータベースのパスワードが設定されます。

apiVersion: v1
kind: Service
metadata:
  name: wordpress
  labels:
    app: wordpress
spec:
  ports:
    - port: 80
  selector:
    app: wordpress
    tier: frontend
  type: LoadBalancer
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: wp-pv-claim
  labels:
    app: wordpress
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi
---
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
  name: wordpress
  labels:
    app: wordpress
spec:
  selector:
    matchLabels:
      app: wordpress
      tier: frontend
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: wordpress
        tier: frontend
    spec:
      containers:
      - image: wordpress:4.8-apache
        name: wordpress
        env:
        - name: WORDPRESS_DB_HOST
          value: wordpress-mysql
        - name: WORDPRESS_DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-pass
              key: password
        ports:
        - containerPort: 80
          name: wordpress
        volumeMounts:
        - name: wordpress-persistent-storage
          mountPath: /var/www/html
      volumes:
      - name: wordpress-persistent-storage
        persistentVolumeClaim:
          claimName: wp-pv-claim
  1. MySQLのDeploymentの設定ファイルをダウンロードします。

    curl -LO https://k8s.io/examples/application/wordpress/mysql-deployment.yaml
    
  2. WordPressの設定ファイルをダウンロードします。

    curl -LO https://k8s.io/examples/application/wordpress/wordpress-deployment.yaml
    
  3. これらをkustomization.yamlファイルに追加します。

cat <<EOF >>./kustomization.yaml
resources:
  - mysql-deployment.yaml
  - wordpress-deployment.yaml
EOF

適用と確認

kustomization.yamlには、WordPressのサイトとMySQLデータベースのためのすべてのリソースが含まれています。次のコマンドでこのディレクトリを適用できます。

kubectl apply -k ./

これで、すべてのオブジェクトが存在していることを確認できます。

  1. 次のコマンドを実行して、Secretが存在していることを確認します。

    kubectl get secrets
    

    結果は次のようになるはずです。

    NAME                    TYPE                                  DATA   AGE
    mysql-pass-c57bb4t7mf   Opaque                                1      9s
    
  2. 次のコマンドを実行して、PersistentVolumeが動的にプロビジョニングされていることを確認します。

    kubectl get pvc
    

    結果は次のようになるはずです。

    NAME             STATUS    VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS       AGE
    mysql-pv-claim   Bound     pvc-8cbd7b2e-4044-11e9-b2bb-42010a800002   20Gi       RWO            standard           77s
    wp-pv-claim      Bound     pvc-8cd0df54-4044-11e9-b2bb-42010a800002   20Gi       RWO            standard           77s
    
  3. 次のコマンドを実行して、Podが実行中であることを確認します。

    kubectl get pods
    

    結果は次のようになるはずです。

    NAME                               READY     STATUS    RESTARTS   AGE
    wordpress-mysql-1894417608-x5dzt   1/1       Running   0          40s
    
  4. 次のコマンドを実行して、Serviceが実行中であることを確認します。

    kubectl get services wordpress
    

    結果は次のようになるはずです。

    NAME        TYPE            CLUSTER-IP   EXTERNAL-IP   PORT(S)        AGE
    wordpress   LoadBalancer    10.0.0.89    <pending>     80:32406/TCP   4m
    
  5. 次のコマンドを実行して、WordPress ServiceのIPアドレスを取得します。

    minikube service wordpress --url
    

    結果は次のようになるはずです。

    http://1.2.3.4:32406
    
  6. IPアドレスをコピーして、ブラウザーで読み込み、サイトを表示しましょう。

    WordPressによりセットアップされた次のスクリーンショットのようなページが表示されるはずです。

    wordpress-init

クリーンアップ

  1. 次のコマンドを実行して、Secret、Deployment、Service、およびPersistentVolumeClaimを削除します。

    kubectl delete -k ./
    

次の項目

6.3 - 例: StatefulSetを使用したCassandraのデプロイ

このチュートリアルでは、Apache CassandraをKubernetes上で実行する方法を紹介します。 データベースの一種であるCassandraには、データの耐久性(アプリケーションの 状態)を提供するために永続ストレージが必要です。 この例では、カスタムのCassandraのseed providerにより、Cassandraクラスターに参加した新しいCassandraインスタンスを検出できるようにします。

StatefulSetを利用すると、ステートフルなアプリケーションをKubernetesクラスターにデプロイするのが簡単になります。 このチュートリアルで使われている機能のより詳しい情報は、StatefulSetを参照してください。

目標

  • Cassandraのheadless Serviceを作成して検証する。
  • StatefulSetを使用してCassandra ringを作成する。
  • StatefulSetを検証する。
  • StatefulSetを編集する。
  • StatefulSetとPodを削除する。

始める前に

Kubernetesクラスターが必要、かつそのクラスターと通信するためにkubectlコマンドラインツールが設定されている必要があります。 このチュートリアルは、コントロールプレーンのホストとして動作していない少なくとも2つのノードを持つクラスターで実行することをおすすめします。 まだクラスターがない場合、minikubeを使って作成するか、 以下のいずれかのKubernetesプレイグラウンドも使用できます:

このチュートリアルを完了するには、PodServiceStatefulSetの基本についてすでに知っている必要があります。

Minikubeのセットアップに関する追加の設定手順

Cassandraのheadless Serviceを作成する

Kubernetesでは、Serviceは同じタスクを実行するPodの集合を表します。

以下のServiceは、Cassandra Podとクラスター内のクライアント間のDNSルックアップに使われます:

apiVersion: v1
kind: Service
metadata:
  labels:
    app: cassandra
  name: cassandra
spec:
  clusterIP: None
  ports:
  - port: 9042
  selector:
    app: cassandra

cassandra-service.yamlファイルから、Cassandra StatefulSetのすべてのメンバーをトラッキングするServiceを作成します。

kubectl apply -f https://k8s.io/examples/application/cassandra/cassandra-service.yaml

検証 (オプション)

Cassandra Serviceを取得します。

kubectl get svc cassandra

結果は次のようになります。

NAME        TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
cassandra   ClusterIP   None         <none>        9042/TCP   45s

cassandraという名前のServiceが表示されない場合、作成に失敗しています。よくある問題のトラブルシューティングについては、Serviceのデバッグを読んでください。

StatefulSetを使ってCassandra ringを作成する

以下に示すStatefulSetマニフェストは、3つのPodからなるCassandra ringを作成します。

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: cassandra
  labels:
    app: cassandra
spec:
  serviceName: cassandra
  replicas: 3
  selector:
    matchLabels:
      app: cassandra
  template:
    metadata:
      labels:
        app: cassandra
    spec:
      terminationGracePeriodSeconds: 1800
      containers:
      - name: cassandra
        image: gcr.io/google-samples/cassandra:v13
        imagePullPolicy: Always
        ports:
        - containerPort: 7000
          name: intra-node
        - containerPort: 7001
          name: tls-intra-node
        - containerPort: 7199
          name: jmx
        - containerPort: 9042
          name: cql
        resources:
          limits:
            cpu: "500m"
            memory: 1Gi
          requests:
            cpu: "500m"
            memory: 1Gi
        securityContext:
          capabilities:
            add:
              - IPC_LOCK
        lifecycle:
          preStop:
            exec:
              command: 
              - /bin/sh
              - -c
              - nodetool drain
        env:
          - name: MAX_HEAP_SIZE
            value: 512M
          - name: HEAP_NEWSIZE
            value: 100M
          - name: CASSANDRA_SEEDS
            value: "cassandra-0.cassandra.default.svc.cluster.local"
          - name: CASSANDRA_CLUSTER_NAME
            value: "K8Demo"
          - name: CASSANDRA_DC
            value: "DC1-K8Demo"
          - name: CASSANDRA_RACK
            value: "Rack1-K8Demo"
          - name: POD_IP
            valueFrom:
              fieldRef:
                fieldPath: status.podIP
        readinessProbe:
          exec:
            command:
            - /bin/bash
            - -c
            - /ready-probe.sh
          initialDelaySeconds: 15
          timeoutSeconds: 5
        # These volume mounts are persistent. They are like inline claims,
        # but not exactly because the names need to match exactly one of
        # the stateful pod volumes.
        volumeMounts:
        - name: cassandra-data
          mountPath: /cassandra_data
  # These are converted to volume claims by the controller
  # and mounted at the paths mentioned above.
  # do not use these in production until ssd GCEPersistentDisk or other ssd pd
  volumeClaimTemplates:
  - metadata:
      name: cassandra-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: fast
      resources:
        requests:
          storage: 1Gi
---
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: fast
provisioner: k8s.io/minikube-hostpath
parameters:
  type: pd-ssd

cassandra-statefulset.yamlファイルから、CassandraのStatefulSetを作成します:

# cassandra-statefulset.yaml を編集せずにapplyできる場合は、このコマンドを使用してください
kubectl apply -f https://k8s.io/examples/application/cassandra/cassandra-statefulset.yaml

クラスターに合わせてcassandra-statefulset.yamlを編集する必要がある場合、 https://k8s.io/examples/application/cassandra/cassandra-statefulset.yaml をダウンロードして、修正したバージョンを保存したフォルダからマニフェストを適用してください。

# cassandra-statefulset.yaml をローカルで編集する必要がある場合、このコマンドを使用してください
kubectl apply -f cassandra-statefulset.yaml

CassandraのStatefulSetを検証する

  1. CassandraのStatefulSetを取得します

    kubectl get statefulset cassandra
    

    結果は次のようになるはずです:

    NAME        DESIRED   CURRENT   AGE
    cassandra   3         0         13s
    

    StatefulSetリソースがPodを順番にデプロイします。

  2. Podを取得して順序付きの作成ステータスを確認します

    kubectl get pods -l="app=cassandra"
    

    結果は次のようになるはずです:

    NAME          READY     STATUS              RESTARTS   AGE
    cassandra-0   1/1       Running             0          1m
    cassandra-1   0/1       ContainerCreating   0          8s
    

    3つすべてのPodのデプロイには数分かかる場合があります。デプロイが完了すると、同じコマンドは次のような結果を返します:

    NAME          READY     STATUS    RESTARTS   AGE
    cassandra-0   1/1       Running   0          10m
    cassandra-1   1/1       Running   0          9m
    cassandra-2   1/1       Running   0          8m
    
  3. 1番目のPodの中でCassandraのnodetoolを実行して、ringのステータスを表示します。

    kubectl exec -it cassandra-0 -- nodetool status
    

    結果は次のようになるはずです:

    Datacenter: DC1-K8Demo
    ======================
    Status=Up/Down
    |/ State=Normal/Leaving/Joining/Moving
    --  Address     Load       Tokens       Owns (effective)  Host ID                               Rack
    UN  172.17.0.5  83.57 KiB  32           74.0%             e2dd09e6-d9d3-477e-96c5-45094c08db0f  Rack1-K8Demo
    UN  172.17.0.4  101.04 KiB  32           58.8%             f89d6835-3a42-4419-92b3-0e62cae1479c  Rack1-K8Demo
    UN  172.17.0.6  84.74 KiB  32           67.1%             a6a1e8c2-3dc5-4417-b1a0-26507af2aaad  Rack1-K8Demo
    

CassandraのStatefulSetを変更する

kubectl editを使うと、CassandraのStatefulSetのサイズを変更できます。

  1. 次のコマンドを実行します。

    kubectl edit statefulset cassandra
    

    このコマンドを実行すると、ターミナルでエディタが起動します。変更が必要な行はreplicasフィールドです。 以下の例は、StatefulSetファイルの抜粋です:

    # Please edit the object below. Lines beginning with a '#' will be ignored,
    # and an empty file will abort the edit. If an error occurs while saving this file will be
    # reopened with the relevant failures.
    #
    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
      creationTimestamp: 2016-08-13T18:40:58Z
      generation: 1
      labels:
      app: cassandra
      name: cassandra
      namespace: default
      resourceVersion: "323"
      uid: 7a219483-6185-11e6-a910-42010a8a0fc0
    spec:
      replicas: 3
    
  2. レプリカ数を4に変更し、マニフェストを保存します。

    これで、StatefulSetが4つのPodを実行するようにスケールされました。

  3. CassandraのStatefulSetを取得して、変更を確かめます:

    kubectl get statefulset cassandra
    

    結果は次のようになるはずです:

    NAME        DESIRED   CURRENT   AGE
    cassandra   4         4         36m
    

クリーンアップ

StatefulSetを削除したりスケールダウンしても、StatefulSetに関係するボリュームは削除されません。 StatefulSetに関連するすべてのリソースを自動的に破棄するよりも、データの方がより貴重であるため、安全のためにこのような設定になっています。

  1. 次のコマンドを実行して(単一のコマンドにまとめています)、CassandraのStatefulSetに含まれるすべてのリソースを削除します:

    grace=$(kubectl get pod cassandra-0 -o=jsonpath='{.spec.terminationGracePeriodSeconds}') \
      && kubectl delete statefulset -l app=cassandra \
      && echo "Sleeping ${grace} seconds" 1>&2 \
      && sleep $grace \
      && kubectl delete persistentvolumeclaim -l app=cassandra
    
  2. 次のコマンドを実行して、CassandraをセットアップしたServiceを削除します:

    kubectl delete service -l app=cassandra
    

Cassandraコンテナの環境変数

このチュートリアルのPodでは、Googleのコンテナレジストリgcr.io/google-samples/cassandra:v13イメージを使用しました。このDockerイメージはdebian-baseをベースにしており、OpenJDK 8が含まれています。

このイメージには、Apache Debianリポジトリの標準のCassandraインストールが含まれます。 環境変数を利用すると、cassandra.yamlに挿入された値を変更できます。

環境変数 デフォルト値
CASSANDRA_CLUSTER_NAME 'Test Cluster'
CASSANDRA_NUM_TOKENS 32
CASSANDRA_RPC_ADDRESS 0.0.0.0

次の項目

6.4 - 分散システムコーディネーターZooKeeperの実行

このチュートリアルでは、StatefulSetPodDisruptionBudgetsPodアンチアフィニティを使って、Kubernetes上でのApache Zookeeperの実行をデモンストレーションします。

始める前に

このチュートリアルを始める前に、以下のKubernetesの概念について理解しておく必要があります。

少なくとも4つのノードのクラスターが必要で、各ノードは少なくとも2つのCPUと4GiBのメモリが必須です。このチュートリアルでは、クラスターのノードをcordonおよびdrainします。 つまり、クラスターがそのノードの全てのPodを終了して退去させ、ノードが一時的にスケジュールできなくなる、ということです。 このチュートリアル専用のクラスターを使うか、起こした破壊がほかのテナントに干渉しない確証を得ることをお勧めします。

このチュートリアルでは、クラスターがPersistentVolumeの動的なプロビジョニングが行われるように設定されていることを前提としています。 クラスターがそのように設定されていない場合、チュートリアルを始める前に20GiBのボリュームを3つ、手動でプロビジョニングする必要があります。

目標

このチュートリアルを終えると、以下の知識を得られます。

  • StatefulSetを使ってZooKeeperアンサンブルをデプロイする方法。
  • アンサンブルを一貫して設定する方法。
  • ZooKeeperサーバーのデプロイをアンサンブルに広げる方法。
  • 計画されたメンテナンス中もサービスが利用可能であることを保証するためにPodDisruptionBudgetsを使う方法。

ZooKeeper

Apache ZooKeeperは、分散アプリケーションのための、分散型オープンソースコーディネーションサービスです。 ZooKeeperでは、データの読み書き、および更新の監視ができます。 データは階層化されてファイルシステム内に編成され、アンサンブル(ZooKeeperサーバーのセット)内の全てのZooKeeperサーバーに複製されます。 データへの全ての操作はアトミックかつ逐次的に一貫性を持ちます。 ZooKeeperは、アンサンブル内の全てのサーバー間でステートマシンを複製するためにZab合意プロトコルを使ってこれを保証します。

アンサンブルはリーダーを選出するのにZabプロトコルを使い、選出が完了するまでデータを書き出しません。 完了すると、アンサンブルは複製するのにZabを使い、書き込みが承認されてクライアントに可視化されるより前に、全ての書き込みをクォーラムに複製することを保証します。 重み付けされたクォーラムでなければ、クォーラムは現在のリーダーを含むアンサンブルの過半数を占めるコンポーネントです。 例えばアンサンブルが3つのサーバーを持つ時、リーダーとそれ以外のもう1つのサーバーを含むコンポーネントが、クォーラムを構成します。 アンサンブルがクォーラムに達しない場合、アンサンブルはデータを書き出せません。

ZooKeeperサーバー群はそれらの全てのステートマシンをメモリに保持し、それぞれの変化をストレージメディア上の永続的なWAL(Write Ahead Log)に書き出します。 サーバーがクラッシュした時には、WALをリプレーすることで以前のステートに回復できます。 WALを際限のない増加から防ぐために、ZooKeeperサーバーは、メモリステートにあるものをストレージメディアに定期的にスナップショットします。 これらのスナップショットはメモリに直接読み込むことができ、スナップショットより前の全てのWALエントリは破棄され得ます。

ZooKeeperアンサンブルの作成

以下のマニフェストはHeadless ServiceServicePodDisruptionBudgetStatefulSetを含んでいます。

apiVersion: v1
kind: Service
metadata:
  name: zk-hs
  labels:
    app: zk
spec:
  ports:
  - port: 2888
    name: server
  - port: 3888
    name: leader-election
  clusterIP: None
  selector:
    app: zk
---
apiVersion: v1
kind: Service
metadata:
  name: zk-cs
  labels:
    app: zk
spec:
  ports:
  - port: 2181
    name: client
  selector:
    app: zk
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: zk-pdb
spec:
  selector:
    matchLabels:
      app: zk
  maxUnavailable: 1
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: zk
spec:
  selector:
    matchLabels:
      app: zk
  serviceName: zk-hs
  replicas: 3
  updateStrategy:
    type: RollingUpdate
  podManagementPolicy: OrderedReady
  template:
    metadata:
      labels:
        app: zk
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: "app"
                    operator: In
                    values:
                    - zk
              topologyKey: "kubernetes.io/hostname"
      containers:
      - name: kubernetes-zookeeper
        imagePullPolicy: Always
        image: "registry.k8s.io/kubernetes-zookeeper:1.0-3.4.10"
        resources:
          requests:
            memory: "1Gi"
            cpu: "0.5"
        ports:
        - containerPort: 2181
          name: client
        - containerPort: 2888
          name: server
        - containerPort: 3888
          name: leader-election
        command:
        - sh
        - -c
        - "start-zookeeper \
          --servers=3 \
          --data_dir=/var/lib/zookeeper/data \
          --data_log_dir=/var/lib/zookeeper/data/log \
          --conf_dir=/opt/zookeeper/conf \
          --client_port=2181 \
          --election_port=3888 \
          --server_port=2888 \
          --tick_time=2000 \
          --init_limit=10 \
          --sync_limit=5 \
          --heap=512M \
          --max_client_cnxns=60 \
          --snap_retain_count=3 \
          --purge_interval=12 \
          --max_session_timeout=40000 \
          --min_session_timeout=4000 \
          --log_level=INFO"
        readinessProbe:
          exec:
            command:
            - sh
            - -c
            - "zookeeper-ready 2181"
          initialDelaySeconds: 10
          timeoutSeconds: 5
        livenessProbe:
          exec:
            command:
            - sh
            - -c
            - "zookeeper-ready 2181"
          initialDelaySeconds: 10
          timeoutSeconds: 5
        volumeMounts:
        - name: datadir
          mountPath: /var/lib/zookeeper
      securityContext:
        runAsUser: 1000
        fsGroup: 1000
  volumeClaimTemplates:
  - metadata:
      name: datadir
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 10Gi

ターミナルを開き、マニフェストを作成するために kubectl applyコマンドを使います。

kubectl apply -f https://k8s.io/examples/application/zookeeper/zookeeper.yaml

これはzk-hs Headless Service、zk-cs Service、zk-pdb PodDisruptionBudget、 zk StatefulSetを作成します。

service/zk-hs created
service/zk-cs created
poddisruptionbudget.policy/zk-pdb created
statefulset.apps/zk created

StatefulSetのPodを作成するStatefulSetコントローラーを監視するため、kubectl getを使います。

kubectl get pods -w -l app=zk

zk-2 PodがRunningおよびReadyになったら、CTRL-Cでkubectlを終了してください。

NAME      READY     STATUS    RESTARTS   AGE
zk-0      0/1       Pending   0          0s
zk-0      0/1       Pending   0         0s
zk-0      0/1       ContainerCreating   0         0s
zk-0      0/1       Running   0         19s
zk-0      1/1       Running   0         40s
zk-1      0/1       Pending   0         0s
zk-1      0/1       Pending   0         0s
zk-1      0/1       ContainerCreating   0         0s
zk-1      0/1       Running   0         18s
zk-1      1/1       Running   0         40s
zk-2      0/1       Pending   0         0s
zk-2      0/1       Pending   0         0s
zk-2      0/1       ContainerCreating   0         0s
zk-2      0/1       Running   0         19s
zk-2      1/1       Running   0         40s

StatefulSetコントローラーが3つのPodを作成し、各PodはZooKeeperサーバー付きのコンテナを持ちます。

リーダーの選出のファシリテート

匿名のネットワークにおいてリーダー選出を終了するアルゴリズムがないので、Zabはリーダー選出のための明示的なメンバーシップ設定を要します。 アンサンブルの各サーバーはユニーク識別子を持つ必要があり、全てのサーバーは識別子のグローバルセットを知っている必要があり、各識別子はネットワークアドレスと関連付けられている必要があります。

zk StatefulSetのPodのホスト名を取得するためにkubectl execを使います。

for i in 0 1 2; do kubectl exec zk-$i -- hostname; done

StatefulSetコントローラーは各Podに、その順序インデックスに基づくユニークなホスト名を提供します。 ホスト名は<statefulset名>-<順序インデックス>という形をとります。 zk StatefulSetのreplicasフィールドが3にセットされているので、このセットのコントローラーは、ホスト名にそれぞれzk-0zk-1zk-2が付いた3つのPodを作成します。

zk-0
zk-1
zk-2

ZooKeeperアンサンブルのサーバーは、ユニーク識別子として自然数を使い、それぞれのサーバーの識別子をサーバーのデータディレクトリ内のmyidというファイルに格納します。

各サーバーのmyidファイルの内容を調べるには、以下のコマンドを使います。

for i in 0 1 2; do echo "myid zk-$i";kubectl exec zk-$i -- cat /var/lib/zookeeper/data/myid; done

識別子が自然数で順序インデックスは正の整数なので、順序に1を加算することで識別子を生成できます。

myid zk-0
1
myid zk-1
2
myid zk-2
3

zk StatefulSet内の各Podの完全修飾ドメイン名(FQDN)を取得するには、以下のコマンドを使います。

for i in 0 1 2; do kubectl exec zk-$i -- hostname -f; done

zk-hs Serviceは、全Podのためのドメインzk-hs.default.svc.cluster.localを作成します。

zk-0.zk-hs.default.svc.cluster.local
zk-1.zk-hs.default.svc.cluster.local
zk-2.zk-hs.default.svc.cluster.local

Kubernetes DNSのAレコードは、FQDNをPodのIPアドレスに解決します。 KubernetesがPodを再スケジュールした場合、AレコードはPodの新しいIPアドレスに更新されますが、Aレコードの名前は変更されません。

ZooKeeperはそのアプリケーション設定をzoo.cfgという名前のファイルに格納します。 zk-0 Pod内のzoo.cfgファイルの内容を見るには、kubectl execを使います。

kubectl exec zk-0 -- cat /opt/zookeeper/conf/zoo.cfg

ファイル末尾にあるserver.1server.2server.3のプロパティの、123はZooKeeperサーバーのmyidファイル内の識別子に対応します。 これらはzk StatefulSet内のPodのFQDNにセットされます。

clientPort=2181
dataDir=/var/lib/zookeeper/data
dataLogDir=/var/lib/zookeeper/log
tickTime=2000
initLimit=10
syncLimit=2000
maxClientCnxns=60
minSessionTimeout= 4000
maxSessionTimeout= 40000
autopurge.snapRetainCount=3
autopurge.purgeInterval=0
server.1=zk-0.zk-hs.default.svc.cluster.local:2888:3888
server.2=zk-1.zk-hs.default.svc.cluster.local:2888:3888
server.3=zk-2.zk-hs.default.svc.cluster.local:2888:3888

合意形成

合意(consensus)プロトコルは、各参加者の識別子がユニークであることを要件としています。 Zabプロトコル内で同じユニーク識別子を主張する2つの参加者はないものとします。 これは、システム内のプロセスが、どのプロセスがどのデータをコミットしたかを同意できるようにするために必須です。 2つのPodが同じ順序値で起動されたなら、2つのZooKeeperサーバーはどちらもそれら自身を同じサーバーとして認識してしまうでしょう。

kubectl get pods -w -l app=zk
NAME      READY     STATUS    RESTARTS   AGE
zk-0      0/1       Pending   0          0s
zk-0      0/1       Pending   0         0s
zk-0      0/1       ContainerCreating   0         0s
zk-0      0/1       Running   0         19s
zk-0      1/1       Running   0         40s
zk-1      0/1       Pending   0         0s
zk-1      0/1       Pending   0         0s
zk-1      0/1       ContainerCreating   0         0s
zk-1      0/1       Running   0         18s
zk-1      1/1       Running   0         40s
zk-2      0/1       Pending   0         0s
zk-2      0/1       Pending   0         0s
zk-2      0/1       ContainerCreating   0         0s
zk-2      0/1       Running   0         19s
zk-2      1/1       Running   0         40s

各PodのAレコードは、PodがReadyになった時に記入されます。そのため、ZooKeeperサーバー群のFQDNはある1つのエンドポイント、すなわちmyidファイルで設定された識別子を主張するユニークなZooKeeperサーバーに解決されます。

zk-0.zk-hs.default.svc.cluster.local
zk-1.zk-hs.default.svc.cluster.local
zk-2.zk-hs.default.svc.cluster.local

これは、ZooKeeperのzoo.cfgファイル内のserversプロパティが正しく設定されたアンサンブルを表していることを保証します。

server.1=zk-0.zk-hs.default.svc.cluster.local:2888:3888
server.2=zk-1.zk-hs.default.svc.cluster.local:2888:3888
server.3=zk-2.zk-hs.default.svc.cluster.local:2888:3888

サーバーが値のコミットを試みるためにZabプロトコルを使う時、(リーダー選出が成功していて、少なくともPodのうちの2つがRunningおよびReadyならば)それぞれのサーバーは双方の合意をとって値をコミット、あるいは、(もし双方の状態が合わなければ)それを行うことに失敗します。 あるサーバーが別のサーバーを代行して書き込みを承認する状態は発生しません。

アンサンブルの健全性テスト

最も基本的な健全性テストは、データを1つのZooKeeperサーバーに書き込み、そのデータを別のサーバーで読み取ることです。

以下のコマンドは、worldをアンサンブル内のzk-0 Podのパス/helloに書き込むのに、zkCli.shスクリプトを実行します。

kubectl exec zk-0 -- zkCli.sh create /hello world
WATCHER::

WatchedEvent state:SyncConnected type:None path:null
Created /hello

zk-1 Podからデータを取得するには、以下のコマンドを使います。

kubectl exec zk-1 -- zkCli.sh get /hello

zk-0に作成したデータは、アンサンブル内の全てのサーバーで利用できます。

WATCHER::

WatchedEvent state:SyncConnected type:None path:null
world
cZxid = 0x100000002
ctime = Thu Dec 08 15:13:30 UTC 2016
mZxid = 0x100000002
mtime = Thu Dec 08 15:13:30 UTC 2016
pZxid = 0x100000002
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0

永続的なストレージの提供

ZooKeeperの概要のセクションで言及したように、 ZooKeeperは全てのエントリを永続的なWALにコミットし、定期的にメモリ状態のスナップショットをストレージメディアに書き出します。 永続性を提供するためにWALを使用するのは、複製されたステートマシンを立てるために合意プロトコルを使うアプリケーションでよくあるテクニックです。

zk StatefulSetを削除するために、kubectl deleteコマンドを使います。

kubectl delete statefulset zk
statefulset.apps "zk" deleted

StatefulSet内のPodの終了を観察します。

kubectl get pods -w -l app=zk

zk-0が完全に終了したら、CTRL-Cでkubectlを終了します。

zk-2      1/1       Terminating   0         9m
zk-0      1/1       Terminating   0         11m
zk-1      1/1       Terminating   0         10m
zk-2      0/1       Terminating   0         9m
zk-2      0/1       Terminating   0         9m
zk-2      0/1       Terminating   0         9m
zk-1      0/1       Terminating   0         10m
zk-1      0/1       Terminating   0         10m
zk-1      0/1       Terminating   0         10m
zk-0      0/1       Terminating   0         11m
zk-0      0/1       Terminating   0         11m
zk-0      0/1       Terminating   0         11m

zookeeper.yamlのマニフェストを再適用します。

kubectl apply -f https://k8s.io/examples/application/zookeeper/zookeeper.yaml

これはzk StatefulSetオブジェクトを作成しますが、マニフェストのその他のAPIオブジェクトはすでに存在しているので変更されません。

StatefulSetコントローラーがStatefulSetのPodを再作成するのを見てみます。

kubectl get pods -w -l app=zk

zk-2 PodがRunningおよびReadyになったら、CTRL-Cでkubectlを終了します。

NAME      READY     STATUS    RESTARTS   AGE
zk-0      0/1       Pending   0          0s
zk-0      0/1       Pending   0         0s
zk-0      0/1       ContainerCreating   0         0s
zk-0      0/1       Running   0         19s
zk-0      1/1       Running   0         40s
zk-1      0/1       Pending   0         0s
zk-1      0/1       Pending   0         0s
zk-1      0/1       ContainerCreating   0         0s
zk-1      0/1       Running   0         18s
zk-1      1/1       Running   0         40s
zk-2      0/1       Pending   0         0s
zk-2      0/1       Pending   0         0s
zk-2      0/1       ContainerCreating   0         0s
zk-2      0/1       Running   0         19s
zk-2      1/1       Running   0         40s

健全性テストで入力した値をzk-2 Podから取得するには、以下のコマンドを使います。

kubectl exec zk-2 zkCli.sh get /hello

zk StatefulSet内の全てのPodを終了して再作成したにもかかわらず、アンサンブルは元の値をなおも供給します。

WATCHER::

WatchedEvent state:SyncConnected type:None path:null
world
cZxid = 0x100000002
ctime = Thu Dec 08 15:13:30 UTC 2016
mZxid = 0x100000002
mtime = Thu Dec 08 15:13:30 UTC 2016
pZxid = 0x100000002
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0

zk StatefulSetのspecvolumeClaimTemplatesフィールドは、各PodにプロビジョニングされるPersistentVolumeを指定します。

volumeClaimTemplates:
  - metadata:
      name: datadir
      annotations:
        volume.alpha.kubernetes.io/storage-class: anything
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 20Gi

StatefulSetコントローラーは、StatefulSet内の各PodのためにPersistentVolumeClaimを生成します。

StatefulSetPersistentVolumeClaimsを取得するために、以下のコマンドを使います。

kubectl get pvc -l app=zk

StatefulSetがそのPodを再作成した時、StatefulSetはPodのPersistentVolumeを再マウントします。

NAME           STATUS    VOLUME                                     CAPACITY   ACCESSMODES   AGE
datadir-zk-0   Bound     pvc-bed742cd-bcb1-11e6-994f-42010a800002   20Gi       RWO           1h
datadir-zk-1   Bound     pvc-bedd27d2-bcb1-11e6-994f-42010a800002   20Gi       RWO           1h
datadir-zk-2   Bound     pvc-bee0817e-bcb1-11e6-994f-42010a800002   20Gi       RWO           1h

StatefulSetのコンテナtemplatevolumeMountsセクションは、ZooKeeperサーバーのデータディレクトリにあるPersistentVolumeをマウントします。

volumeMounts:
- name: datadir
  mountPath: /var/lib/zookeeper

zk StatefulSet内のPodが(再)スケジュールされると、ZooKeeperサーバーのデータディレクトリにマウントされた同じPersistentVolumeを常に得ます。 Podが再スケジュールされたとしても、全ての書き込みはZooKeeperサーバーのWALおよび全スナップショットに行われ、永続性は残ります。

一貫性のある設定の保証

リーダーの選出のファシリテートおよび合意形成のセクションで記したように、ZooKeeperのアンサンブル内のサーバー群は、リーダーを選出しクォーラムを形成するための一貫性のある設定を必要とします。 また、プロトコルがネットワーク越しで正しく動作するために、Zabプロトコルの一貫性のある設定も必要です。 この例では、設定を直接マニフェストに埋め込むことで一貫性のある設定を達成します。

zk StatefulSetを取得しましょう。

kubectl get sts zk -o yaml
…
command:
      - sh
      - -c
      - "start-zookeeper \
        --servers=3 \
        --data_dir=/var/lib/zookeeper/data \
        --data_log_dir=/var/lib/zookeeper/data/log \
        --conf_dir=/opt/zookeeper/conf \
        --client_port=2181 \
        --election_port=3888 \
        --server_port=2888 \
        --tick_time=2000 \
        --init_limit=10 \
        --sync_limit=5 \
        --heap=512M \
        --max_client_cnxns=60 \
        --snap_retain_count=3 \
        --purge_interval=12 \
        --max_session_timeout=40000 \
        --min_session_timeout=4000 \
        --log_level=INFO"
…

このcommandでは、ZooKeeperサーバーを開始するためにコマンドラインパラメータで設定を渡しています。 設定をアンサンブルへ渡すのには環境変数を使うこともできます。

ログの設定

zkGenConfig.shスクリプトで生成されたファイルの1つは、ZooKeeperのログを制御します。 ZooKeeperはLog4jを使い、デフォルトではログの設定に基づいて、ログ設定に時間およびサイズベースでのローリングファイルアペンダー(ログのローテーション)を使用します。

zk StatefulSet内のPodの1つからログ設定を取得するには、以下のコマンドを使います。

kubectl exec zk-0 cat /usr/etc/zookeeper/log4j.properties

以下のログ設定は、ZooKeeperにログの全てを標準出力ファイルストリームに書き出す処理をさせます。

zookeeper.root.logger=CONSOLE
zookeeper.console.threshold=INFO
log4j.rootLogger=${zookeeper.root.logger}
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.Threshold=${zookeeper.console.threshold}
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} [myid:%X{myid}] - %-5p [%t:%C{1}@%L] - %m%n

これはログコンテナ内のログを安全にとるための、最もシンプルと思われる方法です。 アプリケーションはログを標準出力に書き出し、Kubernetesがログのローテーションを処理してくれます。 Kubernetesは、標準出力と標準エラー出力に書き出されるアプリケーションのログがローカルストレージメディアを使い尽くさないことを保証する、健全維持ポリシーも実装しています。

Podの1つから末尾20行を取得するために、kubectl logsを使ってみます。

kubectl logs zk-0 --tail 20

kubectl logsを利用するか、Kubernetes Dashboardから、標準出力または標準エラーに書き出されたアプリケーションログを参照できます。

2016-12-06 19:34:16,236 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52740
2016-12-06 19:34:16,237 [myid:1] - INFO  [Thread-1136:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52740 (no session established for client)
2016-12-06 19:34:26,155 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52749
2016-12-06 19:34:26,155 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52749
2016-12-06 19:34:26,156 [myid:1] - INFO  [Thread-1137:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52749 (no session established for client)
2016-12-06 19:34:26,222 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52750
2016-12-06 19:34:26,222 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52750
2016-12-06 19:34:26,226 [myid:1] - INFO  [Thread-1138:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52750 (no session established for client)
2016-12-06 19:34:36,151 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52760
2016-12-06 19:34:36,152 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52760
2016-12-06 19:34:36,152 [myid:1] - INFO  [Thread-1139:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52760 (no session established for client)
2016-12-06 19:34:36,230 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52761
2016-12-06 19:34:36,231 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52761
2016-12-06 19:34:36,231 [myid:1] - INFO  [Thread-1140:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52761 (no session established for client)
2016-12-06 19:34:46,149 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52767
2016-12-06 19:34:46,149 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52767
2016-12-06 19:34:46,149 [myid:1] - INFO  [Thread-1141:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52767 (no session established for client)
2016-12-06 19:34:46,230 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52768
2016-12-06 19:34:46,230 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52768
2016-12-06 19:34:46,230 [myid:1] - INFO  [Thread-1142:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52768 (no session established for client)

Kubernetesは多くのログソリューションを統合しています。 クラスターおよびアプリケーションに最も適合するログソリューションを選べます。 クラスターレベルのロギングとアグリゲーションとして、ログをローテートおよび輸送するためのサイドカーコンテナをデプロイすることを検討してください。

非特権ユーザーの設定

コンテナ内で特権ユーザーとしての実行をアプリケーションに許可するベストプラクティスは、議論の的です。 アプリケーションが非特権ユーザーとして動作することを組織で必須としているなら、エントリポイントがそのユーザーとして実行できるユーザーを制御するセキュリティコンテキストを利用できます。

zk StatefulSetのPod templateは、SecurityContextを含んでいます。

securityContext:
  runAsUser: 1000
  fsGroup: 1000

Podのコンテナ内で、UID 1000はzookeeperユーザーに、GID 1000はzookeeperグループにそれぞれ相当します。

zk-0 PodからのZooKeeperプロセス情報を取得してみましょう。

kubectl exec zk-0 -- ps -elf

securityContextオブジェクトのrunAsUserフィールドが1000にセットされているとおり、ZooKeeperプロセスは、rootとして実行される代わりにzookeeperユーザーとして実行されています。

F S UID        PID  PPID  C PRI  NI ADDR SZ WCHAN  STIME TTY          TIME CMD
4 S zookeep+     1     0  0  80   0 -  1127 -      20:46 ?        00:00:00 sh -c zkGenConfig.sh && zkServer.sh start-foreground
0 S zookeep+    27     1  0  80   0 - 1155556 -    20:46 ?        00:00:19 /usr/lib/jvm/java-8-openjdk-amd64/bin/java -Dzookeeper.log.dir=/var/log/zookeeper -Dzookeeper.root.logger=INFO,CONSOLE -cp /usr/bin/../build/classes:/usr/bin/../build/lib/*.jar:/usr/bin/../share/zookeeper/zookeeper-3.4.9.jar:/usr/bin/../share/zookeeper/slf4j-log4j12-1.6.1.jar:/usr/bin/../share/zookeeper/slf4j-api-1.6.1.jar:/usr/bin/../share/zookeeper/netty-3.10.5.Final.jar:/usr/bin/../share/zookeeper/log4j-1.2.16.jar:/usr/bin/../share/zookeeper/jline-0.9.94.jar:/usr/bin/../src/java/lib/*.jar:/usr/bin/../etc/zookeeper: -Xmx2G -Xms2G -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.local.only=false org.apache.zookeeper.server.quorum.QuorumPeerMain /usr/bin/../etc/zookeeper/zoo.cfg

デフォルトでは、PodのPersistentVolumeがZooKeeperサーバーのデータディレクトリにマウントされている時、rootユーザーのみがそこにアクセス可能です。 この設定はZooKeeperのプロセスがそのWALに書き込んだりスナップショットに格納したりするのを妨げることになります。

zk-0 PodのZooKeeperデータディレクトリのファイルパーミッションを取得するには、以下のコマンドを使います。

kubectl exec -ti zk-0 -- ls -ld /var/lib/zookeeper/data

securityContextオブジェクトのfsGroupフィールドが1000にセットされているので、PodのPersistentVolumeの所有権はzookeeperグループにセットされ、ZooKeeperのプロセスがそのデータを読み書きできるようになります。

drwxr-sr-x 3 zookeeper zookeeper 4096 Dec  5 20:45 /var/lib/zookeeper/data

ZooKeeperプロセスの管理

ZooKeeperドキュメントでは、「You will want to have a supervisory process that manages each of your ZooKeeper server processes (JVM).(各ZooKeeperサーバープロセス(JVM)を管理する監督プロセスを持ちたくなります)」と述べています。 分散型システム内で失敗したプロセスを再起動するのにwatchdog(監督プロセス)を使うのは、典型的パターンです。 アプリケーションをKubernetesにデプロイする時には、監督プロセスのような外部ユーティリティを使うよりもむしろ、アプリケーションのwatchdogとしてKubernetesを使うべきです。

アンサンブルのアップデート

zk StatefulSetRollingUpdateアップデート戦略を使うように設定されています。 サーバーに割り当てられるcpusの数を更新するのに、kubectl patchを利用できます。

kubectl patch sts zk --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/resources/requests/cpu", "value":"0.3"}]'
statefulset.apps/zk patched

更新の状況を見るには、kubectl rollout statusを使います。

kubectl rollout status sts/zk
waiting for statefulset rolling update to complete 0 pods at revision zk-5db4499664...
Waiting for 1 pods to be ready...
Waiting for 1 pods to be ready...
waiting for statefulset rolling update to complete 1 pods at revision zk-5db4499664...
Waiting for 1 pods to be ready...
Waiting for 1 pods to be ready...
waiting for statefulset rolling update to complete 2 pods at revision zk-5db4499664...
Waiting for 1 pods to be ready...
Waiting for 1 pods to be ready...
statefulset rolling update complete 3 pods at revision zk-5db4499664...

これはPod群を終了し、逆の順番で1つずつそれらを新しい設定で再作成します。 これはクォーラムがローリングアップデート中に維持されることを保証します。

履歴や過去の設定を見るには、kubectl rollout historyコマンドを使います。

kubectl rollout history sts/zk

出力は次のようになります:

statefulsets "zk"
REVISION
1
2

変更をロールバックするには、kubectl rollout undoコマンドを使います。

kubectl rollout undo sts/zk

出力は次のようになります:

statefulset.apps/zk rolled back

プロセスの失敗の取り扱い

再起動ポリシーは、Pod内のコンテナのエントリポイントへのプロセスの失敗をKubernetesがどのように取り扱うかを制御します。 StatefulSet内のPodにおいて唯一妥当なRestartPolicyはAlwaysで、これはデフォルト値です。 ステートフルなアプリケーションでは、このデフォルトポリシーの上書きは絶対にすべきではありません

zk-0 Pod内で実行されているZooKeeperサーバーのプロセスツリーを調査するには、以下のコマンドを使います。

kubectl exec zk-0 -- ps -ef

コンテナのエントリポイントとして使われるコマンドはPID 1、エントリポイントの子であるZooKeeperプロセスはPID 27となっています。

UID        PID  PPID  C STIME TTY          TIME CMD
zookeep+     1     0  0 15:03 ?        00:00:00 sh -c zkGenConfig.sh && zkServer.sh start-foreground
zookeep+    27     1  0 15:03 ?        00:00:03 /usr/lib/jvm/java-8-openjdk-amd64/bin/java -Dzookeeper.log.dir=/var/log/zookeeper -Dzookeeper.root.logger=INFO,CONSOLE -cp /usr/bin/../build/classes:/usr/bin/../build/lib/*.jar:/usr/bin/../share/zookeeper/zookeeper-3.4.9.jar:/usr/bin/../share/zookeeper/slf4j-log4j12-1.6.1.jar:/usr/bin/../share/zookeeper/slf4j-api-1.6.1.jar:/usr/bin/../share/zookeeper/netty-3.10.5.Final.jar:/usr/bin/../share/zookeeper/log4j-1.2.16.jar:/usr/bin/../share/zookeeper/jline-0.9.94.jar:/usr/bin/../src/java/lib/*.jar:/usr/bin/../etc/zookeeper: -Xmx2G -Xms2G -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.local.only=false org.apache.zookeeper.server.quorum.QuorumPeerMain /usr/bin/../etc/zookeeper/zoo.cfg

別のターミナルで、以下のコマンドを使ってzk StatefulSet内のPodを見てみます。

kubectl get pod -w -l app=zk

別のターミナルで、以下のコマンドを使ってPod zk-0内のZooKeeperプロセスを終了します。

kubectl exec zk-0 -- pkill java

ZooKeeperプロセスの終了は、その親プロセスの終了を引き起こします。 コンテナのRestartPolicyはAlwaysなので、親プロセスが再起動(restart)されます。

NAME      READY     STATUS    RESTARTS   AGE
zk-0      1/1       Running   0          21m
zk-1      1/1       Running   0          20m
zk-2      1/1       Running   0          19m
NAME      READY     STATUS    RESTARTS   AGE
zk-0      0/1       Error     0          29m
zk-0      0/1       Running   1         29m
zk-0      1/1       Running   1         29m

アプリケーションが、そのビジネスロジックを実装するプロセスを立ち上げるのにスクリプト(zkServer.shなど)を使っている場合、スクリプトは子プロセスとともに終了する必要があります。 これは、Kubernetesがアプリケーションのコンテナを、そのビジネスロジックを実装しているプロセスが失敗した時に再起動することを保証します。

生存性(liveness)テスト

失敗したプロセスを再起動するための設定をアプリケーションに施すのは、分散型システムの健全さを保つのに十分ではありません。 システムのプロセスが生きていることもあれば無反応なこともあり、あるいはそうでなく不健全という状況もあります。 アプリケーションのプロセスが不健全で再起動すべきであることをKubernetesに通知するには、liveness probeを使うのがよいでしょう。

zk StatefulSetのPod templateでliveness probeを指定します。

  livenessProbe:
    exec:
      command:
      - sh
      - -c
      - "zookeeper-ready 2181"
    initialDelaySeconds: 15
    timeoutSeconds: 5

プローブはサーバーの健全さをテストするのに、ZooKeeperのruok 4文字コマンドを使うbashスクリプトを呼び出します。

OK=$(echo ruok | nc 127.0.0.1 $1)
if [ "$OK" == "imok" ]; then
    exit 0
else
    exit 1
fi

ターミナルウィンドウで、zk StatefulSet内のPodを見るのに以下のコマンドを使います。

kubectl get pod -w -l app=zk

別のウィンドウで、Pod zk-0のファイルシステムからzookeeper-readyスクリプトを削除するために以下のコマンドを使います。

kubectl exec zk-0 -- rm /opt/zookeeper/bin/zookeeper-ready

ZooKeeperプロセスの失敗のためにliveness probeを使う時、アンサンブル内の不健全なプロセスが再起動されることを保証するために、Kubernetesは自動的にプロセスを再起動します。

kubectl get pod -w -l app=zk
NAME      READY     STATUS    RESTARTS   AGE
zk-0      1/1       Running   0          1h
zk-1      1/1       Running   0          1h
zk-2      1/1       Running   0          1h
NAME      READY     STATUS    RESTARTS   AGE
zk-0      0/1       Running   0          1h
zk-0      0/1       Running   1         1h
zk-0      1/1       Running   1         1h

準備性(readiness)テスト

準備性は生存性と同じではありません。 プロセスが生きているのであれば、スケジュールされ健全です。 プロセスの準備ができたら、入力を処理できます。 生存性はなくてはならないものですが、準備性の状態には十分ではありません。 プロセスは生きてはいるが準備はできていない時、特に初期化および終了の間がそのケースに相当します。

readiness probeを指定するとKubernetesは、準備性チェックに合格するまで、アプリケーションのプロセスがネットワークトラフィックを受け取らないことを保証します。

ZooKeeperサーバーにとって、健全性は準備性を意味します。 そのため、zookeeper.yamlマニフェストからのreadiness probeは、liveness probeと同一です。

  readinessProbe:
    exec:
      command:
      - sh
      - -c
      - "zookeeper-ready 2181"
    initialDelaySeconds: 15
    timeoutSeconds: 5

liveness probeとreadiness probeが同一だとしても、両方を指定することが重要です。 これは、ZooKeeperアンサンブル内の健全なサーバーだけがネットワークトラフィックを受け取ることを保証します。

ノードの失敗の許容

ZooKeeperはデータの変更を正しくコミットするのにサーバーのクォーラムを必要とします。 3つのサーバーのアンサンブルにおいては、書き込みの成功のために2つのサーバーは健全でなければなりません。 クォーラムベースのシステムにおいて、可用性を保証するために、メンバーは障害ドメインにデプロイされます。 個々のマシンの損失による障害を避けるためのベストプラクティスは、同じマシン上でアプリケーションの複数のインスタンスがコロケート(同じ場所に配置)されないようにすることです。

デフォルトでKubernetesは、同じノードのStatefulSetにPodをコロケートします。 3つのサーバーアンサンブルを作成していたとして、2つのサーバーが同じノードにあり、そのノードが障害を起こした場合、ZooKeeperサービスのクライアントは、少なくともPodの1つが再スケジュールされるまで障害に見舞われることになります。

クリティカルシステムのプロセスがノードの失敗イベントで再スケジュールできるよう、追加のキャパシティを常にプロビジョンしておくべきです。 そうしておけば、障害は単にKubernetesのスケジューラーがZooKeeperのサーバーの1つを再スケジュールするまでの辛抱です。 ただし、ダウンタイムなしでノードの障害への耐性をサービスに持たせたいなら、podAntiAffinityをセットすべきです。

zk StatefulSet内のPodのノードを取得するには、以下のコマンドを使います。

for i in 0 1 2; do kubectl get pod zk-$i --template {{.spec.nodeName}}; echo ""; done

zk StatefulSet内の全てのPodは、別々のノードにデプロイされます。

kubernetes-node-cxpk
kubernetes-node-a5aq
kubernetes-node-2g2d

これはzk StatefulSet内のPodにPodAntiAffinityの指定があるからです。

affinity:
  podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
            - key: "app"
              operator: In
              values:
                - zk
        topologyKey: "kubernetes.io/hostname"

requiredDuringSchedulingIgnoredDuringExecutionフィールドは、topologyKeyで定義されたドメイン内でappラベルの値がzkの2つのPodが絶対にコロケートすべきでないことを、Kubernetes Schedulerに指示します。 topologyKeykubernetes.io/hostnameは、ドメインが固有ノードであることを示しています。 異なるルール、ラベル、セレクターを使って、物理・ネットワーク・電源といった障害ドメイン全体に広がるアンサンブルにこのテクニックを広げることができます。

メンテナンス時の存続

このセクションでは、ノードをcordon(スケジュール不可化)およびdorain(解放)します。もし共有クラスターでこのチュートリアルを試しているのであれば、これがほかのテナントに有害な影響を及ぼさないことを確認してください。

前のセクションでは、計画外のノード障害に備えてどのようにPodをノード全体に広げるかを示しましたが、計画されたメンテナンスのため引き起こされる一時的なノード障害に対して計画する必要もあります。

クラスター内のノードを取得するために、以下のコマンドを使います。

kubectl get nodes

このチュートリアルでは、4つのノードのあるクラスターを仮定しています。 クラスターが4つよりも多くある場合には、4つのノード以外全てをcordonするためにkubectl cordonを使ってください。 ノードを4つに制約することで、以下のメンテナンスシミュレーションにおいてzookeeper Podをスケジュールした時に、KubernetesがアフィニティとPodDisruptionBudget制約に遭遇することを保証します。

kubectl cordon <ノード名>

zk-pdbPodDisruptionBudgetを取得するために、以下のコマンドを使います。

kubectl get pdb zk-pdb

max-unavailableフィールドは、zk StatefulSetの最大で1つのPodがいつでも利用できなくなる可能性があるということを、Kubernetesに指示します。

NAME      MIN-AVAILABLE   MAX-UNAVAILABLE   ALLOWED-DISRUPTIONS   AGE
zk-pdb    N/A             1                 1

1つ目のターミナルで、zk StatefulSet内のPodを見るのに以下のコマンドを使います。

kubectl get pods -w -l app=zk

次に別のターミナルで、Podが現在スケジュールされているノードを取得するために、以下のコマンドを使います。

for i in 0 1 2; do kubectl get pod zk-$i --template {{.spec.nodeName}}; echo ""; done

出力は次のようになります:

kubernetes-node-pb41
kubernetes-node-ixsl
kubernetes-node-i4c4

zk-0 Podがスケジュールされているノードをcordonおよびdrainするには、kubectl drainを使います。

kubectl drain $(kubectl get pod zk-0 --template {{.spec.nodeName}}) --ignore-daemonsets --force --delete-emptydir-data

出力は次のようになります:

node "kubernetes-node-pb41" cordoned

WARNING: Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-node-pb41, kube-proxy-kubernetes-node-pb41; Ignoring DaemonSet-managed pods: node-problem-detector-v0.1-o5elz
pod "zk-0" deleted
node "kubernetes-node-pb41" drained

クラスターに4つのノードがあるので、kubectl drainは成功し、zk-0が別のノードに再スケジュールされます。

NAME      READY     STATUS    RESTARTS   AGE
zk-0      1/1       Running   2          1h
zk-1      1/1       Running   0          1h
zk-2      1/1       Running   0          1h
NAME      READY     STATUS        RESTARTS   AGE
zk-0      1/1       Terminating   2          2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Pending   0         0s
zk-0      0/1       Pending   0         0s
zk-0      0/1       ContainerCreating   0         0s
zk-0      0/1       Running   0         51s
zk-0      1/1       Running   0         1m

最初のターミナルでStatefulSetのPodを見守り、zk-1がスケジュールされたノードをdrainします。

kubectl drain $(kubectl get pod zk-1 --template {{.spec.nodeName}}) --ignore-daemonsets --force --delete-emptydir-data

出力は次のようになります:

"kubernetes-node-ixsl" cordoned
WARNING: Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-node-ixsl, kube-proxy-kubernetes-node-ixsl; Ignoring DaemonSet-managed pods: node-problem-detector-v0.1-voc74
pod "zk-1" deleted
node "kubernetes-node-ixsl" drained

zk StatefulSetがPodのコロケーションを抑止するPodAntiAffinityルールを含んでいるので、zk-1 Podはスケジュールされず、またスケジュール可能なのは2つのノードだけなので、PodはPendingの状態のままになっています。

kubectl get pods -w -l app=zk

出力は次のようになります:

NAME      READY     STATUS    RESTARTS   AGE
zk-0      1/1       Running   2          1h
zk-1      1/1       Running   0          1h
zk-2      1/1       Running   0          1h
NAME      READY     STATUS        RESTARTS   AGE
zk-0      1/1       Terminating   2          2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Pending   0         0s
zk-0      0/1       Pending   0         0s
zk-0      0/1       ContainerCreating   0         0s
zk-0      0/1       Running   0         51s
zk-0      1/1       Running   0         1m
zk-1      1/1       Terminating   0         2h
zk-1      0/1       Terminating   0         2h
zk-1      0/1       Terminating   0         2h
zk-1      0/1       Terminating   0         2h
zk-1      0/1       Pending   0         0s
zk-1      0/1       Pending   0         0s

StatefulSetのPodを見続け、zk-2がスケジュールされているノードをdrainします。

kubectl drain $(kubectl get pod zk-2 --template {{.spec.nodeName}}) --ignore-daemonsets --force --delete-emptydir-data

出力は次のようになります:

node "kubernetes-node-i4c4" cordoned

WARNING: Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-node-i4c4, kube-proxy-kubernetes-node-i4c4; Ignoring DaemonSet-managed pods: node-problem-detector-v0.1-dyrog
WARNING: Ignoring DaemonSet-managed pods: node-problem-detector-v0.1-dyrog; Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-node-i4c4, kube-proxy-kubernetes-node-i4c4
There are pending pods when an error occurred: Cannot evict pod as it would violate the pod's disruption budget.
pod/zk-2

kubectlを終了するためにCTRL-Cを押します。

zk-2を退去させるとzk-budget違反になってしまうので、3つ目のノードはdrainできません。ただし、ノードはcordonされたままとなります。

健全性テスト中に入力した値をzk-0から取得するには、zkCli.shを使います。

kubectl exec zk-0 zkCli.sh get /hello

PodDisruptionBudgetが遵守されているので、サービスはまだ利用可能です。

WatchedEvent state:SyncConnected type:None path:null
world
cZxid = 0x200000002
ctime = Wed Dec 07 00:08:59 UTC 2016
mZxid = 0x200000002
mtime = Wed Dec 07 00:08:59 UTC 2016
pZxid = 0x200000002
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0

最初のノードをuncordon(スケジュール可能化)するには、kubectl uncordonを使います。

kubectl uncordon kubernetes-node-pb41

出力は次のようになります:

node "kubernetes-node-pb41" uncordoned

zk-1はこのノードで再スケジュールされます。zk-1がRunningおよびReadyになるまで待ちます。

kubectl get pods -w -l app=zk

出力は次のようになります:

NAME      READY     STATUS    RESTARTS   AGE
zk-0      1/1       Running   2          1h
zk-1      1/1       Running   0          1h
zk-2      1/1       Running   0          1h
NAME      READY     STATUS        RESTARTS   AGE
zk-0      1/1       Terminating   2          2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Pending   0         0s
zk-0      0/1       Pending   0         0s
zk-0      0/1       ContainerCreating   0         0s
zk-0      0/1       Running   0         51s
zk-0      1/1       Running   0         1m
zk-1      1/1       Terminating   0         2h
zk-1      0/1       Terminating   0         2h
zk-1      0/1       Terminating   0         2h
zk-1      0/1       Terminating   0         2h
zk-1      0/1       Pending   0         0s
zk-1      0/1       Pending   0         0s
zk-1      0/1       Pending   0         12m
zk-1      0/1       ContainerCreating   0         12m
zk-1      0/1       Running   0         13m
zk-1      1/1       Running   0         13m

試しにzk-2がスケジュールされているノードをdrainしてみます。

kubectl drain $(kubectl get pod zk-2 --template {{.spec.nodeName}}) --ignore-daemonsets --force --delete-emptydir-data

出力は次のようになります:

node "kubernetes-node-i4c4" already cordoned
WARNING: Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-node-i4c4, kube-proxy-kubernetes-node-i4c4; Ignoring DaemonSet-managed pods: node-problem-detector-v0.1-dyrog
pod "heapster-v1.2.0-2604621511-wht1r" deleted
pod "zk-2" deleted
node "kubernetes-node-i4c4" drained

今度はkubectl drainは成功しました。

zk-2の再スケジュールができるように、2つ目のノードをuncordonします。

kubectl uncordon kubernetes-node-ixsl

出力は次のようになります:

node "kubernetes-node-ixsl" uncordoned

サービスがメンテナンス中も利用可能なままであることを保証するために、PodDisruptionBudgetsとあわせてkubectl drainを利用できます。 メンテナンスでノードがオフラインになる前にノードをcordonして、Podを退去させるのにdrainが使われている場合、Disruption Budget(停止状態の予算)を表すサービスは遵守すべきバジェットを持ちます。 クリティカルサービスでは、Podをすぐに再スケジュールできるよう、追加のキャパティを常に割り当てておくべきです。

クリーンアップ

  • クラスターの全てのノードをuncordonするために、kubectl uncordonを実行してください。
  • このチュートリアルで使ったPersistentVolumeの永続的なストレージメディアを削除する必要があります。 全てのストレージが回収されたことを確実とするために、お使いの環境、ストレージ設定、プロビジョニング方法に基いて必要な手順に従ってください。

7 - クラスター

7.1 - AppArmorを使用してコンテナのリソースへのアクセスを制限する

FEATURE STATE: Kubernetes v1.4 [beta]

AppArmorは、Linux標準のユーザー・グループをベースとしたパーミッションを補完するLinuxカーネルのセキュリティモジュールであり、プログラムのアクセスを限定されたリソースセットに制限するために利用されます。AppArmorを設定することで、任意のアプリケーションの攻撃サーフェイスとなりうる面を減らしたり、より優れた多重の防御を提供できます。AppArmorは、たとえばLinuxのcapability、ネットワークアクセス、ファイルのパーミッションなど、特定のプログラムやコンテナに必要なアクセスを許可するようにチューニングされたプロファイルにより設定を行います。各プロファイルは、許可されなかったリソースへのアクセスをブロックするenforcingモードと、ルール違反を報告するだけのcomplainモードのいずれかで実行できます。

AppArmorを利用すれば、コンテナに許可することを制限したりシステムログを通してよりよい監査を提供することで、デプロイをよりセキュアにする助けになります。しかし、AppArmorは銀の弾丸ではなく、アプリケーションコードの悪用からの防御を強化できるだけであることを心に留めておくことが重要です。制限の強い優れたプロファイルを提供し、アプリケーションとクラスターを別の角度から強化することが重要です。

目標

  • プロファイルをノードに読み込む方法の例を見る
  • Pod上でプロファイルを矯正する方法を学ぶ
  • プロファイルが読み込まれたかを確認する方法を学ぶ
  • プロファイルに違反した場合に何が起こるのかを見る
  • プロファイルが読み込めなかった場合に何が起こるのかを見る

始める前に

以下のことを確認してください。

  1. Kubernetesのバージョンがv1.4以上であること。KubernetesのAppArmorのサポートはv1.4で追加されました。v1.4より古いバージョンのKubernetesのコンポーネントは、新しいAppArmorのアノテーションを認識できないため、AppArmorの設定を与えたとしても黙って無視されてしまいます。Podが期待した保護を確実に受けられるようにするためには、次のようにノードのKubeletのバージョンを確認することが重要です。

    kubectl get nodes -o=jsonpath=$'{range .items[*]}{@.metadata.name}: {@.status.nodeInfo.kubeletVersion}\n{end}'
    
    gke-test-default-pool-239f5d02-gyn2: v1.4.0
    gke-test-default-pool-239f5d02-x1kf: v1.4.0
    gke-test-default-pool-239f5d02-xwux: v1.4.0
    
  2. AppArmorカーネルモジュールが有効であること。LinuxカーネルがAppArmorプロファイルを強制するためには、AppArmorカーネルモジュールのインストールと有効化が必須です。UbuntuやSUSEなどのディストリビューションではデフォルトで有効化されますが、他の多くのディストリビューションでのサポートはオプションです。モジュールが有効になっているかチェックするには、次のように/sys/module/apparmor/parameters/enabledファイルを確認します。

    cat /sys/module/apparmor/parameters/enabled
    Y
    

    KubeletがAppArmorをサポートしていれば(>= v1.4)、カーネルモジュールが有効になっていない場合にはAppArmorオプションが付いたPodを拒否します。

  1. コンテナランタイムがAppArmorをサポートしていること。現在、Kubernetesがサポートするすべての一般的なコンテナランタイム、DockerCRI-Ocontainerdなどは、AppArmorをサポートしています。関連するランタイムのドキュメントを参照して、クラスターがAppArmorを利用するための要求を満たしているかどうかを検証してください。

  2. プロファイルが読み込まれていること。AppArmorがPodに適用されるのは、各コンテナが実行されるべきAppArmorプロファイルを指定したときです。もし指定されたプロファイルがまだカーネルに読み込まれていなければ、Kubelet(>= v1.4)はPodを拒否します。どのプロファイルがノードに読み込まれているのかを確かめるには、次のようなコマンドを実行して/sys/kernel/security/apparmor/profilesをチェックします。

    ssh gke-test-default-pool-239f5d02-gyn2 "sudo cat /sys/kernel/security/apparmor/profiles | sort"
    
    apparmor-test-deny-write (enforce)
    apparmor-test-audit-write (enforce)
    docker-default (enforce)
    k8s-nginx (enforce)
    

    ノード上でのプロファイルの読み込みの詳細については、プロファイルを使用したノードのセットアップを参照してください。

KubeletのバージョンがAppArmorサポートに対応しているもの(>= v1.4)である限り、Kubeletは必要条件を1つでも満たさないAppArmorオプションが付けられたPodをリジェクトします。また、ノード上のAppArmorのサポートは、次のようにready conditionのメッセージで確認することもできます(ただし、この機能は将来のリリースで削除される可能性があります)。

kubectl get nodes -o=jsonpath='{range .items[*]}{@.metadata.name}: {.status.conditions[?(@.reason=="KubeletReady")].message}{"\n"}{end}'
gke-test-default-pool-239f5d02-gyn2: kubelet is posting ready status. AppArmor enabled
gke-test-default-pool-239f5d02-x1kf: kubelet is posting ready status. AppArmor enabled
gke-test-default-pool-239f5d02-xwux: kubelet is posting ready status. AppArmor enabled

Podをセキュアにする

AppArmorのプロファイルは各コンテナごとに指定します。Podのコンテナで実行するAppArmorのプロファイルを指定するには、Podのメタデータに次のようなアノテーションを追加します。

container.apparmor.security.beta.kubernetes.io/<container_name>: <profile_ref>

ここで、<container_name>はプロファイルを適用するコンテナの名前であり、<profile_ref>には適用するプロファイルを指定します。profile_refは次の値のうち1つを指定します。

  • runtime/default: ランタイムのデフォルトのプロファイルを適用する
  • localhost/<profile_name>: <profile_name>という名前でホストにロードされたプロファイルを適用する
  • unconfined: いかなるプロファイルもロードされないことを示す

アノテーションとプロファイルの名前のフォーマットの詳細については、APIリファレンスを参照してください。

KubernetesのAppArmorの強制では、まずはじめにすべての前提条件が満たされているかどうかをチェックします。その後、強制を行うためにプロファイルの選択をコンテナランタイムに委ねます。前提条件が満たされなかった場合、Podはリジェクトされ、実行されません。

プロファイルが適用されたかどうか確認するには、AppArmor securityオプションがコンテナ作成イベントに一覧されているかどうかを確認します。

kubectl get events | grep Created
22s        22s         1         hello-apparmor     Pod       spec.containers{hello}   Normal    Created     {kubelet e2e-test-stclair-node-pool-31nt}   Created container with docker id 269a53b202d3; Security:[seccomp=unconfined apparmor=k8s-apparmor-example-deny-write]

proc attrを調べることで、コンテナのルートプロセスが正しいプロファイルで実行されているかどうかを直接確認することもできます。

kubectl exec <pod_name> -- cat /proc/1/attr/current
k8s-apparmor-example-deny-write (enforce)

この例は、クラスターがすでにAppArmorのサポート付きでセットアップ済みであることを前提としています。

まず、使用したいプロファイルをノード上に読み込む必要があります。このプロファイルは、すべてのファイル書き込みを拒否します。

#include <tunables/global>

profile k8s-apparmor-example-deny-write flags=(attach_disconnected) {
  #include <abstractions/base>

  file,

  # Deny all file writes.
  deny /** w,
}

Podがどのノードにスケジュールされるかは予測できないため、プロファイルはすべてのノードに読み込ませる必要があります。この例では、単純にSSHを使ってプロファイルをインストールしますが、プロファイルを使用したノードのセットアップでは、他のアプローチについて議論しています。

NODES=(
    # SSHでアクセス可能なノードのドメイン名
    gke-test-default-pool-239f5d02-gyn2.us-central1-a.my-k8s
    gke-test-default-pool-239f5d02-x1kf.us-central1-a.my-k8s
    gke-test-default-pool-239f5d02-xwux.us-central1-a.my-k8s)
for NODE in ${NODES[*]}; do ssh $NODE 'sudo apparmor_parser -q <<EOF
#include <tunables/global>

profile k8s-apparmor-example-deny-write flags=(attach_disconnected) {
  #include <abstractions/base>

  file,

  # Deny all file writes.
  deny /** w,
}
EOF'
done

次に、deny-writeプロファイルを使用した単純な "Hello AppArmor" Podを実行します。

apiVersion: v1
kind: Pod
metadata:
  name: hello-apparmor
  annotations:
    # Tell Kubernetes to apply the AppArmor profile "k8s-apparmor-example-deny-write".
    # Note that this is ignored if the Kubernetes node is not running version 1.4 or greater.
    container.apparmor.security.beta.kubernetes.io/hello: localhost/k8s-apparmor-example-deny-write
spec:
  containers:
  - name: hello
    image: busybox
    command: [ "sh", "-c", "echo 'Hello AppArmor!' && sleep 1h" ]
kubectl create -f ./hello-apparmor.yaml

Podイベントを確認すると、PodコンテナがAppArmorプロファイル "k8s-apparmor-example-deny-write" を使用して作成されたことがわかります。

kubectl get events | grep hello-apparmor
14s        14s         1         hello-apparmor   Pod                                Normal    Scheduled   {default-scheduler }                           Successfully assigned hello-apparmor to gke-test-default-pool-239f5d02-gyn2
14s        14s         1         hello-apparmor   Pod       spec.containers{hello}   Normal    Pulling     {kubelet gke-test-default-pool-239f5d02-gyn2}   pulling image "busybox"
13s        13s         1         hello-apparmor   Pod       spec.containers{hello}   Normal    Pulled      {kubelet gke-test-default-pool-239f5d02-gyn2}   Successfully pulled image "busybox"
13s        13s         1         hello-apparmor   Pod       spec.containers{hello}   Normal    Created     {kubelet gke-test-default-pool-239f5d02-gyn2}   Created container with docker id 06b6cd1c0989; Security:[seccomp=unconfined apparmor=k8s-apparmor-example-deny-write]
13s        13s         1         hello-apparmor   Pod       spec.containers{hello}   Normal    Started     {kubelet gke-test-default-pool-239f5d02-gyn2}   Started container with docker id 06b6cd1c0989

コンテナがこのプロファイルで実際に実行されていることを確認するために、コンテナのproc attrをチェックします。

kubectl exec hello-apparmor -- cat /proc/1/attr/current
k8s-apparmor-example-deny-write (enforce)

最後に、ファイルへの書き込みを行おうとすることで、プロファイルに違反すると何が起こるか見てみましょう。

kubectl exec hello-apparmor touch -- /tmp/test
touch: /tmp/test: Permission denied
error: error executing remote command: command terminated with non-zero exit code: Error executing in Docker Container: 1

まとめとして、読み込まれていないプロファイルを指定しようとするとどうなるのか見てみましょう。

kubectl create -f /dev/stdin <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: hello-apparmor-2
  annotations:
    container.apparmor.security.beta.kubernetes.io/hello: localhost/k8s-apparmor-example-allow-write
spec:
  containers:
  - name: hello
    image: busybox
    command: [ "sh", "-c", "echo 'Hello AppArmor!' && sleep 1h" ]
EOF
pod/hello-apparmor-2 created
kubectl describe pod hello-apparmor-2
Name:          hello-apparmor-2
Namespace:     default
Node:          gke-test-default-pool-239f5d02-x1kf/
Start Time:    Tue, 30 Aug 2016 17:58:56 -0700
Labels:        <none>
Annotations:   container.apparmor.security.beta.kubernetes.io/hello=localhost/k8s-apparmor-example-allow-write
Status:        Pending
Reason:        AppArmor
Message:       Pod Cannot enforce AppArmor: profile "k8s-apparmor-example-allow-write" is not loaded
IP:
Controllers:   <none>
Containers:
  hello:
    Container ID:
    Image:     busybox
    Image ID:
    Port:
    Command:
      sh
      -c
      echo 'Hello AppArmor!' && sleep 1h
    State:              Waiting
      Reason:           Blocked
    Ready:              False
    Restart Count:      0
    Environment:        <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-dnz7v (ro)
Conditions:
  Type          Status
  Initialized   True
  Ready         False
  PodScheduled  True
Volumes:
  default-token-dnz7v:
    Type:    Secret (a volume populated by a Secret)
    SecretName:    default-token-dnz7v
    Optional:   false
QoS Class:      BestEffort
Node-Selectors: <none>
Tolerations:    <none>
Events:
  FirstSeen    LastSeen    Count    From                        SubobjectPath    Type        Reason        Message
  ---------    --------    -----    ----                        -------------    --------    ------        -------
  23s          23s         1        {default-scheduler }                         Normal      Scheduled     Successfully assigned hello-apparmor-2 to e2e-test-stclair-node-pool-t1f5
  23s          23s         1        {kubelet e2e-test-stclair-node-pool-t1f5}             Warning        AppArmor    Cannot enforce AppArmor: profile "k8s-apparmor-example-allow-write" is not loaded

PodのステータスはPendingとなり、Pod Cannot enforce AppArmor: profile "k8s-apparmor-example-allow-write" is not loaded(PodはAppArmorを強制できません: プロファイル "k8s-apparmor-example-allow-write" はロードされていません)という役に立つエラーメッセージが表示されています。同じメッセージのイベントも記録されています。

管理

プロファイルを使用したノードのセットアップ

現在、KubernetesはAppArmorのプロファイルをノードに読み込むネイティブの仕組みは提供していません。しかし、プロファイルをセットアップする方法は、以下のように様々な方法があります。

  • 各ノード上に正しいプロファイルがロードされていることを保証するPodを実行するDaemonSetを利用する方法。ここに実装例があります。
  • ノードの初期化時に初期化スクリプト(例: Salt、Ansibleなど)や初期化イメージを使用する。
  • で示したような方法で、プロファイルを各ノードにコピーし、SSHで読み込む。

スケジューラーはどのプロファイルがどのノードに読み込まれているのかがわからないため、すべてのプロファイルがすべてのノードに読み込まれていなければなりません。もう1つのアプローチとしては、各プロファイル(あるいはプロファイルのクラス)ごとにノードラベルを追加し、node selectorを用いてPodが必要なプロファイルを読み込んだノードで実行されるようにする方法もあります。

PodSecurityPolicyを使用したプロファイルの制限

PodSecurityPolicy extensionが有効になっている場合、クラスター全体でAppArmorn制限が適用されます。PodSecurityPolicyを有効にするには、apiserver上で次のフラグを設定する必要があります。

--enable-admission-plugins=PodSecurityPolicy[,others...]

AppArmorのオプションはPodSecurityPolicy上でアノテーションとして指定します。

apparmor.security.beta.kubernetes.io/defaultProfileName: <profile_ref>
apparmor.security.beta.kubernetes.io/allowedProfileNames: <profile_ref>[,others...]

defaultProfileNameオプションには、何も指定されなかった場合にコンテナにデフォルトで適用されるプロファイルを指定します。allowedProfileNamesオプションには、Podコンテナの実行が許可されるプロファイルのリストを指定します。両方のオプションが指定された場合、デフォルトは許可されなければいけません。プロファイルはコンテナ上で同じフォーマットで指定されます。完全な仕様については、APIリファレンスを参照してください。

AppArmorの無効化

クラスター上でAppArmorを利用可能にしたくない場合、次のコマンドラインフラグで無効化できます。

--feature-gates=AppArmor=false

無効化すると、AppArmorプロファイルを含むPodは"Forbidden"エラーで検証に失敗します。ただし、デフォルトのdockerは非特権Pod上では"docker-default"というプロファイルを常に有効化し(AppArmorカーネルモジュールが有効である場合)、フィーチャーゲートで無効化したとしても有効化し続けることに注意してください。AppArmorを無効化するオプションは、AppArmorが一般利用(GA)になったときに削除される予定です。

AppArmorを使用するKubernetes v1.4にアップグレードする

クラスターをv1.4にアップグレードするために、AppArmorに関する操作は必要ありません。ただし、既存のPodがAppArmorのアノテーションを持っている場合、検証(またはPodSecurityPolicy admission)は行われません。もしpermissiveなプロファイルがノードに読み込まれていた場合、悪意のあるユーザーがPodの権限を上述のdocker-defaultより昇格させるために、permissiveなプロファイルを再適用する恐れがあります。これが問題となる場合、apparmor.security.beta.kubernetes.ioのアノテーションを含むすべてのPodのクラスターをクリーンアップすることを推奨します。

一般利用可能(General Availability)への更新パス

AppArmorが一般利用可能(GA)になったとき、現在アノテーションで指定しているオプションはフィールドに変換されます。移行中のすべてのアップグレードとダウングレードの経路をサポートするのは非常に微妙であるため、以降が必要になったときに詳細に説明する予定です。最低2リリースの間はフィールドとアノテーションの両方がサポートされるようにする予定です。最低2リリースの後は、アノテーションは明示的に拒否されるようになります。

Profilesの作成

AppArmorのプロファイルを正しく指定するのはやっかいな作業です。幸い、その作業を補助するツールがいくつかあります。

  • aa-genprofおよびaa-logprofは、アプリケーションの動作とログを監視することによりプロファイルのルールを生成します。詳しい説明については、AppArmor documentationを参照してください。
  • baneは、Docker向けのAppArmorのプロファイル・ジェネレータです。簡略化されたプロファイル言語を使用しています。

プロファイルの生成には、アプリケーションを開発用ワークステーション上でDockerで実行することを推奨します。しかし、実際にPodが実行されるKubernetesノード上でツールを実行してはいけない理由はありません。

AppArmorに関する問題をデバッグするには、システムログをチェックして、特に何が拒否されたのかを確認できます。AppArmorのログはdmesgにverboseメッセージを送り、エラーは通常システムログまたはjournalctlで確認できます。詳しい情報は、AppArmor failuresで提供されています。

APIリファレンス

Podアノテーション

コンテナが実行するプロファイルを指定します。

  • key: container.apparmor.security.beta.kubernetes.io/<container_name> ここで、<container_name>はPod内のコンテナの名前を一致させます。Pod内の各コンテナごとに別々のプロファイルを指定できます。
  • value: 下で説明するプロファイルのリファレンス

プロファイルのリファレンス

  • runtime/default: デフォルトのランタイムプロファイルを指します。
    • (PodSecurityPolicyのデフォルトを設定せずに)プロファイルを指定しない場合と同等ですが、AppArmorを有効化する必要があります。
    • Dockerの場合、非特権コンテナではdocker-defaultプロファイルが選択され、特権コンテナではunconfined(プロファイルなし)が選択されます。
  • localhost/<profile_name>: 名前で指定されたノード(localhost)に読み込まれたプロファイルを指します。
  • unconfined: これは実質的にコンテナ上のAppArmorを無効化します。

これ以外のプロファイルリファレンスはすべて無効です。

PodSecurityPolicyアノテーション

何も指定されなかった場合にコンテナに適用するデフォルトのプロファイルは、以下のように指定します。

  • key: apparmor.security.beta.kubernetes.io/defaultProfileName
  • value: 上で説明したプロファイルのリファレンス

Podコンテナが指定することを許可するプロファイルのリストは、以下のように指定します。

  • key: apparmor.security.beta.kubernetes.io/allowedProfileNames
  • value: カンマ区切りの上述のプロファイルリファレンスのリスト
    • プロファイル名ではエスケープしたカンマは不正な文字ではありませんが、ここでは明示的に許可されません。

次の項目

追加のリソースとしては以下のものがあります。

8 - Service

8.1 - アプリケーションをServiceに接続する

コンテナに接続するためのKubernetesモデル

さて、継続的に実行され、複製されたアプリケーションができたので、これをネットワーク上に公開できます。

Kubernetesは、Podがどのホストに配置されているかにかかわらず、ほかのPodと通信できることを引き受けます。 Kubernetesは各Podにそれぞれ固有のクラスタープライベートなIPアドレスを付与するので、Pod間のリンクや、コンテナのポートとホストのポートのマップを明示的に作成する必要はありません。 これは、Pod内のコンテナは全てlocalhost上でお互いのポートに到達でき、クラスター内の全てのPodはNATなしに互いを見られるということを意味します。このドキュメントの残りの部分では、このようなネットワークモデルの上で信頼性のあるServiceを実行する方法について、詳しく述べていきます。

このチュートリアルでは概念のデモンストレーションのために、シンプルなnginx Webサーバーを例として使います。

Podをクラスターへ公開

これは前出の例でも行いましたが、もう一度やってみて、ネットワークからの観点に着目してみましょう。 nginx Podを作成し、コンテナのポート指定も記載します:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  selector:
    matchLabels:
      run: my-nginx
  replicas: 2
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
      - name: my-nginx
        image: nginx
        ports:
        - containerPort: 80

この設定で、あなたのクラスターにはどのノードからもアクセス可能になります。Podを実行中のノードを確認してみましょう:

kubectl apply -f ./run-my-nginx.yaml
kubectl get pods -l run=my-nginx -o wide
NAME                        READY     STATUS    RESTARTS   AGE       IP            NODE
my-nginx-3800858182-jr4a2   1/1       Running   0          13s       10.244.3.4    kubernetes-minion-905m
my-nginx-3800858182-kna2y   1/1       Running   0          13s       10.244.2.5    kubernetes-minion-ljyd

PodのIPアドレスを確認します:

kubectl get pods -l run=my-nginx -o custom-columns=POD_IP:.status.podIPs
    POD_IP
    [map[ip:10.244.3.4]]
    [map[ip:10.244.2.5]]

あなたのクラスター内のどのノードにもsshで入ることができて、双方のIPアドレスに対して問い合わせるためにcurlのようなツールを使えるようにしておくのがよいでしょう。 各コンテナはノード上でポート80を使っておらず、トラフィックをPodに流すための特別なNATルールもなんら存在しないことに注意してください。 つまり、全て同じcontainerPortを使った同じノード上で複数のnginx Podを実行でき、Serviceに割り当てられたIPアドレスを使って、クラスター内のほかのどのPodあるいはノードからもそれらにアクセスできます。 背後にあるPodにフォワードするためにホストNode上の特定のポートを充てたいというのであれば、それも可能です。とはいえ、ネットワークモデルではそのようなことをする必要がありません。

興味があれば、さらなる詳細について Kubernetesネットワークモデル を読んでください。

Serviceの作成

さて、フラットなクラスター全体のアドレス空間内でnginxを実行中のPodが得られました。 理論的にはこれらのPodと直接対話することは可能ですが、ノードが死んでしまった時には何が起きるでしょうか? ノードと一緒にPodは死に、Deploymentが新しいPodを異なるIPアドレスで作成します。 これがServiceが解決する問題です。

KubernetesのServiceは、全て同じ機能を提供する、クラスター内のどこかで実行するPodの論理的な集合を定義した抽象物です。 作成時に各Serviceは固有のIPアドレス(clusterIPとも呼ばれます)を割り当てられます。 このアドレスはServiceのライフスパンと結び付けられており、Serviceが生きている間は変わりません。 PodはServiceと対話できるよう設定され、Serviceのメンバーである複数のPodへ自動的に負荷分散されたServiceへ通信する方法を知っています。

kubectl exposeを使って、2つのnginxレプリカのためのServiceを作成できます:

kubectl expose deployment/my-nginx
service/my-nginx exposed

これはkubectl apply -fを以下のyamlに対して実行するのと同じです:

apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  labels:
    run: my-nginx
spec:
  ports:
  - port: 80
    protocol: TCP
  selector:
    run: my-nginx

この指定は、run: my-nginxラベルの付いた任意のPod上のTCPポート80を宛先とし、それを抽象化されたServiceポート(targetPortはコンテナがトラフィックを許可するポート、portは抽象化されたServiceポートで、ほかのPodがServiceにアクセスするのに使う任意のポートです)で公開するServiceを作成します。 Service定義内でサポートされているフィールドのリストを見るには、Service APIオブジェクトを参照してください。 Serviceを確認してみましょう:

kubectl get svc my-nginx
NAME       TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
my-nginx   ClusterIP   10.0.162.149   <none>        80/TCP    21s

前述したとおり、ServiceはPodのグループに支えられています。 これらのPodはEndpointSlicesを通して公開されています。 Serviceのセレクターは継続的に評価され、その結果はServiceに接続されているEndpointSliceにlabelsを使って「投稿(POST)」されます。

Podが死ぬと、エンドポイントとして含まれていたEndpointSliceからそのPodは自動的に削除されます。 Serviceのセレクターにマッチする新しいPodが、Serviceのために自動的にEndpointSliceに追加されます。 エンドポイントを確認し、IPアドレスが最初のステップで作成したPodと同じであることに注目してください:

kubectl describe svc my-nginx
Name:                my-nginx
Namespace:           default
Labels:              run=my-nginx
Annotations:         <none>
Selector:            run=my-nginx
Type:                ClusterIP
IP Family Policy:    SingleStack
IP Families:         IPv4
IP:                  10.0.162.149
IPs:                 10.0.162.149
Port:                <unset> 80/TCP
TargetPort:          80/TCP
Endpoints:           10.244.2.5:80,10.244.3.4:80
Session Affinity:    None
Events:              <none>
kubectl get endpointslices -l kubernetes.io/service-name=my-nginx
NAME             ADDRESSTYPE   PORTS   ENDPOINTS               AGE
my-nginx-7vzhx   IPv4          80      10.244.2.5,10.244.3.4   21s

今や、あなたのクラスター内のどのノードからもnginx Serviceに<CLUSTER-IP>:<PORT>でcurlを使用してアクセスできるはずです。Service IPは完全に仮想であり、物理的なケーブルで接続されるものではありません。どのように動作しているのか興味があれば、さらなる詳細についてサービスプロキシーを読んでください。

Serviceへのアクセス

KubernetesはServiceを探す2つの主要なモードとして、環境変数とDNSをサポートしています。 前者はすぐに動かせるのに対し、後者はCoreDNSクラスターアドオンが必要です。

環境変数

PodをNode上で実行する時、kubeletはアクティブなServiceのそれぞれに環境変数のセットを追加します。 これは順序問題を生みます。なぜそうなるかの理由を見るために、実行中のnginx Podの環境を調査してみましょう(Podの名前は環境によって異なります):

kubectl exec my-nginx-3800858182-jr4a2 -- printenv | grep SERVICE
KUBERNETES_SERVICE_HOST=10.0.0.1
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_PORT_HTTPS=443

Serviceについて何も言及がないことに注意してください。 これは、Serviceの前にレプリカを作成したからです。 このようにすることでの不利益のもう1つは、スケジューラーが同一のマシンに両方のPodを置く可能性があることです(もしそのマシンが死ぬと全Serviceがダウンしてしまいます)。 2つのPodを殺し、Deploymentがそれらを再作成するのを待つことで、これを正しいやり方にできます。 今回は、レプリカの前にServiceが存在します。 これにより、正しい環境変数だけでなく、(全てのノードで等量のキャパシティを持つ場合)Podに広がった、スケジューラーレベルのServiceが得られます:

kubectl scale deployment my-nginx --replicas=0; kubectl scale deployment my-nginx --replicas=2;

kubectl get pods -l run=my-nginx -o wide
NAME                        READY     STATUS    RESTARTS   AGE     IP            NODE
my-nginx-3800858182-e9ihh   1/1       Running   0          5s      10.244.2.7    kubernetes-minion-ljyd
my-nginx-3800858182-j4rm4   1/1       Running   0          5s      10.244.3.8    kubernetes-minion-905m

Podが、いったん殺されて再作成された後、異なる名前を持ったことに気付いたでしょうか。

kubectl exec my-nginx-3800858182-e9ihh -- printenv | grep SERVICE
KUBERNETES_SERVICE_PORT=443
MY_NGINX_SERVICE_HOST=10.0.162.149
KUBERNETES_SERVICE_HOST=10.0.0.1
MY_NGINX_SERVICE_PORT=80
KUBERNETES_SERVICE_PORT_HTTPS=443

DNS

Kubernetesは、DNS名をほかのServiceに自動的に割り当てる、DNSクラスターアドオンServiceを提供しています。 クラスター上でそれを実行しているならば、確認できます:

kubectl get services kube-dns --namespace=kube-system
NAME       TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)         AGE
kube-dns   ClusterIP   10.0.0.10    <none>        53/UDP,53/TCP   8m

本セクションの以降では、長寿命のIPアドレス(my-nginx)を持つServiceと、そのIPアドレスに名前を割り当てているDNSサーバーがあることを想定しています。 ここではCoreDNSクラスターアドオン(アプリケーション名kube-dns)を使い、標準的な手法(例えばgethostbyname())を使ってクラスター内の任意のPodからServiceと対話してみます。 CoreDNSが動作していない時には、 CoreDNS READMECoreDNSのインストールを参照して有効化してください。 テストするために、別のcurlアプリケーションを実行してみましょう:

kubectl run curl --image=radial/busyboxplus:curl -i --tty
Waiting for pod default/curl-131556218-9fnch to be running, status is Pending, pod ready: false
Hit enter for command prompt

次にenterを押し、nslookup my-nginxを実行します:

[ root@curl-131556218-9fnch:/ ]$ nslookup my-nginx
Server:    10.0.0.10
Address 1: 10.0.0.10

Name:      my-nginx
Address 1: 10.0.162.149

Serviceのセキュア化

これまではクラスター内からnginxサーバーだけにアクセスしてきました。 Serviceをインターネットに公開する前に、通信経路がセキュアかどうかを確かめたいところです。 そのためには次のようなものが必要です:

  • https用の自己署名証明書(まだ本人証明を用意していない場合)
  • 証明書を使うよう設定されたnginxサーバー
  • 証明書をPodからアクセスできるようにするSecret

これら全てはnginx https exampleから取得できます。 go環境とmakeツールのインストールが必要です (もしこれらをインストールしたくないときには、後述の手動手順に従ってください)。簡潔には:

make keys KEY=/tmp/nginx.key CERT=/tmp/nginx.crt
kubectl create secret tls nginxsecret --key /tmp/nginx.key --cert /tmp/nginx.crt
secret/nginxsecret created
kubectl get secrets
NAME                  TYPE                                  DATA      AGE
nginxsecret           kubernetes.io/tls                     2         1m

configmapも同様:

kubectl create configmap nginxconfigmap --from-file=default.conf
configmap/nginxconfigmap created
kubectl get configmaps
NAME             DATA   AGE
nginxconfigmap   1      114s

以下に示すのは、makeを実行したときに問題が発生する場合(例えばWindowsなど)の手動手順です:

# 公開鍵と秘密鍵のペアを作成する
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /d/tmp/nginx.key -out /d/tmp/nginx.crt -subj "/CN=my-nginx/O=my-nginx"
# 鍵をbase64エンコーディングに変換する
cat /d/tmp/nginx.crt | base64
cat /d/tmp/nginx.key | base64

以下のようなyamlファイルを作成するために、前のコマンドからの出力を使います。 base64エンコードされた値は、全て1行で記述する必要があります。

apiVersion: "v1"
kind: "Secret"
metadata:
  name: "nginxsecret"
  namespace: "default"
type: kubernetes.io/tls
data:
  tls.crt: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURIekNDQWdlZ0F3SUJBZ0lKQUp5M3lQK0pzMlpJTUEwR0NTcUdTSWIzRFFFQkJRVUFNQ1l4RVRBUEJnTlYKQkFNVENHNW5hVzU0YzNaak1SRXdEd1lEVlFRS0V3aHVaMmx1ZUhOMll6QWVGdzB4TnpFd01qWXdOekEzTVRKYQpGdzB4T0RFd01qWXdOekEzTVRKYU1DWXhFVEFQQmdOVkJBTVRDRzVuYVc1NGMzWmpNUkV3RHdZRFZRUUtFd2h1CloybHVlSE4yWXpDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBSjFxSU1SOVdWM0IKMlZIQlRMRmtobDRONXljMEJxYUhIQktMSnJMcy8vdzZhU3hRS29GbHlJSU94NGUrMlN5ajBFcndCLzlYTnBwbQppeW1CL3JkRldkOXg5UWhBQUxCZkVaTmNiV3NsTVFVcnhBZW50VWt1dk1vLzgvMHRpbGhjc3paenJEYVJ4NEo5Ci82UVRtVVI3a0ZTWUpOWTVQZkR3cGc3dlVvaDZmZ1Voam92VG42eHNVR0M2QURVODBpNXFlZWhNeVI1N2lmU2YKNHZpaXdIY3hnL3lZR1JBRS9mRTRqakxCdmdONjc2SU90S01rZXV3R0ljNDFhd05tNnNTSzRqYUNGeGpYSnZaZQp2by9kTlEybHhHWCtKT2l3SEhXbXNhdGp4WTRaNVk3R1ZoK0QrWnYvcW1mMFgvbVY0Rmo1NzV3ajFMWVBocWtsCmdhSXZYRyt4U1FVQ0F3RUFBYU5RTUU0d0hRWURWUjBPQkJZRUZPNG9OWkI3YXc1OUlsYkROMzhIYkduYnhFVjcKTUI4R0ExVWRJd1FZTUJhQUZPNG9OWkI3YXc1OUlsYkROMzhIYkduYnhFVjdNQXdHQTFVZEV3UUZNQU1CQWY4dwpEUVlKS29aSWh2Y05BUUVGQlFBRGdnRUJBRVhTMW9FU0lFaXdyMDhWcVA0K2NwTHI3TW5FMTducDBvMm14alFvCjRGb0RvRjdRZnZqeE04Tzd2TjB0clcxb2pGSW0vWDE4ZnZaL3k4ZzVaWG40Vm8zc3hKVmRBcStNZC9jTStzUGEKNmJjTkNUekZqeFpUV0UrKzE5NS9zb2dmOUZ3VDVDK3U2Q3B5N0M3MTZvUXRUakViV05VdEt4cXI0Nk1OZWNCMApwRFhWZmdWQTRadkR4NFo3S2RiZDY5eXM3OVFHYmg5ZW1PZ05NZFlsSUswSGt0ejF5WU4vbVpmK3FqTkJqbWZjCkNnMnlwbGQ0Wi8rUUNQZjl3SkoybFIrY2FnT0R4elBWcGxNSEcybzgvTHFDdnh6elZPUDUxeXdLZEtxaUMwSVEKQ0I5T2wwWW5scE9UNEh1b2hSUzBPOStlMm9KdFZsNUIyczRpbDlhZ3RTVXFxUlU9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
  tls.key: "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQ2RhaURFZlZsZHdkbFIKd1V5eFpJWmVEZWNuTkFhbWh4d1NpeWF5N1AvOE9ta3NVQ3FCWmNpQ0RzZUh2dGtzbzlCSzhBZi9WemFhWm9zcApnZjYzUlZuZmNmVUlRQUN3WHhHVFhHMXJKVEVGSzhRSHA3VkpMcnpLUC9QOUxZcFlYTE0yYzZ3MmtjZUNmZitrCkU1bEVlNUJVbUNUV09UM3c4S1lPNzFLSWVuNEZJWTZMMDUrc2JGQmd1Z0ExUE5JdWFubm9UTWtlZTRuMG4rTDQKb3NCM01ZUDhtQmtRQlAzeE9JNHl3YjREZXUraURyU2pKSHJzQmlIT05Xc0RadXJFaXVJMmdoY1kxeWIyWHI2UAozVFVOcGNSbC9pVG9zQngxcHJHclk4V09HZVdPeGxZZmcvbWIvNnBuOUYvNWxlQlkrZStjSTlTMkQ0YXBKWUdpCkwxeHZzVWtGQWdNQkFBRUNnZ0VBZFhCK0xkbk8ySElOTGo5bWRsb25IUGlHWWVzZ294RGQwci9hQ1Zkank4dlEKTjIwL3FQWkUxek1yall6Ry9kVGhTMmMwc0QxaTBXSjdwR1lGb0xtdXlWTjltY0FXUTM5SjM0VHZaU2FFSWZWNgo5TE1jUHhNTmFsNjRLMFRVbUFQZytGam9QSFlhUUxLOERLOUtnNXNrSE5pOWNzMlY5ckd6VWlVZWtBL0RBUlBTClI3L2ZjUFBacDRuRWVBZmI3WTk1R1llb1p5V21SU3VKdlNyblBESGtUdW1vVlVWdkxMRHRzaG9reUxiTWVtN3oKMmJzVmpwSW1GTHJqbGtmQXlpNHg0WjJrV3YyMFRrdWtsZU1jaVlMbjk4QWxiRi9DSmRLM3QraTRoMTVlR2ZQegpoTnh3bk9QdlVTaDR2Q0o3c2Q5TmtEUGJvS2JneVVHOXBYamZhRGR2UVFLQmdRRFFLM01nUkhkQ1pKNVFqZWFKClFGdXF4cHdnNzhZTjQyL1NwenlUYmtGcVFoQWtyczJxWGx1MDZBRzhrZzIzQkswaHkzaE9zSGgxcXRVK3NHZVAKOWRERHBsUWV0ODZsY2FlR3hoc0V0L1R6cEdtNGFKSm5oNzVVaTVGZk9QTDhPTm1FZ3MxMVRhUldhNzZxelRyMgphRlpjQ2pWV1g0YnRSTHVwSkgrMjZnY0FhUUtCZ1FEQmxVSUUzTnNVOFBBZEYvL25sQVB5VWs1T3lDdWc3dmVyClUycXlrdXFzYnBkSi9hODViT1JhM05IVmpVM25uRGpHVHBWaE9JeXg5TEFrc2RwZEFjVmxvcG9HODhXYk9lMTAKMUdqbnkySmdDK3JVWUZiRGtpUGx1K09IYnRnOXFYcGJMSHBzUVpsMGhucDBYSFNYVm9CMUliQndnMGEyOFVadApCbFBtWmc2d1BRS0JnRHVIUVV2SDZHYTNDVUsxNFdmOFhIcFFnMU16M2VvWTBPQm5iSDRvZUZKZmcraEppSXlnCm9RN3hqWldVR3BIc3AyblRtcHErQWlSNzdyRVhsdlhtOElVU2FsbkNiRGlKY01Pc29RdFBZNS9NczJMRm5LQTQKaENmL0pWb2FtZm1nZEN0ZGtFMXNINE9MR2lJVHdEbTRpb0dWZGIwMllnbzFyb2htNUpLMUI3MkpBb0dBUW01UQpHNDhXOTVhL0w1eSt5dCsyZ3YvUHM2VnBvMjZlTzRNQ3lJazJVem9ZWE9IYnNkODJkaC8xT2sybGdHZlI2K3VuCnc1YytZUXRSTHlhQmd3MUtpbGhFZDBKTWU3cGpUSVpnQWJ0LzVPbnlDak9OVXN2aDJjS2lrQ1Z2dTZsZlBjNkQKckliT2ZIaHhxV0RZK2Q1TGN1YSt2NzJ0RkxhenJsSlBsRzlOZHhrQ2dZRUF5elIzT3UyMDNRVVV6bUlCRkwzZAp4Wm5XZ0JLSEo3TnNxcGFWb2RjL0d5aGVycjFDZzE2MmJaSjJDV2RsZkI0VEdtUjZZdmxTZEFOOFRwUWhFbUtKCnFBLzVzdHdxNWd0WGVLOVJmMWxXK29xNThRNTBxMmk1NVdUTThoSDZhTjlaMTltZ0FGdE5VdGNqQUx2dFYxdEYKWSs4WFJkSHJaRnBIWll2NWkwVW1VbGc9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K"

では、このファイルを使ってSecretを作成します:

kubectl apply -f nginxsecrets.yaml
kubectl get secrets
NAME                  TYPE                                  DATA      AGE
nginxsecret           kubernetes.io/tls                     2         1m

Secretにある証明書を使ってhttpsサーバーを開始するために、nginxレプリカを変更します。また、Serviceは(80および443の)両方のポートを公開するようにします:

apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  labels:
    run: my-nginx
spec:
  type: NodePort
  ports:
  - port: 8080
    targetPort: 80
    protocol: TCP
    name: http
  - port: 443
    protocol: TCP
    name: https
  selector:
    run: my-nginx
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  selector:
    matchLabels:
      run: my-nginx
  replicas: 1
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      volumes:
      - name: secret-volume
        secret:
          secretName: nginxsecret
      containers:
      - name: nginxhttps
        image: bprashanth/nginxhttps:1.0
        ports:
        - containerPort: 443
        - containerPort: 80
        volumeMounts:
        - mountPath: /etc/nginx/ssl
          name: secret-volume

nginx-secure-appマニフェストの注目すべきポイント:

  • DeploymentとServiceの指定の両方が同じファイルに含まれています。
  • nginxサーバーは、ポート80でHTTPトラフィック、ポート443でHTTPSトラフィックをサービスし、nginx Serviceは両方のポートを公開します。
  • 各コンテナは、/etc/nginx/sslにマウントされたボリューム経由で鍵にアクセスできます。 これはnginxサーバーが開始するにセットアップされます。
kubectl delete deployments,svc my-nginx; kubectl create -f ./nginx-secure-app.yaml

この時点で、任意のノードからnginxサーバーに到達できます。

kubectl get pods -l run=my-nginx -o custom-columns=POD_IP:.status.podIPs
    POD_IP
    [map[ip:10.244.3.5]]
node $ curl -k https://10.244.3.5
...
<h1>Welcome to nginx!</h1>

最後の手順でcurlに-kパラメーターを与えていることに注意してください。 これは、証明書の生成時点ではnginxを実行中のPodについて何もわからないので、curlにCNameのミスマッチを無視するよう指示する必要があるからです。 Serviceを作成することで、証明書で使われているCNameと、PodがServiceルックアップ時に使う実際のDNS名とがリンクされます。 Podからこれをテストしてみましょう(単純化のため同じSecretが再利用されるので、ServiceにアクセスするのにPodが必要なのはnginx.crtだけです):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: curl-deployment
spec:
  selector:
    matchLabels:
      app: curlpod
  replicas: 1
  template:
    metadata:
      labels:
        app: curlpod
    spec:
      volumes:
      - name: secret-volume
        secret:
          secretName: nginxsecret
      containers:
      - name: curlpod
        command:
        - sh
        - -c
        - while true; do sleep 1; done
        image: radial/busyboxplus:curl
        volumeMounts:
        - mountPath: /etc/nginx/ssl
          name: secret-volume
kubectl apply -f ./curlpod.yaml
kubectl get pods -l app=curlpod
NAME                               READY     STATUS    RESTARTS   AGE
curl-deployment-1515033274-1410r   1/1       Running   0          1m
kubectl exec curl-deployment-1515033274-1410r -- curl https://my-nginx --cacert /etc/nginx/ssl/tls.crt
...
<title>Welcome to nginx!</title>
...

Serviceの公開

アプリケーションのいくつかの部分においては、Serviceを外部IPアドレスで公開したいと思うかもしれません。 Kubernetesはこれに対して2つのやり方をサポートしています: NodePortとLoadBalancerです。 前のセクションで作成したServiceではすでにNodePortを使っていたので、ノードにパブリックIPアドレスがあれば、nginx HTTPSレプリカもトラフィックをインターネットでサービスする準備がすでに整っています。

kubectl get svc my-nginx -o yaml | grep nodePort -C 5
  uid: 07191fb3-f61a-11e5-8ae5-42010af00002
spec:
  clusterIP: 10.0.162.149
  ports:
  - name: http
    nodePort: 31704
    port: 8080
    protocol: TCP
    targetPort: 80
  - name: https
    nodePort: 32453
    port: 443
    protocol: TCP
    targetPort: 443
  selector:
    run: my-nginx
kubectl get nodes -o yaml | grep ExternalIP -C 1
    - address: 104.197.41.11
      type: ExternalIP
    allocatable:
--
    - address: 23.251.152.56
      type: ExternalIP
    allocatable:
...

$ curl https://<EXTERNAL-IP>:<NODE-PORT> -k
...
<h1>Welcome to nginx!</h1>

では、クラウドロードバランサーを使うために、Serviceを再作成してみましょう。 my-nginxTypeNodePortからLoadBalancerに変更してください:

kubectl edit svc my-nginx
kubectl get svc my-nginx
NAME       TYPE           CLUSTER-IP     EXTERNAL-IP        PORT(S)               AGE
my-nginx   LoadBalancer   10.0.162.149     xx.xxx.xxx.xxx     8080:30163/TCP        21s
curl https://<EXTERNAL-IP> -k
...
<title>Welcome to nginx!</title>

EXTERNAL-IP列のIPアドレスが、パブリックインターネットで利用可能なものになっています。 CLUSTER-IPはクラスター/プライベートクラウドネットワーク内でのみ利用可能なものです。

AWSにおいては、LoadBalancerタイプは、IPアドレスではなく(長い)ホスト名を使うELBを作成することに注意してください。 これは標準のkubectl get svcの出力に合わせるには長すぎ、実際それを見るにはkubectl describe service my-nginxを使う必要があります。 これは以下のような見た目になります:

kubectl describe service my-nginx
...
LoadBalancer Ingress:   a320587ffd19711e5a37606cf4a74574-1142138393.us-east-1.elb.amazonaws.com
...

次の項目

8.2 - 送信元IPを使用する

Kubernetesクラスター内で実行されているアプリケーションは、Serviceという抽象化を経由して、他のアプリケーションや外の世界との発見や通信を行います。このドキュメントでは、異なる種類のServiceに送られたパケットの送信元IPに何が起こるのか、そして必要に応じてこの振る舞いを切り替える方法について説明します。

始める前に

用語

このドキュメントでは、以下の用語を使用します。

NAT
ネットワークアドレス変換(network address translation)
送信元NAT
パケットの送信元のIPを置換します。このページでは、通常ノードのIPアドレスを置換することを意味します。
送信先NAT
パケットの送信先のIPを置換します。このページでは、通常PodのIPアドレスを置換することを意味します。
VIP
Kubernetes内のすべてのServiceなどに割り当てられる仮想IPアドレス(virtual IP address)です。
kube-proxy
すべてのノード上でServiceのVIPを管理するネットワークデーモンです。

前提条件

Kubernetesクラスターが必要、かつそのクラスターと通信するためにkubectlコマンドラインツールが設定されている必要があります。 このチュートリアルは、コントロールプレーンのホストとして動作していない少なくとも2つのノードを持つクラスターで実行することをおすすめします。 まだクラスターがない場合、minikubeを使って作成するか、 以下のいずれかのKubernetesプレイグラウンドも使用できます:

以下の例では、HTTPヘッダー経由で受け取ったリクエストの送信元IPをエコーバックする、小さなnginxウェブサーバーを使用します。次のコマンドでウェブサーバーを作成できます。

kubectl create deployment source-ip-app --image=registry.k8s.io/echoserver:1.4

出力は次のようになります。

deployment.apps/source-ip-app created

目標

  • 単純なアプリケーションを様々な種類のService経由で公開する
  • それぞれの種類のServiceがどのように送信元IPのNATを扱うかを理解する
  • 送信元IPを保持することに関わるトレードオフを理解する

Type=ClusterIPを使用したServiceでの送信元IP

kube-proxyがiptablesモード(デフォルト)で実行されている場合、クラスター内部からClusterIPに送られたパケットに送信元のNATが行われることは決してありません。kube-proxyが実行されているノード上でhttp://localhost:10249/proxyModeにリクエストを送って、kube-proxyのモードを問い合わせてみましょう。

kubectl get nodes

出力は次のようになります。

NAME                           STATUS     ROLES    AGE     VERSION
kubernetes-node-6jst   Ready      <none>   2h      v1.13.0
kubernetes-node-cx31   Ready      <none>   2h      v1.13.0
kubernetes-node-jj1t   Ready      <none>   2h      v1.13.0

これらのノードの1つでproxyモードを取得します(kube-proxyはポート10249をlistenしています)。

# このコマンドは、問い合わせを行いたいノード上のシェルで実行してください。
curl http://localhost:10249/proxyMode

出力は次のようになります。

iptables

source IPアプリのServiceを作成することで、送信元IPが保持されているかテストできます。

kubectl expose deployment source-ip-app --name=clusterip --port=80 --target-port=8080

出力は次のようになります。

service/clusterip exposed
kubectl get svc clusterip

出力は次のようになります。

NAME         TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)   AGE
clusterip    ClusterIP   10.0.170.92   <none>        80/TCP    51s

そして、同じクラスター上のPodからClusterIPにアクセスします。

kubectl run busybox -it --image=busybox --restart=Never --rm

出力は次のようになります。

Waiting for pod default/busybox to be running, status is Pending, pod ready: false
If you don't see a command prompt, try pressing enter.

これで、Podの内部でコマンドが実行できます。

# このコマンドは、"kubectl run" のターミナルの内部で実行してください
ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
3: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1460 qdisc noqueue
    link/ether 0a:58:0a:f4:03:08 brd ff:ff:ff:ff:ff:ff
    inet 10.244.3.8/24 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::188a:84ff:feb0:26a5/64 scope link
       valid_lft forever preferred_lft forever

そして、wgetを使用してローカルのウェブサーバーに問い合わせます。

# "10.0.170.92" の部分をService名が"clusterip"のIPv4アドレスに置き換えてください
wget -qO - 10.0.170.92
CLIENT VALUES:
client_address=10.244.3.8
command=GET
...

client_addressは常にクライアントのPodのIPアドレスになります。これは、クライアントのPodとサーバーのPodが同じノード内にあっても異なるノードにあっても変わりません。

Type=NodePortを使用したServiceでの送信元IP

Type=NodePortを使用したServiceに送られたパケットは、デフォルトで送信元のNATが行われます。NodePort Serviceを作ることでテストできます。

kubectl expose deployment source-ip-app --name=nodeport --port=80 --target-port=8080 --type=NodePort

出力は次のようになります。

service/nodeport exposed
NODEPORT=$(kubectl get -o jsonpath="{.spec.ports[0].nodePort}" services nodeport)
NODES=$(kubectl get nodes -o jsonpath='{ $.items[*].status.addresses[?(@.type=="InternalIP")].address }')

クラウドプロバイダーで実行する場合、上に示したnodes:nodeportに対してファイアウォールのルールを作成する必要があるかもしれません。それでは、上で割り当てたノードポート経由で、クラスターの外部からServiceにアクセスしてみましょう。

for node in $NODES; do curl -s $node:$NODEPORT | grep -i client_address; done

出力は次のようになります。

client_address=10.180.1.1
client_address=10.240.0.5
client_address=10.240.0.3

これらは正しいクライアントIPではなく、クラスターのinternal IPであることがわかります。ここでは、次のようなことが起こっています。

  • クライアントがパケットをnode2:nodePortに送信する
  • node2は、パケット内の送信元IPアドレスを自ノードのIPアドレスに置換する(SNAT)
  • node2は、パケット内の送信先IPアドレスをPodのIPアドレスに置換する
  • パケットはnode1にルーティングされ、endpointにルーティングされる
  • Podからの応答がnode2にルーティングされて戻ってくる
  • Podからの応答がクライアントに送り返される

図で表すと次のようになります。

graph LR; client(client)-->node2[Node 2]; node2-->client; node2-. SNAT .->node1[Node 1]; node1-. SNAT .->node2; node1-->endpoint(Endpoint); classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; class node1,node2,endpoint k8s; class client plain;

クライアントのIPが失われることを回避するために、Kubernetesにはクライアントの送信元IPを保持する機能があります。service.spec.externalTrafficPolicyの値をLocalに設定すると、kube-proxyはローカルに存在するエンドポイントへのプロキシーリクエストだけをプロキシーし、他のノードへはトラフィックを転送しなくなります。このアプローチでは、オリジナルの送信元IPアドレスが保持されます。ローカルにエンドポイントが存在しない場合には、そのノードに送信されたパケットは損失します。そのため、エンドポイントに到達するパケットに適用する可能性のあるパケット処理ルールでは、送信元IPが正しいことを信頼できます。

次のようにしてservice.spec.externalTrafficPolicyフィールドを設定します。

kubectl patch svc nodeport -p '{"spec":{"externalTrafficPolicy":"Local"}}'

出力は次のようになります。

service/nodeport patched

そして、再度テストしてみます。

for node in $NODES; do curl --connect-timeout 1 -s $node:$NODEPORT | grep -i client_address; done

出力は次のようになります。

client_address=198.51.100.79

今度は、正しいクライアントIPが含まれる応答が1つだけ得られました。これは、エンドポイントのPodが実行されているノードから来たものです。

ここでは、次のようなことが起こっています。

  • クライアントがパケットをエンドポイントが存在しないnode2:nodePortに送信する
  • パケットが損失する
  • クライアントがパケットをエンドポイントが存在するnode1:nodePortに送信する
  • node1は、正しい送信元IPを持つパケットをエンドポイントにルーティングする

図で表すと次のようになります。

graph TD; client --> node1[Node 1]; client(client) --x node2[Node 2]; node1 --> endpoint(endpoint); endpoint --> node1; classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; class node1,node2,endpoint k8s; class client plain;

Type=LoadBalancerを使用したServiceでの送信元IP

Type=LoadBalancerを使用したServiceに送られたパケットは、デフォルトで送信元のNATが行われます。Ready状態にあるすべてのスケジュール可能なKubernetesのNodeは、ロードバランサーからのトラフィックを受付可能であるためです。そのため、エンドポイントが存在しないノードにパケットが到達した場合、システムはエンドポイントが存在するノードにパケットをプロシキーします。このとき、(前のセクションで説明したように)パケットの送信元IPがノードのIPに置換されます。

ロードバランサー経由でsource-ip-appを公開することで、これをテストできます。

kubectl expose deployment source-ip-app --name=loadbalancer --port=80 --target-port=8080 --type=LoadBalancer

出力は次のようになります。

service/loadbalancer exposed

ServiceのIPアドレスを表示します。

kubectl get svc loadbalancer

出力は次のようになります。

NAME           TYPE           CLUSTER-IP    EXTERNAL-IP       PORT(S)   AGE
loadbalancer   LoadBalancer   10.0.65.118   203.0.113.140     80/TCP    5m

次に、Serviceのexternal-ipにリクエストを送信します。

curl 203.0.113.140

出力は次のようになります。

CLIENT VALUES:
client_address=10.240.0.5
...

しかし、Google Kubernetes EngineやGCE上で実行している場合、同じservice.spec.externalTrafficPolicyフィールドをLocalに設定すると、ロードバランサーからのトラフィックを受け付け可能なノードのリストから、Serviceエンドポイントが存在しないノードが強制的に削除されます。この動作は、ヘルスチェックを意図的に失敗させることによって実現されています。

図で表すと次のようになります。

Source IP with externalTrafficPolicy

アノテーションを設定することで動作をテストできます。

kubectl patch svc loadbalancer -p '{"spec":{"externalTrafficPolicy":"Local"}}'

Kubernetesにより割り当てられたservice.spec.healthCheckNodePortフィールドをすぐに確認します。

kubectl get svc loadbalancer -o yaml | grep -i healthCheckNodePort

出力は次のようになります。

  healthCheckNodePort: 32122

service.spec.healthCheckNodePortフィールドは、/healthzでhealth checkを配信しているすべてのノード上のポートを指しています。次のコマンドでテストできます。

kubectl get pod -o wide -l run=source-ip-app

出力は次のようになります。

NAME                            READY     STATUS    RESTARTS   AGE       IP             NODE
source-ip-app-826191075-qehz4   1/1       Running   0          20h       10.180.1.136   kubernetes-node-6jst

curlを使用して、さまざまなノード上の/healthzエンドポイントからデータを取得します。

# このコマンドは選んだノードのローカル上で実行してください
curl localhost:32122/healthz
1 Service Endpoints found

ノードが異なると、得られる結果も異なる可能性があります。

# このコマンドは、選んだノード上でローカルに実行してください
curl localhost:32122/healthz
No Service Endpoints Found

コントロールプレーン上で実行中のコントローラーは、クラウドのロードバランサーを割り当てる責任があります。同じコントローラーは、各ノード上のポートやパスを指すHTTPのヘルスチェックも割り当てます。エンドポイントが存在しない2つのノードがヘルスチェックに失敗するまで約10秒待った後、curlを使用してロードバランサーのIPv4アドレスに問い合わせます。

curl 203.0.113.140

出力は次のようになります。

CLIENT VALUES:
client_address=198.51.100.79
...

クロスプラットフォームのサポート

Type=LoadBalancerを使用したServiceで送信元IPを保持する機能を提供しているのは一部のクラウドプロバイダだけです。実行しているクラウドプロバイダによっては、以下のように異なる方法でリクエストを満たす場合があります。

  1. クライアントとのコネクションをプロキシーが終端し、ノードやエンドポイントとの接続には新しいコネクションが開かれる。このような場合、送信元IPは常にクラウドのロードバランサーのものになり、クライアントのIPにはなりません。

  2. クライアントからロードバランサーのVIPに送信されたリクエストが、中間のプロキシーではなく、クライアントの送信元IPとともにノードまで到達するようなパケット転送が使用される。

1つめのカテゴリーのロードバランサーの場合、真のクライアントIPと通信するために、 HTTPのForwardedヘッダーやX-FORWARDED-FORヘッダー、proxy protocolなどの、ロードバランサーとバックエンドの間で合意されたプロトコルを使用する必要があります。2つ目のカテゴリーのロードバランサーの場合、Serviceのservice.spec.healthCheckNodePortフィールドに保存されたポートを指すHTTPのヘルスチェックを作成することで、上記の機能を活用できます。

クリーンアップ

Serviceを削除します。

kubectl delete svc -l app=source-ip-app

Deployment、ReplicaSet、Podを削除します。

kubectl delete deployment source-ip-app

次の項目

8.3 - Podとそのエンドポイントの終了動作を探る

アプリケーションをServiceに接続するで概略を示したステップに従ってアプリケーションをServiceに接続すると、ネットワーク上で公開され、継続的に実行されて、複製されたアプリケーションが得られます。 このチュートリアルでは、Podを終了する流れを見て、gracefulな(猶予のある)接続ドレインを実装する手法を模索するための手助けをします。

Podの終了手続きとそのエンドポイント

アップグレードやスケールダウンのために、Podを終了しなければならない場面はままあります。 アプリケーションの可用性を高めるために、適切なアクティブ接続ドレインを実装することは重要でしょう。

このチュートリアルでは概念のデモンストレーションのために、シンプルなnginx Webサーバーを例として、対応するエンドポイントの状態に関連したPodの終了および削除の流れを説明します。

エンドポイント終了の流れの例

以下は、Podの終了ドキュメントに記載されている流れの例です。

1つのnginxレプリカを含むDeployment(純粋にデモンストレーション目的です)とServiceがあるとします:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      terminationGracePeriodSeconds: 120 # 非常に長い猶予期間
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80
        lifecycle:
          preStop:
            exec:
              # 実際の活動終了はterminationGracePeriodSecondsまでかかる可能性がある。
              # この例においては、少なくともterminationGracePeriodSecondsの間は待機し、
              # 120秒経過すると、コンテナは強制的に終了される。
              # この間ずっとnginxはリクエストを処理し続けていることに注意。
              command: [
                "/bin/sh", "-c", "sleep 180"
              ]
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      terminationGracePeriodSeconds: 120 # 非常に長い猶予期間
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80
        lifecycle:
          preStop:
            exec:
              # 実際の活動終了はterminationGracePeriodSecondsまでかかる可能性がある。
              # この例においては、少なくともterminationGracePeriodSecondsの間は待機し、
              # 120秒経過すると、コンテナは強制終了される。
              # この間ずっとnginxはリクエストを処理し続けていることに注意。
              command: [
                "/bin/sh", "-c", "sleep 180"
              ]

---

apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  selector:
    app: nginx
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80

PodとServiceが実行中になったら、関連付けられたEndpointSliceの名前を得られます:

kubectl get endpointslice

この出力は以下のようなものになります:

NAME                  ADDRESSTYPE   PORTS   ENDPOINTS                 AGE
nginx-service-6tjbr   IPv4          80      10.12.1.199,10.12.1.201   22m

状態からわかるように、1つのエンドポイントが登録されていることが確認できます:

kubectl get endpointslices -o json -l kubernetes.io/service-name=nginx-service

この出力は以下のようなものになります:

{
    "addressType": "IPv4",
    "apiVersion": "discovery.k8s.io/v1",
    "endpoints": [
        {
            "addresses": [
                "10.12.1.201"
            ],
            "conditions": {
                "ready": true,
                "serving": true,
                "terminating": false

では、Podを終了し、そのPodがgracefulな終了期間設定を守って終了されていることを確認してみましょう:

kubectl delete pod nginx-deployment-7768647bf9-b4b9s

全Podについて調べます:

kubectl get pods

この出力は以下のようなものになります:

NAME                                READY   STATUS        RESTARTS      AGE
nginx-deployment-7768647bf9-b4b9s   1/1     Terminating   0             4m1s
nginx-deployment-7768647bf9-rkxlw   1/1     Running       0             8s

新しいPodがスケジュールされたことを見てとれます。

新しいPodのために新しいエンドポイントが作成される間、古いエンドポイントは終了中の状態のまま残っています:

kubectl get endpointslice -o json nginx-service-6tjbr

この出力は以下のようなものになります:

{
    "addressType": "IPv4",
    "apiVersion": "discovery.k8s.io/v1",
    "endpoints": [
        {
            "addresses": [
                "10.12.1.201"
            ],
            "conditions": {
                "ready": false,
                "serving": true,
                "terminating": true
            },
            "nodeName": "gke-main-default-pool-dca1511c-d17b",
            "targetRef": {
                "kind": "Pod",
                "name": "nginx-deployment-7768647bf9-b4b9s",
                "namespace": "default",
                "uid": "66fa831c-7eb2-407f-bd2c-f96dfe841478"
            },
            "zone": "us-central1-c"
        },
        {
            "addresses": [
                "10.12.1.202"
            ],
            "conditions": {
                "ready": true,
                "serving": true,
                "terminating": false
            },
            "nodeName": "gke-main-default-pool-dca1511c-d17b",
            "targetRef": {
                "kind": "Pod",
                "name": "nginx-deployment-7768647bf9-rkxlw",
                "namespace": "default",
                "uid": "722b1cbe-dcd7-4ed4-8928-4a4d0e2bbe35"
            },
            "zone": "us-central1-c"

これを使うと、終了中のアプリケーションがその状態について、接続ドレイン機能の実装目的でクライアント(ロードバランサーなど)と通信する、ということが可能です。 これらのクライアントではエンドポイントの終了を検出し、そのための特別なロジックを実装できます。

Kubernetesでは、終了中のエンドポイントのready状態は全てfalseにセットされます。 これは後方互換性のために必要な措置で、既存のロードバランサーは通常のトラフィックにはそれを使用しません。 Podの終了時にトラフィックのドレインが必要な場合、実際に準備できているかはserving状態として調べられます。

Podが削除される時には、古いエンドポイントも削除されます。

次の項目