Kubernetes1.10 チュートリアル (ステートレスアプリケーション, Pod間の接続)

# はじめに

Kubernetesの公式ページに、多くのチュートリアルが掲載されています。
Kubernetesの勉強の一環で、チュートリアルを消化していきます。
今回は statekess-applicationをKubernetes上で構築してみます。
https://kubernetes.io/docs/tutorials/stateless-application/guestbook/

なお、Kubernetesのバージョンは、2018年4月時点で最新の1.10を使用しています。

チュートリアルで構築するステートレスアプリケーションは、guestbookアプリケーションです。
ホテルの宿泊先にあるようなゲストブックのことなのでしょうか。
Webページを開くと、誰でもコメントを残すことが出来るシステムとなっています。



# Start up the Redis Master

## Creating the Redis Master Deployment

ゲストブックアプリケーションは、Redisを使用しています。Redisは、メモリを使用したデータ構造化ソフトウェアのようで、高速に key-value を読み書きできるもののようです。

ゲストブックアプリケーションは、データの書き込みにRedis Master Instanceを使用し、データの読み取りに複数のRedis Slave Instanceを使用します。

以下のYAMLマニフェストファイルを作成します。

```
cat <<'EOF' > /root/kube_yaml/guestbook/redis-master-deployment.yaml
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
  name: redis-master
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: k8s.gcr.io/redis:e2e  # or just image: redis
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
        ports:
        - containerPort: 6379
EOF
```

Deploymentを作成し、Redis Master を作成します。

```
kubectl apply -f /root/kube_yaml/guestbook/redis-master-deployment.yaml
```

Pod一覧を確認します。

```
[root@sugi-kubernetes110-master01 guestbook(default kubernetes-admin)]# kubectl get pods -o wide
NAME                            READY     STATUS    RESTARTS   AGE       IP            NODE
redis-master-55db5f7567-hkzhl   1/1       Running   0          5m        10.244.2.18   sugi-kubernetes110-node02.localdomain
```



## Creating the Redis Master Service

guestbook アプリケーションは、Redis Master を接続して、データを書き込む必要があります。
Redis Master Pod に サービス ClusterIP を作成して、アクセス可能な状態にします。
なお、ClusterIPは、クラスタ内でのみアクセス可能なサービスなので、外部からはアクセスできません。

以下のマニフェストファイルを作成します。

```
cat <<'EOF' > /root/kube_yaml/guestbook/redis-master-service.yaml
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
EOF
```

Serviceを作成します

```
kubectl apply -f /root/kube_yaml/guestbook/redis-master-service.yaml
```

Service一覧を確認します

```
[root@sugi-kubernetes110-master01 guestbook(default kubernetes-admin)]# kubectl get service -o wide
NAME           TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE       SELECTOR
kubernetes     ClusterIP   10.96.0.1        <none>        443/TCP    2d        <none>
redis-master   ClusterIP   10.110.158.172   <none>        6379/TCP   1m        app=redis,role=master,tier=backend
```



#  Start up the Redis Slaves

## Creating the Redis Slave Deployment

Redis Master のPodは1個ですが、RedisSlaveをいくつか立ち上げることにより、高可用性を実現することができます。

以下のマニフェストファイルを作成します。

```
cat <<'EOF' > /root/kube_yaml/guestbook/redis-slave-deployment.yaml
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
  name: redis-slave
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:v1
        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
EOF
```

Podを作成します。

```
kubectl apply -f /root/kube_yaml/guestbook/redis-slave-deployment.yaml
```

Podの一覧を確認します

```
[root@sugi-kubernetes110-master01 guestbook(default kubernetes-admin)]# kubectl get pods -o wide
NAME                            READY     STATUS    RESTARTS   AGE       IP            NODE
redis-master-55db5f7567-hkzhl   1/1       Running   0          26m       10.244.2.18   sugi-kubernetes110-node02.localdomain
redis-slave-584c66c5b5-59d5k    1/1       Running   0          5m        10.244.2.19   sugi-kubernetes110-node02.localdomain
redis-slave-584c66c5b5-fl2cc    1/1       Running   0          5m        10.244.1.20   sugi-kubernetes110-node01.localdomain
```

## Creating the Redis Slave Service

マニフェストファイルを作成します。

```
cat <<'EOF' > /root/kube_yaml/guestbook/redis-slave-service.yaml
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
EOF
```

ClusterIP のサービスを作成します

```
kubectl apply -f /root/kube_yaml/guestbook/redis-slave-service.yaml
```

Service一覧を確認します

```
[root@sugi-kubernetes110-master01 guestbook(default kubernetes-admin)]# kubectl get svc -o wide
NAME           TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE       SELECTOR
kubernetes     ClusterIP   10.96.0.1        <none>        443/TCP    3d        <none>
redis-master   ClusterIP   10.110.158.172   <none>        6379/TCP   29m       app=redis,role=master,tier=backend
redis-slave    ClusterIP   10.104.192.49    <none>        6379/TCP   11s       app=redis,role=slave,tier=backend
```



# Set up and Expose the Guestbook Frontend

## Creating the Guestbook Frontend Deployment

guestbookアプリケーションのweb frontend Podを作成します。Frontendは、PHP で書かれています。
Write Requestは、Redis Master に対して読み込みに行きます。Read Request は Redis Slave に読み込みにいきます。

以下のマニフェストファイルを作成します。

```
cat <<'EOF' > /root/kube_yaml/guestbook/frontend-deployment.yaml
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
  name: frontend
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
EOF
```

Podを作成します

```
kubectl apply -f /root/kube_yaml/guestbook/frontend-deployment.yaml
```

Podの一覧を確認します

```
kubectl get pods -o wide -l app=guestbook -l tier=frontend
```



## Creating the Frontend Service

Redis Master と Redis Slave に作成したServiceは、クラスタ内でのみアクセス可能なサービスのClusterIPです。マニフェストファイル内でServiceのTypeを指定しない場合、DefaultとしてClusterIPが作成されます。
クラスタ外部からアクセスしたい場合、ServiceTypeの NodePort と LoadBalancer の2パターンが存在します。オンプレミスの現在の環境では、LoadBalancerが使用できないので、NodePortを使用して Frontend Pod を公開します。
(MetalLBを使用するとオンプレミスでもLoadBalancerが使用できるらしい。)

以下のマニフェストファイルを作成します。
NodePort で公開します。

```
cat <<'EOF' > /root/kube_yaml/guestbook/frontend-service.yaml
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:![001.png](https://qiita-image-store.s3.amazonaws.com/0/104247/23a02a3d-d5fe-a6b0-c5ab-d4140861d492.png)

    app: guestbook
    tier: frontend
EOF
```

Serviceを作成します

```
kubectl apply -f /root/kube_yaml/guestbook/frontend-service.yaml
```

Service一覧を確認します
NodePortが作成されています

```
[root@sugi-kubernetes110-master01 guestbook(default kubernetes-admin)]# kubectl get service
NAME           TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
frontend       NodePort    10.107.155.181   <none>        80:32423/TCP   10s
kubernetes     ClusterIP   10.96.0.1        <none>        443/TCP        3d
redis-master   ClusterIP   10.110.158.172   <none>        6379/TCP       3h
redis-slave    ClusterIP   10.104.192.49    <none>        6379/TCP       2h
```

## NodePortにアクセス

NodePortは、Kubernetesクラスタのどのホストに対してもアクセスが可能です。
クラスタ外のWindowsなどのクライアントマシンから、以下のURLへアクセスします。
ポート番号の32423は、kubectl get serviceで表示したNodePortのPORT(s) 列から確認できます。

http://192.168.120.220:32423/

Messages に文字を入力して、Submit を押すと、ページに文字が追加されていきます。

![001.png](https://qiita-image-store.s3.amazonaws.com/0/104247/a0c2ad0e-947f-cccc-d484-e2cd63ec4efd.png)


## FrontendのPodにアクセスして、html, php, dns, 環境変数などを確認

Podの一覧を確認します

```
[root@sugi-kubernetes110-master01 ~(default kubernetes-admin)]# kubectl get pods
NAME                            READY     STATUS    RESTARTS   AGE
frontend-5c548f4769-rktkj       1/1       Running   0          3h
frontend-5c548f4769-whgcm       1/1       Running   0          3h
frontend-5c548f4769-zv6wv       1/1       Running   0          3h
redis-master-55db5f7567-hkzhl   1/1       Running   0          4h
redis-slave-584c66c5b5-59d5k    1/1       Running   0          4h
redis-slave-584c66c5b5-fl2cc    1/1       Running   0          4h
```

FrontendPodの1個に遠隔からbashを起動して、プロンプトにはいります

```
kubectl exec -it frontend-5c548f4769-rktkj bash
```

Index.htmlファイルを確認します
HTMLのボタンなどが、JavaScriptと連動しているような雰囲気を感じます

```
root@frontend-5c548f4769-rktkj:/var/www/html# cat /var/www/html/index.html 
<html ng-app="redis">
  <head>
    <title>Guestbook</title>
    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.12/angular.min.js"></script>
    <script src="controllers.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.13.0/ui-bootstrap-tpls.js"></script>
  </head>
  <body ng-controller="RedisCtrl">
    <div style="width: 50%; margin-left: 20px">
      <h2>Guestbook</h2>
    <form>
    <fieldset>
    <input ng-model="msg" placeholder="Messages" class="form-control" type="text" name="input"><br>
    <button type="button" class="btn btn-primary" ng-click="controller.onRedis()">Submit</button>
    </fieldset>
    </form>
    <div>
      <div ng-repeat="msg in messages track by $index">
        {{msg}}
      </div>
    </div>
    </div>
  </body>
</html>
root@frontend-5c548f4769-rktkj:/var/www/html# 
```

コンテナ内のJavaScriptファイルを確認します
同様に詳細はわかりませんが、PHPと連動している雰囲気を感じます

```
root@frontend-5c548f4769-rktkj:/var/www/html# cat /var/www/html/controllers.js 
var redisApp = angular.module('redis', ['ui.bootstrap']);

/**
 * Constructor
 */
function RedisController() {}

RedisController.prototype.onRedis = function() {
    this.scope_.messages.push(this.scope_.msg);
    this.scope_.msg = "";
    var value = this.scope_.messages.join();
    this.http_.get("guestbook.php?cmd=set&key=messages&value=" + value)
            .success(angular.bind(this, function(data) {
                this.scope_.redisResponse = "Updated.";
            }));
};

redisApp.controller('RedisCtrl', function ($scope, $http, $location) {
        $scope.controller = new RedisController();
        $scope.controller.scope_ = $scope;
        $scope.controller.location_ = $location;
        $scope.controller.http_ = $http;

        $scope.controller.http_.get("guestbook.php?cmd=get&key=messages")
            .success(function(data) {
                console.log(data);
                $scope.messages = data.data.split(",");
            });
});
root@frontend-5c548f4769-rktkj:/var/www/html# 
```

PHPファイルを確認します

```
root@frontend-5c548f4769-rktkj:/var/www/html# cat /var/www/html/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();
} ?>
root@frontend-5c548f4769-rktkj:/var/www/html# 
```

PHPの内容を抜粋して確認していきます
12行目を抜粋します。

```
  $host = 'redis-master';      <----------------------- FrontendPodからアクセスする RedisMaster を定義しています。これは、KubernetesのService名と同一の名称を指定しています
```

FrontendPodのresolv.confを確認します

```
root@frontend-5c548f4769-rktkj:/var/www/html# cat /etc/resolv.conf 
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local localdomain
options ndots:5
```

resolv.confで参照している 10.96.0.10 は、 kube-dns の ClusterIPサービスを参照しています

```
[root@sugi-kubernetes110-master01 manifests(default kubernetes-admin)]# kubectl get svc -o wide --all-namespaces
NAMESPACE     NAME           TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)         AGE       SELECTOR
default       frontend       NodePort    10.107.155.181   <none>        80:32423/TCP    2h        app=guestbook,tier=frontend
default       kubernetes     ClusterIP   10.96.0.1        <none>        443/TCP         3d        <none>
default       redis-master   ClusterIP   10.110.158.172   <none>        6379/TCP        5h        app=redis,role=master,tier=backend
default       redis-slave    ClusterIP   10.104.192.49    <none>        6379/TCP        4h        app=redis,role=slave,tier=backend
kube-system   kube-dns       ClusterIP   10.96.0.10       <none>        53/UDP,53/TCP   3d        k8s-app=kube-dns
```

FrontendPodから、redis-masterを名前解決してみます
redis-master.default.svc.cluster.local　は、サービス名.ネームスペース名.svc.cluster.local というルールで、自動的に kube-dns にレコードが付与されます。
10.110.158.172は、redis-masterの ClusterIP サービスが名前解決されています

```
root@frontend-5c548f4769-rktkj:/var/www/html# ping redis-master
PING redis-master.default.svc.cluster.local (10.110.158.172): 56 data bytes
```

FrontendPodの環境変数を確認します

```
root@frontend-5c548f4769-rktkj:~# env                    
REDIS_SLAVE_PORT_6379_TCP=tcp://10.104.192.49:6379
REDIS_SLAVE_SERVICE_HOST=10.104.192.49
HOSTNAME=frontend-5c548f4769-rktkj
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT=tcp://10.96.0.1:443
TERM=xterm
PHP_INI_DIR=/usr/local/etc/php
PHP_FILENAME=php-5.6.20.tar.xz
REDIS_SLAVE_PORT=tcp://10.104.192.49:6379
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_HOST=10.96.0.1
GET_HOSTS_FROM=dns
REDIS_MASTER_PORT_6379_TCP_ADDR=10.110.158.172
REDIS_MASTER_PORT_6379_TCP=tcp://10.110.158.172:6379
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
REDIS_SLAVE_PORT_6379_TCP_PROTO=tcp
GPG_KEYS=0BD78B5F97500D450838F95DFE857D9A90D90EC1 6E4F6AB321FDC07F2C332E3AC2BF0BC433CFC8B3
REDIS_MASTER_SERVICE_PORT=6379
PWD=/root
REDIS_SLAVE_SERVICE_PORT=6379
REDIS_MASTER_SERVICE_HOST=10.110.158.172
SHLVL=1
HOME=/root
REDIS_SLAVE_PORT_6379_TCP_ADDR=10.104.192.49
PHP_SHA256=2b87d40213361112af49157a435e0d4cdfd334c9b7c731c8b844932b1f444e7a
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_SERVICE_PORT_HTTPS=443
REDIS_MASTER_PORT_6379_TCP_PORT=6379
REDIS_SLAVE_PORT_6379_TCP_PORT=6379
REDIS_MASTER_PORT_6379_TCP_PROTO=tcp
PHP_EXTRA_BUILD_DEPS=apache2-dev
REDIS_MASTER_PORT=tcp://10.110.158.172:6379
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
PHP_VERSION=5.6.20
PHP_EXTRA_CONFIGURE_ARGS=--with-apxs2
_=/usr/bin/env
OLDPWD=/etc
```

上記の環境変数の中で、以下を抜粋します。
Kubernetesで作成したServiceは、Kubeletが自動的に環境変数に作成します。
https://kubernetes.io/docs/concepts/services-networking/service/#environment-variables

注意点として、Pod作成時点のServiceのみ環境変数へ定義するので、
Pod作成後に作成したServiceは環境変数には定義されません。kube-dnsは、動的に追加されるので、環境変数よりはDNSを使用するのが良いと思います

```
root@frontend-5c548f4769-rktkj:~# env | grep REDIS_MASTER
REDIS_MASTER_PORT_6379_TCP_ADDR=10.110.158.172
REDIS_MASTER_PORT_6379_TCP=tcp://10.110.158.172:6379
REDIS_MASTER_SERVICE_PORT=6379
REDIS_MASTER_SERVICE_HOST=10.110.158.172
REDIS_MASTER_PORT_6379_TCP_PORT=6379
REDIS_MASTER_PORT_6379_TCP_PROTO=tcp
REDIS_MASTER_PORT=tcp://10.110.158.172:6379
```

## RedisMasterでRedis上のデータを確認

RedisMasterPodにbashログインします

```
[root@sugi-kubernetes110-master01 ~(default kubernetes-admin)]# kubectl exec -it redis-master-55db5f7567-hkzhl bash
[ root@redis-master-55db5f7567-hkzhl:/data ]$ 
```

redis-cli を起動し、全てのkeyを確認します
messagesが表示されました

```
[ root@redis-master-55db5f7567-hkzhl:/data ]$ redis-cli 
127.0.0.1:6379> 
127.0.0.1:6379> keys '*'
1) "messages"
```

get コマンドで messages に対応する value を確認します

```
127.0.0.1:6379> get messages
",test_one,aaa"
```

上記のvalueがWebページで表示されていることがわかります

![001.png](https://qiita-image-store.s3.amazonaws.com/0/104247/a0c2ad0e-947f-cccc-d484-e2cd63ec4efd.png)


# cleaning up

作成したDeployment, Serviceを削除します

```
kubectl delete deployment frontend redis-master redis-slave
```



# 参考URL

チュートリアルページ
Example: Deploying PHP Guestbook application with Redis
https://kubernetes.io/docs/tutorials/stateless-application/guestbook/