Edited at

これでもかって言うくらいコピペで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は後で消しておいた方がサイズが小さくなると思います)

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です。


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を以下に記載します。

$ 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みたいなイメージで考えてくれて結構です。これを下記のコマンドでサクッと作ります。

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

ディスクサイズは適当です。30G程度あれば事足りるはずです。

ここで重要なのは--machine-type=g1-smallです。ここにマシンスペックごとの料金が書いています。GKEは「f1-micro」「ノード数1」「USリージョン」であれば無料で使えるのですが、前提条件に書いている通り、データベースもコンテナで動かすにはメモリもCPUも足りません。ですので、g1-smallは最低でも必要です。


データベースの永続ストレージの追加

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

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

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


環境変数の登録

Railsにはデータベースの接続情報やsecret_key_baseといった情報を必要とします。しかし簡単に人には知られたくない情報でもあります。そこでKubernetesには機密情報を扱う機能を保持していますので、それの機能を使用して登録します。

ドキュメントに書いている通り、マニュアルで登録するには必要文字列はBase64でエンコードして登録する必要があります。

$ echo -n "database_user" | base64

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

上記のようにBase64にエンコードした文字列をSecretファイルに定義します。

注意:secret_key_baseはちゃんと長い文字列を使用しましょう。


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ファイルができれば後はそれを登録するだけです。

# ymlの保存先はお好きなとこで

$ kubectl create -f ~/.kube/secret.yml
$ kubectl get secret
# NAME TYPE DATA AGE
# rails Opaque 3 1d

登録できれば上記のようになるはずです。

暗号化だけであればyaml_vaultを使うのもありです。


PostgreSQL(データベース)


PostgreSQLコンテナの起動

コンテナの起動となるdeploymentの作成をします。

作成は下記のYMLファイルを使用します。


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はわかりやすい名前を好きにつけて下さい。ここで大切なのはenvvolumesです。

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

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

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

$ 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の設定を追加します。


postgresql-service.yml

apiVersion: v1

kind: Service
metadata:
name: postgresql
labels:
app: postgresql
spec:
type: ClusterIP
ports:
- port: 5432
selector:
app: postgresql

ここで大事なのでtypeです。上記の設定ではClusterIPとしていますので、クラスタ内からの接続に限定しています。

これを起動するには同様に以下のコマンドです。

$ 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ファイルを以下に記載します。


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/<コンテナ名>:<コンテナタグ>というような設定をしますが、ここには環境変数のように わざと 設定しています。これについては以下の起動コマンドを見れば理由がわかると思います。

$ 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を登録します。


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を使用します。


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"

大事なのはアプリケーションコンテナの起動でも説明しましたが、imageenvです。

これに関しては説明したので省略します。

あとはcommand部分です。本記事ではshellファイルを用意してそれを実行するようにしています。shellファイルは以下のようなものです。


script/deploy-tasks.sh

#!/bin/sh

set -e

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


これを実行することで、データベースの作成からマイグレートを実行します。

ただし、今回はJobが正常に実行できたかどうかを判定する必要があります。そのために以下のような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を以下のように実行すれば、マイグレートが実行できます。

$ 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を使用することで解消できるわけです。

$ export RAILS_IMAGE=us.gcr.io/rails-test-182304/rails:latest

$ cat kubernetes/rails.yml | envsubst | kubectl apply -f -

これでコンテナのデプロイも可能となるわけです。

これで以下のコマンドで出てきたhttp://<EXTERNAL-IP>:3000でサイトにアクセスできるはずです。

$ kubectl get service


トラブルシューティング

こうは書いても結構つまることが多々あります。こう書いている自分も色々と詰まりましたので、自身のログ参照方法を書いておきます。


Cloud Computeにsshログインする

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

$ 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"となるコンテナが起動しない。もしくは起動と失敗を繰り返す場合にはログを確認すると早いです。

kubectl describe pod <pod name>

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


標準出力を確認する

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

$ kubectl logs <podのNAME>

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


起動コンテナの中に入る

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

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