tcpSocket設定で覚えるKubernetesとOpenShiftのヘルスチェック機能 (ソケットサーバーPodをビルド&デプロイ)

本記事は[Kubernetes2 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/kubernetes2) 11日目のエントリになります。
筆者は普段OpenShiftばかり触っているため、たまにKubernetesを触ると「あれ？あのコマンドないのか…！」みたいなことがよく起きます💦

参考図書: [Kubernetes完全ガイドの「10.1 ヘルスチェック」と「6.5 NodePort Service」](https://www.amazon.co.jp/dp/B07HFS7TDT)

あと、[記事中に出てくるサンプルスクリプトやマニフェストはGitHubにも上げています](https://github.com/zaki-lknr/tcp-probe-example)のでご参考ください。

# Kubernetesのヘルスチェックについて

Kubernetesが標準で使える2種類のヘルスチェック機能についておさらい。

## liveness probeとreadiness probe

|種類|説明|失敗時の動作|
|---|---|---|
|liveness probe|生きているかどうか|対象コンテナを破棄して再作成|
|readiness probe|ready状態かどうか|対象Podに対する外部(Service)からのトラフィックの対象外となり、成功するまで待つ|

Kubernetesにはこの2種類のヘルスチェック機能が標準で使用できるようになっており、正しく設定しておけばアプリケーションPodの異常時には自動で回復するようになっています。(オートヒーリング)

平たく言うと、livenessは「なんか調子悪いんで再起動(正確には再作成)しとこ」で、readinessは「準備中(しばらく待てば動くようになる)」です。

livness probeでヘルスチェック失敗時の動作は以下の通りで、エラーになったコンテナは破棄・新しいコンテナがデプロイされます。(図はpodになってるけど、心の目でコンテナに置き換えてください🙇)

![liveness probe](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256859/cf6addea-5a29-efa6-530d-ed9d1bb63bf4.png)

一方、readiness probeのヘルスチェック失敗時は、エラーになっているコンテナのPodへのトラフィックが流れないところまでは同じですが、エラーが解消されるまでコンテナは破棄されません。

この2種類のヘルスチェックについて、それぞれ以下の3つの方式でチェックが可能です。

|方式|機能|失敗判定|
|---|---|---|
|`exec`|コンテナ内で任意のコマンドを実行する|リターンコードが非0|
|`httpGet`|HTTP GETを実行(HTTPなアプリケーションPod前提)|レスポンスコードが200,300番台でない|
|`tcpSocket`|任意のポートへTCP接続する|接続失敗時|

本記事では情報量的にマイナー…のような感じのするtcpSocketを使ってヘルスチェックの動きについて説明します。

※ ちなみにhttpGetもtcpSocketも、デフォルトの対象は自コンテナだけど`host`指定で任意のホストに対する処理に変更可能。アプリケーションPodのヘルスチェック設定で、リモートのDBにTCP接続できるかをreadiness設定…など

## tcpSocket

任意のコマンドを実行する`exec`と、指定URLにHTTPアクセス可能か確認する`httpGet`を使ったヘルスチェックは、サンプルも多く確認も簡単だしわざと失敗させてエラーの検知が正しく行われるか・元に戻して回復するかも簡単にできると思います。

対して、自コンテナに対する`tcpSocket`を使ったヘルスチェックって、エラーを起こしてちゃんとヘルスチェックが動くかの確認・解消時に元に戻る回復性の確認、なかなか難しいです。
というのも、プロセスは動作させたままTCPのLISTENだけclose/openさせる必要があるので。

podで動かすアプリケーションがその動作をするかどうかはアプリケーション側で作りこむしかないと思いますが、単体テストのレベルでKubernetesのこの機能がちゃんと動くかは簡単なソケットサーバーを作れば確認できます。

担当プロジェクトの試験項目にこんな感じの(Kubernetes自体の)動作確認があったので、お試しでスクリプト書いてみました。
(今考えると、自コンテナでなくリモートホストにtcpSocket設定するのでもよかったかも…？)

# 指定ポートでLISTENするソケットサーバー

使い慣れた母国語で書けば良いと思います。(GoとかPythonとか流行りの言語はありますが)私はとりあえずPerlで…

```perl:sockserv.pl
#!/usr/bin/perl

use IO::Socket;
use strict;

my $server = new IO::Socket::INET(
    Listen => 3,
    LocalAddr => '0.0.0.0',
    LocalPort => 8080,
    Proto => 'tcp',
    Reuse => 1
    );

die "IO::Socket $!" unless $server;

for (;;) {
    if (my $client = $server->accept()) {
        print $client "カレーは粉でできてるのでカロリーゼロ\n";
        close $client;
    }
    print "disconected...\n";
}
close $server;
exit;
```

ソケットサーバーのコア部分はこんな感じ。
そんなにトリッキーなコードはないのでなんとなくでも分かるはず…
リモートから8080/TCP接続があると、カレーの秘密のメッセージを返して切断し次の接続を待つ、という動作をします。(停止は`Ctrl-c`)

# 外部からのアクセスでLISTENするポートを動的に変更する処理を追加

ヘルスチェックでエラーになる状態から、正常状態に戻せるように処理を実装します。
だいたいこんな感じ。

RESTっぽくGETリクエストで`/port/5000`(数字の部分は可変)にアクセスがあれば、その時点で5000/TCPにLISTENするポートを切り替えるという動作をするスクリプトです。

```perl:sockserv.pl
#!/usr/bin/perl

use IO::Socket;
use strict;
$| = 1;

my $next_port = 8080;

for (;;) {
    my $server = new IO::Socket::INET(
        Listen => 1,
        LocalAddr => '0.0.0.0',
        LocalPort => $next_port,
        Proto => 'tcp',
        Reuse => 1
        );

    die "IO::Socket $!" unless $server;
    print "[DBG] server listening ... 0.0.0.0:$next_port\n";

    if (my $client = $server->accept()) {
        print "[DBG] accept from " . $client->peerhost() . " !\n";
        my $new_port;
        while (<$client>) {
            last if /^\x0d\x0a?$/;
            if (m{^GET /port/(\d+)\s}) {
                $new_port = $1;
            }
        }
        my $body;
        if ($new_port) {
            $body = "Change to New Port: $new_port.\n";
            $next_port = $new_port;
        }
        else {
            $body = "Running Socket Server.\n";
        }
        my $len = length $body;
        print $client <<__EOL__;
HTTP/1.1 200 OK
Content-Type: text/plain;charset=ISO-8859-1
Content-Length: $len

$body
__EOL__
        close $client;
        print "[DBG] disconnected from " . $client->peerhost() .".\n";
    }
    close $server;
}
exit;
```

実行すると最初は8080/TCPでLISTENし、常に200を返すHTTPを喋るサーバーとして動作します。
リクエストパスが`/port/数字`だった場合に限り、その数字でLISTENするように動作が変化します。
ポート番号に対するエラーハンドリングはやってません(70000とか)。

エラーで死んだりPodを削除したりして再デプロイすると、また初期値である8080/TCPでLISTENします。

# probeの動きと回復性について確認

本題のヘルスチェック機能を追加したデプロイ手順です。
Podのデプロイそのものについては、[おまけ以降](#%E4%BB%A5%E4%B8%8B%E3%81%8A%E3%81%BE%E3%81%91)を参照してください。

## readinessProbe

ヘルスチェックがパスするまで待機するreadinessProbeを例にDeploymentに以下の設定を追加します。
この設定で「8080/TCPへの接続に失敗したらヘルスチェック失敗」となり、PodがReady状態でなくなります。

```yaml
apiVersion: apps/v1
kind: Deployment
:
:
spec:
  template:
    spec:
      containers:
      - image: sockserv:v1.0
        name: sockserv
      :
      :
        readinessProbe:
          failureThreshold: 3
          periodSeconds: 10
          successThreshold: 1
          tcpSocket:
            port: 8080
          timeoutSeconds: 1
      :
      :
```

だいたいこんな感じ。
この設定が入るとPodにも同じ設定が含まれる。
(Deploymentが設定変更されることによってPodは再デプロイされる)

Podが正常動作(8080/TCPでLISTEN)していれば、以下の通り普通の出力(実行中コンテナの数であるREADYが1/1になる)。

```
[zaki@minikube socket-probe]$ kubectl get pod -o wide
NAME                       READY   STATUS    RESTARTS   AGE   IP           NODE       NOMINATED NODE   READINESS GATES
sockserv-87895fcf4-cn4qx   1/1     Running   0          15m   172.17.0.5   minikube   <none>           <none>
```

また、Serviceをdescribeすると

```
[zaki@minikube socket-probe]$ kubectl describe svc sockserv
Name:                     sockserv
Namespace:                default
Labels:                   app=sockserv
Annotations:              kubectl.kubernetes.io/last-applied-configuration:
                            {"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"labels":{"app":"sockserv"},"name":"sockserv","namespace":"default"},"spe...
Selector:                 app=sockserv
Type:                     NodePort
IP:                       10.106.177.34
Port:                     8080-tcp  8080/TCP
TargetPort:               8080/TCP
NodePort:                 8080-tcp  30120/TCP
Endpoints:                172.17.0.5:8080
Port:                     8081-tcp  8081/TCP
TargetPort:               8081/TCP
NodePort:                 8081-tcp  32737/TCP
Endpoints:                172.17.0.5:8081
Session Affinity:         None
External Traffic Policy:  Cluster
Events:                   <none>
```

この通り、EndpointsのところにPodのIPアドレスが表示され、このServiceリソースはPodへのトラフィックを流す設定を持っていることがわかります。

ではこのPod(コンテナ)のPerlスクリプトに8080/TCPでなく8081/TCPでLISTENするよう動作変更するよう`curl`を実行します。

### readinessProbeを失敗させる

```
[zaki@minikube socket-probe]$ curl http://192.168.0.82:30120/port/8081
Change to New Port: 8081.
```

これで8080/TCPでLISTENしなくなりました。

```
[zaki@minikube ~]$ kubectl describe pod sockserv-87895fcf4-cn4qx
Name:         sockserv-87895fcf4-cn4qx
Namespace:    default
Priority:     0
Node:         minikube/192.168.0.82

[...snip...]

Events:
  Type     Reason     Age        From               Message
  ----     ------     ----       ----               -------
  Normal   Scheduled  <unknown>  default-scheduler  Successfully assigned default/sockserv-87895fcf4-cn4qx to minikube
  Normal   Pulled     26m        kubelet, minikube  Container image "sockserv:v1.0" already present on machine
  Normal   Created    26m        kubelet, minikube  Created container sockserv
  Normal   Started    26m        kubelet, minikube  Started container sockserv
  Warning  Unhealthy  5s         kubelet, minikube  Readiness probe failed: dial tcp 172.17.0.5:8080: connect: connection refused
[zaki@minikube ~]$
```

ヘルスチェックに失敗して`Unhealthy`がイベントに出力されます。(この状態がつづけばずっと出続けます…永遠？そういえば確認してない…)

Podの状態も、総コンテナ数1に対して、READY状態のコンテナが0になっています。

```
[zaki@minikube ~]$ kubectl get pod
NAME                       READY   STATUS    RESTARTS   AGE
sockserv-87895fcf4-cn4qx   0/1     Running   0          27m
[zaki@minikube ~]$
```

このとき、(出力を取得し忘れましたが)`oc get rs`でReplicaSetの状態を見ると、DESIREDとCURRENTは設定どおり(replica=3なら3)ですが、READYの値はヘルスチェックでエラーになったPodの分が減ります(replica=3でエラー1なら2になる)。

ServiceのEndpoint状態も、対象Podがreadyでないため空欄になります。
※ 空欄はサービス断の状態なので、実際の運用ではPodのスケールを複数にしておき、エラーが発生したPodがいても他のPodでサービス継続できるようにしておく。

```
[zaki@minikube ~]$ kubectl describe svc sockserv
Name:                     sockserv
Namespace:                default
Labels:                   app=sockserv
Annotations:              kubectl.kubernetes.io/last-applied-configuration:
                            {"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"labels":{"app":"sockserv"},"name":"sockserv","namespace":"default"},"spe...
Selector:                 app=sockserv
Type:                     NodePort
IP:                       10.106.177.34
Port:                     8080-tcp  8080/TCP
TargetPort:               8080/TCP
NodePort:                 8080-tcp  30120/TCP
Endpoints:
Port:                     8081-tcp  8081/TCP
TargetPort:               8081/TCP
NodePort:                 8081-tcp  32737/TCP
Endpoints:
Session Affinity:         None
External Traffic Policy:  Cluster
Events:                   <none>
[zaki@minikube ~]$
```

ということで現在Pod内では8081/TCPでLISTENしてるわけだけど、Serviceを使ったヘルスチェックに失敗しているPodへの通信ができないので、ノードOS上からPodのIPアドレスへ直接`curl`します。
(※ `kubectl exec -it <pod-name> bash`でシェルログインして`curl`の方がよかったかも)

```
[zaki@minikube socket-probe]$ curl http://172.17.0.5:8081
Running Socket Server.
```

確かに8081/TCPでLISTENしてる。

ではポート変更。
これでまた8080/TCPがLISTENし、ヘルスチェックにパスできるようにします。

```
[zaki@minikube socket-probe]$ curl http://172.17.0.5:8081/port/8080
Change to New Port: 8080.
[zaki@minikube socket-probe]$ curl http://172.17.0.5:8080
Running Socket Server.
[zaki@minikube socket-probe]$
```

Podの状態

```
[zaki@minikube ~]$ kubectl get pod
NAME                       READY   STATUS    RESTARTS   AGE
sockserv-87895fcf4-cn4qx   1/1     Running   0          33m
```

Serviceの状態

```
[zaki@minikube ~]$ kubectl describe svc sockserv
Name:                     sockserv
Namespace:                default
Labels:                   app=sockserv
Annotations:              kubectl.kubernetes.io/last-applied-configuration:
                            {"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"labels":{"app":"sockserv"},"name":"sockserv","namespace":"default"},"spe...
Selector:                 app=sockserv
Type:                     NodePort
IP:                       10.106.177.34
Port:                     8080-tcp  8080/TCP
TargetPort:               8080/TCP
NodePort:                 8080-tcp  30120/TCP
Endpoints:                172.17.0.5:8080
Port:                     8081-tcp  8081/TCP
TargetPort:               8081/TCP
NodePort:                 8081-tcp  32737/TCP
Endpoints:                172.17.0.5:8081
Session Affinity:         None
External Traffic Policy:  Cluster
Events:                   <none>
[zaki@minikube ~]$
```

元に戻りました。

## livenessProbe

もう一つのヘルスチェック機能であるlivenessProbeを設定してみます。
※ この記事ではreadinessProbeと別々に設定してますが、両方同時に設定ももちろん可能です。

書式はreadinessと同じ。

```yaml
apiVersion: apps/v1
kind: Deployment
:
:
spec:
  template:
    spec:
      containers:
      - image: sockserv:v1.0
        name: sockserv
      :
      :
        readinessProbe:
          failureThreshold: 3
          periodSeconds: 10
          successThreshold: 1
          tcpSocket:
            port: 8080
          timeoutSeconds: 1
      :
      :
```

これで「8080/TCPへの接続に失敗したらヘルスチェック失敗(失敗したコンテナは破棄して再作成)」という動きになります。

では早速確認しましょう。
正常時の動作は前述readinessProbeと同じなので割愛。

### livenessProbeを失敗させる

```
[zaki@minikube socket-probe]$ curl http://192.168.0.82:30120/port/8081
Change to New Port: 8081.
```

これで8080/TCPでLISTENしなくなりました。
しばらく待つと

```
[zaki@minikube socket-probe]$ kubectl get pod -w
NAME                        READY   STATUS    RESTARTS   AGE
sockserv-687b757d7d-7dwbr   1/1     Running   0          7m5s
sockserv-687b757d7d-7dwbr   1/1     Running   1          7m48s

```

livenessProbeによるヘルスチェックに失敗してコンテナが再作成され、RESTARTSが1になっています。
Podのイベントを確認すると、確かに`Liveness probe failed`というイベントが記録されており、`x3`(3回…`failureThreshold: 3`の値)ののちに再作成されていることがわかります。

```
[zaki@minikube ~]$ kubectl get pod
NAME                        READY   STATUS    RESTARTS   AGE
sockserv-687b757d7d-7dwbr   1/1     Running   1          9m7s
[zaki@minikube ~]$ kubectl describe pod sockserv-687b757d7d-7dwbr

:
:

Events:
  Type     Reason     Age                   From               Message
  ----     ------     ----                  ----               -------
  Normal   Scheduled  <unknown>             default-scheduler  Successfully assigned default/sockserv-687b757d7d-7dwbr to minikube
  Warning  Unhealthy  114s (x3 over 2m14s)  kubelet, minikube  Liveness probe failed: dial tcp 172.17.0.4:8080: connect: connection refused
  Normal   Killing    114s                  kubelet, minikube  Container sockserv failed liveness probe, will be restarted
  Normal   Pulled     84s (x2 over 9m12s)   kubelet, minikube  Container image "sockserv:v1.0" already present on machine
  Normal   Created    84s (x2 over 9m12s)   kubelet, minikube  Created container sockserv
  Normal   Started    84s (x2 over 9m12s)   kubelet, minikube  Started container sockserv
```

元通り、30120(Pod内は8080)で`curl`がアクセスできます。

```
[zaki@minikube ~]$ curl http://192.168.0.82:30120
Running Socket Server.
```

これで、livenessProbeもPod異常時はコンテナ再作成を自動でやってくれることが確認できます。

---

# 以下、おまけ

使用したPodのビルドとデプロイ手順。
環境はMinikubeとCodeReady Container(OpenShift)の2パターンで確認。

```
[zaki@minikube ~]$ minikube version
minikube version: v1.5.2
commit: 792dbf92a1de583fcee76f8791cff12e0c9440ad-dirty
[zaki@minikube ~]$ kubectl version
Client Version: version.Info{Major:"1", Minor:"16", GitVersion:"v1.16.3", GitCommit:"b3cbbae08ec52a7fc73d334838e18d17e8512749", GitTreeState:"clean", BuildDate:"2019-11-13T11:23:11Z", GoVersion:"go1.12.12", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"16", GitVersion:"v1.16.2", GitCommit:"c97fe5036ef3df2967d086711e6c0c405941e14b", GitTreeState:"clean", BuildDate:"2019-10-15T19:09:08Z", GoVersion:"go1.12.10", Compiler:"gc", Platform:"linux/amd64"}
[zaki@minikube ~]$
```

```
[zaki@codeready ~]$ crc version
crc version: 1.1.0+95966a9
OpenShift version: 4.2.2 (embedded in binary)
[zaki@codeready ~]$ eval $(crc oc-env)
[zaki@codeready ~]$ oc version
Client Version: v4.3.0
Server Version: 4.2.2
Kubernetes Version: v1.14.6+868bc38
[zaki@codeready ~]$
```

[CRC(on Windows)についてはOpenShiftアドカレで書いた記事もみてねー](https://qiita.com/zaki-lknr/items/ac2223152661886438da)

# build image by Docker

みんな大好きDockerの説明は…大丈夫ですよね

## イメージ確認

```
[zaki@minikube socket-probe]$ sudo docker pull perl
Using default tag: latest
Trying to pull repository docker.io/library/perl ...
latest: Pulling from docker.io/library/perl
16ea0e8c8879: Pull complete
50024b0106d5: Pull complete
ff95660c6937: Pull complete
9c7d0e5c0bc2: Pull complete
29c4fb388fdf: Pull complete
5fec9b86b1a9: Pull complete
e78b734c5532: Pull complete
Digest: sha256:89b9d23c03a95d4f7995e4fbcc4811cf0286f93338aca9407ec1ff525e325b73
Status: Downloaded newer image for docker.io/perl:latest
[zaki@minikube socket-probe]$
```

```
[zaki@minikube socket-probe]$ sudo docker run -it --rm perl bash
root@39dc4cb70047:/# pwd
/
root@39dc4cb70047:/# ls /
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
root@39dc4cb70047:/# ls /opt/
root@39dc4cb70047:/#
```

深い意味はないけど`/opt`にスクリプトを置こう。

## Dockerfile

```Dockerfile:Dockerfile
FROM perl:latest

COPY sockserv.pl /opt
EXPOSE 8080

CMD ["perl", "/opt/sockserv.pl"]
```

## build

```
[zaki@minikube socket-probe]$ sudo docker build -t sockserv .
Sending build context to Docker daemon 11.78 kB
Step 1/4 : FROM perl:latest
 ---> 3101c3fdee39
Step 2/4 : COPY sockserv.pl /opt
 ---> c5956d54043b
Removing intermediate container a8be7c52743f
Step 3/4 : EXPOSE 8080
 ---> Running in dee05c477f4d
 ---> acb76cd27ac8
Removing intermediate container dee05c477f4d
Step 4/4 : CMD perl /opt/sockserv.pl
 ---> Running in 654a04fec9b0
 ---> c7368d69e5a8
Removing intermediate container 654a04fec9b0
Successfully built c7368d69e5a8
[zaki@minikube socket-probe]$
```

```
[zaki@minikube socket-probe]$ sudo docker images sockserv
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
sockserv            latest              c7368d69e5a8        15 seconds ago      857 MB
[zaki@minikube socket-probe]$
```

## run

```
[zaki@minikube socket-probe]$ sudo docker run -it -p 8080:8080 --rm sockserv
[DBG] server listening ... 0.0.0.0:8080
[DBG] accept from 172.17.0.1 !
[DBG] disconnected from 172.17.0.1.
[DBG] server listening ... 0.0.0.0:8080
```

```
[zaki@minikube ~]$ curl http://localhost:8080
Running Socket Server.
[zaki@minikube ~]$
```

## stop

```
[zaki@minikube ~]$ sudo docker ps | grep sock
fa50c6fadcba        sockserv               "perl /opt/sockser..."   37 seconds ago      Up 36 seconds       0.0.0.0:8080->8080/tcp   happy_roentgen
[zaki@minikube ~]$ sudo docker stop fa50
fa50
[zaki@minikube ~]$
```

# Deploy Pod on K8s

`latest`だとレジストリにpullしにいくので`v1.0`というtagを付ける。

※ tagが`latest`の場合で`imagePullPolicy`が未設定の場合は、イメージをすでに持っていてもレジストリにpullしに行ってしまう(`Always`の動作)。
see: [Updating Images](https://kubernetes.io/docs/concepts/containers/images/#updating-images)

`latest`の場合は、DockerHub等のレジストリに`push`しておけば、そこから`pull`してきてちゃんと動作する。

```
[zaki@minikube socket-probe]$ sudo docker images sockserv
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
sockserv            latest              c7368d69e5a8        2 minutes ago       857 MB
[zaki@minikube socket-probe]$ sudo docker tag c736 sockserv:v1.0
[zaki@minikube socket-probe]$ sudo docker images sockserv
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
sockserv            latest              c7368d69e5a8        2 minutes ago       857 MB
sockserv            v1.0                c7368d69e5a8        2 minutes ago       857 MB
[zaki@minikube socket-probe]$
```

## Pod単体でデプロイ

動作確認用にPod単体でデプロイしてみる。

```yaml:pod.yml
apiVersion: v1
kind: Pod
metadata:
  name: sockserv-01
spec:
  containers:
  - image: sockserv:v1.0
    name: sockserv
```

```
[zaki@minikube socket-probe]$ kubectl apply -f pod.yml
pod/sockserv-01 created
[zaki@minikube socket-probe]$
```

動いてる

```
[zaki@minikube socket-probe]$ kubectl get pod -o wide
NAME          READY   STATUS    RESTARTS   AGE   IP           NODE       NOMINATED NODE   READINESS GATES
sockserv-01   1/1     Running   0          35s   172.17.0.4   minikube   <none>           <none>
[zaki@minikube socket-probe]$
```

```
[zaki@minikube socket-probe]$ curl http://172.17.0.4:8080
Running Socket Server.
[zaki@minikube socket-probe]$ curl http://172.17.0.4:8080/port/8081
Change to New Port: 8081.
[zaki@minikube socket-probe]$ curl http://172.17.0.4:8081
Running Socket Server.
[zaki@minikube socket-probe]$ curl http://172.17.0.4:8081/port/8080
Change to New Port: 8080.
[zaki@minikube socket-probe]$
```

こんな感じ。(※ k8sノード上で実行)

確認できたので削除する。

```
[zaki@minikube socket-probe]$ kubectl delete pod sockserv-01
pod "sockserv-01" deleted
[zaki@minikube socket-probe]$
```

## DeploymentとService作成

YAMLを書く。
(タネを明かすと一度OpenShiftで`oc`コマンド使ってデプロイしたDeploymentConfigとServiceをベースに書きました←)

```yaml:deployment-app.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sockserv
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sockserv
  template:
    metadata:
      labels:
        app: sockserv
    spec:
      containers:
        - name: sockserv
          image: sockserv:v1.0
          ports:
            - containerPort: 8080
              protocol: TCP
            - containerPort: 8081
              protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: sockserv
  name: sockserv
spec:
  ports:
  - name: 8080-tcp
    port: 8080
    protocol: TCP
    targetPort: 8080
  - name: 8081-tcp
    port: 8081
    protocol: TCP
    targetPort: 8081
  selector:
    app: sockserv
  type: NodePort
```

外部アクセス用にNodePort設定のServiceを作成する。
ポート番号は特に指定していない。(その場合`apply`時に自動で設定される)
NodePort設定の場合に使用できるポート番号は(デフォルト設定で)30000～32767。自動設定の場合はランダムに割り当てられ、ノードOSでそのポート番号でLISTENするようになる。

```
[zaki@minikube socket-probe]$ kubectl apply -f deployment-app.yml
deployment.apps/sockserv created
service/sockserv created
[zaki@minikube socket-probe]$
```

```
[zaki@minikube socket-probe]$ kubectl get pod
NAME                       READY   STATUS    RESTARTS   AGE
sockserv-bcbcd8fcb-7xn24   1/1     Running   0          10s
[zaki@minikube socket-probe]$ kubectl get svc
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                         AGE
kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP                         4d
sockserv     NodePort    10.106.177.34   <none>        8080:30120/TCP,8081:32737/TCP   12s
[zaki@minikube socket-probe]$
```

```yaml
[zaki@minikube socket-probe]$ kubectl get svc sockserv -o yaml
apiVersion: v1
kind: Service
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"labels":{"app":"sockserv"},"name":"sockserv","namespace":"default"},"spec":{"ports":[{"name":"8080-tcp","port":8080,"protocol":"TCP","targetPort":8080},{"name":"8081-tcp","port":8081,"protocol":"TCP","targetPort":8081}],"selector":{"app":"sockserv"},"type":"NodePort"}}
  creationTimestamp: "2019-12-09T14:17:49Z"
  labels:
    app: sockserv
  name: sockserv
  namespace: default
  resourceVersion: "426861"
  selfLink: /api/v1/namespaces/default/services/sockserv
  uid: c16601ad-1134-45f5-a33e-82ef60c77996
spec:
  clusterIP: 10.106.177.34
  externalTrafficPolicy: Cluster
  ports:
  - name: 8080-tcp
    nodePort: 30120
    port: 8080
    protocol: TCP
    targetPort: 8080
  - name: 8081-tcp
    nodePort: 32737
    port: 8081
    protocol: TCP
    targetPort: 8081
  selector:
    app: sockserv
  sessionAffinity: None
  type: NodePort
status:
  loadBalancer: {}
```

デプロイされた。

ノード上であれば`clusterIP`の`10.106.177.34`でアクセスできるけど、せっかくNodePort設定してるのでノード外からアクセスしてみる。
ノード外からのアクセスは、ノードのIPアドレス+`nodePort`に設定されたポート番号。
Dockerでいうと`-p <host-port>:<container-port>`を使ったポートフォワードみたいな動作になる。

```
zaki@mascarpone% curl -s http://192.168.0.82:30120
Running Socket Server.
zaki@mascarpone% curl -s http://192.168.0.82:30120/port/8081
Change to New Port: 8081.
zaki@mascarpone% curl -s http://192.168.0.82:32737
Running Socket Server.
zaki@mascarpone% curl -s http://192.168.0.82:32737/port/8080
Change to New Port: 8080.
```

## probe設定を追加する

デプロイに使った`deployment-app.yml`を以下のように変更。

```diff
--- a/src/deployment-app.yml
+++ b/src/deployment-app.yml
@@ -15,6 +15,13 @@ spec:
       containers:
         - name: sockserv
           image: sockserv:v1.0
+          readinessProbe:
+            failureThreshold: 3
+            periodSeconds: 10
+            successThreshold: 1
+            tcpSocket:
+              port: 8080
+            timeoutSeconds: 1
           ports:
             - containerPort: 8080
               protocol: TCP
```

再デプロイ。

```
[zaki@minikube socket-probe]$ kubectl apply -f deployment-app.yml
deployment.apps/sockserv configured
service/sockserv unchanged
[zaki@minikube socket-probe]$ kubectl get pod
NAME                       READY   STATUS        RESTARTS   AGE
sockserv-87895fcf4-cn4qx   1/1     Running       0          11s
sockserv-bcbcd8fcb-7xn24   1/1     Terminating   0          25m
```

`kubectl apply`を実行することで、差分が適用される(Serviceは`unchanged`で、Deploymentoのみ`configured`で変更になってる)
`kubectl get pod sockserv-87895fcf4-cn4qx -o yaml`すれば、`readinessProbe`設定が追加されていることを確認できる。

# OpenShiftの場合

OpenShiftだとYAMLを書かずに大半のオペレーションは`oc`コマンドを使ってCLIで操作できる。
probeの設定も`oc set probe`が使える。

(余談)そのせいでマニフェスト使った宣言的な定義を行わずに`oc`でシェルスクリプト化しちゃいがちなのかな…

## build

以下、`probe`ってネームスペースで作業してる。

```
$ oc new-build --strategy=docker --binary --name=sockserv
$ oc start-build sockserv --follow --from-dir=.
```

### Dockerfile

せっかくなのでベースイメージに[Red HatのコンテナカタログにあるPerlのイメージ](https://access.redhat.com/containers/#/registry.access.redhat.com/rhscl/perl-526-rhel7)を使用

```Dockerfile:Dockerfile
FROM registry.redhat.io/rhscl/perl-526-rhel7:latest

COPY sockserv.pl /opt
EXPOSE 8080
EXPOSE 18080
EXPOSE 18081

CMD ["perl", "/opt/sockserv.pl"]
```

※ このDockerfileの内容であれば、FROM指定のイメージはKubernetesと同様にDockerHubにあるPerlのイメージでも動く

### secret作成

pull secretが未設定であれば(buildconfigを作成した後に)追加しておく。
([Red Hat提供のコンテナレジストリは認証が必要なので](https://www.redhat.com/ja/blog/transitioning-red-hat-container-registry))

see: [第6章 Red Hat レジストリーへのアクセスおよびその設定 OpenShift Container Platform 3.11 | Red Hat Customer Portal](https://access.redhat.com/documentation/ja-jp/openshift_container_platform/3.11/html/configuring_clusters/install-config-configuring-red-hat-registry)

```
[zaki@codeready socket-probe]$ oc set build-secret --pull bc/sockserv 12778203-esxi-crc-sample-pull-secret
buildconfig.build.openshift.io/sockserv secret updated
```

この設定がないと(registry.redhat.ioなどのRed Hatの認証が必要なレジストリからの)pullの際に認証エラーとなる

```
error: build error: failed to pull image: After retrying 2 times, Pull image still failed due to error: unable to retrieve auth token: invalid username/password
```

secretの作成は、[registry.redhat.io](https://registry.redhat.io)にwebアクセス、画面右上の[Service Accounts]から。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256859/0f252514-b6c4-0192-3e76-3694082d2c34.png)

[New Service Account]押下

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256859/330ce717-cf0a-ac51-72cc-303d7c6ba5a3.png)

必要事項…といってもsecretのnameになるだけなので、わかりやすい名前を入力する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256859/5f018f38-a5e4-5478-35c7-ba129d123c15.png)

これでレジストリアクセス用のアカウントが作成される。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256859/6cd5a42c-6e86-7dfe-e3c4-aea1f9ca8735.png)

Account nameのところのリンクを開くと、OpenShiftのトークンやdocker loginなど各型式のトークンが用意されてるので、[OpenShift Secret]を開く。
そして、Download secretのところにある、yamlファイルを取得する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256859/ca2c1a7b-b382-4d79-8328-ce55efb2748d.png)

あとはもう分かると思うけど、このyamlファイルで`oc create -f`すれば認証用secretが作成される。

…2019.12.10時点で、4.2の該当ドキュメントがどれかわからなかった。。[この辺](https://access.redhat.com/documentation/ja-jp/openshift_container_platform/4.2/html/registry/registry-authentication-enabled-registry-overview_registry-options)っぽいけどなんか違うし…

### buildconfig

buildconfig作る

```
[zaki@codeready socket-probe]$ oc new-build --strategy=docker --binary --name=sockserv
warning: Cannot find git. Ensure that it is installed and in your path. Git is required to work with git repositories.
    * A Docker build using binary input will be created
      * The resulting image will be pushed to image stream tag "sockserv:latest"
      * A binary build was created, use 'oc start-build --from-dir' to trigger a new build

--> Creating resources with label build=sockserv ...
    imagestream.image.openshift.io "sockserv" created
    buildconfig.build.openshift.io "sockserv" created
--> Success
[zaki@codeready socket-probe]$
[zaki@codeready socket-probe]$ oc get bc
NAME       TYPE     FROM     LATEST
sockserv   Docker   Binary   1
```

### build

```
[zaki@codeready socket-probe]$ oc start-build sockserv --follow --from-dir=.
Uploading directory "." as binary input for the build ...
..
Uploading finished
build.build.openshift.io/sockserv-1 started
Receiving source from STDIN as archive ...
Caching blobs under "/var/cache/blobs".

Pulling image registry.redhat.io/rhscl/perl-526-rhel7:latest ...
Getting image source signatures
Copying blob sha256:48ed3bfd822646e50676cd7606af43e984db141bb1755904362f1eb64684c68a
Copying blob sha256:9c9d2ac50b32c2bb48d07fb4e80e81552acc76ade952777f5deb05fdac8f88c6
Copying blob sha256:ad46648f2433aa416763060fb4baaefe64baeda603d4e572f883c21c5482fea1
Copying blob sha256:d327c1598329494579ba3d62999df41f11bff9a2bfad57fb49b30324404ac42a
Copying blob sha256:b004d0755f636027cdcbe27b2fbb06cad1d47f1aeb3ccce6c2326b6adc018087
Copying config sha256:e4adddcba5731273370935f2af8c6aafb52c24ce5e3d0464dec6e26b78f7fd15
Writing manifest to image destination
Storing signatures
STEP 1: FROM registry.redhat.io/rhscl/perl-526-rhel7:latest
STEP 2: COPY sockserv.pl /opt
6c08bbcf4e0ff364409bfd4e5572d234e4f5fe3a516073ce44f9a7413fbbc892
STEP 3: EXPOSE 8080
8b34fa98a336096f8054930ae5041f89c84ce299db8ab3f22999a9f679e85e8b
STEP 4: EXPOSE 18080
8fee5b0e2956e3a6dc5a43500cd75b313d1d5211f89bf0cdbab983eab06f5351
STEP 5: EXPOSE 18081
0be2f74a11c9fa8b2f81ef693dd69c00e0956411b936a8ce3d0d8d0674f6622b
STEP 6: CMD ["perl","/opt/sockserv.pl"]
8599b070f932a21a040a3a76b7b843397abb09f45ca98b4e28e12d734fbc806f
STEP 7: ENV "OPENSHIFT_BUILD_NAME"="sockserv-1" "OPENSHIFT_BUILD_NAMESPACE"="probe"
a4b7f22e4fce7e9a677276352a91cacf790ef6004a53c47c3e1e17487a708299
STEP 8: LABEL "io.openshift.build.name"="sockserv-1" "io.openshift.build.namespace"="probe"
STEP 9: COMMIT temp.builder.openshift.io/probe/sockserv-1:9cbe3125
9cef1075a708b49aec0a65799370ca6edef1cb1a89af008ed05434a10b92ace0

Pushing image image-registry.openshift-image-registry.svc:5000/probe/sockserv:latest ...
Getting image source signatures
Copying blob sha256:c1ed470e85e6a8c46a191ef90e77a15c8e5b449fe192d2449ce18b317234f50a
Copying blob sha256:d327c1598329494579ba3d62999df41f11bff9a2bfad57fb49b30324404ac42a
Copying blob sha256:9c9d2ac50b32c2bb48d07fb4e80e81552acc76ade952777f5deb05fdac8f88c6
Copying blob sha256:ad46648f2433aa416763060fb4baaefe64baeda603d4e572f883c21c5482fea1
Copying blob sha256:48ed3bfd822646e50676cd7606af43e984db141bb1755904362f1eb64684c68a
Copying blob sha256:b004d0755f636027cdcbe27b2fbb06cad1d47f1aeb3ccce6c2326b6adc018087
Copying config sha256:9cef1075a708b49aec0a65799370ca6edef1cb1a89af008ed05434a10b92ace0
Writing manifest to image destination
Storing signatures
Successfully pushed image-registry.openshift-image-registry.svc:5000/probe/sockserv@sha256:3c792bfde8fba6b0c54eccc41e47c68a7e55dc0177dc048f79abc0455d008f58
Push successful
[zaki@codeready socket-probe]$
```

ImageStreamが出来る

```
[zaki@codeready socket-probe]$ oc get is
NAME       IMAGE REPOSITORY                                                         TAGS     UPDATED
sockserv   default-route-openshift-image-registry.apps-crc.testing/probe/sockserv   latest   3 minutes ago
```

クラスタ内レジストリにもビルドされたコンテナイメージがpushされます。

```
[zaki@codeready socket-probe]$ curl -H "Authorization: Bearer $(oc whoami -t)" -sk https://default-route-openshift-image-registry.apps-crc.testing/v2/probe/sockserv/tags/list | python -m json.tool
{
    "name": "probe/sockserv",
    "tags": [
        "latest"
    ]
}
[zaki@codeready socket-probe]$
```

## deploy

`oc new-app sockserv`でデプロイ & Service作成まで一気にやってくれるけど、ClulsterIPの設定固定(多分…)なので、`oc run`でPodデプロイ、`oc expose`でService作成の2段階で行う。
※ `oc new-app`して、作成されたServiceを`oc edit service`で変更してももちろんOK

```
$ oc run sockserv --image=image-registry.openshift-image-registry.svc:5000/my-app/sockserv:latest
$ oc expose dc/sockserv --port=8080,8081 --type=NodePort
```

### run

```
[zaki@codeready socket-probe]$ oc run sockserv --image=image-registry.openshift-image-registry.svc:5000/probe/sockserv:latest
kubectl run --generator=deploymentconfig/v1 is DEPRECATED and will be removed in a future version. Use kubectl run --generator=run-pod/v1 or kubectl create instead.
deploymentconfig.apps.openshift.io/sockserv created
```

```
[zaki@codeready socket-probe]$ oc get dc
NAME       REVISION   DESIRED   CURRENT   TRIGGERED BY
sockserv   1          1         1         config
[zaki@codeready socket-probe]$ oc get pod -l run=sockserv
NAME               READY   STATUS    RESTARTS   AGE
sockserv-1-552xl   1/1     Running   0          77s
[zaki@codeready socket-probe]$
```

デプロイ成功

ちなみに`oc new-app`に比べて`oc run`でDeploymentConfigを作ると、triggers設定にImageChange設定が含まれないため、イメージの再ビルドを実行・レジストリにpushしてコンテナイメージが更新されても、自動でデプロイはされない。
手動で設定追加するには`oc set triggers dc/sockserv --from-image=probe/sockserv:latest -c sockserv`みたいにやればDeploymentConfigが更新される。

### expose

`oc expose svc/***`でRoute作成でよく使うけど、DeploymentConfigに使えばServiceを作れる。

```
[zaki@codeready socket-probe]$ oc expose dc/sockserv --port=8080,8081 --type=NodePort
service/sockserv exposed
[zaki@codeready socket-probe]$ oc get svc
NAME       TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)                         AGE
sockserv   NodePort   172.30.211.82   <none>        8080:30655/TCP,8081:30656/TCP   3s
[zaki@codeready socket-probe]$
```

これでNodePort設定のServiceが作成される。

```
[zaki@codeready socket-probe]$ curl http://$(crc ip):30655
Running Socket Server.
```

## probeの追加

`oc set probe`で追加できる。
`tcpSocket`の場合は`--open-tcp=<port-number>`でDeploymentConfigに設定できる。

```
$ oc set probe dc sockserv --liveness --open-tcp=8080
```

```
[zaki@codeready socket-probe]$ oc set probe dc sockserv --liveness --open-tcp=8080
deploymentconfig.apps.openshift.io/sockserv probes updated
[zaki@codeready socket-probe]$
```

```
[zaki@codeready socket-probe]$ oc get dc sockserv
NAME       REVISION   DESIRED   CURRENT   TRIGGERED BY
sockserv   2          1         1         config
[zaki@codeready socket-probe]$ oc get dc sockserv -o yaml
apiVersion: apps.openshift.io/v1
kind: DeploymentConfig
metadata:
  name: sockserv
  namespace: probe
:
:
spec:
  template:
    spec:
      containers:
      - image: image-registry.openshift-image-registry.svc:5000/probe/sockserv:latest
        imagePullPolicy: Always
        livenessProbe:
          failureThreshold: 3
          periodSeconds: 10
          successThreshold: 1
          tcpSocket:
            port: 8080
          timeoutSeconds: 1
        name: sockserv
:
:
```

こんな感じ。

readinessを設定(これは設定済みのlivenessには影響しない)

```
$ oc set probe dc sockserv --readiness --open-tcp=8080
deploymentconfig.apps.openshift.io/sockserv probes updated
```

削除するには

```
$ oc set probe dc sockserv --liveness --remove
deploymentconfig.apps.openshift.io/sockserv probes updated
```

一度に同時は無理。
ただし互いに干渉はしないので、2回実行すれば併用できる。

```
$ oc set probe dc sockserv --liveness --open-tcp=8080 --readiness --get-url=http://:8080/health
error: you may only set one of --get-url, --open-tcp, or command
```

削除を同時はOK

```
$ oc set probe dc sockserv --liveness --readiness --remove
deploymentconfig.apps.openshift.io/sockserv probes updated
```

詳しくは、`oc set probe --help`を実行すること。

ちなみに[port番号でなくIANA_SVC_NAMEも使用可能](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#tcpsocketaction-v1-core)ってあるけど、[これ`http`とか書けるってこと](https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml)なのかな…設定しても大体

```
  Warning  Unhealthy  2s (x2 over 12s)  kubelet, crc-shdl4-master-0  Readiness probe errored: strconv.Atoi: parsing "http": invalid syntax
```

って出力されるので、数字じゃないとダメそう…

# まとめ

半分以上おまけになってしまいましたが、Kubernetesを使う上で必須ともいえるヘルスチェックについて、tcpSocketの設定とサンプルスクリプトを使って動作を見てみました。
オンプレでVM上でアプリ動かす従来のやり方(←これが悪いと言ってるわけではなく、ただの対比)では「いかにアプリが落ちないようにするか(ペットのように扱う)」が重要でしたが、クラウドネイティブでは「異常時はさっさと切り捨てて新しく動かす(家畜)」というやり方で可用性を高めますが、Kubernetesのヘルスチェックはまさにこの動作をサポートします。

おまけ編では、KubernetesとOpenShiftそれぞれで、コンテナイメージのビルドとデプロイの手順をざっくりとまとめました。
筆者が普段クラスタのインストールばっかりでアプリのビルド&デプロイにほとんど触る機会がないので備忘録のようなものです。

---

liveness probeの読み方、"らいぶねす"と"りぶねす"とどっちだろうとずっと思ってたのですが、某OpenShiftのサポートエンジニアのnekopさんと直接お話する機会があったので聞いてみたら、"らいぶねす"とのことでした😄