CRD によるカスタムコントローラの実装例の紹介

本エントリは [Z Lab Advent Calendar 2017](https://qiita.com/advent-calendar/2017/zlab) の 17 日のエントリです。
本エントリでは、CustomResourceDefinitions(CRD) を使った カスタムコントローラの実装例を紹介します。実装例として弊社内で開発・利用している Etcd as a Service の概要とその仕組みを紹介します。

# Etcd as a Service 

このエントリでは Etcd as a Service をコマンド一つで etcd クラスタを構築し、その構築したクラスタを管理するサービスと定義します。
弊社内で開発・利用している Etcd as a Service  は以下の機能があります。

- クラスタの作成・削除
    - CRDリソースの作成でクラスタを構築し、削除操作を行うことで削除される
- ローリングアップデートによる更新
    - メンバーを一台づつアップデートしていくことによりダウンタイムゼロでのクラスタの更新
- フェイルオーバー
    - ハードウェアの故障などによりクラスタのメンバーがダウンした場合にそのメンバーを削除して新たなメンバーを追加
- スケールイン・アウト
    - メンバーの追加・削除
- バックアップサービス
    - 定期的にバックアップを取得して保存します
- バックアップデータからのリストア
    - バックアップサービスで取得したバックアップデータを使ってクラスタをリストアします

# 実装方法

Etcd as a Service の主な処理を行うカスタムリソースとカスタムコントローラは etcd と Machine の2つになっています。
etcd は etcd クラスタを表すリソースで、etcd リソースが作成されると etcd コントローラが etcd クラスタを作成し、管理します。
Machine は CloudProvider(OpenStackなど) から提供されるインスタンス (VMなど) を抽象化したリソースで、Machine コントローラは Machine リソースが作成されると Machine コントローラが CloudProvider からインスタンスを作成し、管理します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/100565/650b6138-87d4-bf45-b223-c9ecb4ad9d90.png)

開発したカスタムコントローラは昨年の [Kubernetes Meetup Tokyo#3](https://k8sjp.connpass.com/event/47525/) で紹介した Kubernetes as a Service のときの実装方法と同様に以下の Kubernetes の仕組みを利用して実装しています。

- CustomResourceDefinision(ThirdPartyResource)
- InformerFramework
- IndexedQueue

各仕組みについては[上記のスライド](https://speakerdeck.com/shmurata/thirdpartyresource-woshi-tuta-kubernetes-as-a-service-falseshi-zhuang)に記載してありますのでそちらを参照ください。

etcd クラスタの作成処理自体は StatefullSets のような汎用的な方式ではなく、CoreOS の [etcd-operator](https://github.com/coreos/etcd-operator) を参考に etcd の memer の追加処理などをコントローラ側で実施してクラスタを作成する専用のコントローラを作成するようにしました。Machine コントローラ は Kubernetes as a Service のときに作成したコントローラをそのまま利用しています。

# 実行サンプル

文章だけではわかりにくいと思うので Etcd as a Service を実際に使っていきます。

まず、クラスタを作成します。クラスタを作成するには CRD のリソースのマニフェストを用意します。今回は以下のマニフェストを作成します。

```sample-etcd.yaml
apiVersion: external.zlab.co.jp/v1alpha1
kind: Etcd
metadata:
  name: sample-etcd
spec:
  version: 3.0.17
  replicas: 3
  backupPolicy:
    backupIntervalInSeconds: 3600
    maxBackups: 5
  machineFlavor: large
  machineImageName: container-linux-1465-8-0
  machinePoolName: development
```

このマニフェストを kubectl で apply します。

```
$ kubectl apply -f sample-etcd.yaml
etcd "sample-etcd" created
```

リソースが作成されると このクラスタにデプロイされているカスタムコントローラ(etcd コントローラ)がリソースの作成を検知し、etcd クラスタの作成を開始します。
etcd コントローラはクラスタの作成のために証明書の生成や DNS の登録などを行い、その後 Machine リソースを作成します。
クラスタ作成中の Machine リソースの状態を確認してみます。

```
$ kubectl get machine -o json | jq -c '.items[]|{"name": .metadata.name, "status": .status.phase, "ip": .status.machineState.hostIP }'
{"name":"1-sample-etcd-default","status":"Creating","ip":"182.22.23.11"}
```

このように etcd コントローラがクラスタの作成処理として Machineリソースを作成し、Machine の作成中であることがわかります。Phase が Running になれば Machine の作成が完了したことになります。
一台の Machine が Running になると etcd コントローラが次の Machine を作成します。

```
$ kubectl get m -o json | jq -c '.items[]|{"name": .metadata.name, "status": .status.phase, "ip": .status.machineState.hostIP }'
{"name":"1-sample-etcd-default","status":"Error","ip":"182.22.23.11"}
{"name":"2-sample-etcd-default","status":"Creating","ip":"182.22.23.17"}
```

2台目の作成が始まっています。ここで作成完了したはずの 1-sample-etcd-default の Phase が Error になりますが、これは etcd の member を追加したことにより status が ready のメンバーが過半数以下になってしまい、etcd の health が false を返すためです。

```
$ kubectl get m -o json | jq -c '.items[]|{"name": .metadata.name, "status": .status.phase, "ip": .status.machineState.hostIP }'
{"name":"1-sample-etcd-default","status":"Running","ip":"182.22.23.11"}
{"name":"2-sample-etcd-default","status":"Running","ip":"182.22.23.17"}
{"name":"3-sample-etcd-default","status":"Creating","ip":"182.22.23.18"}
```

上記のように 2-sample-etcd-default が Running になれば 1-sample-etcd-default も Running になります。
replicas を 3 にしたので、3-sample-etcd-default が Running になるとクラスタの作成が完了し、etcd リソースの Phase が Running になります。

```
$ kubectl get e sample-etcd -o yaml
apiVersion: external.zlab.co.jp/v1alpha1
kind: Etcd
metadata:
  ...
spec:
  ...
status:
  clientCertificate:
    secretName: sample-etcd-client-cert
  endpoints:
  - https://1.sample-etcd.default.etcd.sample.zlab.co.jp:2379
  - https://2.sample-etcd.default.etcd.sample.zlab.co.jp:2379
  - https://3.sample-etcd.default.etcd.sample.zlab.co.jp:2379
  lastBackupTime: 2017-12-06T16:26:12Z
  peerCertificate:
    secretName: sample-etcd-peer-cert
  phase: Running
  ready: true
  replicas: 3
  serverCertificate:
    secretName: sample-etcd-server-cert
  storedBackupObjects:
  - default/sample-etcd/2017-12-05T08:22:30Z
  - default/sample-etcd/2017-12-06T15:26:11Z
  - default/sample-etcd/2017-12-06T16:26:12Z
 ```

それではクラスタが出来たのでアクセスしてみます。
作成したクラスタが利用している各証明書は Secret リソースとして保存されているのでクライアント証明書を取得し、接続します。

```
$ kubectl get secret sample-etcd-client-cert -o json | jq -r '.data["certificate"]' | base64 -d > etcd-client.pem
$ kubectl get secret sample-etcd-client-cert -o json | jq -r '.data["private-key"]' | base64 -d > etcd-client.key
$ export ETCDCTL_API=3
# status.endpoints にある値を endpoints として利用する
$ export ETCD_ENDPOINTS=https://1.sample-etcd.default.etcd.sample.zlab.co.jp:2379,https://2.sample-etcd.default.etcd.sample.zlab.co.jp:2379,https://3.sample-etcd.default.etcd.sample.zlab.co.jp:2379
$ etcdctl --cert etcd-client.pem --key etcd-client.key --endpoints $ETCD_ENDPOINTS endpoint  status
https://1.sample-etcd.default.etcd.sample.zlab.co.jp:2379, 6f3636672576c0f2, 3.0.17, 33 kB, true, 19, 33351
https://2.sample-etcd.default.etcd.sample.zlab.co.jp:2379, b9017e59944282a8, 3.0.17, 33 kB, false, 19, 33353
https://3.sample-etcd.default.etcd.sample.zlab.co.jp:2379, ab5b60a8ad3eaa09, 3.0.17, 33 kB, false, 19, 33354
```

値を書き込んでみます。

```
$ etcdctl --cert etcd-client.pem --key etcd-client.key --endpoints $ETCD_ENDPOINTS put /zlab "We are hiring!"
OK
$ etcdctl --cert etcd-client.pem --key etcd-client.key --endpoints $ETCD_ENDPOINTS get /zlab
/zlab
We are hiring!
```

このように普通に etcd として利用できました。
次に etcd のバージョンを v3.2.11 にアップグレードしてみます。

以下のようにマニフェストを変更します。

```sample-etcd.yaml
spec:
+ version: 3.2.11
- version: 3.0.17
  replicas: 3
```

変更したマニフェストを kubectl apply します。

```
$ kubectl apply -f sample-etcd.yaml
etcd "sample-etcd" configured
```
apply するとリソースの変更を検知し、ローリングアップデートが開始します。
ローリングアップデート中も当たり前ですが etcd は普通に利用できます。

```
$ kubectl get m -o json | jq -c '.items[]|{"name": .metadata.name, "status": .status.phase, "ip": .status.machineState.hostIP }'
{"name":"1-sample-etcd-default","status":"Creating","ip":"182.22.22.77"}
{"name":"2-sample-etcd-default","status":"Running","ip":"182.22.22.44"}
{"name":"3-sample-etcd-default","status":"Running","ip":"182.22.22.45"}
$ etcdctl  --cert etcd-client.pem --key etcd-client.key --endpoints $ETCD_ENDPOINTS get /zlab
/zlab
We are hiring!
```

アップデートが完了すると v3.2.11 になっていることがわかります。

```
$ etcdctl --cert etcd-client.pem --key etcd-client.key --endpoints  $ETCD_ENDPOINTS endpoint  status
https://1.sample-etcd.default.etcd.sample.zlab.co.jp:2379, 6f3636672576c0f2, 3.2.11, 33 kB, true, 19, 33351
https://2.sample-etcd.default.etcd.sample.zlab.co.jp:2379, b9017e59944282a8, 3.2.11, 33 kB, false, 19, 33353
https://3.sample-etcd.default.etcd.sample.zlab.co.jp:2379, ab5b60a8ad3eaa09, 3.2.11, 33 kB, false, 19, 33354
```

次にフェイルオーバーを試してみます。
3台のうち1台が障害でダウンしたと仮定して、OpenStack のインスタンスを停止します。

```
$ kubectl get m -o json | jq -c '.items[]|{"name": .metadata.name, "status": .status.phase, "ip": .status.machineState.hostIP }'
{"name":"1-sample-etcd-default","status":"Running","ip":"182.22.22.77"}
{"name":"2-sample-etcd-default","status":"Error","ip":"182.22.22.44"}
{"name":"3-sample-etcd-default","status":"Running","ip":"182.22.22.45"}
```

そうすると該当の Machine リソースの Probe が失敗するようになるため Phase が Error になります。この状態が一定時間以上続くと etcd コントローラが Error のマシンを削除して、再生成を行います。

```
kubectl get m -o json | jq -c '.items[]|{"name": .metadata.name, "status": .status.phase, "ip": .status.machineState.hostIP }'
{"name":"1-sample-etcd-default","status":"Running","ip":"182.22.22.77"}
{"name":"2-sample-etcd-default","status":"Creating","ip":"182.22.22.85"}
{"name":"3-sample-etcd-default","status":"Running","ip":"182.22.22.45"}
```

そのため、このように自動的に復旧されます。

最後にスケールアウトを実施します。 etcd をスケールすることはあまりない気がしますが、今回はreplicasを3から5に変更します。

```sample-etcd.yaml
spec:
  version: 3.2.11
+ replicas: 5
- replicas: 3
```

```
$ kubectl apply -f sample-etcd.yaml
etcd "sample-etcd" configured
```

クラスタの作成のときのように1台ずつ追加されていき、最終的に replicas の数である5台に収束します。

```
$ kubectl get m -o json | jq -c '.items[]|{"name": .metadata.name, "status": .status.phase, "ip": .status.machineState.hostIP }'
{"name":"1-sample-etcd-default","status":"Running","ip":"182.22.22.77"}
{"name":"2-sample-etcd-default","status":"Running","ip":"182.22.22.85"}
{"name":"3-sample-etcd-default","status":"Running","ip":"182.22.22.45"}
{"name":"4-sample-etcd-default","status":"Running","ip":"182.22.22.81"}
{"name":"5-sample-etcd-default","status":"Creating","ip":"182.22.22.94"}
```

# まとめ

今回は CRD を利用したカスタムコントローラの実装例の一つとして弊社内で開発・利用している Etcd as a Service を紹介しました。細かい内容については記載しきれないため興味がありましたら [Kubernetes Meetup Tokyo](https://k8sjp.connpass.com/) などで直接聞いていただければと思います。

CRD とカスタムコントローラを利用することで Kubernetes を拡張でき、様々なことができるようになります。先日行われた [KubeCon 2017](http://events.linuxfoundation.org/events/kubecon-and-cloudnativecon-north-america) でもいくつも CRD とカスタムコントローラ(Operator) を使った話がありましたので、そのうちいくつかを[Z Lab Advent Calendar 2017](https://qiita.com/advent-calendar/2017/zlab)の21日のエントリで紹介予定です。よろしければそちらもご覧ください。

# 参考文献

- https://github.com/coreos/etcd-operator
- http://events.linuxfoundation.org/events/kubecon-and-cloudnativecon-north-america