本記事はKubernetes2 Advent Calendar 2019 11日目のエントリになります。
筆者は普段OpenShiftばかり触っているため、たまにKubernetesを触ると「あれ?あのコマンドないのか…!」みたいなことがよく起きます💦
参考図書: Kubernetes完全ガイドの「10.1 ヘルスチェック」と「6.5 NodePort Service」
あと、記事中に出てくるサンプルスクリプトやマニフェストはGitHubにも上げていますのでご参考ください。
Kubernetesのヘルスチェックについて
Kubernetesが標準で使える2種類のヘルスチェック機能についておさらい。
liveness probeとreadiness probe
種類 | 説明 | 失敗時の動作 |
---|---|---|
liveness probe | 生きているかどうか | 対象コンテナを破棄して再作成 |
readiness probe | ready状態かどうか | 対象Podに対する外部(Service)からのトラフィックの対象外となり、成功するまで待つ |
Kubernetesにはこの2種類のヘルスチェック機能が標準で使用できるようになっており、正しく設定しておけばアプリケーションPodの異常時には自動で回復するようになっています。(オートヒーリング)
平たく言うと、livenessは「なんか調子悪いんで再起動(正確には再作成)しとこ」で、readinessは「準備中(しばらく待てば動くようになる)」です。
livness probeでヘルスチェック失敗時の動作は以下の通りで、エラーになったコンテナは破棄・新しいコンテナがデプロイされます。(図はpodになってるけど、心の目でコンテナに置き換えてください🙇)
一方、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で…
#!/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するポートを切り替えるという動作をするスクリプトです。
#!/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のデプロイそのものについては、おまけ以降を参照してください。
readinessProbe
ヘルスチェックがパスするまで待機するreadinessProbeを例にDeploymentに以下の設定を追加します。
この設定で「8080/TCPへの接続に失敗したらヘルスチェック失敗」となり、PodがReady状態でなくなります。
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と同じ。
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アドカレで書いた記事もみてねー
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
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
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単体でデプロイしてみる。
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をベースに書きました←)
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]$
[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
を以下のように変更。
--- 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のイメージを使用
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提供のコンテナレジストリは認証が必要なので)
see: 第6章 Red Hat レジストリーへのアクセスおよびその設定 OpenShift Container Platform 3.11 | Red Hat Customer Portal
[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にwebアクセス、画面右上の[Service Accounts]から。
[New Service Account]押下
必要事項…といってもsecretのnameになるだけなので、わかりやすい名前を入力する。
これでレジストリアクセス用のアカウントが作成される。
Account nameのところのリンクを開くと、OpenShiftのトークンやdocker loginなど各型式のトークンが用意されてるので、[OpenShift Secret]を開く。
そして、Download secretのところにある、yamlファイルを取得する。
あとはもう分かると思うけど、このyamlファイルでoc create -f
すれば認証用secretが作成される。
…2019.12.10時点で、4.2の該当ドキュメントがどれかわからなかった。。この辺っぽいけどなんか違うし…
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も使用可能ってあるけど、これhttp
とか書けるってことなのかな…設定しても大体
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さんと直接お話する機会があったので聞いてみたら、"らいぶねす"とのことでした😄