Kubernetesによるサービスディスカバリーとロードバランシングの実装方法

アリババのテクニカルエキスパートであるWang Bingshenが、Kubernetesとクラウドネイティブなシナリオを用いたサービスディスカバリーとロードバランシングの目的と実装について説明します。

著者：アリババのテクニカルエキスパート、Wang Bingshen（Xiheng）氏

#1. 必要条件のソース
###サービスディスカバリーの目的
[Kubernetes](https://www.alibabacloud.com/product/kubernetes?spm=a2c65.11461447.0.0.1582611b9btLAp)クラスタ内のアプリケーションは、Podを使用してデプロイされますが、これは特定のマシンにアプリケーションをデプロイする従来のアプローチとは異なります。我々は、他のマシンのIPアドレスを呼び出す方法を知っていますが、Podのライフサイクルは短いです。PodのIPアドレスは、Podが作成されたり破棄されたりするなど、そのライフサイクルの中で変化します。このため、従来の方法では、指定したIPアドレスを介したアプリケーションのアクセスができず、Kubernetesクラスターにアプリケーションを展開することができません。

[Kubernetesクラスター](https://www.alibabacloud.com/product/kubernetes?spm=a2c65.11461447.0.0.1582611b9btLAp)にアプリケーションをデプロイするには、デプロイメントに加えてPodのグループを作成し、Podのセントラルエンドポイントを提供し、Pod間のトラフィックロードバランシングを実装する必要があります。また、アプリケーションをデプロイする際には、テスト環境、ステージング環境、本番環境で同じデプロイメントテンプレートとアクセスモードを維持する必要があります。こうすることで、同じテンプレートを使って異なる環境にアプリケーションを直接公開することができます。

###Kubernetesのサービス:サービスディスカバリーとロードバランシング
アプリケーションは、外部のユーザーが呼び出せるように公開する必要があります。Podは、マシンとは異なるネットワークセグメントにあります。Podのネットワークを外部からアクセスできるように公開するには、サービスディスカバリーを設定する必要があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/0e451b0e-3f51-399b-6371-d2b18db96d76.png)




Kubernetesでは、サービスディスカバリーとロードバランシングがサービスとして提供されています。前述の図は、Kubernetes Serviceのアーキテクチャを示しており、アップリンク方向に外部ネットワークとPodネットワークへのアクセスを提供します。

Kubernetes Serviceは、ダウンリンク方向でPodのグループと相互に連携し、Pod間のロードバランシングを実施します。これにより、サービスディスカバリーのための中央エンドポイントが提供され、外部ネットワークへのアクセスと、同一アドレスを介した異なるPod間のアクセスが可能になります。

#2. ケーススタディ
実際のユースケースとして、KubernetesにおけるPod用のServiceの宣言と使用方法を紹介します。

###サービス構文

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/1a273929-1dc6-6d5d-444a-e93d4233ce05.png)

まず、Kubernetes Serviceの構文を見てみましょう。前述の図は、Kubernetesの宣言構造を示しています。この構造には多くの構文が含まれており、それらは先に説明したいくつかの標準的なKubernetesオブジェクトと似ています。例えば、labelsとselectorフィールドを使用して選択を行い、labelsフィールドを使用して宣言を行うことができます。

このテンプレートでは、サービス発見に使用するKubernetes Serviceのプロトコルとポートを定義します。テンプレートでは、`app:my-service`ラベルを持つ、`my-service`という名前のKubernetes Serviceを宣言しています。このサービスは、`app:MyApp`ラベルを持つPodをバックエンドとして選択します。

サービスの検出には、TCPとポート`80`が定義されています。ターゲットポートは`9376`です。ポート`80`へのアクセス要求は、バックエンドに`app:MyApp`ラベルを持つPodのポート`9376`にルーティングされます。これにより、ロードバランシングが実装されます。

###サービスの作成と表示
ここでは、先ほど宣言したServiceの作成と、作成したServiceの表示について説明します。以下のコマンドを実行します。

- `kubectl apply -f service.yaml`
あるいは

- `kubectl created -f service.yaml`
以上のコマンドでServiceを作成します。Serviceが作成されたら、以下のコマンドを実行します。

- `kubectl describe service`
これにより、作成されたServiceを確認することができます。



![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/65c98e85-720a-9a0f-e679-6a71b856da5e.png)


作成されたServiceの名前は`my-service`です。「`Namespace`」、「`Labels`」、「`Selector`」の各フィールドは、先の宣言のフィールドと同じです。サービス用にIPアドレスが作成され、クラスター内のPodからアクセスできるようになります。このIPアドレスは、すべてのPodのセントラルエンドポイントであり、サービスディスカバリーで使用されます。

「`Endpoints`」フィールドには、「`selector`」フィールドで指定されたPodが表示されます。Podのステータスを表示することができます。たとえば、選択したPodのIPアドレスとターゲットポートを表示できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/90de4365-45f0-5e1c-d792-6207867ea385.png)


前述の図は、そのアーキテクチャを示しています。クラスタ内のサービスとともに、仮想IPアドレスとポートが作成されます。クラスタ内のすべてのPodとノードは、このIPアドレスとポートを介してサービスにアクセスできます。サービスは、選択したPodとそのIPアドレスをバックエンドにマウントします。このようにして、ServiceのIPアドレスを介したアクセス要求がこれらのPodに分散され、負荷分散が行われます。

Podのライフサイクルが変化したとき、たとえばPodが破壊されたとき、サービスは自動的にPodをバックエンドから削除します。これにより、Podのライフサイクルが変更されても、エンドポイントは変更されません。



###クラスタ内のサービスアクセス
クラスター内でサービスを作成した後、クラスター内のPodは以下のいずれかの方法でサービスにアクセスできます。

- Podは、その仮想IPアドレスを介してサービスにアクセスできます。先に作成した`my-service`という名前のServiceの場合、`kubectl get svc`または`kubectl describe service`コマンドを実行すると、Serviceの仮想IPアドレス`172.29.3.27`とポート`80`が表示され、PodがServiceにアクセスするために直接使用することができます。
- サービスと同じ名前空間にあるPodは、DNSで名前が解決された後、その名前を通してサービスにアクセスできます。本サービスと異なる名前空間にあるPodは、`service_name.service_namespace`の形式で本サービスにアクセスできます。例えば、curlを使って、`my-service:80`という形式でServiceにアクセスすることができます。

- サービスと同じ名前空間にあるPodは、起動時にサービスのIPアドレス、ポート、簡単な設定をPodに転送するための環境変数を介して、サービスにアクセスすることができます。Podのコンテナが起動すると、環境変数を読み込んで、同じ名前空間にあるサービスのIPアドレスとポートを取得します。例えば、クラスタ内のPodは、curl `$`を使って環境変数の値を取得することができます。`MY_SERVICE_HOST`はサービスのIPアドレス、`MY_SERVICE`は宣言されたサービス名、`SERVICE_PORT`はサービスのポートを示しています。これにより、Podはクラスタ内の`MY_SERVICE`で示されるServiceにリクエストを送信することができます。

###ヘッドレスサービス
ヘッドレスサービスは、特別なタイプのサービスです。サービスを作成する際に`clusterIP:None`を指定することで、KubernetesにクラスタIPアドレス、つまり前述の仮想IPアドレスが不要であることを伝えることができます。そうすると、KubernetesはこのServiceに仮想IPアドレスを割り当てません。仮想IPアドレスがなくても、Serviceは以下のようにロードバランシングを実装し、中央のエンドポイントを提供することができる。

Podは、DNSのAレコードを介してService名をバックエンドの全PodのIPアドレスに直接解決することができる。クライアントは、解決されたバックエンドのIPアドレスのいずれかを選択できる。AレコードはPodのライフサイクルの変更に伴い変更され、返されるAレコードのリストも変更されます。クライアントは、Podにアクセスするために、DNSで返されたAレコードのリストから適切なIPアドレスを選択する必要があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/b4e55998-29d0-4b0e-3b39-2fa3873ae302.png)

先に宣言したテンプレートと比較して、前図のテンプレートでは`clusterIP:None`が追加されており、仮想IPアドレスが不要であることを示しています。クラスタ内のPodが`my-service`にアクセスすると、サービス名を直接解決して、ServiceにマッチするすべてのPodのIPアドレスを取得します。その後、Podは返されたリストからIPアドレスを選択し、サービスに直接アクセスします。

###サービスをクラスタ外に公開
前節では、クラスター内のノードやPodからサービスにアクセスする方法を紹介しました。本節では、クラスター外でサービスを公開し、インターネットからのアクセスのためにアプリケーションを公開する方法を説明します。`NodePort`や`LoadBalancer`を使ってサービスを外部に公開することができます。

- `NodePort`モードでは、クラスタ内のノードのポートがノードホスト上で公開されます。ノードポートモードでは、クラスタ内のノードのポートがノードホスト上で公開され、アクセスリクエストを受信すると、公開されたポートはホスト上に設定されたサービスの仮想IPアドレスにリクエストを転送します。
- `LoadBalancer`モードでは、フォワーディングレイヤーが追加されます。`NodePort`はクラスタ内のすべてのノードのポートに実装されますが、`LoadBalancer`はすべてのノードにロードバランサーをマウントします。たとえば、Alibaba Cloud Server Load Balancer（SLB）インスタンスをマウントして中央のエンドポイントを提供し、着信トラフィックをクラスター内のすべてのノードのPodに均等に分配することができます。その後、PodはクラスターのIPアドレスに基づいてトラフィックをターゲットPodに転送します。

#3. 実践編
次の例では、Alibaba Cloud Container ServiceでKubernetes Serviceを使用する方法を示します。

###サービスの作成
前提条件：Alibaba Cloudコンテナクラスターを作成し、ローカル端末からAlibaba Cloudコンテナクラスターへの接続を設定しています。

`kubectl get cs`コマンドを実行して、Alibaba Cloudコンテナクラスターが接続されていることを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/04633f23-ae9d-f98b-4896-b9673e8f12c4.png)


Alibaba CloudにKubernetes Serviceを実装するには、以下のテンプレートを使用します。3つのテンプレートがあります。クライアントテンプレートは、サービスで宣言されたPodにトラフィックを均等に分配するKubernetes Serviceにアクセスするために使用します。



![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/ce896aae-267f-dfd3-60dc-4a496fe1d583.png)

Kubernetes Serviceテンプレートを作成し、フロントエンドのポート`80`からバックエンドのポート`80`までトラフィックが均等に分散するようにPodを宣言します。そして、`run:nginx`ラベルを持つバックエンドPodを選択するように`selector`フィールドを設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/48a86941-ccf8-33ce-1590-86222a1cba4c.png)

次に、Kubernetes Deploymentsを使って`run:nginx`ラベルを持つPodのグループを作成します。Deploymentには2つのレプリカがあり、2つのPodにマッチします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/420cbc9c-df32-265b-8eba-717b74cf21b2.png)

`kubectl create -f service.yaml` コマンドを実行して Deployment を作成します。Deploymentが作成されたら、podも作成されているか確認します。次の図のように、Deploymentと一緒に作成された2つのpodは`Running`状態になっています。`kubectl get pod -o wide`コマンドを実行すると、PodのIPアドレスが表示されます。run=nginxに基づくフィルタリングを実装するには、`-l`を使用します。次の図のように、2つのPodのIPアドレスは「`10.0.0.135`」と「`10.0.0.12`」で、どちらも「`run=nginx`」というラベルが付いています。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/b7d39d22-a263-3e97-29c3-82cda4c8c1ef.png)


以下のコマンドを実行して、2つのPodを選択するKubernetes Serviceを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/006eb53f-7c89-2a8f-bab6-fa8c5acfe0be.png)


`kubectl describe svc`コマンドを実行して、Serviceの状態を確認します。次の図に示すように、`nginx`という名前で作成されたKubernetes Serviceは、`run=nginx`というセレクタを使用して、バックエンドのPodとして`10.0.0.12`と`10.0.0.135`を選択しています。バックエンドの2つのPodにトラフィックを均等に分配するために、Kubernetes Service用にクラスタ内の仮想IPアドレスを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/433d8a9b-b8b2-33f6-2142-4dca10d81d64.png)

`client.yaml`コマンドを実行して、Kubernetes ServiceにアクセスするためのクライアントPodを作成します。`kubectl get pod`コマンドを実行して、クライアントPodが作成され、`Running`状態になっていることを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/226595d5-b9c6-6942-b33f-58461387372a.png)


`kubectl exec`コマンドを実行してクライアントPodにアクセスし、3つのアクセスモードを体験します。curlを使用して、Kubernetes ServiceのクラスタIPアドレス（または仮想IPアドレス）に直接アクセスします。クライアントPodにはcurlがインストールされていません。`wget`コマンドを実行して、仮想IPアドレスを入力します。バックエンドの`nginx`というKubernetes Serviceに、中央のエンドポイントでもある仮想IPアドレスを介してアクセスできます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/c820b7b1-be0f-8f56-2bbb-34dc573996d7.png)

サービス名からKubernetes Serviceにアクセスすることもできます。`wget`コマンドを実行してKubernetes Serviceの`nginx`にアクセスすると、先ほどと同じ結果が得られます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/2e2f25b9-7ae0-8af1-60fd-407ed98ec648.png)

クライアントPodがKubernetes Serviceと異なる名前空間にある場合、Serviceが置かれている名前空間の名前を追加することで、Serviceにアクセスすることができます。ここでは、`default`という名前の名前空間を例にしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/1baec49c-db58-7d35-ec3c-cf190f08ca8b.png)

環境変数を使ってKubernetes Serviceにアクセスすることもできます。クライアントPodで`env`コマンドを実行すると、注入された環境変数が表示されます。`nginx` Serviceのすべての設定が登録されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/6420a629-56ee-e0dc-6670-ef0750533742.png)

`wget`コマンドを実行して、環境変数にアクセスします。すると、Kubernetes Serviceにアクセスできるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/bff13348-3679-031e-8314-3247364e962c.png)

外部ネットワークからKubernetes Serviceにアクセスする方法を説明します。`Vim`でKubernetes Serviceの一部の設定を変更します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/5703822d-fb02-e24c-e7cd-cf0d704c8555.png)

`type`フィールドを追加し、`LoadBalancer`に設定して外部からのアクセスを可能にします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/c4554a45-5122-866e-f07e-78a68ad6c6ab.png)


`kubectl apply`コマンドを実行して、変更した内容をServiceに適用します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/542a5446-3dfd-3095-fc83-24907be034cd.png)

では、Serviceにどのような変化が起こるか見てみましょう。`kubectl get svc -o wide`コマンドを実行すると、Service `nginx`に外部アクセス用のIPアドレスである`EXTERNAL-IP`が追加されていることがわかります。前述の通り、本サービスはクラスタ内ではCLUSTER-IPで定義された仮想IPアドレスを介してアクセスされます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/76fbd9ba-a44b-b705-8daa-3a17495f6386.png)


外部IPアドレス`39.98.21.187`にアクセスして、本サービスでどのようにアプリケーションが公開されているかを確認します。端末のWebブラウザに外部IPアドレスを入力して、本サービスにアクセスしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/652a24df-3302-fe70-abcb-0af975ea6855.png)

以下は、Service を使用して Kubernetes でサービスディスカバリーを実装する方法です。Serviceのアクセスアドレスは、Podのライフサイクルとは関係ありません。まず、Serviceに選ばれた2つのPodのIPアドレスを見てみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/b32f624b-b4ff-021a-95bc-07d897a901cb.png)

`kubectl delete`コマンドを実行して、1つ目のPodを削除します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/2997ac35-2cbf-8310-97d0-4c005408a030.png)

すると、Deployment は、末尾が`137`のIPアドレスを持つ別のPodを自動的に作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/b7103722-29c5-3ad3-eaad-2834b8b59c81.png)


describeコマンドを実行すると、次の図のようにService情報が表示されます。エンドポイントはクラスターのIPアドレスのままです。`LoadBalancer`モードでは、外部アクセス用のIPアドレスは変更されません。バックエンドPodのIPアドレスは、ServiceのバックエンドIPアドレスリストに自動的に含まれます。これは、クライアントのアクセスには影響しません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/b33be217-5340-bce3-5419-5e7df6cded4e.png)

このようにして、Podのライフサイクルの変更が、アプリケーション・コンポーネントへの呼び出しに影響を与えることはありません。

#4. アーキテクチャ設計
本章では、Kubernetesの設計と実装について分析します。

###Kubernetesのサービスディスカバリーアーキテクチャ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/f6c1ff42-8d71-2ed5-dddb-d7d97b5cad6d.png)

前述の図は、サービスディスカバリーのためのKubernetes Serviceのアーキテクチャを示しています。

このアーキテクチャには、マスターノードと複数のワーカーノードが含まれています。

- マスターノードは、Kubernetesの制御機能を実装しています。
- ワーカーノードは、ユーザーアプリケーションを実行します。
マスターノードには、すべてのKubernetesオブジェクトを集中管理するためのKubernetes APIサーバーが配備されています。すべてのコンポーネントはAPIサーバーに登録され、Podのライフサイクルの変更など、オブジェクトの変更をリッスンします。

マスターノードには3つの主要なコンポーネントがあります。

- クラウドコントローラーマネージャーは、外部からのアクセスのためにロードバランサーを構成します。
- `CoreDNS` は、API サーバー上の Service のバックエンド Podの変更をリッスンします。サービス名を介してサービスの仮想 IP アドレスに直接アクセスするように DNS 解決を構成できます。また、ヘッドレスサービスが保持するIPアドレスリストのIPアドレスを解決するようにDNS解決を設定することもできます。
- 各ノードの`kube-proxy`がServiceやPodの変更をリッスンするため、実際の状況に応じてクラスタ内のノードやPodを設定したり、仮想IPアドレスを介したアクセスを設定したりすることができます。

実際のアクセスリンクを見てみましょう。例えば、クラスタ内のクライアントPod3がサービスにアクセスしたいとします。クライアントPod3は、`CoreDNS`を介してサービスのIPアドレスを解決し、サービス名と一致するIPアドレスを返します。クライアントPod3は、サービスIPアドレスを通じてリクエストを開始します。リクエストはホストネットワークに送信された後、iptablesや`kube-proxy`で設定されたIP virtual server (IPVS)に基づいてインターセプトされます。その後、リクエストはロードバランサーによってバックエンドの各Podに分配されます。これにより、サービスディスカバリーとロードバランシングのプロセスが実装されます。

インターネットからリクエストが送られてきた場合など、外部からのトラフィックがどのように処理されるかを見てみましょう。ロードバランサーでもある外部のクラウドコントローラーマネージャーが、本サービスの変更を聞き取った後にロードバランサーが設定されます。設定されたロードバランサーは、外部からのアクセスリクエストをノードのポートに転送し、ノードは、`kube-proxy`で設定されたiptablesに基づいて、リクエストをクラスタIPアドレスに転送します。その後、クラスターIPアドレスがバックエンドPodのIPアドレスにマッピングされ、最終的にこのリクエストが送信されます。これにより、サービスディスカバリーとロードバランシングのプロセスが実装されます。これがサービスディスカバリーのためのKubernetes Serviceのアーキテクチャです。

###高度なスキル
Kubernetes Serviceの実装方法と、Serviceのネットワークエラーの診断・修正方法について説明します。

#まとめ
この記事で学んだことをまとめてみましょう。

1、クラウドネイティブなシナリオにおけるサービスディスカバリーとロードバランシングの目的
2、サービスディスカバリーとロードバランシングのために[Kubernetes Service](https://www.alibabacloud.com/product/kubernetes?spm=a2c65.11461447.0.0.1582611b9btLAp)を実装する方法
3、サービスが使用するKubernetesクラスタ内のコンポーネントと、それらのコンポーネントがどのように動作するか。
この記事を読んだ後、Kubernetes Servicesを使用することで、複雑なエンタープライズレベルのアプリケーションを標準的かつ高速にオーケストレーションできるようになることを願っています。

*本ブログは英語版からの翻訳です。オリジナルは[こちら](https://www.alibabacloud.com/blog/getting-started-with-kubernetes-%7C-service-discovery-and-load-balancing-in-kubernetes_596289)からご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。*

*アリババクラウドは日本に２つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
[アリババクラウドジャパン公式ページ](https://www.alibabacloud.com/ja)*