Kubernetesのポッドが起動しない原因と対策

Kubernetesの導入を検討するにあたって、エラーの状況確認から解決に至るまでのオペレーションは押さえておくべき事柄だ。まだ導入に至っていない場合はこういう問題が発生するのかと雑に感じてもらい、導入している場合は問題を解決する際の参考にしてもらえればと思う。
当エントリでは、自分が遭遇した失敗のケースについて原因と解決方法を列挙する。尚、経験ベースなので同じエラーがでても場合によっては別の原因や解決方法があるかもしれない。また、記載したログや設定ファイルのリソース名や値などは適当にマスキングしているので適宜読み替えてほしい。

## どのようなタイミングでポッドが起動しなくなるか

Kubernetesはポッドが落ちた際に自動的にポッドを再起動するなどのマネジメントを行ってくれる。ただし、ポッドを初めて作る際や、構成を変えたポッドやレプリケーションコントローラをローリングアップデートする際に、誤った設定を行ってしまいポッドが起動しない事態に陥る事がよくある。

## ローリングアップデートの失敗

ローリングアップデートをした際に新しいポッドが何かしらの原因で起動しなかった場合、`error: timed out waiting for any update progress to be made`のような雑なメッセージを吐いて失敗する。このような事態に陥ってもあまり慌てる必要がない。なぜなら、ポッド内のすべてのコンテナが正常に稼動することをもってポッドが正常であると判断され、正常なポッドが生成されない限りはローリングアップデートつまりポッドの入れ替えが始まらないからだ。このようにローリングアップデートが始まらなかったケースでは、前の構成がそのまま動いていて起動に失敗する新しい構成は動かずに放置される。そのため、外部からはただただアップデートが反映されてないように観測される[^failure-case]。

[^failure-case]: うまく起動しない構成ではなんとか動かそうとコンテナの構築フローが何度もリトライされる。リトライ時にポッドが一瞬正常に起動してしまいその後落ちるという状況になっている場合、外部からもその不安定なポッドに一瞬アクセスが可能になってしまい、何かがうまくいっていない状況が外部から観測できる状態になる事もある。

### ローリングアップデート実行時に失敗を検知する方法

CIから自動的にローリングアップデートを実行しているなどの場合、失敗を検知したい場合があるだろう。`kubectl rolling-update rc`コマンドは`--output=json`としても結果をJSONで取得できないので、エラーしたのかはexitコードベースで判定する。

#### 失敗時にプロセスを落としたい時

単純にテスト自体を失敗としたい場合は、デプロイ用のスクリプトファイル内で`set -e`しておいて失敗した時点でデプロイプロセスを落とす。

```bash:deploy.sh
set -e
kubectl rolling-update rc --filename=rc.yml
```

#### 失敗時に処理を行いたい時

失敗したことを検知し通知などの処理を行いたいことがあるだろう。この場合は`$?`でexitコードを取得して評価し、`0`以外で終わっている場合は処理を行いプロセスを終了する。

```bash:deploy.sh
kubectl rolling-update rc --filename=rc.yml
if [[ $? != 0 ]]; then
  # do something
  exit $?
fi
```

## 原因の調査方法

`kubectl get pods`コマンドで起動に失敗しているポッドを見つけ、`kubectl describe pod <pod-name>`でポッドの詳細を表示する。下記のようにステータスが表示されるだろう。

```bash:terminal
Name:                           my-pod
...
Status:                         Pending
...
Containers:
  client:
    Image:              gcr.io/my-prj/client:v0
    State:              Waiting
      Reason:           PullImageError
    Ready:              False
    Restart Count:      0
  nginx:
    Image:              gcr.io/my-prj/nginx:v0
    State:              Waiting
      Reason:           PullImageError
    Ready:              False
    Restart Count:      0
  app:
    Image:              gcr.io/my-prj/app:v0
    State:              Waiting
      Reason:           PullImageError
    Ready:              False
    Restart Count:      0
Conditions:
  Type          Status
  Ready         False
...
Events:
  FirstSeen   LastSeen   Count   From                         SubobjectPath                Reason    Message
  ─────────   ────────   ─────   ────                         ─────────────                ──────    ───────
  33m         33m        1       {scheduler }                                              Scheduled Successfully assigned my-pod to my-node-a
  33m         33m        1       {kubelet my-node-a}   implicitly required container POD   Created   Created with docker id XXXXXXXXXXXX
  33m         33m        1       {kubelet my-node-a}   implicitly required container POD   Started   Started with docker id XXXXXXXXXXXX
  33m         33m        1       {kubelet my-node-a}   implicitly required container POD   Pulled    Container image "gcr.io/google_containers/pause:0.8.0" already present on machine
  33m         8s         199     {kubelet my-node-a}   spec.containers{client}             Pulling   Pulling image "gcr.io/my-prj/client:v0"
  32m         5s         199     {kubelet my-node-a}   spec.containers{client}             Failed    Failed to pull image "gcr.io/my-prj/client:v0": Tag v0 not found in repository gcr.io/my-prj/client
  33m         5s         199     {kubelet my-node-a}   spec.containers{nginx}              Pulling   Pulling image "gcr.io/my-prj/nginx:v0"
  33m         3s         199     {kubelet my-node-a}   spec.containers{app}                Pulling   Pulling image "gcr.io/my-prj/app:v0"
  33m         3s         199     {kubelet my-node-a}   spec.containers{nginx}              Failed    Failed to pull image "gcr.io/my-prj/nginx:v0": Tag v0 not found in repository gcr.io/my-prj/nginx
  33m         1s         199     {kubelet my-node-a}   spec.containers{app}                Failed    Failed to pull image "gcr.io/my-prj/app:v0": Tag v0 not found in repository gcr.io/my-prj/app
```

起動に失敗しているコンテナは`.Conteinrs.<ContainerName>.State`が`Running`以外になっていて、`.Conteinrs.<ContainerName>.State.Reason`に失敗した理由が表示されている。また、`Events`にポッド内で行われたコンテナ立ち上げのイベントが記録されているので、この`Reason`が`Failed`になっているイベントにも注目する。

## エラー内容と解決方法

エラー毎に解決方法を示す。

### PullImageError

#### 原因

イメージの取得に失敗している。

#### 解決

- コンテナが使用するイメージがpushされているか確認しよう。
  - イメージのレジストリのURLが間違っていないか。
  - イメージのタグが間違っていないか。

```yaml:rc.yml
apiVersion: v1
kind: ReplicationController
...
spec:
  template:
    spec:
      containers:
        - name: app
          image: gcr.io/my-prj/app:v0  # <- ココ
```

### CrashLoopBackOff

#### 原因

コンテナ内のプロセスの終了を検知してコンテナの再起動を繰り返している。

#### 解決

コンテナで終了するプロセスを動かしていないだろうか？コンテナが正常に稼動しているかはプロセスが落ちていないかで判断するので、プロセスが正常終了した場合であっても「あっプロセスが落ちてる」となって再起動される。そのようなコンテナは再起動したところで再びプロセスが正常終了するだろう。つまり、無限に起動し続けるということになる。

シングルコンテナポッドの場合は、コンテナの再起動ポリシーをプロセスが異常終了した場合のみ再起動するように変更する。[^RestartPolicy]

```yaml:rc.yml
apiVersion: v1
kind: ReplicationController
...
spec:
  template:
    spec:
      restartPolicy: OnFailure  # <- ココ
```

マルチコンテナポッドの場合は、前述のオプションは設定できない。コンテナはDockerfileの`CMD`にお馴染み`tail -f /dev/null`を追加して永続化する。

```Dockerfile:Dockerfile
CMD <your-command> && tail -f /dev/null
```

終了しないはずのプロセスを動かしている場合は、不正に終了している可能性がある。`kubectl -c <container-name> logs <pod-name>`でコンテナで吐かれたログを確認する。

[^RestartPolicy]: [RestartPolicy - The life of a pod - Kubernetes Docs](http://kubernetes.io/v1.1/docs/user-guide/pod-states.html#restartpolicy)

### PodExceedsFreeCPU

#### 原因

ノードに余っているCPUリソース以上のCPUリソースを要求している。

#### 解決

まずはノードが持っているリソースの量を把握する。`kubectl get nodes`でノードの一覧を表示し、`kubectl describe nodes <node-name>`でノードの詳細を表示する。

```bash:terminal
Name:                   my-node-a
...
Capacity:
 pods:          40
 cpu:           2
 memory:        7679608Ki
...
Non-terminated Pods:            (10 in total)
  Namespace   Name       CPU Requests   CPU Limits   Memory Requests Memory Limits
  ─────────   ────       ────────────   ──────────   ─────────────── ─────────────
  default     my-pod-a   400m (20%)     400m (20%)   512Mi (6%)      512Mi (6%)
  default     my-pod-b   400m (20%)     400m (20%)   512Mi (6%)      512Mi (6%)
  default     my-pod-c   400m (20%)     400m (20%)   512Mi (6%)      512Mi (6%)
  default     my-pod-d   400m (20%)     400m (20%)   512Mi (6%)      512Mi (6%)
Allocated resources:
  (Total limits may be over 100%, i.e., overcommitted. More info: http://releases.k8s.io/HEAD/docs/user-guide/compute-resources.md)
  CPU Requests  CPU Limits      Memory Requests Memory Limits
  ────────────  ──────────      ─────────────── ─────────────
  1600m (80%)   1600m (80%)     2048Mi (24%)    2048Mi (24%)
No events.
```

CPUリソースの80%を使っている状況なので、残りの20%しか余剰がない事がわかる。

##### 解決策1. コンテナに割り当てるリソース量を適切に設定する。

一つ一つのポッドに割り当てているCPUリソースを削減してもアプリケーションが機能する場合は、要求するリソースを削減する。[^compute-resources]

[^compute-resources]: [Compute Resources - Kubernetes Docs](http://kubernetes.io/v1.1/docs/user-guide/compute-resources.html)

```yaml:rc.yml
apiVersion: v1
kind: ReplicationController
...
spec:
  template:
    spec:
      containers:
        - name: app
          resources:
            requests:
              cpu: 100m  # <- ココ
```

##### 解決策2. レプリカの数を適切に設定する。

レプリカの数を削減してもアプリケーションが機能する場合は、立ち上げるポッドの数を減らす。

```yaml:rc.yml
apiVersion: v1
kind: ReplicationController
...
spec:
  replicas: 2  # <- ココ
```

##### 解決策3. ノード数を増やす。

解決策1,2のように調整で済むような状況ではなく、絶対的にリソースが足りない場合は`gcloud container cluster resize <cluster-name> --size <num-nodes>`[^gcloud-container-cluster-resize]でノードを増やす。

[^gcloud-container-cluster-resize]: [Resizing a Container Cluster - Container Engine — Google Cloud Platform](https://cloud.google.com/container-engine/docs/resize-cluster)

### OOMKilled

#### 原因

コンテナに割り当てられたメモリを使いきって、コンテナ内で OOM Killer にプロセスを殺されている。

#### 解決

`.spec.template.spec.containers[].resources.limits.memory`の数値を増やして、コンテナが使用できるメモリの上限を上げる。[^compute-resources]

```yaml:rc.yml
apiVersion: v1
kind: ReplicationController
...
spec:
  template:
    spec:
      containers:
        - name: app
          resources:
            limits:
              memory: 1024Mi  # <- ココ
```

## 修正のオペレーション

原因が判明したら起動に至らなかったレプリケーションコントローラを削除する。その後、ローリングアップデートを行うと、現在稼働しているレプリケーションコントローラが生成したポッドが問題解決したレプリケーションコントローラが生成するポッドに順次置き換えられる。

```bash:terminal
kubectl delete rc <failed-rc>
kubectl rolling-update rc --filename=rc.yml
```

## さいごに

構成を変更しない限りはこのようなエラーに出会うことは滅多にないので、一度構成を固定した後は安定的にローリングアップデートできる。Kubernetesのスケーラブルなコンテナマネジメントという特徴からみたリソース管理の雑感としては、カツカツのリソースでやりくりしているといざ水平オートスケールが必要とされる状況でリソースがなくてスケールされない事態が起こりうるので、カツカツのリソースの中でポッドのリソースを細かく設定して運用するよりはお金が許す限りはノードはサクサクと増やしていくのが良いと感じている。
:moneybag: is :whale2::whale2::whale2::whale2::whale2::whale2::whale2::whale2::whale2::whale2: