etcd
kubernetes
CustomResourceDefinitions
Z LabDay 17

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

本エントリは Z Lab Advent Calendar 2017 の 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

開発したカスタムコントローラは昨年の Kubernetes Meetup Tokyo#3 で紹介した Kubernetes as a Service のときの実装方法と同様に以下の Kubernetes の仕組みを利用して実装しています。

  • CustomResourceDefinision(ThirdPartyResource)
  • InformerFramework
  • IndexedQueue

各仕組みについては上記のスライドに記載してありますのでそちらを参照ください。

etcd クラスタの作成処理自体は StatefullSets のような汎用的な方式ではなく、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 などで直接聞いていただければと思います。

CRD とカスタムコントローラを利用することで Kubernetes を拡張でき、様々なことができるようになります。先日行われた KubeCon 2017 でもいくつも CRD とカスタムコントローラ(Operator) を使った話がありましたので、そのうちいくつかをZ Lab Advent Calendar 2017の21日のエントリで紹介予定です。よろしければそちらもご覧ください。

参考文献