GKE に H2O を展開して API モックサーバのようなものをつくる

社内向けに Kubernetes のチュートリアルとして書いた記事。2016年3月に書いたものなので ReplicaSet 使ってなかったりとちょっと古い部分あります。

## やること

HTTP2 サーバの実装である H2O を Google Container Engine (以下 GKE)上のクラスタに展開し、何らかのドキュメントを配信する。 JSON ドキュメントを配信できるようにして、未実装の何らかの API のモックとして使えることを確認する。
## GKE と Kubenetes の基本的な概念

GKE は Google Cloud Platform 上でマネージドな Kubenetes クラスタを提供する。

Kubernetes (["helmsman" や "pilot" を意味するギリシア語](https://en.wikipedia.org/wiki/Kubernetes)。「クーベルネイテス」)は
、 Google が開発するコンテナクラスタマネージャ。 Docker などによるコンテナを管理し、それぞれのやりとりを調整する orchestration という役割を担う。

Docker コンテナの orchestration ツールには [Docker Swarm](https://www.docker.com/products/docker-swarm) も存在するが、 Kubernetes は Docker コンテナ以外のランタイムも対象にしている点で違いがある(現時点では Docker がもっとも一般的な選択肢であるとも認識しているようだ)。

Kubernetes では、デプロイ可能な最小単位を [pod](http://kubernetes.io/docs/user-guide/pods/) と呼ぶ。 Pod は、ひとつ以上のコンテナのグループで、グループ内のコンテナは物理的に配置されたり、共有ストレージがあったりする。 Pod はアプリケーション固有の"論理的ホスト"をモデル化したものでもあり、比較的密に結合したひとつ以上のアプリケーションコンテナを含んでいる。 Pod は鯨などの(主に海洋生物の？)群れを意味する。

同じアプリのコンテナを複数立ち上げて負荷分散をするような場合がある。この場合には、同じイメージから起動した複数の pod を統括し、ある瞬間に稼働している pod を一定に保ったり、その数を増減（スケーリング）する必要がある。この役割を [Replication controllers](http://kubernetes.io/docs/user-guide/replication-controller/) (以下 RC)が担う。

Pod によって提供される機能を抽象化し、論理的なグループを定義する役割を[サービス](http://kubernetes.io/docs/user-guide/services/)が担う。サービスは pod のグループへのアクセスを集約し、他のグループに属する pod にその機能を提供する。設定によって、外部からコンテナへのアクセスを受け付けるエンドポイントにもなり、この場合には負荷分散の機能も提供する。
## 指針
1. H2O と、それによって配信したいドキュメントを含んだ Docker イメージを作成する。
2. (1) から Docker コンテナを pod として起動する。 pod は RC により常時2つ稼働させる。
3. (2) で稼働している pod に、外部からの通信を受けつけるためのサービスを作成する。
## ファイル構成

```
REPO_ROOT
 |
 +-- Dockerfile
 |
 +-- html
 |    |
 |    +-- index.html
 |
 +-- deploy
      |
      +-- h2o-controller.yaml
      |
      +-- h2o-service.yaml
```
## ドキュメントを作成

作業ディレクトリに `html` というディレクトリを作成し、 H2O で配信したいドキュメントを準備しておく。

``` sh
$ mkdir html
$ echo "Served by h2o" > html/index.html
```
## Dockerfile を作成してイメージをビルド

Dockerfile は、 [DockerHub で公開されている h2o のイメージ](https://github.com/lkwg82/h2o.docker)をほぼそのまま使う。

DockerHub の Dockerfile によって作成するイメージは、ドキュメントを含んでいない。ホストのファイルシステムにあるディレクトリをマウントして、コンテナからドキュメントにアクセスさせることを意図している。この方法は Kubernetes では使用できないので、先に作成したドキュメントを、イメージにコピーすることにする。

``` sh
$ cat > Dockerfile
FROM lkwg82/h2o-http2-server

COPY html /var/www/html
^D
```
## gcloud の設定。デプロイ先のクラスタに get-credentials する

GCP 上での操作になるので簡単にまとめておくに留める。

* GCP のプロジェクトを作成しておく。
* GKE タブで新たなクラスタを作成しておく。
* `gcloud` コマンドをインストールし、以下の項目を設定しておく。
    * project
    * container/clusters
    * zone
* `gcloud container clusters get-credentials クラスタ名` しておく。これによって、 `kubectl` コマンドが操作の対象とするクラスタが設定される。

## GKE の Container Registry にイメージを push

pod のベースとなるイメージは、当然 Kubernetes からアクセスできる場所に存在する必要がある。 パブリックなイメージなら、 DockerHub でホストすることができる。

GKE では Container Registry というプライベートな Docker イメージリポジトリを提供しており、手元の docker からイメージを転送することができる。

Dockerfile もまだビルドしていないので、ここでビルドする。このとき、タグ名は "<gci.ioのホスト名>/<GCPのプロジェクト名>/foobar" という形式にする必要がある。ホスト名は、クラスタが存在するゾーンによって異なり、米国なら `us.gcr.io`, 欧州なら `eu.gcr.io`, アジアなら `asia.gcr.io` になる。

``` sh
$ docker build . -t asia.gcr.io/プロジェクト名/h2o
```

この時点では、まだイメージはローカルにしか存在しない。この段階でとりあえず h2o が動作することを確認しておく:

``` sh
$ docker run -p "8080:80" -it asia.gcr.io/プロジェクト名/h2o
$ curl 104.155.228.73
Served by h2o
```

問題なければ Container Registry に push する:

``` sh
$ gcloud docker push asia.gcr.io/プロジェクト名/h2o
```
## RC の設定ファイルを書く

RC は、 Kubernetes を操作するための `kubectl` コマンドで作成したり、削除したりできる。 RC の詳細をコマンドラインオプションで指定することもできるが、 YAML で書くこともできる。今回は YAML で書く。

RC は、同じ機能を備えた pod が一定数稼働することを担保する。ここで指定するのはそのための情報だ。また、それらの pod はサービスからアクセスを割り振りたい場合があるので、そのための識別情報も指定している。

YAML は主に、 RC 自身の情報を記述する `metadata` セクションと、 RC によって作成する pod の情報や管理のための情報を記述する `spec` セクションからなる。

Kubernetes には、「ラベル」と「セレクタ」という考え方がたびたび登場する。

「ラベル」は pod, RC, サービスといったオブジェクトに設定できる key-value ペアの集合で、 key にも value にも任意の文字列を設定できる。 key や value にどんな内容が指定されても、それそのものがオブジェクトの振る舞いに影響を与えるわけではない。それはただオブジェクトを識別するためにだけ使用される。

「セレクタ」は、ラベルと同じで任意の内容を設定できる key-value ペアの集合で、オブジェクトの操作対象をラベルの AND 検索で絞り込むために使用する。

``` yaml
apiVersion: v1
kind: ReplicationController # この YAML で作成するものが RC であることを示す。
metadata:
  name: h2o-example-r1      # RC の名前。 r1 などの連番を接尾しておく。
  labels:                   # RC 自身のラベル。 RC 自身やサービスがこの情報を使うこと
    app: h2o-example        # はなく、 CI/CD ツールが使用することが想定されている。
    tier: frontend
spec:
  replicas: 2               # この RC は、 pod の数を 2 に保つ。
  selector:                 # この RC は、このセレクタで指定されたラベルを持つ
    app: h2o-example        # pod を管理の対象とする。セレクタがマッチする pod
    revision: "1"           # なら、この RC が作成したものでも、主動で作成した
    tier: frontend          # ものでも同様に扱う。
  template:                 # この RC が作成する pod の設定。
    metadata:
      labels:               # この RC により作成された pod には、ここで指定した
        app: h2o-example    # ラベルが設定される。これは当然、 spec.selector での
        revision: "1"       # 指定にマッチしなければならない。
        tier: frontend
    spec:
      containers:           # Pod に含めるコンテナ。複数設定できる。
        - name: h2o-example # コンテナの名前。
          image: asia.gcr.io/プロジェクト名/h2o:latest # コンテナのイメージ。
          resources:
            requests:
              cpu: 100m
              memory: 100Mi
          env:              # 環境変数を指定できる。任意の key-value ペア。
          ports:            # コンテナから外部に開くポート。
            - containerPort: 80
```
## サービスの設定ファイルを書く

サービスの作成も、 `kubectl` に YAML ファイルを渡して行う。サービスは、その IP アドレスにアクセスを受けつけると、セレクタで絞り込んだ pod に割り振る。

``` yaml
apiVersion: v1
kind: Service         # この YAML で作成するものがサービスであることを示す。
metadata:
  name: h2o-example   # このサービスの名前。
  labels:             # このサービスに設定するラベル。
    app: h2o-example
    tier: frontend
spec:
  type: LoadBalancer  # このサービスが LoadBalancer 種であることを示す。 LoadBalancer
  ports:              # 種は外部にアクセスを許す。 ClusterIP 種はそうでない。
  - port: 80          # サービスの IP アドレスの80番に受け付けたアクセスを、
    targetPort: 80    # pod の80番に割り振る。
  selector:           # このサービスのアクセスの割り振り先となる pod をセレクタで指定する。
    app: h2o-example
    tier: frontend
```
## RC とサービスを作成する

`kubectl` の設定ができていればこれは瞬殺:

``` sh
$ kubectl create -f deploy/h2o-controller.yaml
$ kubectl create -f deploy/h2o-service.yaml
```

作成したら、サービスの状態を確認する:

``` sh
$ kubectl get svcNAME          CLUSTER_IP      EXTERNAL_IP      PORT(S)   SELECTOR                        AGE
h2o-example   10.59.255.187   104.155.228.73   80/TCP    app=h2o-example,tier=frontend   9h
kubernetes    10.59.240.1     <none>           443/TCP   <none>                          9h
$ kubectl describe svc h2o-example
Name:                   h2o-example
Namespace:              default
Labels:                 app=h2o-example,tier=frontend
Selector:               app=h2o-example,tier=frontend
Type:                   LoadBalancer
IP:                     10.59.255.187
LoadBalancer Ingress:   104.155.228.73
Port:                   <unnamed>       80/TCP
NodePort:               <unnamed>       30092/TCP
Endpoints:              10.56.0.9:80
Session Affinity:       None
No events.
```

`LoadBalancer Ingress` に書いてあるのが外部に公開されている IP アドレスなので、ここにアクセスする。

``` sh
$ curl 104.155.228.73
Served by h2o
```

LoadBalancer Ingress の項目が表示されない場合がある。このときは少し待つ。サービスの作成はすぐに完了するが、グローバルIPの割り当てに1分くらいかかる。
## API モックっぽいファイルを追加する

もう少しそれっぽい出力を見たいので、もう少しそれっぽいドキュメントを追加することにする。

`html/auth.json` を作成して次のようにする:

``` json
{
  "succeeded": true,
  "auth": {
    "token": "3e4adf1599a4b42ea9cc18a90fac4dad",
    "message": "You are allowed to be yourself. We guarantee.",
    "expire": "2016-03-19 11:21:44"
  }
}
```

Dockerfile をビルドしなおして、このファイルをイメージに含める:

``` sh
$ docker build . -t asia.gcr.io/プロジェクト名/h2o
$ gcloud docker push asia.gcr.io/プロジェクト名/h2o
```
## ローリングアップデートを実行する

システムを停止せずにコンテナのイメージを更新できるように、 [RC にはローリングアップデートの機能が備わっている](http://kubernetes.io/docs/user-guide/replication-controller/#rolling-updates)。ローリングアップデートを行うと、 RC の管理下にある pod をひとつずつ新しいイメージから起動した pod に置き換えていく。

ローリングアップデートをするためには、新しく指定される RC の YAML ファイルが次の要件を満たしている必要がある:

* RC の `metadata.name` が異なる
* RC により作成される pod のための `spec.selector` の内容が、いずれかひとつは古いものと異なる
* `spec.template.metadata.labels` にも同様に、古いものと異なる部分がある

この条件を満たすために、 `metadata.name` にはあらかじめリビジョン番号を含めておいたし、 `spec.selector`, `spec.template.metadata.labels` にはそれぞれ `revision` を含めておいた。この数字をインクリメントすれば良い。

``` yaml
apiVersion: v1
kind: ReplicationController
metadata:
  name: h2o-example-r2
  labels:
    app: h2o-example
    tier: frontend
spec:
  replicas: 1
  selector:
    app: h2o-example
    revision: "2"
    tier: frontend
  template:
    metadata:
      labels:
        app: h2o-example
        revision: "2"
        tier: frontend
    spec:
      containers:
        - name: h2o-example
          image: asia.gcr.io/プロジェクト名/h2o:latest
          resources:
            requests:
              cpu: 100m
              memory: 100Mi
          env:
          ports:
            - containerPort: 80
```

ローリングアップデートの実行時には、古い RC の名前も指定する:

``` sh
$ kubectl rolling-update h2o-example-r1 -f deploy/h2o-controller.yaml
Created h2o-example-r2
Scaling up h2o-example-r2 from 0 to 1, scaling down h2o-example-r1 from 1 to 0 (keep 1 pods available, don't exceed 2 pods)
Scaling h2o-example-r2 up to 1

Scaling h2o-example-r1 down to 0
Update succeeded. Deleting h2o-example-r1
replicationcontroller "h2o-example-r1" rolling updated to "h2o-example-r2"
```

すべての pod が置き換わるまでに1分くらいかかる。

確認してみる:

``` sh
$ curl 104.155.228.73/auth.json
{
  "succeeded": true,
  "auth": {
    "token": "3e4adf1599a4b42ea9cc18a90fac4dad",
    "message": "You are allowed to be yourself. We guarantee.",
    "expire": "2016-03-19 11:21:44"
  }
}
```

まるで認証 API のようだ。
## まとめ

GKE での H2O の展開は、 Dockerfile がコミュニティでメンテされていることもあって容易だった。 Apache や nginx に比べて構成ファイルもシンプルなので Docker と相性が良さそう。

HTML や JSON を配信し、構成に変更があればホットに更新できることも確認した。

しかし SSL が有効ではないので、稲妻は青くならなかった。

<img width="488" alt="2016-03-12 22 58 47" src="https://cloud.githubusercontent.com/assets/135082/13723383/7d2a4be6-e8a6-11e5-87c6-414b7505a2d0.png">