Kubernetes で Docker Private Repository を作ったり CircleCI と連携したりする


この記事は [Kubernetes Advent Calendar](http://qiita.com/advent-calendar/2014/kubernetes) の 20 日目の記事です。

# 概要

CircleCI には実はテストが通った後に Kubernetes にデプロイする機能が存在します。
ここでは、GCE に Kubernetes 環境を構築し、そこに Private Dokcer Repository を作成し、
CircleCI でテストが通ったアプリケーションを Kubernetes 上にデプロイする方法について説明します。

# Kubernetes 環境を手に入れる

ここはさくさく行きます、前述のとおり GCE を使います。
この辺りは今までの AdventCalendar や Qiita の記事を読みましょう。

事前に gcloud の components を update しておきましょう。(忘れててちゃんと動かなかった)

```
$ gcloud components update
```

Kubernetes を git から clone してきて、以下のコマンドを実行します。

```
$ make release
```

もしかしたら boot2docker 等の docker 環境を要求されるかもしれません。用意しておきましょう。

次の手順では、minion から GCS を使うために、```cluster/gce/config-default.sh``` を編集し、
minion の scopes を ```storage-full``` にしておきます。

```diff
--- a/cluster/gce/config-default.sh
+++ b/cluster/gce/config-default.sh
@@ -17,10 +17,10 @@
 # TODO(jbeda): Provide a way to override project
 # gcloud multiplexing for shared GCE/GKE tests.
 GCLOUD=gcloud
 ZONE=us-central1-b
 MASTER_SIZE=n1-standard-1
 MINION_SIZE=n1-standard-1
 NUM_MINIONS=4
 # TODO(dchen1107): Filed an internal issue to create an alias
 # for containervm image, so that gcloud will expand this
 # to the latest supported image.
@@ -34,7 +34,7 @@ MINION_TAG="${INSTANCE_PREFIX}-minion"
 MINION_NAMES=($(eval echo ${INSTANCE_PREFIX}-minion-{1..${NUM_MINIONS}}))
 CLUSTER_IP_RANGE="10.244.0.0/16"
 MINION_IP_RANGES=($(eval echo "10.244.{1..${NUM_MINIONS}}.0/24"))
-MINION_SCOPES=("storage-ro" "compute-rw")
+MINION_SCOPES=("storage-full" "compute-rw")
 # Increase the sleep interval value if concerned about API rate limits. 3, in seconds, is the default.
 POLL_SLEEP_INTERVAL=3
 PORTAL_NET="10.0.0.0/16"
(END)
```

上記の設定が完了したら、Kubernetes 環境を GCE 上に起動します。

```
cluster/kube-up.sh
```

```kube-up.sh``` は [昔書いた時](http://qiita.com/rrreeeyyy/items/da5931ec0d2d858583cf)より色々進化してて感動しました。
やっていることや作られる VM/ストレージは同じですので、
内部で何が起きているか気になる方は昔書いた記事をご覧ください。

認証情報は標準出力でなく ```~/.kunernetes-auth``` に出力されるようになった模様です。

# Kubernetes 上の Private Docker Repository を手に入れる

## google/docker-registry を使う

CircleCI との連携のために docker をがっつり使うので、
private docker repository を用意しましょう。

[google/docker-registry](https://registry.hub.docker.com/u/google/docker-registry/) を使います。
```google/docker-registry``` は GCS 上に private docker repository を持つための物です。
せっかくなので、これも Kubernetes 上に乗せてしまうことにします。

事前に GCS に bucket を作っておきます。bucket の名前は後で設定ファイル内で使う事になるので記録しておきましょう。

```
$ gsutil mb -l ASIA gs://rrreeeyyy_docker_registry_bucket
```

Kubernetes に乗せるために、```google/docker-registry``` のコントローラを作成します。
設定ファイルは以下のようになります。

```docker-registry-controller.json
{
  "id": "dockerRegistryController",
  "kind": "ReplicationController",
  "apiVersion": "v1beta1",
  "desiredState": {
    "replicas": 2,
    "replicaSelector": {"name": "dockerregistry"},
    "podTemplate": {
      "desiredState": {
         "manifest": {
           "version": "v1beta1",
           "id": "dockerregistry",
           "containers": [{
             "name": "dockerregistry",
             "image": "google/docker-registry",
             "cpu": 200,
             "ports": [{"containerPort": 5000, "hostPort": 5000}],
             "env": [{"name": "GCS_BUCKET", "value": "rrreeeyyy_docker_registry_bucket"}]
           }]
         }
      },
      "labels": {
        "name": "dockerregistry",
      }
    }
  },
  "labels": {"name": "dockerregistry"}
}
```

ポイントは、以下のように ```GCS_BUCKET``` に先ほど作った bucket の名前を登録しておくことです。

```"env": [{"name": "GCS_BUCKET", "value": "rrreeeyyy_docker_registry_bucket"}]``` 


以下のコマンドでコントローラを作成します。

```bash
$ cluster/kubectl.sh create -f docker-registry-controller.json
```

次に、どの minion に配置されても参照できるように、サービスを作成します。
設定ファイルは以下のようになります。

```docker-registry-service.json
{
  "id": "dockerregistry",
  "kind": "Service",
  "apiVersion": "v1beta1",
  "port": 5000,
  "containerPort": 5000,
  "labels": {
    "name": "dockerregistry"
  },
  "selector": {
    "name": "dockerregistry"
  }
}
```

サービスを作成します。

```bash
$ cluster/kubectl.sh create -f docker-registry-service.json
```

minion に対して 5000 番を開放する GCE のファイアウォールルールを追加します。

```bash
$ gcloud compute firewall-rules create --allow=tcp:5000 --target-tags=kubernetes-minion kubernetes-minion-5000
```

ここまで来たら、Pod が実際に作成されているか確認してみましょう。

```bash
$ cluster/kubectl.sh get pods
NAME                                   IMAGE(S)                       HOST                                                              LABELS                STATUS
04f4c9f8-8771-11e4-b820-42010af08e38   google/docker-registry         kubernetes-minion-1.c.xxxx-xxxx-123.internal/xxx.xxx.xxx.xxx    name=dockerregistry   Running
04f629c1-8771-11e4-b820-42010af08e38   google/docker-registry         kubernetes-minion-2.c.xxxx-xxxx-123.internal/xxx.xxx.xxx.xxx   name=dockerregistry   Running
```

どちらかの minion の ```:5000``` に対して curl を打ってみましょう。

```bash
$ curl xxx.xxx.xxx.xxx:5000
"docker-registry server (prod) (v0.8.1)"
```

このように表示されれば docker-registy がちゃんと Kubernetes 上で動いていそうです。

(※実際に使用する時はアクセス制限や basic 認証をちゃんと投入する必要がありそうです。)

# CircleCI と連携する

これで Private Docker Repository を手に入れることが出来ました。
さて、CircleCI と連携してみましょう。

CircleCI が公開している、テスト用の Rails アプリを使ってみましょう。
([circleci/docker-hello-google](https://github.com/circleci/docker-hello-google))

```circle.yml``` を見ると以下のようになっています。

```yaml

machine:
  services:
    - docker
    
dependencies:
  cache_directories:
    - ~/kubernetes
  pre:
    - ./ensure-kubernetes-installed.sh
    - docker build -t $EXTERNAL_REGISTRY_ENDPOINT/hello:$CIRCLE_SHA1 .

test:
  post:
    - docker run -d -p 3000:3000 -e "SECRET_KEY_BASE=abcd1234" $EXTERNAL_REGISTRY_ENDPOINT/hello:$CIRCLE_SHA1; sleep 10
    - curl --retry 10 --retry-delay 5 -v http://localhost:3000

deployment:
  prod:
    branch: master
    commands:
      - docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS $EXTERNAL_REGISTRY_ENDPOINT
      - envsubst < .kubernetes_auth.template > ~/.kubernetes_auth
      - ./deploy.sh
```

ポイントは、dependencies で Kubernetes をインストールしている所と、
deployment の所で ```./deploy.sh``` を実行している所です。

```ensure-kubernetes-installed.sh``` は以下のようになっています。

```bash
#!/bin/bash

# Exit on any error
set -e

KUBERNETES_VERSION=41eb15bcff4f114e95788f1e3a5ad3645c4e53fd

# Exit if already installed
if [[ -d ~/kubernetes ]]; then
  echo "Kubernetes already installed"
  exit 0
else
  echo "Installing Kubernetes..."
fi

# Clone repo
(cd ~ && git clone https://github.com/GoogleCloudPlatform/kubernetes.git)
(cd ~/kubernetes && git reset --hard $KUBERNETES_VERSION)

# Build go source
(cd ~/kubernetes && hack/build-go.sh)

```

どうやら普通に Kubernetes をインストールしているだけみたいです。
二回目以降はキャッシュするので、```Kubernetes already installed``` と表示されてスキップされていました。

deployment の部分ですが、```deploy.sh``` は以下のようになっています。

```bash
#!/bin/bash

# Exit on any error
set -e

KUBE_CMD=${KUBERNETES_ROOT:-~/kubernetes}/cluster/kubecfg.sh

# Deploy image to private GCS-backed registry
docker push $EXTERNAL_REGISTRY_ENDPOINT/hello:$CIRCLE_SHA1

# Update Kubernetes replicationController
envsubst < kubernetes/rails-controller.json.template > rails-controller.json
$KUBE_CMD -c rails-controller.json \
    update replicationControllers/railscontroller

# Roll over Kubernetes pods
$KUBE_CMD rollingupdate railscontroller
```

こちらは、```$EXTERNAL_REGISTRY_ENDPOINT``` に対して Docker image を pushし、コントローラを登録した上で、rollingupdate を掛けています。

rollingupdate は [update-demo](https://github.com/GoogleCloudPlatform/kubernetes/blob/master/examples/update-demo/README.md#step-four-update-the-docker-image) に解説がありますが、pod を 1 台ずつアップデートする命令のようです。

というわけで、早速このリポジトリを fork して、CircleCI に掛けてみました。
CircleCI に掛ける際は、以下の環境変数をセットしないと、deployment が途中で止まってしまうので注意して下さい。


- KUBE_MASTER_IP
- KUBERNETES_PASS
- KUBERNETES_USER
- DOCKER_USER
- DOCKER_EMAIL
- DOCKER_PASS
- RAILS_SECRET
- INTERNAL_REGISTRY_ENDPOINT
- EXTERNAL_REGISTRY_ENDPOINT

Kubernetes は、上記の環境変数がセットされていない場合には、プラットフォームに応じたコマンドで自動的に上記の値を検出しようとします。
例えば、```KUBE_MASTER_IP``` がセットされていないような場合には、```gcutil``` 等のコマンドを実行するようです。
CI 環境には ```gcutil``` のようなコマンドは入っていないため、そこで deployment が止まってしまいます。

また、コンテナ内で最新の kubernetes を使いたい場合は以下を設定する必要がありそうです。
(```ensure-kubernetes-installed.sh``` のハッシュ値を書き換えることで使う Kubernetes のバージョンを変えられます)

- PROJECT

さて、正しく環境変数をセットしてリポジトリを push すると、CI の最後に以下のように deploy.sh を実行しています。

![deploy.sh](https://qiita-image-store.s3.amazonaws.com/0/48761/a2198971-5de1-84a2-c21b-6a897a8cd9df.png "deploy.sh")

deploy.sh が実行されたら、手元の kubectl.sh で pods の状況を確認してみましょう。

```zsh
➜  git:(master) ✗ ./cluster/kubectl.sh get pods
a59fdaf0-8792-11e4-b820-42010af08e38   xxx.xxx.xxx.xxx:5000/hello:latest   kubernetes-minion-1.c.xxxxx-xxxxx-xxx.internal/xxx.xxx.xxx.xxx    name=rails            Running
a5a05c0a-8792-11e4-b820-42010af08e38   xxx.xxx.xxx.xxx:5000/hello:latest   kubernetes-minion-2.c.xxxxx-xxxxx-xxx.internal/xxx.xxx.xxx.xxx   name=rails            Running
```

しばらくしてから minion の :3000 にアクセスすると、以下のように表示されます。
(GCE 側で :3000 を minion に対して許可する firewall-rule を追加しておきましょう)

![circleci-k8s.png](https://qiita-image-store.s3.amazonaws.com/0/48761/2f33e843-976d-f67c-b29f-ab43403d8f3d.png "circleci-k8s.png")


# 感想

一昔前(9月頃)に触った時に比べ、かなりの機能が充実してきました。
今回のように、CI 環境と連携して、テストが通ったら Kubernetes 環境へデプロイ、のようなことも出来るようになっていました。

残る課題は Kubernetes 上にデプロイしたアプリのストレージ永続化です。

現在、GCE を使用した場合には GCS をコンテナがマウントすることが出来るようになっています。
(参考: [kubernetes/volumes](https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/volumes.md) )

もしくは、minion の特定のディレクトリをコンテナがマウントすることも出来ます。
そのため、分散ストレージ等を minion に用意すればストレージを永続化出来るでしょう。

という環境をしっかりと用意すれば、
例えば社内の Docker 用 PaaS 環境として Kubernetes はうってつけではないでしょうか。
まだ実サービスに投入するにはやや怖いですが、本記事のようにテストしたコンテナを適当に分散して、
再起動等の管理もよしなにやってくれるので、便利なように思えます。
引き続き目が離せないプロダクトの 1 つであると感じました。

# まとめ

- GCE+Kubenetes で Docker Private Repository を作成
- Kubernetes + CircleCI でテストが通ったコンテナをデプロイ
- 社内 PaaS とかで使えそう。