これでもかって言うくらいコピペでKubernetes(Google Kubernetes Engine)に環境変数の設定からRailsアプリケーションのmigrateも考慮したデプロイが動くコマンドを書いていく

# はじめに
AWSもKubernetesに対応したEKSを発表しました。今やDockerとKubernetesは切っても切れない関係になっていくと思ってます。
今回は既にKubernetesの使用を前提としているGoogle Kubernetes Engine(GKE)でのRailsアプリケーションのデプロイを書いておきます。ただし、GKEへのデプロイって結構記事はあるんですが、migrateが絡んだ記事や実用になるような記事ってなかなか書いてないなと思い、ここに書いておきます。
初めてKubernetesやGKEに触れる方の参考になればと思います。

# 前提条件
1. アプリケーションはRailsで、バックエンドもフロントエンドもRailsとする。
2. データベースはPostgreSQLを使用するが、Google Cloud SQLではなく、Dockerコンテナとして立ち上げるものとする。
3. gcloudやkubectlのインストールや設定は説明しませんので、その辺はググって下さい。

# Railsアプリケーション
まずはDockerコンテナで動かすRailsアプリケーションを作成する必要があります。
簡単ですが、以下にDockerコンテナで動作するRailsアプリケーションを作成していきます。
ポイントは解説していきます。（正直Railsアプリケーションにフォーカスしているので読み飛ばしても可）

## Dockerファイル
これは至って簡単です。単純にコンテナにすればいいだけなので、以下にサンプルを作っておきます。（あくまでサンプルです。本当は`alpine-sdk`は後で消しておいた方がサイズが小さくなると思います）

```dockerfile
FROM ruby:2.4.1-alpine

RUN apk update && apk upgrade && apk add --update --no-cache alpine-sdk tzdata postgresql-dev nodejs
RUN mkdir /app
WORKDIR /app

ARG BUNDLE_OPTIONS

ADD Gemfile /app/Gemfile
ADD Gemfile.lock /app/Gemfile.lock
RUN bundle install --path vendor/bundle -j4 ${BUNDLE_OPTIONS}
ADD . /app

RUN bundle exec rake assets:precompile

EXPOSE  3000
```


## database.ymlの接続設定
次にデータベースへの接続情報を持つ`database.yml`です。

```yaml:database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

production:
  <<: *default
  database: rails_production
  host: <%= ENV['DATABASE_HOST'] %>
  username: <%= ENV['DATABASE_USERNAME'] %>
  password: <%= ENV['DATABASE_PASSWORD'] %>
  port: <%= ENV['DATABASE_PORT'] %>
```

データベースの接続を環境変数から読み込むように設定します。このように設定することで環境設定にさえ設定すればどこへでも接続することが可能となるわけです。

## DockerイメージのPUSH

実際作るDockerイメージのbuildからDockerレジストリへのPUSHを以下に記載します。

```bash
$ docker build -t rails:latest build --build-arg BUNDLE_OPTIONS='--without development test' .
$ docker tag rails:latest us.gcr.io/<プロジェクト>-xxxxxxx/<イメージ名>:<コンテナタグ>
$ gcloud docker -- push us.gcr.io/<プロジェクト>-xxxxxxx/<イメージ名>:<コンテナタグ>
```

環境変数を渡すには`--build-arg`にて渡すことが可能です。これで`bundle install`に余計なgemをインストールしないようにしています。

# Google Kubernetes Engine(GKE)
ここからの記事の本題です。
以下を実行していくことで簡単にRailsアプリケーションをデプロイまで持っていけるはずです。

## クラスタの作成
AWSでいうとECSみたいなイメージで考えてくれて結構です。これを下記のコマンドでサクッと作ります。

```bash
$ gcloud container clusters create --machine-type=g1-small --disk-size=30 --num-nodes=3 test-cluster
```

ディスクサイズは適当です。30G程度あれば事足りるはずです。
ここで重要なのは`--machine-type=g1-small`です。[ここ](https://cloud.google.com/compute/pricing?hl=ja)にマシンスペックごとの料金が書いています。GKEは「f1-micro」「ノード数1」「USリージョン」であれば無料で使えるのですが、前提条件に書いている通り、データベースもコンテナで動かすにはメモリもCPUも足りません。ですので、`g1-small`は最低でも必要です。

## データベースの永続ストレージの追加
クラスタ作成にディスクは作成しましたが、データベースには共通して必要なディスクがいるのでそれを作成します。

```bash
$ gcloud compute disks create --size 5GB postgresql-disk
```

サイズはかなり適当です。データが多くて必要になりそうなら100GB程度でも用意しておいて下さい。

## 環境変数の登録

Railsにはデータベースの接続情報や`secret_key_base`といった情報を必要とします。しかし簡単に人には知られたくない情報でもあります。そこでKubernetesには[機密情報を扱う機能](https://kubernetes.io/docs/concepts/configuration/secret/)を保持していますので、それの機能を使用して登録します。
ドキュメントに書いている通り、マニュアルで登録するには必要文字列はBase64でエンコードして登録する必要があります。

```bash
$ echo -n "database_user" | base64
#=> ZGF0YWJhc2VfdXNlcg==
$ echo -n "database_password" | base64
#=> ZGF0YWJhc2VfcGFzc3dvcmQ=
$ echo -n "secret_key_base" | base64
#=> c2VjcmV0X2tleV9iYXNl
```

上記のようにBase64にエンコードした文字列をSecretファイルに定義します。
注意：secret_key_baseはちゃんと長い文字列を使用しましょう。

```yaml:secret.yml
apiVersion: v1
kind: Secret
metadata:
  name: rails
type: Opaque
data:
  database_user: ZGF0YWJhc2VfdXNlcg==
  database_password: ZGF0YWJhc2VfcGFzc3dvcmQ=
  secret_key_base: c2VjcmV0X2tleV9iYXNl
```

`name`は何のsecret情報なのかわかりやすい名前をつけましょう。
上のYMLファイルができれば後はそれを登録するだけです。

```bash
# ymlの保存先はお好きなとこで
$ kubectl create -f ~/.kube/secret.yml
$ kubectl get secret
# NAME       TYPE        DATA      AGE
# rails      Opaque      3         1d
```

登録できれば上記のようになるはずです。
暗号化だけであれば[yaml_vault](https://github.com/joker1007/yaml_vault)を使うのもありです。

## PostgreSQL（データベース）
### PostgreSQLコンテナの起動
コンテナの起動となるdeploymentの作成をします。
作成は下記のYMLファイルを使用します。

```yaml:postgresql.yml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: postgresql
  labels:
    app: postgresql
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgresql
  template:
    metadata:
      labels:
        app: postgresql
    spec:
      containers:
        - image: postgres:9.6-alpine
          name: postgresql
          env:
            - name: POSTGRES_USER
              valueFrom:
                secretKeyRef:
                  name: rails
                  key: database_user
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: rails
                  key: database_password
          ports:
            - containerPort: 5432
              name: postgresql
          volumeMounts:
            - name: postgresql-persistent-storage
              mountPath: /var/lib/postgresql
      volumes:
        - name: postgresql-persistent-storage
          gcePersistentDisk:
            pdName: postgresql-disk
            fsType: ext4
```

`name`はわかりやすい名前を好きにつけて下さい。ここで大切なのは`env`と`volumes`です。

まずは`env`ですが、これは先程登録したsecret情報を参照するという記述になっております。上記を記載することによりPostgreSQLのコンテナ起動に以下のユーザが作成されます。

次に`volumes`ですが、これがデータベースのストレージを定義しています。最初にデータベースのストレージを作成しましたが、ここで使用する設定が出てきます。この設定をすることによりデータの永続化が可能となります。

これを以下のコマンドで登録してします。

```bash
$ kubectl create -f kubernetes/postgresql.yml
$ kubectl get deployment
# NAME         DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
# postgresql   1         1         1            1           1d
```

このように起動するはずです。

### PostgreSQLサービスの起動
上記のdeploymentを設定しただけ、接続は一切できません。そこでserviceの設定を追加します。

```yaml:postgresql-service.yml
apiVersion: v1
kind: Service
metadata:
  name: postgresql
  labels:
    app: postgresql
spec:
  type: ClusterIP
  ports:
    - port: 5432
  selector:
    app: postgresql
```

ここで大事なので`type`です。上記の設定では`ClusterIP`としていますので、クラスタ内からの接続に限定しています。
これを起動するには同様に以下のコマンドです。

```bash
$ kubectl create -f kubernetes/postgresql-service.yml
$ kubectl get service
# NAME         TYPE           CLUSTER-IP     EXTERNAL-IP      PORT(S)          AGE
# kubernetes   ClusterIP      10.3.240.1     <none>           443/TCP          1d
# postgresql   ClusterIP      10.3.253.218   <none>           5432/TCP         1d
```

これでデータベースへ接続できる状態となりました。

## Railsアプリケーション
データベースが準備できたので、アプリケーションのコンテナを起動していきます。
基本的にはデータベースと同様にdeploymentを設定し、serviceを設定します。

### アプリケーションコンテナの起動
まずはアプリケーションのdeploymentとなるYMLファイルを以下に記載します。

```yaml:rails.yml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: rails
  labels:
    app: rails
spec:
  replicas: 1
  selector:
    matchLabels:
      app: rails
  template:
    metadata:
      labels:
        app: rails
    spec:
      containers:
        - image: $RAILS_IMAGE
          name: rails
          env:
            - name: RAILS_ENV
              value: "production"
            - name: DATABASE_HOST
              value: postgresql
            - name: DATABASE_USERNAME
              valueFrom:
                secretKeyRef:
                  name: rails
                  key: database_user
            - name: DATABASE_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: rails
                  key: database_password
            - name: SECRET_KEY_BASE
              valueFrom:
                secretKeyRef:
                  name: rails
                  key: secret_key_base
            - name: DATABASE_PORT
              value: "5432"
          ports:
            - containerPort: 3000
              name: rails
          command: ["bundle", "exec", "rails", "s", "-p", "3000", "-b", "0.0.0.0"]
```

ここも重要な設定項目を以下に説明します。
まずはなんといっても`env`項目です。ここではまずデータベースの設定と同様にsecretに設定したデータベースの接続情報を参照するように記載しています。更にデータベースの接続先ホストを`DATABASE_HOST`の環境設定に記載しています。値は`postgresql`と記載していますが、これは先に設定したデータベースのコンテナ（service）での`name`を指定して下さい。kubernetesはpods間のネットワークが自動的に構築されます。docker-composeにも同じようなものがありますが、それと似たようなものと考えて結構です。（これがあるがゆえにminikubeに移行した方がいいと言われる1つかなと思います）

次に重要なのが`image`です。ここはGoogle Container Registryに登録されたイメージを記載します。本来ならばここには`image: us.gcr.io/<プロジェクト>-xxxxxxx/<コンテナ名>:<コンテナタグ>`というような設定をしますが、ここには環境変数のように **わざと** 設定しています。これについては以下の起動コマンドを見れば理由がわかると思います。

```bash
$ export RAILS_IMAGE=us.gcr.io/rails-test-182304/rails:latest
$ cat kubernetes/rails.yml | envsubst | kubectl create -f -
```

そうです。`image`の設定を外から渡せるようにしているのです。これの最大の利点は最後に説明するアプリケーションコンテナの更新時に最大の効果を発揮します。
簡単に説明しておくと`envsubst`は環境変数に登録した部分を変換してくれます。これにより`cat`により出力されたYMLファイルの環境変数部分を変換した上で、`kubectl create`に渡すようになるわけです。

### アプリケーションサービスの起動
このままではデータベース同様にアプリケーションにアクセスができませんので、serviceを登録します。

```yaml:rails-service.yml
apiVersion: v1
kind: Service
metadata:
  name: rails
  labels:
    app: rails
spec:
  type: LoadBalancer
  ports:
    - port: 3000
  selector:
    app: rails
```

大事なのは`type: LoadBalancer`の設定です。これによりEXTERNAL-IPを取得します。
注意：静的IPではないので必要な場合はload-balancer-ipを指定すること

## アプリケーションのデプロイ＆マイグレーション
データベース、アプリケーションが起動しました。
が、大事なことを忘れています。そうです、データベースにまだテーブルがありません。それもそのはずで、ここまで一切マイグレーションを実行していないからです。

### データベースの作成＆マイグレーション
マイグレーションを実施するのですが、実施には[Job](https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/)を使用します。

```yaml:job.yml
apiVersion: batch/v1
kind: Job
metadata:
  name: deploy-tasks
spec:
  template:
    metadata:
      name: deploy-tasks
      labels:
        name: deploy-tasks
    spec:
      nodeSelector:
        cloud.google.com/gke-nodepool: default-pool
      restartPolicy: Never
      containers:
        - name: deploy-tasks-runner
          image: $RAILS_IMAGE
          command: ["/app/script/deploy-tasks.sh"]
          env:
            - name: RAILS_ENV
              value: "production"
            - name: DATABASE_HOST
              value: postgresql
            - name: DATABASE_USERNAME
              valueFrom:
                secretKeyRef:
                  name: rails
                  key: database_user
            - name: DATABASE_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: rails
                  key: database_password
            - name: SECRET_KEY_BASE
              valueFrom:
                secretKeyRef:
                  name: rails
                  key: secret_key_base
            - name: DATABASE_PORT
              value: "5432"
```

大事なのはアプリケーションコンテナの起動でも説明しましたが、`image`と`env`です。
これに関しては説明したので省略します。
あとは`command`部分です。本記事ではshellファイルを用意してそれを実行するようにしています。shellファイルは以下のようなものです。

```shell:script/deploy-tasks.sh
#!/bin/sh

set -e

bundle exec rails db:create
bundle exec rails db:migrate
# 他にしたい処理があれば記載する
```

これを実行することで、データベースの作成からマイグレートを実行します。
ただし、今回はJobが正常に実行できたかどうかを判定する必要があります。そのために以下のようなshellファイルを作成し、実行すればそれを検出することが可能です。

```shell:deploy.sh
#!/bin/bash

# 前のJobが残っていたらまずは消す
kubectl delete job deploy-tasks 2&> /dev/null || true
# マイグレート用のJobを作成し、実行します
cat kubernetes/deploy-tasks-job.yml | envsubst | kubectl create -f -
# Jobが正常に実行されるまで待ちます
while [ true ]; do
  phase=`kubectl get pods -a --selector="name=deploy-tasks" -o 'jsonpath={.items[0].status.phase}' || 'false'`
  if [[ "$phase" != 'Pending' ]]; then
    break
  fi
done

# Jobの終了状態を取得します
while [ true ]; do
  succeeded=`kubectl get jobs deploy-tasks -o 'jsonpath={.status.succeeded}'`
  failed=`kubectl get jobs deploy-tasks -o 'jsonpath={.status.failed}'`
  if [[ "$succeeded" == "1" ]]; then
    break
  elif [[ "$failed" -gt "0" ]]; then
    kubectl describe job deploy-tasks
    kubectl delete job deploy-tasks
    echo 'マイグレートに失敗！'
    exit 1
  fi
done
kubectl delete job deploy-tasks || true
```

上記のshellを以下のように実行すれば、マイグレートが実行できます。

```bash
$ export RAILS_IMAGE=us.gcr.io/rails-test-182304/rails:latest
$ ./script/deploy.sh
```

### アプリケーションのデプロイ
アプリケーションのコンテナを立てたことは立てのですが、プログラムを更新したい場合はコンテナを更新する必要があります。ここで更新する方法は2つあります。
1. `kubectl set image deployment/rails rails:1.0.0`
2. `kubectl apply -f kubernetes/rails.yaml`
まず1ですが、既にデプロイされている設定に新たなイメージを記載する方法です。逆に2は作成したYMLファイルを更新してからコンテナを更新させる方法です。
1に関しては初回設定をずっと使用し続けます。逆に途中で変更するには2を使用する必要があります。なので設定も変更することを考慮して2を使用するとします。
しかし、2を使用するにはコンテナのアップデートをわざわざYMLファイルに記載しないといけませんが、`envsubst`を使用することで解消できるわけです。

```bash
$ export RAILS_IMAGE=us.gcr.io/rails-test-182304/rails:latest
$ cat kubernetes/rails.yml | envsubst | kubectl apply -f -
```

これでコンテナのデプロイも可能となるわけです。
これで以下のコマンドで出てきた`http://<EXTERNAL-IP>:3000`でサイトにアクセスできるはずです。

```bash
$ kubectl get service
```

# トラブルシューティング
こうは書いても結構つまることが多々あります。こう書いている自分も色々と詰まりましたので、自身のログ参照方法を書いておきます。

## Cloud Computeにsshログインする
コンテナを実行しているクラウドサーバにsshにログインします。ログイン先は以下のコマンドで確認可能です。

```bash
$ kubectl get pods -o wide
# NAME                          READY     STATUS    RESTARTS   AGE       IP         NODE
# postgresql-1102438392-7cc4p   1/1       Running   0          1d        10.0.2.6   gke-rails-cluster-default-pool-df1514fd-30s7
# rails-4074712412-k9np7        1/1       Running   0          1d        10.0.1.4   gke-rails-cluster-default-pool-df1514fd-qpth
$ gcloud compute ssh gke-rails-cluster-default-pool-df1514fd-qpth
```

これでsshにログインできると思います。何か調査するお供にどうぞ。

## ログや状態を確認する
deploymentを設定しても`kubectl get pods`を叩くと"Ready"や"0/1"となるコンテナが起動しない。もしくは起動と失敗を繰り返す場合にはログを確認すると早いです。

```bash
kubectl describe pod <pod name>
```

取得したpodの名前を上記のコマンドで使用すると状態を確認することができます。

## 標準出力を確認する
標準出力だけを参照する場合は

```bash
$ kubectl logs <podのNAME>
```

で標準出力の参照が可能です。

## 起動コンテナの中に入る
ログを見てもよくわからない場合はコンテナに入ってしまいましょう。最終手段ですが…

```bash
kubectl exec -it <pod name> /bin/sh
```

これでコンテナの中に入れるようになるはずです。ただし、コンテナが起動していることが最低条件です。

# 最後に
Railsアプリケーションを例に本記事を書きましたが、Railsだけではなく他のアプリケーションでも応用可能だと思います。
よいGKE(Kubernetes)ライフをお送り下さい。

# 参考URL
https://cloud.google.com/kubernetes-engine/docs/tutorials?hl=ja
https://engineering.adwerx.com/rails-on-kubernetes-8cd4940eacbe