GKE で Pod 起動時に no space left on device エラー

GKE が便利すぎて初歩的なミスにハマった件のメモ書きです。

リリース作業でいつも通りローリングアップデートをしていたら特定のノードに割り当てられた Pod が立ち上がらなくなりました。問題が生じた Pod を調べてみると

```
$ kubectl describe pod app-rc-20160824103923-yagec
Name:        app-rc-20160824103923-yagec
Namespace:   default
Node:        gke-app-f-default-pool-a5959866-l50l/10.240.0.21
Start Time:  Wed, 24 Aug 2016 10:40:18 +0900
Labels:      name=app-pods,version=20160824103923
Status:      Pending
IP:          10.244.1.5
Controllers: ReplicationController/app-rc-20160824103923
Containers:
  node-server:
    Container ID:
    Image:         asia.gcr.io/<project-id>/<image-name>:5.3.7
    Image ID:
    Ports:         3000/TCP, 3001/TCP
    QoS Tier:
      cpu:         Burstable
      memory:      BestEffort
    Requests:
      cpu:         100m
    State:         Waiting
      Reason:      ImagePullBackOff
    Ready:         False
    Restart Count: 0

  ...SKIP...

Conditions:
  Type        Status
  Ready       False
Volumes:
  default-token-2c258:
    Type:    Secret (a volume populated by a Secret)
    SecretName:    default-token-2c258
Events:
  Type        Reason        Message
  --------    ------        -------
  Normal      Scheduled     Successfully assigned app-rc-20160824103923-yagec to gke-app-f-default-pool-a5959866-l50l
  Warning     Failed        Failed to pull image "asia.gcr.io/<project-id>/<image-name>:5.3.7": Error pulling image (5.3.7) from asia.gcr.io/<project-id>/<image-name>, Untar re-exec error: exit status 1: output: open /hoge/fuga/sample.js: no space left on device
  Normal      Created       Created container with docker id 254072a475ee
  Normal      Started       Started container with docker id 254072a475ee
  Warning     FailedSync    Error syncing pod, skipping: failed to "StartContainer" for "node-server" with ErrImagePull: "Error pulling image (5.3.7) from asia.gcr.io/<project-id>/<image-name>, Untar re-exec error: exit status 1: output: open /hoge/fuga/sample.js: no space left on device"
```

のように「no space left on device」エラーが生じていました。該当ノードのディスク状況を見てみると

```
user@gke-app-f-default-pool-a5959866-l50l:~$ sudo df -h
Filesystem      Size  Used Avail Use% Mounted on
rootfs           99G   41G   54G  44% /
udev             10M     0   10M   0% /dev
tmpfs           356M  328K  356M   1% /run
/dev/sda1        99G   41G   54G  44% /
tmpfs           5.0M     0  5.0M   0% /run/lock
tmpfs           712M  1.6M  711M   1% /run/shm
cgroup          1.8G     0  1.8G   0% /sys/fs/cgroup
/dev/sda1        99G   41G   54G  44% /var/lib/docker/aufs
```

まだまだ余裕な感じ... と思ったら inode 領域を見てみると

```
user@gke-app-f-default-pool-a5959866-l50l:~$ sudo df -i
Filesystem      Inodes   IUsed  IFree IUse% Mounted on
rootfs         6553600 6493651  59949  100% /
udev            455023     280 454743    1% /dev
tmpfs           455630     226 455404    1% /run
/dev/sda1      6553600 6493651  59949  100% /
tmpfs           455630       3 455627    1% /run/lock
tmpfs           455630      14 455616    1% /run/shm
cgroup          455630      11 455619    1% /sys/fs/cgroup
/dev/sda1      6553600 6493651  59949  100% /var/lib/docker/aufs
```

でたー、inode 領域フルー。どこが使用しているのか調べてみると

```
user@gke-app-f-default-pool-a5959866-l50l:/$ for DIR in $(sudo ls -lat | grep "^d" | grep -v "\." | awk '{print $9}'); do echo "$(sudo find ./${DIR} -true | wc -l) $(pwd)/${DIR}"; done | sort -n
1 //lost+found
1 //media
1 //mnt
1 //selinux
1 //tmp
2 //lib64
4 //opt
44 //home
97 //bin
145 //sbin
213 //srv
231 //boot
267 //run
286 //dev
1334 //etc
4271 //lib
8775 //root
13668 //sys
26493 //usr
87879 //proc
6940816 //var
```

ログファイルでも溜まっているのか...? 

```
root@gke-app-f-default-pool-a5959866-l50l:/var/lib/docker/aufs# for DIR in $(ls -lat | grep "^d" | grep -v "\." | awk '{print $9}'); do echo "$(find ./${DIR} -true | wc -l) $(pwd)/${DIR}"; done | sort -n
671 /var/lib/docker/aufs/layers
598975 /var/lib/docker/aufs/mnt
6335092 /var/lib/docker/aufs/diff
```

どうやら `/var/lib/docker/aufs/diff` 配下が食べ尽くしている。このディレクトリには Docker イメージの差分ファイルが蓄積されている。確かにローリングアップデートを繰り返していた環境で不要となったイメージを消してなかった... これは GKE の管轄外で利用者側が気にしてやらないといけない。

原因判明したけど GKE の各ホストに一括でコマンドを投げるためには Ansible...? ノード台数の変動やクラスタの再構築でいちいち変わるのも面倒だなー、と考えた結果、泥臭く「現在稼働しているノードを取得して、それぞれで不要となったイメージを削除する」シェルスクリプトになりました。

```
#!/bin/sh

#----------------------------
# config
#----------------------------

QUIET=true
MAX_IMAGES_NUM=20

GCR_DOMAIN=xxxxxxxxxx
PROJECT_ID=yyyyyyyyyy
IMAGE_NAME=zzzzzzzzzz

#----------------------------
# cleanup old dcoker images
#----------------------------

#
function log() {
  /bin/echo "$@"
}

#
function log_n() {
  /bin/echo -n "$@"
}

#
log "Cleanup to old docker images into GKE nodes"
for GKE_NODE in $(kubectl get node | grep '^gke' | awk '{print $1}');
do

  #
  CURRENT_IMAGES=$(gcloud compute ssh ${GKE_NODE} "sudo docker images | grep ^${GCR_DOMAIN}/${PROJECT_ID}/${IMAGE_NAME}")
  CURRENT_IMAGES_NUM=$(log "${CURRENT_IMAGES}" | wc -l | tr -d ' ')
  REMOVE_IMAGES_NUM=$(expr ${CURRENT_IMAGES_NUM} - ${MAX_IMAGES_NUM})

  # status
  log "=================================================================="
  log "Cleanup old docker images into GKE node"
  log ""
  log "GKE_NODE=${GKE_NODE}"
  log "MAX_IMAGES_NUM=${MAX_IMAGES_NUM}"
  log "CURRENT_IMAGES_NUM=${CURRENT_IMAGES_NUM}"
  log "=================================================================="
  log ""

  # current
  log "Current docker images:"
  if [ ${CURRENT_IMAGES_NUM} -gt 0 ] ; then
    for CURRENT_IMAGE_NAME in $(log "${CURRENT_IMAGES}" | awk '{print sprintf("%s:%s", $1, $2)}') ;
    do
      log ${CURRENT_IMAGE_NAME}
    done
  fi
  log ""

  # cleanup
  log "Remove old docker images:"
  if [ ${REMOVE_IMAGES_NUM} -gt 0 ] ; then
    for REMOVE_IMAGE_NAME in $(log "${CURRENT_IMAGES}" | tail -n ${REMOVE_IMAGES_NUM} | awk '{print sprintf("%s:%s", $1, $2)}') ;
    do
      log "==> remove : ${REMOVE_IMAGE_NAME}"
      if [ x"${QUIET}" != x"true" ] ; then
        log_n "are you ok [y/N] ? "
        read ANSWER
        if [ "${ANSWER}" != "y" ] ; then
          log "cannceled"
          exit 1
        fi
      fi
      gcloud compute ssh ${GKE_NODE} "sudo docker rmi ${REMOVE_IMAGE_NAME}"
  done
  fi
  log ""

done
```

こんな感じのスクリプトをローリングアップデート時に実行するように仕込んで、各ノードの inode 領域を解放することにしました。

まとめると inode 領域を喰い尽くさないように不要となった Docker イメージを削除しよう。GKE 各ノードのディスク容量監視に加えて inode 領域監視も入れようって話でした。GKE が悪いというより便利すぎて inode 領域のことスッカリ忘れてた。

おしまい