GKE上でgcloudコマンドを叩く為のPodをDeployしたら詰まった話

# はじめに
この記事は[ZOZOテクノロジーズ #5 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/zozo_tech5)の21日目の記事となります。
昨日は、@yskur さんの[PMがエンジニアと一緒にER図ランチをしている話](https://note.com/yskur/n/ne29bb41b383d)でした。


今年は全部で5つのAdvent Calendarが公開されています。

[ZOZOテクノロジーズ #1 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/zozo_tech)
[ZOZOテクノロジーズ #2 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/zozo_tech2)
[ZOZOテクノロジーズ #3 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/zozo_tech3)
[ZOZOテクノロジーズ #4 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/zozo_tech4)
[ZOZOテクノロジーズ #5 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/zozo_tech5)

# どんな人向けの記事か
- Containerについて勉強中
- Kubernetesについて勉強中
- Kubernetesに自分でPodをDeployした経験があまり無い人

# 概要
GKE上に検証用としてgcloudコマンドが叩けるPodが必要になったので作成した際に、思った以上に詰まる点が多かったので備忘録として記載します。
私はKubernetesをはじめとしたContainer技術にはあまり縁が無く学習中なので、非常に簡単なところから詰まりました。
この記事がどこかの誰かの助けになれば幸いです。

# 1. 誤った目算
GKE上にてgcloudコマンドを実行するPodをDeployする際に一番最初に思いついたフローが下記となります。

1. ローカルにUbuntuの公式Containerをpull
2. Ubuntu Containerを起動してgcloudコマンドをInstall
3. 作成したContainerをImage化する
4. 作成したImageでGKE上でrunして動作確認

ローカルでContainerを検証した際と同じフローで考えていました。
しかし、Image作成を進めていたところ社内のKubernetes先生から下記条件を与えられました。

- Productionに向けて経験を積むためにもDockerfileでbuildしたContainerを使う
- GKEにて利用するContainerなので[Google Container Registry](https://cloud.google.com/container-registry/?hl=ja)(以下、GCR)を使う
- KubernetesへのデプロイはDeploymentリソースのManifestファイルを作成して行う

この時指摘を受けたのですが、自前のImageを手で作ってデプロイするのはあまり適切ではないとの事です。
確かに<font color="Red">『どういった手順で作成されたContainerなのか他メンバーが判断しにくい』</font>ので運用しにくいですね。

# 2. Dockerfileを作成
Dockerfileを作成するにあたって、当初の予定通りUbuntuの公式イメージをベースに考えていました。
しかし、Ubuntuの公式イメージにgcloudコマンドのインストールを試みたところ必須パッケージ等が色々と不足しておりスムーズに作成できません。
そこで、Googleが公開している[cloud-sdk-docker](https://github.com/GoogleCloudPlatform/cloud-sdk-docker/blob/master/Dockerfile)を利用する事にしました。
また、今回はgcloudコマンドにてサービスアカウントを利用するためにサービスアカウントを認証させています。

```Dockerfile:Dockerfile
ROM debian:buster
ARG CLOUD_SDK_VERSION=272.0.0
ENV CLOUD_SDK_VERSION=$CLOUD_SDK_VERSION
ENV PATH "$PATH:/opt/google-cloud-sdk/bin/"
RUN apt-get -qqy update && apt-get install -qqy \
        curl \
        gcc \
        python-dev \
        python-pip \
        apt-transport-https \
        lsb-release \
        openssh-client \
        git \
        make \
        gnupg && \
    pip install -U crcmod && \
    echo 'deb http://deb.debian.org/debian/ sid main' >> /etc/apt/sources.list && \
    export CLOUD_SDK_REPO="cloud-sdk-$(lsb_release -c -s)" && \
    echo "deb https://packages.cloud.google.com/apt $CLOUD_SDK_REPO main" > /etc/apt/sources.list.d/google-cloud-sdk.list && \
    curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - && \
    apt-get update && \
    apt-get install -y google-cloud-sdk=${CLOUD_SDK_VERSION}-0 \
        google-cloud-sdk-app-engine-python=${CLOUD_SDK_VERSION}-0 \
        google-cloud-sdk-app-engine-python-extras=${CLOUD_SDK_VERSION}-0 \
        google-cloud-sdk-app-engine-java=${CLOUD_SDK_VERSION}-0 \
        google-cloud-sdk-app-engine-go=${CLOUD_SDK_VERSION}-0 \
        google-cloud-sdk-datalab=${CLOUD_SDK_VERSION}-0 \
        google-cloud-sdk-datastore-emulator=${CLOUD_SDK_VERSION}-0 \
        google-cloud-sdk-pubsub-emulator=${CLOUD_SDK_VERSION}-0 \
        google-cloud-sdk-bigtable-emulator=${CLOUD_SDK_VERSION}-0 \
        google-cloud-sdk-cbt=${CLOUD_SDK_VERSION}-0 && \
    gcloud --version
VOLUME ["/root/.config"]
COPY service_account.key .
RUN gcloud auth activate-service-account --key-file service_account.key
```

# 3. docker build
作成したDockerfileを元にbuild作業を行います。
Image名ですが、GCRへのpushを行う場合には下記要件に従った名前にする必要があります。

```
gcr.io/[PROJECT-ID]/[IMAGE]:[TAG]
```

そこで今回はgcloud-devというImage名で作成してみました。

```bash:dockerbuild
$ docker build -t gcr.io/[PROJECT-ID]/gcloud-dev:v1 .
[+] Building 256.9s (9/9) FINISHED
 => [internal] load build definition from Dockerfile                                                                                                                                                                           0.0s
 => => transferring dockerfile: 1.64kB                                                                                                                                                                                         0.0s
 => [internal] load .dockerignore                                                                                                                                                                                              0.0s
 => => transferring context: 2B                                                                                                                                                                                                0.0s
 => [internal] load metadata for docker.io/library/debian:buster                                                                                                                                                               2.6s
 => CACHED [1/4] FROM docker.io/library/debian:buster@sha256:79f0b1682af1a6a29ff63182c8103027f4de98b22d8fb50040e9c4bb13e3de78                                                                                                  0.0s
 => [internal] load build context                                                                                                                                                                                              0.0s
 => => transferring context: 41B                                                                                                                                                                                               0.0s
 => [2/4] RUN apt-get -qqy update && apt-get install -qqy         curl         gcc         python-dev         python-pip         apt-transport-https         lsb-release         openssh-client         git         make     214.8s
 => [3/4] COPY service_account.key .                                                                                                                                                                                           0.1s
 => [4/4] RUN gcloud auth activate-service-account --key-file service_account.key                                                                                                                                              5.5s
 => exporting to image                                                                                                                                                                                                        33.7s
 => => exporting layers                                                                                                                                                                                                       33.7s
 => => writing image sha256:hogefugahogefugahogefugahogefugahogefugahogefugahogefuga                                                                                                                                   0.0s
 => => naming to gcr.io/[PROJECT-ID]/gcloud-dev:v1
```

作成したものを確認してみます。

```bash:images
$ docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
gcr.io/[PROJECT-ID]/gcloud-dev    v1                  hogehogeho        5 minutes ago       1.71GB
```

# 4. GCRへのpush
名前を適切に定義しているのでGCRへのpushはコマンド一つで可能です。

``` bash:gcr-upload
$ gcloud docker -- push gcr.io/[PROJECT-ID]/gcloud-dev:v1
WARNING: `gcloud docker` will not be supported for Docker client versions above 18.03.

As an alternative, use `gcloud auth configure-docker` to configure `docker` to
use `gcloud` as a credential helper, then use `docker` as you would for non-GCR
registries, e.g. `docker pull gcr.io/project-id/my-image`. Add
`--verbosity=error` to silence this warning: `gcloud docker
--verbosity=error -- pull gcr.io/project-id/my-image`.

See: https://cloud.google.com/container-registry/docs/support/deprecation-notices#gcloud-docker

The push refers to repository [gcr.io/[PROJECT-ID]/gcloud-dev]
cab060f85f59: Pushed
d6d840cf61fa: Pushed
b26a6aae8eca: Pushed
f2b4f0674ba3: Layer already exists
v1: digest: sha256:hogehogehogehogehogehogehogehogehogehogehogehogehogehoge size: 1158


Updates are available for some Cloud SDK components.  To install them,
please run:
  $ gcloud components updat
```

GCPのWebUIから見ても正常にpushされている事がわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528118/d417a67b-49cf-473a-ac27-bdbcf7a31641.png)

*cliにも出ていますがver18.03以上のDockerクライアントではgcloud dockerコマンドは動作保証されておりません。(2019年12月現在)。ver18.03以上のDockerクライアントを利用する場合は記載してあるURL先にある対応が必要です。*

# 5. kubectl run を使ったPodの動作確認
作成したContainerImageからPodを作成して正常に動作するかを確認します。

```bash:run
$ kubectl run gcloud-dev --image=gcr.io/[PROJECT-ID]/gcloud-dev:v1
kubectl run --generator=deployment/apps.v1 is DEPRECATED and will be removed in a future version. Use kubectl run --generator=run-pod/v1 or kubectl create instead.
deployment.apps/gcloud-dev created

$ kubectl get pods
NAME                                           READY   STATUS             RESTARTS   AGE
gcloud-dev-795d47546c-vcw9d                    0/1     CrashLoopBackOff   6          11m
```

＿人人人人人人人人人人人人＿
＞　CrashLoopBackOff　＜
￣Y^Y^Y^Y^Y^Y^Y^Y^Y^Y￣

# 6. 原因調査
Podの詳細情報を見るためにdescribeコマンドを叩いてみます。

```bash:describe
$ kubectl describe pod gcloud-dev-795d47546c-vcw9d
Name:           gcloud-dev-795d47546c-vcw9d
~省略~
Events:
  Type     Reason     Age                  From                                                          Message
  ----     ------     ----                 ----                                                          -------
  Normal   Scheduled  24m                  default-scheduler                                             Successfully assigned default/gcloud-dev-795d47546c-vcw9d to gke-gke-spanner-dev--gke-spanner-dev--59f9fd2a-mm2d
  Normal   Pulling    24m                  kubelet, gke-gke-spanner-dev--gke-spanner-dev--59f9fd2a-mm2d  Pulling image "gcr.io/[PROJECT-ID]/gcloud-dev:v1"
  Normal   Pulled     22m                  kubelet, gke-gke-spanner-dev--gke-spanner-dev--59f9fd2a-mm2d  Successfully pulled image "gcr.io/[PROJECT-ID]/gcloud-dev:v1"
  Normal   Created    21m (x5 over 22m)    kubelet, gke-gke-spanner-dev--gke-spanner-dev--59f9fd2a-mm2d  Created container gcloud-dev
  Normal   Started    21m (x5 over 22m)    kubelet, gke-gke-spanner-dev--gke-spanner-dev--59f9fd2a-mm2d  Started container gcloud-dev
  Normal   Pulled     21m (x4 over 22m)    kubelet, gke-gke-spanner-dev--gke-spanner-dev--59f9fd2a-mm2d  Container image "gcr.io/[PROJECT-ID]/gcloud-dev:v1" already present on machine
  Warning  BackOff    4m5s (x89 over 22m)  kubelet, gke-gke-spanner-dev--gke-spanner-dev--59f9fd2a-mm2d  Back-off restarting failed container
```

特に明示的なerrorを吐かずにrestartが走っています。

CrashLoopBackOffになる原因調べてみたところ、『Pod内Containerのプロセス終了を検知して再起動している』といった場合もCrashLoopBackOffになるとの事でした。

勘の良い方、Containerに詳しい方ならお気づきですね。
今回作成したContainerでは<font color="Red">foregroundで動かすようなプロセスを作成していません</font>でした。

GKEに限らずKubernetesではセルフヒーリングの機能があります。
これはPodがCrash等の理由でdown時に適切な数に戻るように自動で再作成してくれる機能です。
今回はPod内のプロセスが終了(そもそも無い)したのでセルフヒーリングによるrestartを繰り返そうとした結果CrashLoopBackOffとなったみたいです。

# 6. 解決策
今回はContainer内でgcloudコマンドを使う事が目的なので、適当にNginxを起動してContainerが異常終了する事を防ぎます。
解決方法としてImage作成時のDockerfileにて下記2つの処理を追加しました。

- NginxのInstall
- Container起動時にNginxのプロセスを起動

作成したものが以下となります。

```Dockerfile:Dockerfile:v2
ROM debian:buster
ARG CLOUD_SDK_VERSION=272.0.0
ENV CLOUD_SDK_VERSION=$CLOUD_SDK_VERSION
ENV PATH "$PATH:/opt/google-cloud-sdk/bin/"
RUN apt-get -qqy update && apt-get install -qqy \
        curl \
        gcc \
        python-dev \
        python-pip \
        apt-transport-https \
        lsb-release \
        openssh-client \
        git \
        make \
        gnupg && \
    pip install -U crcmod && \
    echo 'deb http://deb.debian.org/debian/ sid main' >> /etc/apt/sources.list && \
    export CLOUD_SDK_REPO="cloud-sdk-$(lsb_release -c -s)" && \
    echo "deb https://packages.cloud.google.com/apt $CLOUD_SDK_REPO main" > /etc/apt/sources.list.d/google-cloud-sdk.list && \
    curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - && \
    apt-get update && \
    apt-get install -y google-cloud-sdk=${CLOUD_SDK_VERSION}-0 \
        google-cloud-sdk-app-engine-python=${CLOUD_SDK_VERSION}-0 \
        google-cloud-sdk-app-engine-python-extras=${CLOUD_SDK_VERSION}-0 \
        google-cloud-sdk-app-engine-java=${CLOUD_SDK_VERSION}-0 \
        google-cloud-sdk-app-engine-go=${CLOUD_SDK_VERSION}-0 \
        google-cloud-sdk-datalab=${CLOUD_SDK_VERSION}-0 \
        google-cloud-sdk-datastore-emulator=${CLOUD_SDK_VERSION}-0 \
        google-cloud-sdk-pubsub-emulator=${CLOUD_SDK_VERSION}-0 \
        google-cloud-sdk-bigtable-emulator=${CLOUD_SDK_VERSION}-0 \
        google-cloud-sdk-cbt=${CLOUD_SDK_VERSION}-0 \
        nginx && \
    gcloud --version
VOLUME ["/root/.config"]
COPY service_account.key .
RUN gcloud auth activate-service-account --key-file service_account.key
CMD ["/usr/sbin/nginx", "-g", "daemon off;"]
```

こちらで『gcr.io/[PROJECT-ID]/gcloud-dev:v2』としてGCRにpushしました。

# 7. kubectl run で改めてPodの動作確認
作成したv2にて改めてPodを動かしてみます。

```bash:run-v2
$ kubectl run gcloud-dev --image=gcr.io/[PROJECT-ID]/gcloud-dev:v2
kubectl run --generator=deployment/apps.v1 is DEPRECATED and will be removed in a future version. Use kubectl run --generator=run-pod/v1 or kubectl create instead.
deployment.apps/gcloud-dev created
$ kubectl get pods
NAME                                           READY   STATUS    RESTARTS   AGE
gcloud-dev-6bf59ffc9b-r77zd                    1/1     Running   0          8s
$ kubectl exec -it gcloud-dev-6bf59ffc9b-r77zd /bin/bash
root@gcloud-dev-6bf59ffc9b-r77zd:/# gcloud --version
Google Cloud SDK 272.0.0
alpha 2019.11.16
app-engine-go 
app-engine-java 1.9.77
app-engine-python 1.9.87
app-engine-python-extras 1.9.87
beta 2019.11.16
bigtable 
bq 2.0.50
cbt 
cloud-datastore-emulator 2.1.0
core 2019.11.16
datalab 20190610
gsutil 4.46
kubectl 2019.11.16
pubsub-emulator 0.1.0
root@gcloud-dev-6bf59ffc9b-r77zd:/# 
```

正常にrunningになりましたね。
gcloudコマンドも動いてそうで一安心です。

# 8. DeploymentリソースのManifestファイルを作成
Containerの動作確認はできたのでDeploymentリソースのManifestファイルを作成します。
今回はServiceリソースの作成は検証に必要ないのでPod作成のみとなります。

```yaml:Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment-gcloud-dev
spec:
  selector:
    matchLabels:
      app: gcloud-dev
  replicas: 2 
  template: 
    metadata:
      labels:
        app: gcloud-dev
    spec:
      containers:
      - name: gcloud-container
        image: gcr.io/[PROJECT-ID]/gcloud-dev:v2
        ports:
        - containerPort: 80
```

作成したDeploymentリソースを使ってGKE上にデプロイしてみます。

```bash:create
$ kubectl create -f deployment-gcloud-dev.yaml
deployment.apps/deployment-gcloud-dev created
$ kubectl get deployment
NAME                          READY   UP-TO-DATE   AVAILABLE   AGE
deployment-gcloud-dev         2/2     2            2           14s
$ kubectl get pods
NAME                                           READY   STATUS    RESTARTS   AGE
deployment-gcloud-dev-54f87c5f77-564t8         1/1     Running   0          19s
deployment-gcloud-dev-54f87c5f77-5gw7w         1/1     Running   0          19s
```

正常にデプロイできました。
これにて完了となります。


# 雑感
冒頭に記載した通り、私自身はKubernetesをはじめとしたContainer技術を勉強中で本を読みながら環境を作った程度だったので非常に細かな躓きが多くなってしまいました。
本の通りに動かすのではなく、実際に自分で要件を持って進めたことで細部を知ることができる良い機会となりました。

今回の記事では要件に従い、『どのように考えて』『どのように失敗して』『どのように解決したか』を順を追って記載しました。
まだまだContainer/Kubernetesが広範囲で利用されているとは言い難く勉強中の方がたくさん居ると思います。
そういった方達に最初の1歩の助けとしてカジュアルに読んで頂ければ幸いです。

Container/Kubernetes勉強中の皆様、長い道程ですが一緒にがんばりましょう！