パソコン上のK8sクラスタでGuestBookチュートリアルを実施する

CNCF Kubernetesドキュメントのチュートリアル [Example: Deploying PHP Guestbook application with Redis](https://kubernetes.io/docs/tutorials/stateless-application/guestbook/)をパソコン上のアップストリーム Kubenetesクラスタで実施しました。このパソコン上の環境は、[15Stepで習得 Dockerから入るKubernetes コンテナ開発からK8s本番運用まで](https://kubernetes.io/docs/tutorials/stateless-application/expose-external-ip-address/)の学習環境2です。もちろん、[IBM Cloud OpenShift](https://cloud.ibm.com/docs/openshift?topic=openshift-getting-started&locale=ja) で、動かして試すこともできる。

この記事では、チュートリアルには書いていないポイントや、パソコン上のオンプレミス環境で動作せる場合の違いについて、書いてみたい。そのため、チュートリアルと合わせて参照することをお勧めしたい。

本記事で利用した学習用Kubenetes環境は、https://github.com/takara9/vagrant-kubernetes を使用している.これは macOS、Windows10、Linuxなどの環境でも利用できるので、パブリック・クラウドを利用するだけでは解らない裏側の様子も垣間見ることができ、オンプレミスでベンダー依存を制御した環境を構築するスキル獲得にも役立つと思う。


## チュートリアルの構成

このチュートリアルで構築する構成は、次の図のようになる。

![configuration.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42829/d4e932fe-0bbf-2297-b839-b0c3b237562d.png)


ブラウザからFrontendのURLをアクセスして、メッセージをサブミットすると、PHPのアプリケーションは、redis-masterを参照して、Redisマスターサーバーにデータが書き込まれる。また、FrontendのURLを再読みするなど、参照だけを実行した場合、PHPのアプリケーションは、サービス redis-slaveを参照して Redis スレーブサーバーからデータを取得する。

ウェブアプリケーションの参照と書込みの比率は、８対２などと見做されるので、参照トラフィックの負荷をRedisスレーブで処理して、書込みトラフィックはRedisマスターでうけるという構成が出来ている。

障害対策の点では、frontend、redis-master、redis-slave の各アプリケーションのポッドは、それぞれのデプロイメント・コントローラーで、起動数を監視されており、もし、ポッド停止するなどの異常が発生した場合、自己回復を実行する。

負荷対策の点では、frontendのアプリケーションは、ポッド数を増加させることで、アクセス増に対して処理能力を向上できる。これに対応するように、参照トラフィック用のredis-slaveの数を増加させる事ができる。redis-masterとredis-slaveのポッドは、redis-slaveの設定によって、redis-masterをマスターサーバーとして、データを同期するように設定されているため、redis-slaveのポッドを増やすだけで、参照側の性能を向上させることができる。

最終的にredis-masterのポッド（コンテナ）が全体の性能ネックになるが、redis-masterのポッド内のコンテナに与えるCPUやメモリ量を増やす対策（スケールアップ）によって対策することになる。


## K8s構築コードの取得と修正

[15Stepで習得 Dockerから入るKubernetes コンテナ開発からK8s本番運用まで](https://kubernetes.io/docs/tutorials/stateless-application/expose-external-ip-address/)の学習環境2を利用する。

筆者が、最近リリースされたばかりの 1.17.0 で動作を試したところ、同一ノード上のredisマスターとスレーブ間で、データ同期中にタイムアウトが発生するという問題があったので、今回は、本書の内容と同じ、バージョン 1.14 を利用する。もちろん、1.14のサポート終了が迫っているため、継続して、1.17での原因と対策を追跡していきたい。

注）1.17.0 では動作不良を起こしていたが、その後にリリースされた1.17.1では問題が発生しなかった。そこでGitHubのブランチ 1.17 のバージョンは1.17.1以降を利用する。

~~~console:
tkr@luigi:~$ git clone https://github.com/takara9/vagrant-kubernetes k8s-1.14
tkr@luigi:~$ cd k8s-1.14
~~~

この作業は、オプションで、パソコン外部からアクセスしたい場合に、次のような方法で対処できる。IPアドレスは、パソコンと同じネットワークアドレスと同じである必要がある。パソコン上のブラウザでアクセスに限定する際には、この作業は必要ない。

Vagrantfileの13行目と41行目のコメントマークを外して、パソコン側のIPアドレスを有効化する。これでパソコンの外部からK8sクラスタへアクセスできる。

~~~file:Vagrantfile
<中略>
    11	    machine.vm.hostname = 'node1'
    12	    machine.vm.network :private_network,ip: "172.16.20.12"
    13	    machine.vm.network :public_network, ip: "192.168.1.92", bridge: "en0: Ethernet"
    14	    machine.vm.provider "virtualbox" do |vbox|
<中略>
    39	    machine.vm.hostname = 'node2'
    40	    machine.vm.network :private_network,ip: "172.16.20.13"
    41	    machine.vm.network :public_network, ip: "192.168.1.93", bridge: "en0: Ethernet"
    42	    machine.vm.provider "virtualbox" do |vbox|
~~~


以下のコマンドで、K8sクラスタを起動する。

~~~
tkr@luigi:~/k8s-1.14$ vagrant up
~~~

環境変数をセットして、起動が完了したら、`kubectl get node`を実行して動作を確認する。

~~~console:
tkr@luigi:~/k8s-1.14$ export KUBECONFIG=`pwd`/kubeconfig/config
tkr@luigi:~/k8s-1.14$ kubectl get node
NAME     STATUS   ROLES    AGE   VERSION
master   Ready    master   12m   v1.14.3
node1    Ready    <none>   12m   v1.14.3
node2    Ready    <none>   12m   v1.14.3
~~~



## アプリ専用の名前空間作成と切替

チュートリアルには無い作業であるが、他のアプリケーション環境と混ざらないように、専用の名前空間(Namespace)を作成して、チュートリアルのアプリケーションの実行環境を分離する。

ゲストブック専用の名前空間 guestbook を作成する。

~~~console:
tkr@luigi:~/k8s-1.14$ kubectl create ns guestbook
~~~

名前空間を切り替える為に、コンテキストを作成する。

~~~console:
tkr@luigi:~/k8s-1.17$ kubectl config set-context gb --namespace=guestbook --cluster=kubernetes --user=kubernetes-admin
~~~

コンテキストが出来たことを確認する。

~~~console:
tkr@luigi:~/k8s-1.17$ kubectl config get-contexts
CURRENT   NAME                          CLUSTER      AUTHINFO           NAMESPACE
          gb                            kubernetes   kubernetes-admin   guestbook
*         kubernetes-admin@kubernetes   kubernetes   kubernetes-admin   
~~~

次のコマンドでは、コンテキストの切替を固定的にできる。これで、名前空間 guestbook がデフォルトの名前空間になる。

~~~console:
tkr@luigi:~/k8s-1.17$ kubectl config use-context gb
Switched to context "gb".
~~~

現在、自分が、どのコンテキストを利用しているか、確認する。 NAMESPACE列 gustbookになっていればOKである。

~~~console:
tkr@luigi:~/k8s-1.17$ kubectl config get-contexts
CURRENT   NAME                          CLUSTER      AUTHINFO           NAMESPACE
*         gb                            kubernetes   kubernetes-admin   guestbook
          kubernetes-admin@kubernetes   kubernetes   kubernetes-admin 
~~~


## Redisマスターの起動

ここからは、[Example: Deploying PHP Guestbook application with Redis](https://kubernetes.io/docs/tutorials/stateless-application/guestbook/) に従うので、重複を出来るだけ避けて進める。そのため、本家のページを合わせて参照すると、良くわかるとおもう。

Redisのマスターサーバーをデプロイメントによって起動して、サービスを設定する。 

~~~console:
tkr@luigi:~/k8s-1.14$ kubectl apply -f https://k8s.io/examples/application/guestbook/redis-master-deployment.yaml
~~~

~~~console:
tkr@luigi:~/k8s-1.14$ kubectl apply -f https://k8s.io/examples/application/guestbook/redis-master-service.yaml
~~~


## Redisスレーブの起動

Redisのマスターからデータを同期するスレーブを作成する。同様にサービスも作成する。

~~~console:
tkr@luigi:~/k8s-1.14$ kubectl apply -f https://k8s.io/examples/application/guestbook/redis-slave-deployment.yaml
~~~

~~~console:
tkr@luigi:~/k8s-1.14$ kubectl apply -f https://k8s.io/examples/application/guestbook/redis-slave-service.yaml
~~~

このマニフェストの書かれたコンテナ・イメージの作成は、次のGitリポジトリにあるので、アプリケーションや設定を変更することもできる。https://github.com/kubernetes/examples/tree/master/guestbook

Redisスレーブのコンテナは、次の起動スクリプトで起動される。elseの下が通常使われる起動コマンドで、`redis-master`は前述のマスターのサービス名となる。

~~~file:run.sh
if [[ ${GET_HOSTS_FROM:-dns} == "env" ]]; then
  redis-server --slaveof ${REDIS_MASTER_SERVICE_HOST} 6379
else
  redis-server --slaveof redis-master 6379
fi
~~~

`redis-master`は、K8sの内部DNSによって、IPアドレスが解決され、Redisマスターにアクセスできる。

~~~console:
tkr@luigi:~/k8s-1.14$ kubectl get svc
NAME           TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
redis-master   ClusterIP   10.32.0.252   <none>        6379/TCP       3h10m
redis-slave    ClusterIP   10.32.0.34    <none>        6379/TCP       3h10m
~~~

Redisマスターが止まってしまった場合、このチュートリアルの構成では、スレーブのマスターへの昇格は、自動では無いので、SREとして対応が必要となる。このチュートリアルでは、Redisに永続ストレージを利用していない為、ポッドの停止と共に、データは失われる。


## フロントエンドの起動

最後にフロント側のアプリケーションを起動して、サービスをデプロイして、外部からアクセスできるようにする。これでパソコンのブラウザからアクセスできるようになる。

~~~console:
tkr@luigi:~/k8s-1.14$ kubectl apply -f https://k8s.io/examples/application/guestbook/frontend-deployment.yaml
~~~

~~~console:
tkr@luigi:~/k8s-1.14$ kubectl apply -f https://k8s.io/examples/application/guestbook/frontend-service.yaml
~~~

フロントエンドのサービスのタイプは、NodePortとして公開する。ノードのIPアドレスにNodePortのポート番号でアクセスができる。


~~~console:
tkr@luigi:~/k8s-1.14$ kubectl get svc
NAME           TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
frontend       NodePort    10.32.0.178   <none>        80:31767/TCP   3h6m
redis-master   ClusterIP   10.32.0.252   <none>        6379/TCP       3h10m
redis-slave    ClusterIP   10.32.0.34    <none>        6379/TCP       3h10m
~~~

このアプリケーションは、PHPで開発されており、Gitリポジトリに、コードが置かれてる。https://github.com/kubernetes/examples/tree/master/guestbook/php-redis

以下のPHPのコードは、インプットをRedisマスターに書き込み、参照時は スレーブから読み取るように作られている。 読み込みと書き込みの切替は、`controllers.js` からのパラメータを受け取りPHP側の if文で分岐する。 そして、else内の`redis-slave`は、複数のRedisスレーブへのリクエスト分散を行うため、スレーブとして起動されたポッドのIPアドレスがランダムの変換される。

~~~file:guestbook.php
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
require 'Predis/Autoloader.php';
Predis\Autoloader::register();
if (isset($_GET['cmd']) === true) {
  $host = 'redis-master';
  if (getenv('GET_HOSTS_FROM') == 'env') {
    $host = getenv('REDIS_MASTER_SERVICE_HOST');
  }
  header('Content-Type: application/json');
  if ($_GET['cmd'] == 'set') {
    $client = new Predis\Client([
      'scheme' => 'tcp',
      'host'   => $host,
      'port'   => 6379,
    ]);
    $client->set($_GET['key'], $_GET['value']);
    print('{"message": "Updated"}');
  } else {
    $host = 'redis-slave';
    if (getenv('GET_HOSTS_FROM') == 'env') {
      $host = getenv('REDIS_SLAVE_SERVICE_HOST');
    }
    $client = new Predis\Client([
      'scheme' => 'tcp',
      'host'   => $host,
      'port'   => 6379,
    ]);
    $value = $client->get($_GET['key']);
    print('{"data": "' . $value . '"}');
  }
} else {
  phpinfo();
} ?>
~~~


## アクセステスト

ブラウザから、ノード１192.168.1.92 または ノード２192.168.1.93 などのIPアドレスに、`kubectl get svc`で表示されるポート番号を合わせたURL 例えば、`http://192.168.1.92:31767` をアクセスすることで、ゲストブックの入力フィールドが表示される。入力フィールドにメッセージをインプットして、「Submit」をクリックすることで、Redisへデータを書き込む。

![guestbook.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42829/2f909947-ac46-417d-a044-0b3df9d207e1.png)

このアプリケーションは、マルチバイト文字に対応していませんから、再読み込みで、日本語表示は正しく表示されなくなります。

## まとめ

このチュートリアルでは、簡単なYAMLファイルを適用するだけで、ポッド数を増減させることによりスケール、デプロイメント・コントローラーによるポッドの起動で可用性を体験することができた。このKubernetesの特徴を活かすためには、適したアプリケーションの設計が必要であることも、理解できるとおもう。

本来 redisは、永続データを保持するステートフルなアプリケーションであるため、このチュートリアルの構成だけで本番適用は難しいが、この構成に付け足して行けば良い。