GKE上のコンテナプロセスをデバッグする方法

GKEではコンテナ上で動くプロセスを簡単に調査したりデバッグを行ったりすることが出来ます。
この記事ではGKE上のコンテナプロセスに対してデバッグツールを適用して調査する方法を紹介します。
取り上げるデバッグツールはstrace/tcpdump/htop/netstat/ngrepです。

# コンテナプロセスのデバッグで使えるコマンド集

まずはコンテナプロセスのデバッグ時に使えるコマンドを紹介していきます。


## 調べたい対象のコンテナプロセスの情報を集める

ローカルからkubectlでデバッグに必要となる情報を集めます。

### ノードの特定
対象のpodが動作するノードを特定します。

```sh:[ローカルで実行]
$ NODE_NAME=$(kubectl get pods --selector=app=$APP_NAME -o jsonpath='{.items[0].spec.nodeName}'); echo $NODE_NAME
```

### uidの取得

ノード上でのコンテナプロセスのPIDを特定するのに必要となるuidを取得します。

```sh:[ノーカルで実行]
$ UUID=$(kubectl get pods --selector=app=$APP_NAME -o jsonpath='{.items[0].metadata.uid}'); echo $UUID
```

### IPの取得

podのクラスターIPを特定します。

```sh:[ローカルで実行]
$ IP=$(kubeclt get po  --selector=app=nginx -o jsonpath='{.items[*].status.podIP}'); echo $IP
```

## ノードへのsshとデバッグ用のtoolboxの起動

次にノード上にsshしてデバッグツールを使う下準備をします。

### ノードへのssh
以下でノードへとsshします。

```sh:[ノーカルで実行]
$ gcloud compute ssh hands-on-user@$NODE_NAME -q
```

### PIDを特定

uidを使ってコンテナプロセスのPIDを特定します。
該当するプロセスが複数ある場合は、実行されているコマンド(下の例ではngixが動いている)から判別してください。

```sh:[ノード内で実行]
$ ps -ea -o pid,ppid,cmd:50,cgroup --cumulative -H | grep $UUID | cut -c -100
  24209   24192         /pause                                     12:devices:/kubepods/burstable/po
  24265   24248         nginx: master process nginx -g daemon off; 12:devices:/kubepods/burstable/po
  24279   24265           nginx: worker process                    12:devices:/kubepods/burstable/po
```
### toolboxの起動

[toolbox](https://cloud.google.com/container-optimized-os/docs/how-to/toolbox)を起動します。これはGKEのノード/CoreOS上で提供されている、ホストマシンを汚すことなくデッバク用のツールをインストールして実行できるコンテナ環境です。

```sh:[ノード内で実行]
$ sudo toolbox bash
```

## コンテナプロセスに対してデバッグツールを使用
実際にデバッグツールを使用していきます。
### strace

toolbox起動後に、`$PID`にコンテナプロセスのPIDを指定して以下を実行します。

```sh:[toolbox内で実行]
$ apt-get install -y strace; strace -vf -s 1024 -p $PID
```

### tcpdump



interfaceに`cbr0`を指定してtcpdumpを実行します(cbr0については[GEKのネットワーク構成](https://cloud.google.com/kubernetes-engine/docs/concepts/network-overview)を参照)。

```sh:[toolbox内で実行]
$ apt-get install -y tcpdump; tcpdump -ni cbr0 host $IP
```

### htop

htopを実行します。

```sh:[toolbox内で実行]
$ apt-get install -y htop; htop
```

### netstat

コンテナ(`$PID`で指定)のネットワークスペース内に入ります。

```sh:[toolbox内で実行]
$ nsenter --target $PID --net
```
netstatを実行します。

```sh:[コンテナのNS内で実行]
$ netstat -anp; exit;
```

### ngrep

ngrepをインストールします。

```sh:[toolbox内で実行]
$ apt-get install -y ngrep
```

コンテナのネットワークネームスペース内でngrepを実行します。

```sh:[toolbox内で実行]
$ nsenter -t $PID --net ngrep -d any port 80

```

## 参考文献

[GKEのトラブルシュートの方法](https://cloud.google.com/kubernetes-engine/docs/troubleshooting)
[toolboxの使い方](https://cloud.google.com/container-optimized-os/docs/how-to/toolbox)
[toolboxのGitHub](https://github.com/coreos/toolbox)



# 実際にGKE上でコマンドを試す手順

この記事で紹介したコマンドを実際に試してみたい人のために、検証用のGKEクラスタ/コンテナプロセスを立ち上げる手順を紹介します。
環境を汚さずに検証できるように、コンテナ内からのCloud SDKの実行、専用のプロジェクトの新規作成を行っているので安心して試せるような手順になっています。


## Cloud SDKとプロジェクトのセットアップ

Cloud SDKを使用するためにコンテナを起動します。

```sh:[ローカルで実行]
$ docker run -it --rm --name=gcloud google/cloud-sdk:254.0.0-alpine bin/bash
```

以下を打って表示されるリンク先に飛んでクレデンシャルを取得して Cloud SDKを認証します。

```sh:[コンテナ内で実行]
$ gcloud auth login
Go to the following link in your browser:

    https://accounts.google.com/o/oauth2/auth?redirect_uri=....


Enter verification code:
```
## プロジェクトの作成

プロジェクトを作成します。

```sh:[コンテナ内で実行]

$ PROJECT_NAME=strace-hands-on-$(head -c 100 /dev/random | xxd -p -l 5); echo $PROJECT_NAME
$ gcloud projects create $PROJECT_NAME
```

Cloud SDKをハンズオンのプロジェクト用に設定していきます。

```sh:[コンテナ内で実行]
$ {
    gcloud config set project $PROJECT_NAME
    gcloud config set compute/region asia-northeast1
    gcloud config set compute/zone asia-northeast1-b
}
```

## GKEクラスタの作成
Kubernetes Engine APIを有効化します。

以下のコマンドの出力結果のURLから有効化出来ます。

```sh:[コンテナ内で実行]
$ echo https://console.cloud.google.com/apis/api/container.googleapis.com/overview\?project\=$PROJECT_NAME
https://console.cloud.google.com/apis/api/container.googleapis.com/overview?project=strace-hands-on-fdffdcb6c5
```

クラスタを作成します。

```sh:[コンテナ内で実行]
$ gcloud container clusters create --preemptible --num-nodes=2 strace-hands-on
```

恐らく以下のようなエラーが出ると思いますが、Kubernetes Engine APIは有効化まで時間がかかるので、少し時間を置いてからリトライしてみたください。

```sh:[コンテナ内で実行]
ERROR: (gcloud.container.clusters.create) ResponseError: code=403, message=Google Compute Engine: Access Not Configured. Compute Engine API has not been used in project 311149706395 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/compute.googleapis.com/overview?project=311149706395 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.
```

## kubectl設定

kubectlをインストールします

```sh:[コンテナ内で実行]
$ gcloud components install --quiet kubectl
```

kubectlをクラスタに向けます。

```sh:[コンテナ内で実行]
$ gcloud container clusters get-credentials strace-hands-on
```

## nginxのpodをデプロイする

以下を打ってnginxのdeploymentとserviceを作成します。

```sh:[コンテナ内で実行]
$ cat <<EOF | kubectl apply -f - 
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 1
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80
        resources:
          limits:
            cpu: 150m
---
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  selector:
    app: nginx
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
EOF
```
podが動いていることを確認します。

```sh:[コンテナ内で実行]
$ kubectl get po
NAME                                READY   STATUS    RESTARTS   AGE
nginx-deployment-77d9857f95-rggjf   1/1     Running   0          94m
```

nginxにリクエストを送るためのpodを起動します。

```sh:[コンテナ内で実行]
$ kubectl run alpine -it --image=alpine /bin/sh
```

アタッチしたalpineのpodで以下を実行してnginxのpodからレスポンスが返ることを確認します。

```sh:[pod内で実行]
$ apk update && apk add curl
$ curl nginx 2>/dev/null | head -n 5
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
```

## nginxのコンテナプロセスが動くノードにssh

以下を打ってCloud SDKのコンテナにアタッチしたパネルを新しく用意します。

```sh:[ローカルで実行]
$ docker exec -it gcloud /bin/bash
```

コンテナプロセスのuidを特定してメモしておきます。

```sh:[コンテナ内で実行]
$ APP_NAME=nginx; UUID=$(kubectl get pods --selector=app=$APP_NAME -o jsonpath='{.items[0].metadata.uid}'); echo $UUID
98d46832-aed8-11e9-9238-42010a920046
```

nginxのコンテナプロセスが動くノードへとsshします。

```sh:[コンテナ内で実行]
$ NODE_NAME=$(kubectl get pods --selector=app=nginx -o jsonpath='{.items[0].spec.nodeName}'); \
  gcloud compute ssh hands-on-user@$NODE_NAME -q
```

## nginxのコンテナプロセスに対してデバッグツールを適用

コンテナプロセスのPIDを特定します。

```sh:[ノード内で実行]
$ UUID=98d46832-aed8-11e9-9238-42010a920046
$ ps -ea -o pid,cmd:50,cgroup --cumulative -H | grep $UUID | cut -c -100 | grep worker
   3768           nginx: worker process                    12:memory:/kubepods/burstable/pod98d46832
$ PID=3768
```

toolboxを起動します。

```sh:[ノード内で実行]
$ sudo toolbox bash
Spawning container root-gcr.io_google-containers_toolbox-20180309-00 on /var/lib/toolbox/root-gcr.io_google-containers_toolbox-20180309-00.
Press ^] three times within 1s to kill container.
```

デバッグツールをインストールします。

```sh:[toolbox内で実行]
$ apt-get update && apt-get install -y strace ngrep
```

straceを実行します。

```sh:[toolbox内で実行]
$ PID=3768; strace -vf -s 1024 -p $PID
strace: Process 3768 attached
epoll_wait(8,
```

alpine podからリクエストを送ると、

```sh:[pod内で実行]
$ curl nginx
```
以下のようにnginxがリクエスト処理時に発行するシステムコールが観察できるはずです。
余談ですが、nginxは静的ファイルをレスポンスとして返す場合、通常の[write(2)](http://man7.org/linux/man-pages/man2/write.2.html)ではなく[sendfile(2)](http://man7.org/linux/man-pages/man2/sendfile.2.html)を使うんですね。

```sh
strace: Process 3768 attached
epoll_wait(8, [{EPOLLIN, {u32=4087242768, u64=140282014097424}}], 512, -1) = 1
accept4(6, {sa_family=AF_INET, sin_port=htons(52840), sin_addr=inet_addr("10.0.1.8")}, [110->16], SOCK_NONBLOCK) = 3
epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=4087243201, u64=140282014097857}}) = 0
epoll_wait(8, [{EPOLLIN, {u32=4087243201, u64=140282014097857}}], 512, 60000) = 1
recvfrom(3, "GET / HTTP/1.1\r\nHost: nginx\r\nUser-Agent: curl/7.65.1\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 69
stat("/usr/share/nginx/html/index.html", {st_dev=makedev(0, 204), st_ino=1050025, st_mode=S_IFREG|0644, st_nlink=1, st_uid=0, st_gid=0, st_blksize=4096, st_blocks=8, st_size=612, st_atime=2014-12-23T16:25:09+0000, st_mtime=2014-12-23T16:25:09+0000, st_ctime=2019-07-25T12:34:57+0000.523040476}) = 0
open("/usr/share/nginx/html/index.html", O_RDONLY|O_NONBLOCK) = 10
fstat(10, {st_dev=makedev(0, 204), st_ino=1050025, st_mode=S_IFREG|0644, st_nlink=1, st_uid=0, st_gid=0, st_blksize=4096, st_blocks=8, st_size=612, st_atime=2014-12-23T16:25:09+0000, st_mtime=2014-12-23T16:25:09+0000, st_ctime=2019-07-25T12:34:57+0000.523040476}) = 0
writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.7.9\r\nDate: Thu, 25 Jul 2019 12:45:24 GMT\r\nContent-Type: text/html\r\nContent-Length: 612\r\nLast-Modified: Tue, 23 Dec 2014 16:25:09 GMT\r\nConnection: keep-alive\r\nETag: \"54999765-264\"\r\nAccept-Ranges: bytes\r\n\r\n", iov_len=237}], 1) = 237
sendfile(3, 10, [0] => [612], 612)      = 612
write(5, "10.0.1.8 - - [25/Jul/2019:12:45:24 +0000] \"GET / HTTP/1.1\" 200 612 \"-\" \"curl/7.65.1\" \"-\"\n", 89) = 89
close(10)                               = 0
setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0
recvfrom(3, "", 1024, 0, NULL, NULL)    = 0
close(3)                                = 0
epoll_wait(8,
```

続いて、ngrepを使用してみます。
Ctrl+Cでstraceをデタッチして以下を打ちます。


```sh:[toolbox内で実行]
$ nsenter -t $PID --net ngrep -d any port 80
interface: any
filter: (ip or ip6) and ( p:ort 80 )
```

再びalpine podからリクエストを送ると、

```sh:[pod内で実行]
$ curl nginx
```

以下のようにnginxの通信の中身を見ることが出来ます。

```sh
interface: any
filter: (ip or ip6) and ( port 80 )
####
T 10.0.1.8:53908 -> 10.0.0.6:80 [AP]
  GET / HTTP/1.1..Host: nginx..User-Agent: curl/7.65.1..Accept: */*....
##
T 10.0.0.6:80 -> 10.0.1.8:53908 [AP]
  HTTP/1.1 200 OK..Server: nginx/1.7.9..Date: Thu, 25 Jul 2019 12:56:40 GMT..Content-Type: text/html..Content-Length: 612..Las
  t-Modified: Tue, 23 Dec 2014 16:25:09 GMT..Connection: keep-alive..ETag: "54999765-264"..Accept-Ranges: bytes....
#
T 10.0.0.6:80 -> 10.0.1.8:53908 [AP]
  <!DOCTYPE html>.<html>.<head>.<title>Welcome to nginx!</title>.<style>.    body {.        width: 35em;.        margin: 0 aut
  o;.        font-family: Tahoma, Verdana, Arial, sans-serif;.    }.</style>.</head>.<body>.<h1>Welcome to nginx!</h1>.<p>If y
  ou see this page, the nginx web server is successfully installed and.working. Further configuration is required.</p>..<p>For
   online documentation and support please refer to.<a href="http://nginx.org/">nginx.org</a>.<br/>.Commercial support is avai
  lable at.<a href="http://nginx.com/">nginx.com</a>.</p>..<p><em>Thank you for using nginx.</em></p>.</body>.</html>.
#####
```

## 後片付け

alpine podを`exit`して、ローカルのコンテナに復帰します。

```sh:[pod内で実行]
$ exit
```

クラスターを削除します。

```sh:[コンテナ内で実行]
$ gcloud container clusters delete strace-hands-on -q
```

プロジェクトを削除します。

```sh:[コンテナ内で実行]
$ gcloud projects delete $PROJECT_NAME -q
```

最後にCloud SDKのコンテナを全てexitして後始末は完了です。

# toolboxコンテナのネームスペースについて補足

コンテナは、プロセスに固有の[ネームスペース](http://man7.org/linux/man-pages/man7/namespaces.7.html)(=プロセスからのOSの各種リソースの見え方)を割り当てることで、プロセスを隔離したものですが、ネームスペースの割り当て方によっては、通常のプロセスとコンテナのプロセスとの中間状態にあるようなプロセスを作成できます。

toolboxはその一種で、ほぼ通常のホストのプロセスにように振る舞いながら、ファイルシステムだけ固有の名前空間が割り当てられているので、ホストマシンを汚すことなくデバッグツールのインストールと実行を行うことができます。

以下では、toolboxなどのネームスペースを見ていきます。

まず、ホストのデフォルトのネームスペースは以下のようになっています。

```sh:ホストのネームスペース
$ ls -la /proc/$$/ns
total 0
dr-x--x--x 2 hands-on-user hands-on-user 0 Jul 25 08:20 .
dr-xr-xr-x 9 hands-on-user hands-on-user 0 Jul 25 08:15 ..
lrwxrwxrwx 1 hands-on-user hands-on-user 0 Jul 25 08:20 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 hands-on-user hands-on-user 0 Jul 25 08:20 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 hands-on-user hands-on-user 0 Jul 25 08:20 mnt -> 'mnt:[4026531840]'
lrwxrwxrwx 1 hands-on-user hands-on-user 0 Jul 25 08:20 net -> 'net:[4026531993]'
lrwxrwxrwx 1 hands-on-user hands-on-user 0 Jul 25 08:20 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 hands-on-user hands-on-user 0 Jul 25 08:20 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 hands-on-user hands-on-user 0 Jul 25 08:20 user -> 'user:[4026531837]'
lrwxrwxrwx 1 hands-on-user hands-on-user 0 Jul 25 08:20 uts -> 'uts:[4026531838]'
```

一方で、toolboxのネームスペースは以下のようになっており、[cgroup](https://en.wikipedia.org/wiki/Cgroups)(CPUやメモリなどの使用を制限)を[mnt](http://man7.org/linux/man-pages/man7/mount_namespaces.7.html)(ファイルシステムのマウントポイントを隔離)以外はホストの通常のプロセスのネームスペースと同一であることが分かります。

```sh:toolboxのネームスペース
$ ls -la /proc/$$/ns
total 0
dr-x--x--x 2 root root 0 Jul 25 08:22 .
dr-xr-xr-x 9 root root 0 Jul 25 08:21 ..
lrwxrwxrwx 1 root root 0 Jul 25 08:22 cgroup -> cgroup:[4026532621]
lrwxrwxrwx 1 root root 0 Jul 25 08:22 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 Jul 25 08:22 mnt -> mnt:[4026532622]
lrwxrwxrwx 1 root root 0 Jul 25 08:22 net -> net:[4026531993]
lrwxrwxrwx 1 root root 0 Jul 25 08:22 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 Jul 25 08:22 pid_for_children -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 Jul 25 08:22 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Jul 25 08:22 uts -> uts:[4026531838]
```

一方でk8sにデプロイされたコンテナプロセスの場合は、全てのネームスペースが固有なものを割り当てられており、ホストマシンのシステムからはほぼ完全に隔離されています。

```sh:コンテナプロセスのネームスペース
$ sudo ls -la /proc/$PID/ns
total 0
dr-x--x--x 2 root root 0 Jul 25 08:28 .
dr-xr-xr-x 9 root root 0 Jul 25 08:12 ..
lrwxrwxrwx 1 root root 0 Jul 25 08:28 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Jul 25 08:28 ipc -> 'ipc:[4026532233]'
lrwxrwxrwx 1 root root 0 Jul 25 08:28 mnt -> 'mnt:[4026532618]'
lrwxrwxrwx 1 root root 0 Jul 25 08:28 net -> 'net:[4026532236]'
lrwxrwxrwx 1 root root 0 Jul 25 08:28 pid -> 'pid:[4026532620]'
lrwxrwxrwx 1 root root 0 Jul 25 08:28 pid_for_children -> 'pid:[4026532620]'
lrwxrwxrwx 1 root root 0 Jul 25 08:28 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Jul 25 08:28 uts -> 'uts:[4026532619]'
```

また、[nsenter](http://man7.org/linux/man-pages/man1/nsenter.1.html)コマンドによって、ネームスペースの一部をコンテナプロセスなどと共有することができますが、以下のようにネットワークネームスペースがコンテナプロセスのものと確かに同一になっていることが分かります。

```sh:nsenterで変更されたネットワークネームスペース
$ nsenter -t $PID --net sh -c "ls -la /proc/\$\$/ns" | grep net
lrwxrwxrwx 1 root root 0 Jul 25 08:31 net -> net:[4026532236]
```

```sh:コンテナプロセスのネットワークネームスペース
$ sudo ls -la /proc/$PID/ns | grep net
lrwxrwxrwx 1 root root 0 Jul 25 08:28 net -> net:[4026532236]
```

## 参考文献

コンテナのネームスペースに関しては以下が参考になります。

[コンテナ技術入門 - 仮想化との違いを知り、要素技術を触って学ぼう](https://employment.en-japan.com/engineerhub/entry/2019/02/05/103000)
[manpagesのnamaspacesのページ](http://man7.org/linux/man-pages/man7/namespaces.7.html)