Kubernetes Operator

この記事は [リクルートライフスタイル Advent Calendar 2018](https://qiita.com/advent-calendar/2018/recruitlifestyle) の18日目の記事です。

## はじめに

[CETチーム](https://engineer.recruit-lifestyle.co.jp/techblog/2018-02-21-rls-meetup-7/) 兼 新規サービス開発を担当している @shotat です。

先週シアトルで [KubeCon + CloudNativeCon North America 2018](https://events.linuxfoundation.org/events/kubecon-cloudnativecon-north-america-2018/) （以下KubeCon） が開催されました。自分も現地シアトルへ向かい、KubeConに参加してきました。なお、KubeConはキューブ（`kjúːb`）コンと発音するようです。

KubeConの規模は年々増加傾向にあり、今回は8000人以上のattendeeがいたようで、Kubernetes Communityの盛り上がりを肌で感じることができました。
KubeConではService meshやMachine Learning等様々なテーマが語られていたのですが、中でも CRD(CustomResourceDefinition)・Custom Controller・Operator 等、KubernetesのExtensibilityについての言及が非常に多く、キーノートや様々なセッションで繰り返し登場していたのが印象的でした。

本記事では、 上述の Kubernetes Operator についての概要について簡単にまとめ、実際に動かして見ます。

## Kubernetes Operator の概要

### Operator とは

Kubernetes OperatorはCoreOSによって2016年に提唱された概念で、以下のブログに詳細が記述されています。

- [CoreOS Blog - Introducing Operators: Putting Operational Knowledge into Software
](https://coreos.com/blog/introducing-operators.html)
- [CoreOS - Kubernetes Operators](https://coreos.com/operators/)

Operatorを一言でまとめると、 "アプリケーション特化型のKubernetes Controller" です。
複雑になりがちなStatefulアプリケーションの運用（scaling, backup, failover, upgrade 等）を自動化することがOperatorの役割です。

### 何故 Operator が必要なのか

StatelessなアプリケーションであればKubernetesのDeployments等である程度十分に実用レベルの運用が可能といえます。
しかし、Statefulなアプリケーションはより複雑な運用タスクが必要であり、アプリケーション毎に最適な運用方法は異なります。
Statefulなアプリケーションを正しくスケーリング・アップデートするにはアプリケーション固有の運用知見が必要になります。
こういったアプリケーション固有の運用を人間の手で行うのは非常に骨が折れるため、運用知見をコード化してKubernetes上の仕組みに乗せてしまおう！という思想がOperatorに繋がります。

### 仕組み

#### Kubernetes Control Loop

前提としてまずKubernetesの Control Loop について簡単に復習しておきます。詳細は [Kubernetesの公式ドキュメント](https://kubernetes.io/docs/concepts/) をご参照下さい。

Kubernetes内ではDesired Stateと実際のCurrent State（Actual State, Observed State表記もあり）を一致・更新させる処理のループを回しており、これをControl Loopと呼びます。
またDesired StateとCurrent Stateの乖離を埋める働きをReconciliationと呼びます。

Desired Stateは `kubectl apply ...` コマンド等でKubernetesユーザがAPI経由で直接登録・更新することができ、実際にどうやってCurrent　StateをDesired Stateに近づけていくかはKubernetesユーザはほとんど意識しなくて良いインタフェースになっています。

つまり、ユーザはKubernetesに対して宣言的に `What` を伝えるだけで良く、 `How` の部分（Reconciliation Logic）はKubernetes内部に隠蔽されています。

#### Operator

Operatorは `Custom Resource` と `Custom Controller` から成り立ちます。
Custom ResourceはKubernetes上に登録可能な独自リソースであり、上述の `What` 部に相当します。
Custom Resourceを作成するには [CRD (CustomResourceDefinition)](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/#customresourcedefinitions) という機能を利用します。
CRDに独自のリソース（Kind: MySQL等）とそのスペックを定義してKubernetes上に登録することで、Custom Resourceの利用が可能になります。

一方Custom Controllerは上述の `How` に相当し、どのようにCurrent StateとDesired Stateを一致させるか（Reconciliation Logic）を担います。Operatorの文脈ではCustom ResourceをCustom Controllerが扱う対象とします。

つまり、Custom Resourceに定義されたDesired Stateを実現するのがCustom Controllerであり、これらを一括りにOperatorと呼びます。

## Operator を動かしてみる

理解を深めるために実際にOperatorを利用し、動作を観察してみます。
KubeConのキーノートではUberが M3DB Operator について紹介していたので、今回はそちらを使ってみようと思います。
なお、M3DBはUberが開発している時系列データベースです。

- https://github.com/m3db/m3db-operator

また、以下のGitHub Repositoryに他にもいくつかのOperatorが公開されています。

- https://github.com/operator-framework/awesome-operators

### GKE クラスタの作成

https://github.com/m3db/m3db-operator のREADMEに記述されている以下の制約に従ってGKEクラスタを立てます。

- Kubernetes 1.10 and 1.11
- Kubernetes clusters with nodes in at least 3 zones

```sh
$ gcloud container clusters create m3db-example \
    --zone us-central1-a \
    --node-locations us-central1-a,us-central1-b,us-central1-c
```

クラスタが構築できたらkubectlを先程構築したクラスタに向けましょう。

```sh
$ gcloud container clusters get-credentials m3db-example --zone us-central1-a --project <PROJECT_NAME>

Fetching cluster endpoint and auth data.
kubeconfig entry generated for m3db-example.
```

[GKEのドキュメント](https://cloud.google.com/kubernetes-engine/docs/how-to/role-based-access-control#defining_permissions_in_a_role) にも記載されているように、RoleBindingを設定が必要です。

```sh
$ kubectl create clusterrolebinding cluster-admin-binding --clusterrole=cluster-admin --user=<name@domain.com>

clusterrolebinding.rbac.authorization.k8s.io/cluster-admin-binding created
```

### Operatorのデプロイ

ドキュメントに従い、以下のコマンドでM3DB Operatorをデプロイします。

```sh
$ kubectl apply -f https://raw.githubusercontent.com/m3db/m3db-operator/v0.1.1/bundle.yaml
```

`bundle.yaml` の中身を覗いてみます。すると、m3db-operatorは `quay.io/m3db/m3db-operator:v0.1.1` というImageを使ったStatefulSetであるということが分かります。
また、必ずしもStatefulSetを使う必要はなく、他のOperatorの例を見ると普通のDeploymentだったりします。

```yaml
# ...中略
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: m3db-operator
  namespace: default
spec:
  serviceName: m3db-operator
  replicas: 1
  selector:
    matchLabels:
      name: m3db-operator
  template:
    metadata:
      labels:
        name: m3db-operator
    spec:
      containers:
        - name: m3db-operator
          image: quay.io/m3db/m3db-operator:v0.1.1
          command:
          - m3db-operator
          imagePullPolicy: Always
          env:
            - name: ENVIRONMENT
              value: production
      serviceAccount: m3db-operator
```

この時点でOperatorのログを確認してみると、CustomResourceDefinitionが生成されていることが分かります。

```sh
$ kubectl logs pod/m3db-operator-0 | grep CRD
{"level":"error","ts":1544814329.1473405,"caller":"k8sops/crd.go:67","msg":"could not get CRD","error":"customresourcedefinitions.apiextensions.k8s.io \"m3dbclusters.operator.m3db.io\" not found","stacktrace":"github.com/m3db/m3db-operator/pkg/k8sops.(*k8sops).GetCRD\n\t/go/src/github.com/m3db/m3db-operator/pkg/k8sops/crd.go:67\ngithub.com/m3db/m3db-operator/pkg/k8sops.(*k8sops).CreateCRD\n\t/go/src/github.com/m3db/m3db-operator/pkg/k8sops/crd.go:75\ngithub.com/m3db/m3db-operator/pkg/controller.(*Controller).Init\n\t/go/src/github.com/m3db/m3db-operator/pkg/controller/controller.go:223\nmain.main\n\t/go/src/github.com/m3db/m3db-operator/cmd/m3db-operator/main.go:187\nruntime.main\n\t/usr/local/go/src/runtime/proc.go:198"}
{"level":"info","ts":1544814329.6673043,"caller":"k8sops/crd.go:113","msg":"CRD created"}
```

kubectlコマンドでCRDが生成されていることも確認してみます。

```sh
$ kubectl get crd
NAME                                    CREATED AT
m3dbclusters.operator.m3db.io           2018-12-14T19:05:29Z
```

```sh
$ kubectl describe crd m3dbclusters.operator.m3db.io
Name:         m3dbclusters.operator.m3db.io
Namespace:
Labels:       <none>
Annotations:  <none>
API Version:  apiextensions.k8s.io/v1beta1
Kind:         CustomResourceDefinition
Metadata:
  Creation Timestamp:  2018-12-14T19:05:29Z
  Generation:          1
  Resource Version:    3077
  Self Link:           /apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions/m3dbclusters.operator.m3db.io
  UID:                 38d89036-ffd3-11e8-9a7e-42010a800140
Spec:
  Group:  operator.m3db.io
  Names:
    Kind:       M3DBCluster
    List Kind:  M3DBClusterList
    Plural:     m3dbclusters
    Singular:   m3dbcluster
  Scope:        Namespaced
  Version:      v1alpha1
Status:
  Accepted Names:
    Kind:       M3DBCluster
    List Kind:  M3DBClusterList
    Plural:     m3dbclusters
    Singular:   m3dbcluster
  Conditions:
    Last Transition Time:  2018-12-14T19:05:29Z
    Message:               no conflicts found
    Reason:                NoConflicts
    Status:                True
    Type:                  NamesAccepted
    Last Transition Time:  2018-12-14T19:05:29Z
    Message:               the initial names have been accepted
    Reason:                InitialNamesAccepted
    Status:                True
    Type:                  Established
Events:                    <none>
```

このCustomResourceDefinitionでは `M3DBCluster` というCustom Resourceが定義されていることが分かります。
M3DB Operator（Controller）はこの M3DBCluster リソースを扱うことになります。

### M3DB クラスタの構築

M3DBのクラスタを構築してみます。
M3DBはクラスタトポロジやメタデータを etcd に保存するため、まずはetcdを構築します。

```sh
$ kubectl apply -f https://raw.githubusercontent.com/m3db/m3db-operator/v0.1.1/example/etcd/etcd-basic.yaml
```

CustomResourceDefinitionに従い、M3DBCluster リソースを作成してみましょう。
以下のファイルを `m3db-cluster.yaml` として保存し、applyします。

```yaml
apiVersion: operator.m3db.io/v1alpha1
kind: M3DBCluster
metadata:
  name: simple-cluster
spec:
  image: quay.io/m3db/m3dbnode:latest
  replicationFactor: 3
  numberOfShards: 256
  isolationGroups:
    - name: us-central1-a
      numInstances: 1
    - name: us-central1-b
      numInstances: 1
    - name: us-central1-c
      numInstances: 1
  podIdentityConfig:
    sources:
      - PodUID
  namespaces:
    - name: metrics-10s:2d
      preset: 10s:2d
```

```sh
$ kubectl apply -f m3db-cluster.yaml
m3dbcluster.operator.m3db.io/simple-cluster created
```


上記のmanifestをapplyして少し待つとKubernetes上にM3DBが構築されます。

```sh
$ kubectl get pods
NAME                    READY   STATUS    RESTARTS   AGE
etcd-0                  1/1     Running   0          ...
etcd-1                  1/1     Running   0          ...
etcd-2                  1/1     Running   0          ...
m3db-operator-0         1/1     Running   0          ...
simple-cluster-rep0-0   1/1     Running   0          ...
simple-cluster-rep1-0   1/1     Running   0          ...
simple-cluster-rep2-0   1/1     Running   0          ...
```

```sh
$ kubectl get m3dbcluster
NAME             CREATED AT
simple-cluster   ...
```

無事Operatorを使ってクラスタを立てることができました。

## Operator を作成する

今まで、OperatorがCustom ControllerとCustom Resourceによって動作することを確認してきましたが、実際にはどうやってOperatorを作成すれば良いのでしょうか。Custom ControllerにはKubernetes APIを利用してReconciliation Logicを記述する必要がありますが、フルスクラッチで処理を書くのは相当厳しいと思います。

### Operator Framework

そこで、Operator Frameworkの利用が可能です。
中でもOperator SDKを使うことで、Kubernetes APIの詳細を知らずともCRDの生成やReconsilication Logicの実装に集中できるようになります（Golangがサポートされています）。

- https://github.com/operator-framework/operator-sdk

Operator Frameworkを利用することでKubernetes APIを詳細に理解せずともReconcile ロジックの実装に集中できるようになります。

（実際にOperator Frameworkで何かOperatorを作る...というところまでやりたかったのですが、それはまた別の機会に。）

## まとめ

本記事ではKubernetes Operatorを紹介しました。
今後はメジャーなStatefulアプリケーション運用のベストプラクティスを吸収したOperatorがOSSとして急速に発展していくのではないかと期待しています。