Kubernetesを用いてGoogle Container Engineでコンテナクラスタをデプロイする〜入門編〜

# はじめに

Google Cloud Next'17 in Tokyoでの[セッション動画リスト](https://www.youtube.com/playlist?list=PLmoHjhhs3ySFRWcYi3JgN940OuBMNO1LT)がようやく公開されました。

Google Container Engine（GKE）上でコンテナクラスタを本番運用する事例は日本でまだまだ少ないということもあり、公開されたセッション動画は非常に有用な情報源となっています。

このエントリでは、Kubernetesを用いてGKE上でコンテナクラスタを構築し、ローカルで開発したアプリケーションをデプロイする手順をまとめます。

## Kubernetesとは

[Kubernetes](https://kubernetes.io/)はDockerコンテナのオーケストレーションおよび管理を行うためのツールです。元々はGoogleが内部でコンテナ運用するために作られたBorgというツールがありましたが、昨今のコンテナブームに後押しされてKubernetesとしてオープンソース化されています。

複数のDockerコンテナを管理するツールとしてはdocker-composeがありますが、コンテナクラスタの制御まではできず、本番環境で運用するのは非常に困難です。

KubernetesはGoogle製のツールということで、Google Container Engineと簡単に連携することができ、kubectlを用いてコンテナクラスタの構築・運用をコマンドで実行することができます。

設定ファイルもyaml形式で記載することができ、設定ファイル自体をソース管理して運用することが可能です。Dockerコンテナを自動ビルドしてくれるトリガー機能など、コンテナクラスタを運用するのに便利な機能がGCPで提供されており、低コストでコンテナクラスタを運用するのに最適な環境になっています。

## Kubernetes用語の整理

まずはじめに、Kubernetesでの用語を整理します。

- Pod
    - 複数のコンテナとディスクのまとまりをPodと呼びます。Kubernetesでデプロイする最小単位です。
    - 1つのコンテナだけをPodにすれば、Dockerコンテナと同じ感覚で使うことができます。
- Node
    - Podを動作させるマシンのことです。GCPではGoogle Compute Engine（GCE）上のVMとして動作します。
    - 複数のNodeを組み合わせて、コンテナクラスタを構築します。NodeとPod全体を管理するマスターNodeはGKE上で自動的に構築されます。
    - Nodeのリソース状況を見て、PodをどのNodeに配置するかなどの面倒な運用はマスターNodeがやってくれるため、運用担当者が監視して逐一設定するというような手間がかかりません。
- Service
    - Podにアクセスするためのポリシーをルール化する仕組みです。
        - 例えば、Webサーバー用のPodにはポート80、MySQLサーバーにはポート3306にアクセスするなど。
    - ロードバランシング設定もできるため、Webサーバ用途のPodを外部に公開することもSeviceだけで可能です。
        - ただし、規模の大きなロードバランシングにはIngressという機能を使うのがベターです。
- Deployment
    - その名の通り、複数のPodをデプロイしたりロールバックしたりするなどの仕組みです。

# デモについて

Kubernetesの動作確認をするためのデモアプリケーションをGithubリポジトリで準備しました。

https://github.com/yusukixs/kubernetes_demo
デモの流れは以下のとおりです。

1. ローカル環境のDockerコンテナでアプリケーションの動作確認
1. Google Container Clusterの作成
1. ローカルでビルドしたDockerイメージをContainer RegistryにPUSH
1. Container RegistryからPodにデプロイ
1. Podのスケールアップ
1. 修正したソースコードを反映したDockerイメージのビルドとPUSH
1. Rolling UpdateによるPodの差し替え
1. Podのロールバック

# ローカル環境のDockerコンテナでアプリケーションの動作確認

## ローカルでdocker build

まずはローカル環境でデモ用のNode.jsアプリケーションを動作させてみましょう。

```bash
$ git clone https://github.com/yusukixs/kubernetes_demo.git
$ cd kubernetes_demo
$ npm install
$ node server.js
```

ソース自体は「Hello World!」を表示するだけの簡単なものです。
ブラウザで `http://localhost:3000/` にアクセスすると、Hello World! が表示されれば成功です。

```js:server.js
var express = require('express');
var app = express();

app.set('view engine', 'jade');
app.set('views', __dirname + '/views');

app.get('/', function (req, res) {
    res.render('index', {
        title: 'Kubernetes Demo',
        message: 'Hello World!'
    });
});

app.listen(3000, function () {
    console.log('listening');
});
```

次にこのアプリケーション用のDockerイメージをビルドしてコンテナを実行します。

```console
$ docker build -t kubernetes_demo:1.0 .
$ docker run -p 3000:3000 -d -it --rm --name nodejs kubernetes_demo:1.0
$ open http://localhost:3000/
```

同様にブラウザで `http://localhost:3000/` にアクセスして、Hello World! が表示されることを確認してください。

# Google Container Clusterの作成からコンテナデプロイまで

## GCloud SDKのセットアップ

Google Cloud Platformの操作にはGCloud SDKを利用します。下記ページを参考にGCloud SDKをインストールしてください。
https://cloud.google.com/sdk/downloads?hl=ja

あらかじめ、Google Cloud Platformでプロジェクトを作成しておき、GCloudをセットアップします。

```sh
$ gcloud init
```

ダイアログ形式で設定を入力します。GCP用のGoogleアカウントでログインし、GKEを利用するGCPプロジェクトを選択します。

## kubectlインストール

Kubernetesを操作するため、kubectlをインストールします。

```sh
# インストール済みコンポーネントのアップデート
$ gcloud components update
# kubectlのインストール
$ gcloud components update kubectl
```

## Google Container Engineにクラスタ作成

Google Container Engine上にコンテナクラスタを作成します。

```sh
# nodejs-cluster という名称のGKEクラスターを東京リージョンに作成
$ gcloud container clusters create --num-nodes=2 nodejs-cluster \
--zone asia-northeast1-a \
--machine-type g1-small \
--enable-autoscaling --min-nodes=2 --max-nodes=5

Creating cluster nodejs-cluster...
kubeconfig entry generated for nodejs-cluster.
NAME            ZONE               MASTER_VERSION  MASTER_IP       MACHINE_TYPE  NODE_VERSION  NUM_NODES  STATUS
nodejs-cluster  asia-northeast1-a  1.6.4           35.187.xxx.xxx  g1-small      1.6.4         2          RUNNING
```

- デフォルトのNode数は2。
- オートスケールで最大5まで拡張。
- container createのオプションは[こちら]を参照してください。(https://cloud.google.com/sdk/gcloud/reference/container/clusters/create?hl=ja)を参照

GKEでは、Google Compute EngineのVMインスタンスをNodeとして利用しています。このNode内でPodが動作します。

ここでオートスケールの設定をしておくと、Podが増えすぎてリソース不足になってしまいPodが追加できなくなってもNodeを自動的に作成し、Podを追加できるようにしてくれます。
急にアクセスが増えてWebサーバー用のPodを追加するというような用途で利用できます。ただし、Nodeを作成するということは、VMインスタンスを作成するということでもあるため、Node立ち上げには30秒以上はかかります。

後述しますが、Podにも並列オートスケール機能が備わっており、特定のPodのリソースが不足した場合に同じPodを自動的に増やすこともできます。
Podの追加は1, 2秒で済むため、Nodeのリソースにはある程度は余裕を持たせたほうが高速にスケールアウトできます。

## Dockerイメージのアップロード

GCPには[Container Registry](https://cloud.google.com/container-registry/?hl=ja)というサービスがあり、ローカルで作成したDockerイメージをアップロードすることができます。
[Docker Hub](https://hub.docker.com/)にDockerイメージをアップロードすると、不特定多数にイメージが公開されてしまいますが、Container Registryではプライベート環境として利用することができるので、Dockerイメージをセキュアに共有することが可能です。

それでは、ローカルでContainer Registry用にDockerイメージをビルドして、アップロードしてみます。
初めてContainer Registryを利用する場合には、GCPの管理コンソールで利用可能な状態にしておいてください。

```sh
# GCPプロジェクトIDを環境変数に入れる（今回の例では kubernetes-demo-172306）
$ GCP_PROJECT=$(gcloud config get-value project)
$ echo $GCP_PROJECT
kubernetes-demo-172306

# Container Registryの格納先をタグにしてDockerイメージをビルドする
# タグの形式は、asia.gcr.io/[GCP_PROJECT_ID]/[IMAGE_NAME]:[TAG]
$ docker build -t asia.gcr.io/$GCP_PROJECT/nodejs:1.0 .

$ docker images
REPOSITORY                                  TAG                 IMAGE ID            CREATED             SIZE
asia.gcr.io/kubernetes-demo-172306/nodejs   1.0                 ad780aab2439        21 seconds ago      70.5 MB

# DockerイメージをContainer Registryにアップロード
$ gcloud docker -- push asia.gcr.io/$GCP_PROJECT/nodejs:1.0
The push refers to a repository [asia.gcr.io/kubernetes-demo-172306/nodejs]
ce77bebfcbc9: Pushed
5a27379b8054: Pushed
2d746cdb49cd: Pushed
2829efa70233: Pushed
d7d38a95e657: Pushed
5accac14015f: Pushed
1.0: digest: sha256:10a57a2b62c1ba3767c6d8ee9a2f84a9ef9e2e2da6b66ab5e9160cccadd32ea4 size: 1579
```

[Container Registry](https://console.cloud.google.com/gcr)を開き、ローカルでビルドしたDockerイメージが登録されていることを確認してください。

## Deployment

次は、アップロードしたこのイメージからコンテナをGKEのPodとしてデプロイします。

```sh
$ kubectl run nodejs-deploy \
--image=asia.gcr.io/$GCP_PROJECT/nodejs:1.0 \
--replicas=1 \
--port=3000 \
--limits=cpu=200m \
--command -- node app/server.js

deployment "nodejs-deploy" created

# pod一覧を取得。STATUSがRunnnigになっていればPodは正しく動作している。
$ kubectl get pod
NAME                             READY     STATUS    RESTARTS   AGE
nodejs-deploy-4288962420-nfsr8   1/1       Running   0          13s
```

## Serviceを作成し、ロードバランシングで外部公開

デプロイしたNode.js用のPodを外部からアクセスできるようにするため、Seviceを作成します。

```sh
# Serviceを作成し、ロードバランサでPodを外部に公開
$ kubectl expose deployment nodejs-deploy --port=80 --target-port=3000 --type=LoadBalancer

service "nodejs-deploy" exposed

# ロードバランサにIPアドレスが割り当てられるのを確認する
$ watch kubectl get service
NAME         CLUSTER-IP    EXTERNAL-IP   PORT(S)   AGE
kubernetes   10.27.240.1   <none>        443/TCP   57m
nodejs-deploy   10.27.242.119   <pending>   80:32413/TCP   13s

# しばらく待つとIPアドレスが紐付けられる
nodejs-deploy   10.27.242.119   35.187.xxx.xxx   80:32413/TCP   52s
```

`http://<EXTERNAL-IPに割り当てられたアドレス>/` にアクセスし、「Hello World!」が表示されたら成功です。GKE上で動作しているNode.jsコンテナへロードバランサを通してアクセスできています。

# 

## Pod台数を増やしてスケールしてみる

ここからがKubernetesの真骨頂です。

Node.jsアプリケーション用のPodは1つしか作成していないので、これだけでは大量のアクセスを捌くことができません。そこでこのPodを10個まで増やしてみます。


```sh
$ kubectl scale deploy nodejs-deploy --replicas=10

# watchコマンドで監視すると、作成されたPodがRunningになっていくのがわかります
$ watch kubectl get pod
NAME                             READY     STATUS              RESTARTS   AGE
nodejs-deploy-4288962420-1jznl   0/1       Pending             0          3s
nodejs-deploy-4288962420-bq9xm   0/1       Pending             0          3s
nodejs-deploy-4288962420-cfwrw   0/1       Pending             0          3s
nodejs-deploy-4288962420-d410r   0/1       Pending             0          3s
nodejs-deploy-4288962420-k0rqs   1/1       Running             0          3s
nodejs-deploy-4288962420-mn85w   0/1       ContainerCreating   0          3s
nodejs-deploy-4288962420-n0q4p   1/1       Running             0          5m
nodejs-deploy-4288962420-rh1v9   0/1       Pending             0          3s
nodejs-deploy-4288962420-s0g8s   0/1       Pending             0          3s
nodejs-deploy-4288962420-s5x7t   0/1       Pending             0          3s
```

しばらく待っても複数のpodがpending状態のままの場合、nodeに作成できるPod数に限界が来ています。コンテナクラスタにオートスケールを設定している場合、nodeを新たに作成してPodを作成できるようにしてくれます。

```sh
# get nodeでnode一覧を表示すると、2つだったnodeが4つに自動的に増えていきます
$ watch kubectl get node
NAME                                            STATUS    AGE       VERSION
gke-nodejs-cluster-default-pool-d264d025-fglb   Ready     1h        v1.6.4
gke-nodejs-cluster-default-pool-d264d025-hv90   Ready     3m        v1.6.4
gke-nodejs-cluster-default-pool-d264d025-k5q3   Ready     3m        v1.6.4
gke-nodejs-cluster-default-pool-d264d025-m62m   Ready     1h        v1.6.4

# Nodeが4つになったら、全てのPodがRunningになりました！
$ watch kubectl get pod
NAME                             READY     STATUS    RESTARTS   AGE
nodejs-deploy-4288962420-1jznl   1/1       Running   0          7m
nodejs-deploy-4288962420-bq9xm   1/1       Running   0          7m
nodejs-deploy-4288962420-cfwrw   1/1       Running   0          7m
nodejs-deploy-4288962420-d410r   1/1       Running   0          7m
nodejs-deploy-4288962420-k0rqs   1/1       Running   0          7m
nodejs-deploy-4288962420-mn85w   1/1       Running   0          7m
nodejs-deploy-4288962420-n0q4p   1/1       Running   0          12m
nodejs-deploy-4288962420-rh1v9   1/1       Running   0          7m
nodejs-deploy-4288962420-s0g8s   1/1       Running   0          7m
nodejs-deploy-4288962420-s5x7t   1/1       Running   0          7m
```

## Rolling Update

次は、ローカルでNode.jsアプリケーションの更新がされ、稼働中のPodに更新内容を反映してみましょう。

まずは、ローカルでserver.jsのメッセージを`Hello World!`から`Hello GKE World!`に変更します。

```js:server.js
app.get('/', function (req, res) {
    res.render('index', {
        title: 'Kubernetes Demo',
        message: 'Hello GKE World!'
    });
});
```

Dockerイメージのタグにつけるバージョン番号を1.0から2.0に変更して、Dockerイメージをビルドし、Container RegistryにPUSHします。

```sh
$ docker build -t asia.gcr.io/$GCP_PROJECT/nodejs:2.0 .
$ gcloud docker -- push asia.gcr.io/$GCP_PROJECT/nodejs:2.0
```

`set image` コマンドでタグ2.0の新しいイメージをPodに設定させます。
同一のPodが2つ以上稼働している場合、Rolling Updateという機能で既に稼働しているPodが1つずつ入れ替わっていきます。全ての新しいPodが作成されるまで、古いPodは削除されないため、ダウンタイムを発生させることなくPodを最新のものに入れ替えることができます。

```sh
$ kubectl set image deployment/nodejs-deploy nodejs-deploy=asia.gcr.io/$GCP_PROJECT/nodejs:2.0
deployment "nodejs-deploy" image updated

# Rolling UpdateでPodが新しいものに順次切り替わっていくのを眺めます
$ watch kubectl get pod
NAME                             READY     STATUS        RESTARTS   AGE
nodejs-deploy-105340789-lkz9j    1/1       Running       0          19s
nodejs-deploy-105340789-rw45w    0/1       Pending       0          19s
nodejs-deploy-105340789-z3xld    0/1       Pending       0          16s
nodejs-deploy-4288962420-2964m   1/1       Terminating   0          7m
nodejs-deploy-4288962420-7zmw6   1/1       Running       0          7m
nodejs-deploy-4288962420-8sfpq   1/1       Running       0          7m
nodejs-deploy-4288962420-cdg2h   1/1       Running       0          7m
nodejs-deploy-4288962420-k0rqs   1/1       Running       0          41m
nodejs-deploy-4288962420-kkx08   1/1       Running       0          7m
nodejs-deploy-4288962420-n0q4p   1/1       Running       0          46m
nodejs-deploy-4288962420-p1m03   1/1       Running       0          7m
nodejs-deploy-4288962420-qxhm8   1/1       Running       0          7m
nodejs-deploy-4288962420-z8z2z   1/1       Terminating   0          7m
```

全てのPodが切り替わったら、再びブラウザで同じURLにアクセスしてみます。
「Hello GKE World!」に切り替わっていたらローリングアップデートが成功です。

## デプロイ履歴とロールバック

アップデートした新バージョンに不具合が見つかった場合、Deploymentのヒストリーを辿ってロールバックすることもできます。

```sh
$ kubectl rollout history deployment/nodejs-deploy
deployments "nodejs-deploy"
REVISION        CHANGE-CAUSE
1               <none>

# ロールバックで1つ前に戻す
$ kubectl rollout undo deployment/nodejs-deploy
```

# GKEデモ環境の削除

今回のデモはここまでで終了です。
このまま放置するとGCPで課金されてしまうため、不要な場合は下記コマンドで削除してください。
[次のエントリ](http://qiita.com/yusukixs/items/f2cafb5ae2e770571d39)でもこのクラスタを利用するので、続けて試したい場合はまだ削除しないことをオススメします。

```sh
$ kubectl delete deployment,service,pod --all
$ gcloud container clusters delete nodejs-cluster
```

# 最後に

今回はコンソールコマンドでKubernetesを操作しただけでしたが、同様の設定はYAMLファイルに記載して実行することができます。

コマンドだけの運用ではオペレーションミスが発生する可能性があるため、実際の運用では設定ファイルもソース管理してミスを防ぐチェックをする体制を構築したほうがよいでしょう。

次回の記事は、別のエントリで設定ファイルによるデプロイ手順についてです。

※追記 

[Kubernetesを用いてGoogle Container Engineでコンテナクラスタをデプロイする〜設定ファイル編〜](http://qiita.com/yusukixs/items/f2cafb5ae2e770571d39)