Redash を GKE 上で起動しようとしてやったこと

Redash 沢山作らないといけないとなった時に [GKE](https://cloud.google.com/kubernetes-engine/) でやろうとしたら何箇所か手こずったのでメモ書き。
(随時追加)

## 前提

- 1 から構成用の yaml ファイルを作るのは辛いので [Kompose](https://github.com/kubernetes/kompose) で [Redash](https://github.com/getredash/redash) のソースに含まれている `docker-compose.production.yml` を変換して使う
- 当然 HTTPS
- 作り直しても IP 変わらないように static な IP を指定する
- postgresql のデータは永続化する

```bash
$ kompose convert -f docker-compose.production.yml
```

## tl;dr

ざっくりこんな感じの docker-compose.yaml 作って前述の通り何箇所か修正すれば良い。

```
$ cat docker-compose.yaml
version: '2'
services:
  redash:
    image: redash/redash:latest
    command: server
    depends_on:
      - postgres
      - redis
    ports:
      - "5000:5000"
    environment:
      PYTHONUNBUFFERED: 0
      REDASH_LOG_LEVEL: "INFO"
      REDASH_REDIS_URL: "redis://redis:6379/0"
      REDASH_DATABASE_URL: "postgresql://postgres@postgres/postgres"
      REDASH_COOKIE_SECRET: **************
      REDASH_WEB_WORKERS: 4
    labels:
      kompose.service.type: NodePort
      kompose.service.expose: "redash.example.com"
      kompose.service.expose.tls-secret: <tls-secret-resource-name>
    restart: always
  worker:
    image: redash/redash:latest
    command: scheduler
    environment:
      PYTHONUNBUFFERED: 0
      REDASH_LOG_LEVEL: "INFO"
      REDASH_REDIS_URL: "redis://redis:6379/0"
      REDASH_DATABASE_URL: "postgresql://postgres@postgres/postgres"
      QUEUES: "queries,scheduled_queries,celery"
      WORKERS_COUNT: 2
    restart: always
  redis:
    image: redis:3.0-alpine
    ports:
      - "6379:6379"
    restart: always
  postgres:
    image: postgres:9.5.6-alpine
    ports:
      - "5432:5432"
    labels:
      kompose.volume.size: 1Gi
    environment:
      PGDATA: /var/lib/postgresql/data/pgdata
    volumes:
      - "postgres:/var/lib/postgresql/data"
    restart: always

$ kompose convert -f docker-compose.yml --stdout\
  | yq -y ".items[]"\
  | yq -y "select(.kind == \"Ingress\").metadata.annotations[\"kubernetes.io/ingress.allow-http\"] = \"false\""\
  | yq -y "select(.kind == \"Ingress\").metadata.annotations[\"kubernetes.io/ingress.global-static-ip-name\"] = \"<static-ip-label>\""\
  | yq -y "select(.kind == \"Deployment\" and .metadata.name == \"redash\").spec.template.spec.containers[0].readinessProbe = {httpGet:{path:\"/static/images/favicon-32x32.png\",port:5000}}"\
  | tee gke.yaml
```

## 1. Kompose して起動すると Deployment の Name がとても衝突しやすい名称

docker-composer.yaml に記載されたサービス名?が Deployment の Name として使用される変換ロジックなのでさもありなん。

```bash
$ kompose up -f docker-compose.production.yml
```

で起動するだけならさらっとできるけどそんな理由で却下。

### A. Namespace を指定する

```bash
$ # Ex.
$ kompose convert -f docker-compose.production.yml -o <redash-on-gke.yml>
$ kubectl apply -n <namespace> -f <redash-on-gke.yml>
```

とか

## 2. Redash サーバーのフロントに立ってる Worker コンテナが起動しない

こんなエラー

```bash
$ kubectl logs $(basename $(kubectl get pod -o name -n <namespace> -l 'io.kompose.service==worker'))
Starting scheduler and 2 workers for queues: queries,scheduled_queries,celery...
[2018-08-28 06:14:10,224][PID:1][INFO][root] Generating grammar tables from /usr/lib/python2.7/lib2to3/Grammar.txt
[2018-08-28 06:14:10,270][PID:1][INFO][root] Generating grammar tables from /usr/lib/python2.7/lib2to3/PatternGrammar.txt
Traceback (most recent call last):
  File "/usr/local/bin/celery", line 11, in <module>
...
redis.exceptions.ConnectionError: Error -2 connecting to redis:6379. Name or service not known.
```

### A. Service `redis` を作る

元の docker-compose.yml で redis は `ports` が指定されていません。
docker で起動するならそれでも問題ないですが、 GKE で起動するにはクラスター内の別 Pod と通信するための仕様に沿う必要があります。

クラスター内の別 Pod と通信するには Service を用意してあげる必要があります。
また Redis でエラーになってますが、どうせ PostgreSQL も同じことになるので一緒に対応します。

```diff
@@ -36,5 +36,9 @@ services:
   redis:
     image: redis:3.0-alpine
+    ports:
+      - "6379:6379"
     restart: always
   postgres:
     image: postgres:9.5.6-alpine
+    ports:
+      - "5432:5432"

```

## 3. Redash サーバーのフロントに立ってる Nginx コンテナも起動しない

こんなエラー

```bash
$ kubectl logs $(basename $(kubectl get pod -o name -n <namespace> -l 'io.kompose.service==nginx'))
2018/08/28 06:12:26 [emerg] 1#1: host not found in upstream "redash:5000" in /etc/nginx/conf.d/default.conf:2
nginx: [emerg] host not found in upstream "redash:5000" in /etc/nginx/conf.d/default.conf:2
```

### A-1. `server` を `redash` に変更する

GKE では `metadata.name` がクラスタ内で使用できるホスト名となる。
kompose を介す場合は docker-compose.yml の services に記載したサービス名が該当する。

```diff
@@ -7,5 +7,5 @@
workers.
 version: '2'
 services:
-  server:
+  redash:
     image: redash/redash:latest
```

### A-2. `nginx` を使用しない

後述するがそもそも HTTPS 対応において `nginx` は不要となる。
そのため docker-compose.yml からこの段階で `nginx` を削除してしまう。
そうすればエラーも何もなくなる。

```diff
@@ -33,10 +37,1 @@ services:
     restart: always
-  nginx:
-    image: redash/nginx:latest
-    ports:
-      - "80:80"
-    depends_on:
-      - server
-    links:
-      - server:redash
-    restart: always
```

### A-3. 両方やる

どっちにしろ `server` という名称は用途が分からなくなるので `redash` に変えてしまうのはあり、という話。

## 4. Redash サーバーのフロントに立ってる Nginx コンテナだと HTTPS に (多分) できない

HTTP で良ければ `nginx` の Service のタイプを `LoadBalancer` にすれば可能。
kompose 経由で指定する場合は labels に `kompose.service.type: LoadBalancer` を追加する

```diff
@@ -53,4 +53,6 @@ services:
       - server
     links:
       - server:redash
+    labels:
+      kompose.service.type: LoadBalancer
     restart: always
```

ただ HTTPS にする方法がなさそう。

### A. Ingress を利用する

[Ingress](https://cloud.google.com/kubernetes-engine/docs/tutorials/http-balancer) という機能がある。
kompose 経由で証明書を使うように指定すると自動的に Ingress を使うための設定も吐き出させれる。
`nginx` は前述の通り消してしまい、 `redash` の labels に以下を追加すると SSL 証明書まで設定された HTTPS 通信が実現できる。
Ingress はデフォルトで HTTP ポートも開けるのでこの段階では redash には HTTP/HTTPS の両方でアクセスできる。(HTTP を塞ぐ方法は後述)
また Ingress は実際には **GCP Load Balancing backend service** 

```diff
@@ -21,4 +21,7 @@ services:
       REDASH_DATABASE_URL: "postgresql://postgres@postgres/postgres"
       REDASH_COOKIE_SECRET: veryverysecret
       REDASH_WEB_WORKERS: 4
+    labels:
+      kompose.service.expose: "redash.example.com"
+      kompose.service.expose.tls-secret: <tls-secret-resource-name>
     restart: always
```

- `kompose.service.expose: ...` は SNI のホスト名として使用されるが不要なら `"true"` にしておく
- `kompose.service.expose.tls-secret: <tls-secret-resource-name>` で証明書を指定する

#### 証明書を `<tls-secret-resource-name>` を GKE に登録するには

こればかりは秘密鍵を含むので kompose を利用することも出来ない。

```bash
$ kubectl create secret tls <tls-secret-resource-name> -n <namespace> --key=privkey.pem --cert=fullchain.pem
```

## 5. Ingress で HTTP を塞ぐ

Kompose は対応していないので生成した &lt;redash-on-gke.yml> を書き換える。
具体的には Ingress の `metadata.annotations` に `"kubernetes.io/ingress.allow-http": "false"` を追加する。

## 6. Ingress から Service に届かない

待てども繋がるようにならない。
これはいくつか理由がある。
ここが一番手間だった。

### A-1. Service を NodePort にする

Ingress から Service に通信するには NodePort にする必要がある。

```diff
@@ -24,2 +24,3 @@ services:
     labels:
+      kompose.service.type: NodePort
       kompose.service.expose: "redash.example.com"
```

### A-2. Ingress のヘルスチェックが通るようにする必要がある。

Ingress は勝手にヘルスチェックしてステータスコード 200 を要求する。
ヘルスチェックするデフォルトのパスは `/` だが、 redash の `/` にリクエストすると `/login` へのリダイレクトになって `302` となるのでヘルスチェックが成功しない。

Kompose は対応していないので生成した &lt;redash-on-gke.yml> を書き換える。
具体的には Deployment `redash` の `spec.template.spec.containers[*]` に `readinessProbe` を追加する。

```diff
@@ -131,6 +131,10 @@ items:
           ports:
           - containerPort: 5000
           resources: {}
+          readinessProbe:
+            httpGet:
+              path: /static/images/favicon-32x32.png
+              port: 5000
         restartPolicy: Always
```

`/login` にするとログイン画面のアクセスのリミットに引っかかるので画像ファイルへのアクセスにすると良いと思う。

### A-3. GCE Load Balancing Backend Services のリソース数上限を上げる

[GKE ダッシュボード上でサービスを見る](https://console.cloud.google.com/kubernetes/discovery) と以下のような警告が Ingress に対して出ている。

> googleapi: Error 403: Quota 'BACKEND_SERVICES' exceeded. Limit: 3.0 globally., quotaExceeded

で実際調べて見ると上限に達している。

```bash
$ gcloud compute project-info describe --project <project-name> | grep BACKEND_SERVICES -B1 -A1
- limit: 3.0
  metric: BACKEND_SERVICES
  usage: 3.0
```

これはもうリソース数上限を上げるか他の Ingress と共用するかしかない。

### A-4. PostgreSQL に redash 用のデータを構築する

Ingress から Service に届くようになったはずだけどまだ画面が表示されないはず。

以下のようなエラーとなると思う。

```bash
$ kubectl logs $(basename $(kubectl get pod -o name -n <namespace> -l 'io.kompose.service==postgres'))
...
ERROR:  relation "queries" does not exist at character 979
```

これは DB の構築がまだであるためなので redash サーバー経由で postgres のデータベースを構築する。

```bash
$ kubectl exec -it --namespace=<namespace> $(basename $(kubectl get pod -o name -n <namespace> -l 'io.kompose.service==redash')) -- /app/bin/docker-entrypoint create_db
```

## 7. static な IP を指定したい

### A. GCP で払い出しておいた IP を指定することができる。

```bash
$ gcloud compute addresses create <static-ip-label> --global
```

で IP を払い出しておく。
これも docker-compose.yml の段階でどうにかすることは現状できないようなので、  &lt;redash-on-gke.yml> を書き換える。
具体的には Ingress の `metadata.annotations["kubernetes.io/ingress.global-static-ip-name"] = "<static-ip-label>"` を追加する。

## 8. データの永続化をしたい

### A. volumes を使えば勝手に PersistentVolumeClaim が生成される

```diff
@@ -46,6 +46,10 @@ services:
     image: postgres:9.5.6-alpine
     ports:
       - "5432:5432"
-    # volumes:
-    #   - /opt/postgres-data:/var/lib/postgresql/data
+    labels:
+      kompose.volume.size: 1Gi
+    environment:
+      PGDATA: "/var/lib/postgresql/data/db-files/"
+    volumes:
+      - "postgres:/var/lib/postgresql/data"
     restart: always
```

- volumes がコメントアウトされていてローカルディスクを使用するようになっているので、 docker-compose.yml でコメントアウトを外す
- 指定した「**:**」 の左側が PersistentVolumeClaim の名前となるので絶対パスから修正する
- GKE でマウントした直下にはシステム上のファイルが生成されてしまい空のディレクトリにならないため postgres の初期化ができない
    - そのため環境変数 **PGDATA** で postgres のデータフォルダの階層を変更する
    - デフォルトの `PGDATA=/var/lib/postgresql/data` のままで、マウント先を `/var/lib/postgresql` としても仕様なのか pod 再構築時に必ず初期化してしまって永続化できない
- **kompose.volume.size** を使用するとディスクサイズが指定可能

## 心の声

全部指定したいな...
ソース読んで見ますかね。