GKEに Rails + nginx(+Phusion Passenger ) + CloudSQL を利用したアプリをデプロイする

# はじめに
GKEにRailsアプリをデプロイした際の備忘録です。

# やりたかったこと
- GKEにRailsアプリをリリースする
- Railsはnginxと連携させる
- DBにはCloudSQLを利用する

# 参考にしたサイト

構築にあたって以下の解説＆Githubリポジトリを参考にさせていただきました。

- [Deploying Rails on Kubernetes – Adwerx Engineering](https://engineering.adwerx.com/rails-on-kubernetes-8cd4940eacbe)
- [jbielick/rails\-kube\-demo: A sample application for development in docker\-compose and deployment via Kubernetes \(GKE\)](https://github.com/jbielick/rails-kube-demo)

GithubリポジトリのサンプルアプリはDBに接続するための設定は記述されてますが、Modelが作成されてなかったため、追加しました。

```shell-session
$ rails g scaffold User name:string age:integer
$ rake db:migrate
```

```ruby:config/routes.rb
Rails.application.routes.draw do
  resources :users       # 追記
  root to: 'users#index' # 変更
  get '/_health', to: 'application#health'
end
```

# 注意点
解説内のコマンドはカレントディレクトリがリポジトリの直下になってることを前提にしてます。

```shell-session
$ git clone https://github.com/jbielick/rails-kube-demo.git
$ cd rails-kube-demo
```


# 利用したサービス

## Google Container Service (GKE)

Podに２つのコンテナを配置する構成にしました。

### raiisコンテナ
nginxとRailsアプリが配置されてるコンテナです。
Railsはpumaを利用せず、`Phusion Passenger`を利用してnginxと連携します。

### cloudsql-proxyコンテナ
CloudSQLと安全に接続するために利用するコンテナです。

## Google Cloud SQL
GKEにデプロイしたRailsアプリのデータベースとして利用しました。
Railsアプリとは`cloudsql-proxy`コンテナを経由して接続するようにしました。

# プロジェクト

## 作成
Google Cloud Platform(GCP)のコンソールでプロジェクトを作成しておく。

## API
作成したプロジェクトで以下のAPIを有効化しておく

- Google Container Engine API
- Google Cloud Container Builder API
- Google Cloud SQL API

# Cloud SDK

## インストール
以下のサイトを参考にCloud SDKのインストールと初期化を実施する。

[クイックスタート  \|  Cloud SDK  \|  Google Cloud Platform](https://cloud.google.com/sdk/docs/quickstarts?hl=ja)

## アップデート
Cloud SDKをインストール済みの場合は念のためアップデートしておく。

```shell-session
$ gcloud components update
```

## バージョン確認

```shell-session
$ gcloud -v
Google Cloud SDK 170.0.1
bq 2.0.25
core 2017.09.08
gcloud
gsutil 4.27
kubectl
```

## グローバル設定

gcloudコマンドからプロジェクト操作が行えるようにするために認証や設定を済ませておく。

参考：[開始方法  \|  Container Engine  \|  Google Cloud Platform](https://cloud.google.com/container-engine/docs/before-you-begin?hl=ja#gcloud)

```shell-session
$ gcloud auth login
$ gcloud config set project [PROJECT_ID]
$ gcloud config set compute/zone [ZONE]
```

補足：
[PROJECT_ID] プロジェクトIDを指定
[ZONE] [リージョンとゾーン](https://cloud.google.com/compute/docs/regions-zones/regions-zones?hl=ja#available)(us-west1-a など)を指定

# kubectl

GKEを利用するために`kubectl`が必要なためインストールしておく。

参考：[開始方法  \|  Container Engine  \|  Google Cloud Platform](https://cloud.google.com/container-engine/docs/before-you-begin?hl=ja#kubectl)

## インストール

```shell-session
$ gcloud components install kubectl
```

## バージョン確認

```shell-session
$ kubectl version --short
Client Version: v1.7.5
```



# CloudSQL

## インスタンス作成

インスタンスを作成して、ユーザ、データベースを追加しておく。

参考：
[インスタンスを作成する](https://cloud.google.com/sql/docs/mysql/create-instance)
[ユーザーを作成する](https://cloud.google.com/sql/docs/mysql/create-manage-users#creating)
[データベースを作成する](https://cloud.google.com/sql/docs/mysql/create-manage-databases#creating_a_database)

## サービスアカウント作成

サービスアカウントを作成して秘密鍵ファイル(json)をダウンロードしておく。

参考：[サービス アカウントを作成する](https://cloud.google.com/sql/docs/container-engine-connect#create_a_service_account)



# コンテナクラスタ

## 作成

```shell-session
$ gcloud container clusters create [CLUSTER-NAME] \
  --machine-type=[MACHINE] \
  --zone=[ZONE]

Creating cluster test-cluster...done.
Created [https://container.googleapis.com/v1/projects/xxxx-xxxx-xxxxxx/zones/us-west1-a/clusters/test-cluster].
kubeconfig entry generated for test-cluster.
NAME          ZONE        MASTER_VERSION  MASTER_IP     MACHINE_TYPE   NODE_VERSION  NUM_NODES  STATUS
test-cluster  us-west1-a  1.6.9           xx.xxx.xx.xx  n1-standard-1  1.6.9         3          RUNNING
```

補足：
[CLUSTER-NAME] クラスタの名称(test-cluster など)を指定
[MACHINE] [マシンタイプ](https://cloud.google.com/compute/docs/machine-types?hl=ja)(n1-standard-1 など)を指定

## 情報取得

```shell-session
$ gcloud container clusters list
$ gcloud container get-server-config
$ kubectl cluster-info
```



## 管理コンソール

### 認証情報のダウンロード

```shell-session
$ gcloud container clusters get-credentials [CLUSTER-NAME] \
    --zone [ZONE] --project [PROJECT_ID]
```

### Proxy起動

```shell-session
$ kubectl proxy
```
ブラウザで下記のURLにアクセスするとkubernetesの管理画面にアクセスできる
http://localhost:8001/ui

<img width="1106" alt="kubernetes.png" src="https://qiita-image-store.s3.amazonaws.com/0/141934/def1f7a8-8647-7eda-2312-e8e93e1e3a2f.png">

# Secret

## Rails

### SECRET_KEY_BASEをSecretに設定

```shell-session
$ kubectl create secret generic rails --from-literal=secret-key-base=1d9b8645dba9b62317546565b43a4c485ee045e51c6f9bab65efcfdc69e97fde8030703e3f3912ef297e11fed30296441059d81f32ab23a13b8497158a35f654
```

補足：
SECRET_KEY_BASEの生成コマンド

```shell-session
$ bundle exec rake secret
1d9b8645dba9b62317546565b43a4c485ee045e51c6f9bab65efcfdc69e97fde8030703e3f3912ef297e11fed30296441059d81f32ab23a13b8497158a35f654
```

## CloudSQL

### インスタンスアクセスのためのSecretを設定

```shell-session
$ kubectl create secret generic cloudsql-oauth-credentials --from-file=credentials.json=[PATH_TO_CREDENTIAL_FILE]
```

補足：
[PATH_TO_CREDENTIAL_FILE] 秘密鍵ファイル(json)のパス

### データベースアクセスのためのSecretを設定

```shell-session
$ kubectl create secret generic cloudsql --from-literal=username=[PROXY_USERNAME] --from-literal=password=[PASSWORD]
```

補足：
[PROXY_USERNAME] DBユーザ名を指定
[PASSWORD] DBパスワードを指定



# コンテナイメージ

## データベース設定

`database`にはCloudSQLで作成したデータベース名を指定します。
`host`,`username`,`password`を環境変数から取得するようにします。

```yaml:config/database.yml
database: xxxxx
host: <%= ENV['MYSQL_SERVICE_HOST'] %>
username: <%= ENV['MYSQL_USER'] %>
password: <%= ENV['MYSQL_PASSWORD'] %>
```

補足：
`nginx+rails`コンテナで利用する環境変数名は事前に`nginx/env.conf`に記述しておく必要があります。

## ビルド

Container Builderを利用してコンテナのビルドを実施する。
ビルドに成功するとContainer Registryにイメージが登録される。

参考：[既存の Docker ビルドのクイックスタート  \|  Cloud Container Builder のドキュメント  \|  Google Cloud Platform](https://cloud.google.com/container-builder/docs/docker-build?hl=ja)

```shell-session
$ gcloud container builds submit --tag gcr.io/[PROJECT_ID]/[IMAGE] .

...
------------------------------------------------------------------------------------------------------------------------------------------

ID                                    CREATE_TIME                DURATION  SOURCE                                                                                      IMAGES                                    STATUS
e9c6555c-e7ff-4dc8-bd12-8ac9d9d49c7d  2017-09-10T00:20:04+00:00  3M59S     gs://xxxx-xxxx-xxxxxx_cloudbuild/source/1505002801.08-94f9c069c40644748a73c38b01428dc7.tgz  gcr.io/xxxx-xxxx-xxxxxx/my-app (+1 more)  SUCCESS
```

補足：
[IMAGE] Continer Registry上の管理名(my-app など)を指定

## 備考

ビルド中に以下のようなエラーが発生した場合はGemfileの編集とGemfile.lockの更新を行う。

```shell-session
...
Step 17/19 : RUN bundle exec rake assets:precompile
 ---> Running in cfabcec0d769
rake aborted!
TZInfo::DataSourceNotFound: tzinfo-data is not present. Please add gem 'tzinfo-data' to your Gemfile and run bundle install
/usr/local/rvm/gems/ruby-2.3.3/gems/activesupport-5.0.6/lib/active_support/railtie.rb:22:in `rescue in block in <class:Railtie>'
/usr/local/rvm/gems/ruby-2.3.3/gems/activesupport-5.0.6/lib/active_support/railtie.rb:19:in `block in <class:Railtie>'
/usr/local/rvm/gems/ruby-2.3.3/gems/railties-5.0.6/lib/rails/initializable.rb:30:in `instance_exec'
/usr/local/rvm/gems/ruby-2.3.3/gems/railties-5.0.6/lib/rails/initializable.rb:30:in `run'
/usr/local/rvm/gems/ruby-2.3.3/gems/railties-5.0.6/lib/rails/initializable.rb:55:in `block in run_initializers'
/usr/local/rvm/gems/ruby-2.3.3/gems/railties-5.0.6/lib/rails/initializable.rb:54:in `run_initializers'
/usr/local/rvm/gems/ruby-2.3.3/gems/railties-5.0.6/lib/rails/application.rb:352:in `initialize!'
/home/app/config/environment.rb:5:in `<top (required)>'
/usr/local/rvm/gems/ruby-2.3.3/gems/railties-5.0.6/lib/rails/application.rb:328:in `require'
/usr/local/rvm/gems/ruby-2.3.3/gems/railties-5.0.6/lib/rails/application.rb:328:in `require_environment!'
/usr/local/rvm/gems/ruby-2.3.3/gems/railties-5.0.6/lib/rails/application.rb:448:in `block in run_tasks_blocks'
/usr/local/rvm/gems/ruby-2.3.3/gems/sprockets-rails-3.2.1/lib/sprockets/rails/task.rb:62:in `block (2 levels) in define'
/usr/local/rvm/gems/ruby-2.3.3@global/gems/rake-12.0.0/exe/rake:27:in `<top (required)>'
/usr/local/rvm/gems/ruby-2.3.3/bin/ruby_executable_hooks:15:in `eval'
/usr/local/rvm/gems/ruby-2.3.3/bin/ruby_executable_hooks:15:in `<main>'
TZInfo::ZoneinfoDirectoryNotFound: None of the paths included in TZInfo::ZoneinfoDataSource.search_path are valid zoneinfo directories.
/usr/local/rvm/gems/ruby-2.3.3/gems/tzinfo-1.2.3/lib/tzinfo/zoneinfo_data_source.rb:180:in `initialize'
/usr/local/rvm/gems/ruby-2.3.3/gems/tzinfo-1.2.3/lib/tzinfo/data_source.rb:180:in `new'
/usr/local/rvm/gems/ruby-2.3.3/gems/tzinfo-1.2.3/lib/tzinfo/data_source.rb:180:in `create_default_data_source'
/usr/local/rvm/gems/ruby-2.3.3/gems/tzinfo-1.2.3/lib/tzinfo/data_source.rb:40:in `block in get'
/usr/local/rvm/gems/ruby-2.3.3/gems/tzinfo-1.2.3/lib/tzinfo/data_source.rb:39:in `synchronize'
/usr/local/rvm/gems/ruby-2.3.3/gems/tzinfo-1.2.3/lib/tzinfo/data_source.rb:39:in `get'
/usr/local/rvm/gems/ruby-2.3.3/gems/activesupport-5.0.6/lib/active_support/railtie.rb:20:in `block in <class:Railtie>'
/usr/local/rvm/gems/ruby-2.3.3/gems/railties-5.0.6/lib/rails/initializable.rb:30:in `instance_exec'
/usr/local/rvm/gems/ruby-2.3.3/gems/railties-5.0.6/lib/rails/initializable.rb:30:in `run'
/usr/local/rvm/gems/ruby-2.3.3/gems/railties-5.0.6/lib/rails/initializable.rb:55:in `block in run_initializers'
/usr/local/rvm/gems/ruby-2.3.3/gems/railties-5.0.6/lib/rails/initializable.rb:54:in `run_initializers'
/usr/local/rvm/gems/ruby-2.3.3/gems/railties-5.0.6/lib/rails/application.rb:352:in `initialize!'
/home/app/config/environment.rb:5:in `<top (required)>'
/usr/local/rvm/gems/ruby-2.3.3/gems/railties-5.0.6/lib/rails/application.rb:328:in `require'
/usr/local/rvm/gems/ruby-2.3.3/gems/railties-5.0.6/lib/rails/application.rb:328:in `require_environment!'
/usr/local/rvm/gems/ruby-2.3.3/gems/railties-5.0.6/lib/rails/application.rb:448:in `block in run_tasks_blocks'
/usr/local/rvm/gems/ruby-2.3.3/gems/sprockets-rails-3.2.1/lib/sprockets/rails/task.rb:62:in `block (2 levels) in define'
/usr/local/rvm/gems/ruby-2.3.3@global/gems/rake-12.0.0/exe/rake:27:in `<top (required)>'
/usr/local/rvm/gems/ruby-2.3.3/bin/ruby_executable_hooks:15:in `eval'
/usr/local/rvm/gems/ruby-2.3.3/bin/ruby_executable_hooks:15:in `<main>'
Tasks: TOP => environment
(See full trace by running task with --trace)
The command '/bin/sh -c bundle exec rake assets:precompile' returned a non-zero code: 1
ERROR
ERROR: build step "gcr.io/cloud-builders/docker@sha256:c803d6b936f86702a2a52e4601cf8387eb05d941cc9b3df030fad6b7362b7ce6" failed: exit status 1

------------------------------------------------------------------------------------------------------------------------------------------

ERROR: (gcloud.container.builds.submit) build b77a5051-7651-495c-8000-b2536d9654ca completed with status "FAILURE"
```

Gemfileの編集

```ruby:Gemfile
# gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
gem 'tzinfo-data'
```

Gemfile.lockの更新（bundle installを実行）

```
$ rm Gemfile.lock
$ bundle install
```



# Deployment&Service

## マイグレーション

CloudSQLのマイグレーションを済ませておく

ローカルマシンから実施する場合はCloudSQLの`承認済みネットワーク`にローカルマシンのIPアドレスを一時的に追加して実施する。

参考：[外部アプリケーションから Cloud SQL に接続する](https://cloud.google.com/sql/docs/external?hl=ja#appaccessIP)

```shell-session
$ export MYSQL_SERVICE_HOST=xxx.xxx.xxx.xxx
$ export MYSQL_USER=xxxxx
$ export MYSQL_PASSWORD=xxxxx
$ rake db:migrate RAILS_ENV=production
```

## デプロイ

Ingress(負荷分散)を利用しないyamlファイルを作成してデプロイします。

CloudSQLのproxyとの接続には`127.0.0.1:3306`が利用されます。
固定値なので`MYSQL_SERVICE_HOST`の値はyamlファイルに直接記述してます。

`MYSQL_USER`,`MYSQL_PASSWORD`,`SECRET_KEY_BASE`については事前に設定したSecretから取得するようにしてます。

参考：[container\-engine\-samples/cloudsql\_deployment\.yaml at master · GoogleCloudPlatform/container\-engine\-samples](https://github.com/GoogleCloudPlatform/container-engine-samples/blob/master/cloudsql/cloudsql_deployment.yaml)

```yaml:kube/deploy+service.yml
# Deployment
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 1
  template:
    metadata:
      labels:
        name: my-app
    spec:
      containers:
        - image: gcr.io/[PROJECT_ID]/[IMAGE]:latest
          imagePullPolicy: Always
          name: rails
          env:
            - name: MYSQL_SERVICE_HOST
              value: 127.0.0.1:3306
            - name: MYSQL_USER
              valueFrom:
                secretKeyRef:
                  name: cloudsql
                  key: username
            - name: MYSQL_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: cloudsql
                  key: password
            - name: SECRET_KEY_BASE
              valueFrom:
                secretKeyRef:
                  name: rails
                  key: secret-key-base
        - image: b.gcr.io/cloudsql-docker/gce-proxy:1.05
          name: cloudsql-proxy
          command: ["/cloud_sql_proxy", "--dir=/cloudsql",
                    "-instances=[INSTANCE_CONNECTION_NAME]=tcp:3306",
                    "-credential_file=/secrets/cloudsql/credentials.json"]
          volumeMounts:
            - name: cloudsql-oauth-credentials
              mountPath: /secrets/cloudsql
              readOnly: true
            - name: ssl-certs
              mountPath: /etc/ssl/certs
            - name: cloudsql
              mountPath: /cloudsql
      volumes:
        - name: cloudsql-oauth-credentials
          secret:
            secretName: cloudsql-oauth-credentials
        - name: ssl-certs
          hostPath:
            path: /etc/ssl/certs
        - name: cloudsql
          emptyDir:
---
# Service
apiVersion: v1
kind: Service
metadata:
  name: my-app
spec:
  type: LoadBalancer
  selector:
    name: my-app
  ports:
  - port: 80
    targetPort: 3000
```

```shell-session
$ kubectl create -f kube/deploy+service.yml
deployment "my-app" created
service "my-app" created
```

## アクセス確認
`EXTERNAL-IP`を確認して、ブラウザでアクセスする。

```shell-session
$ kubectl get service
NAME         CLUSTER-IP     EXTERNAL-IP     PORT(S)        AGE
kubernetes   xx.xx.xxx.x    <none>          443/TCP        1h
my-app       xx.xx.xxx.xx   xx.xxx.xxx.xx   80:31466/TCP   1m
```

http://[EXTERNAL-IP]

## 削除
正常にアクセスできたらDeploymentとServiceを削除する。

```shell-session
$ kubectl delete service my-app
service "my-app" deleted
```

```shell-session
$ kubectl delete deployment my-app
deployment "my-app" deleted
```



# Deployment & Service & Ingress

## デプロイ
replicasとIngress(負荷分散)を利用するyamlファイルを作成してデプロイします。

```yaml:kube/all.yml
# Deployment
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 3
  template:
    metadata:
      labels:
        name: my-app
    spec:
      containers:
        - image: gcr.io/[PROJECT_ID]/[IMAGE]:latest
          imagePullPolicy: Always
          name: rails
          env:
            - name: MYSQL_SERVICE_HOST
              value: 127.0.0.1:3306
            - name: MYSQL_USER
              valueFrom:
                secretKeyRef:
                  name: cloudsql
                  key: username
            - name: MYSQL_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: cloudsql
                  key: password
            - name: SECRET_KEY_BASE
              valueFrom:
                secretKeyRef:
                  name: rails
                  key: secret-key-base
        - image: b.gcr.io/cloudsql-docker/gce-proxy:1.05
          name: cloudsql-proxy
          command: ["/cloud_sql_proxy", "--dir=/cloudsql",
                    "-instances=[INSTANCE_CONNECTION_NAME]=tcp:3306",
                    "-credential_file=/secrets/cloudsql/credentials.json"]
          volumeMounts:
            - name: cloudsql-oauth-credentials
              mountPath: /secrets/cloudsql
              readOnly: true
            - name: ssl-certs
              mountPath: /etc/ssl/certs
            - name: cloudsql
              mountPath: /cloudsql
      volumes:
        - name: cloudsql-oauth-credentials
          secret:
            secretName: cloudsql-oauth-credentials
        - name: ssl-certs
          hostPath:
            path: /etc/ssl/certs
        - name: cloudsql
          emptyDir:
---
# Service
apiVersion: v1
kind: Service
metadata:
  name: my-app
spec:
  type: NodePort
  selector:
    name: my-app
  ports:
    - port: 3000
---
# Ingress
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: my-app
spec:
  rules:
  - http:
     paths:
      - path: /*
        backend:
          serviceName: my-app
          servicePort: 3000
```

```shell-session
$ kubectl create -f kube/all.yml
deployment "my-app" created
service "my-app" created
ingress "my-app" created
```

## アクセス確認
`ADDRESS`を確認して、ブラウザでアクセスする。
(アクセスできるようになるまで5分程度かかります)

```shell-session
$ kubectl get ingress
NAME      HOSTS     ADDRESS         PORTS     AGE
my-app    *         xx.xxx.xx.xxx   80        4m
```

http://[ADDRESS]



# 削除
不要になったdeployment, service, ingress を削除する

## deployment, service, ingress

```shell-session
$ kubectl delete deployment my-app
deployment "my-app" deleted
```

```shell-session
$ kubectl delete service my-app
service "my-app" deleted
```

```shell-session
$ kubectl delete ingress my-app
ingress "my-app" deleted
``` 

## コンテナクラスタ

不要になったコンテナクラスタを削除する

```shell-session
$ gcloud container clusters delete [CLUSTER-NAME]
The following clusters will be deleted.
 - [test-cluster] in [us-west1-a]

Do you want to continue (Y/n)?  Y

Deleting cluster test-cluster...done.
Deleted [https://container.googleapis.com/v1/projects/xxxx-xxxx-xxxxxx/zones/us-west1-a/clusters/test-cluster].
```