Kubernetes Operator for Nutanix はじめの一歩

# はじめに
この記事は[Nutanix Advent Calender 2021](https://adventar.org/calendars/6154)の12/3分の記事です。Nutanixを制御対象とし、Kubernetes Operatorｍに入門してみましたのでその成果を記します。

# Kubernets Operatorとは？
Kubernetesの機能を拡張し、独自のリソース定義とコントローラを実装することでKubernetesによる運用自律化の仕組みをあらゆるアプリケーション・ミドルウェア・インフラに応用出来るようにするものです。

https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/

# Operatorを実装するためのフレームワーク
- [Kubebuilder](https://github.com/kubernetes-sigs/kubebuilder)
- [kopf](https://github.com/nolar/kopf)
- [Operator Framework](https://github.com/operator-framework)

今回はさくっとOperatorの仕組みを押さえたい、とりあえずシンプルなユースケースとしてNutanix AHVの仮想マシン制御する実装を行いたいので1番手っ取り早く実装できそうな**kopf**を選択しました。Githubのスター数を見ているとRedhat社が推しているOperator Frameworkが主流のようです。

# 必要なパーツ
- Kubernetesクラスタ: kubeadmを使用して1ノードクラスタを事前に作成しました。
- CRD(Custom Resource Definition): 独自APIリソースのスキーマをyaml形式で定義します
- カスタムコントローラ
    - ソースコード: ControllerのロジックをPythonで実装します。
    - Dockerfile: ソースコードとベースイメージを合わせてOperatorとして動作させるためのコンテナイメージを定義します

# 実装
## 1. CRDの準備
今回はカスタムリソースを利用してAHV VMを作成する所までやりたいので以下の**ahvvms.nutanix.com**なるリソースを定義するCRDを準備しました。

```yaml:1_crd.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: ahvvms.nutanix.com
spec:
  group: nutanix.com
  scope: Namespaced
  names:
    kind: AhvVm
    plural: ahvvms
    singular: ahvvm
    shortNames:
      - avm
  versions:
    - name: v1alpha1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                num_sockets:
                  minimum: 1
                  maximum: 4
                  type: integer
                num_vcpus_per_socket:
                  minimum: 1
                  maximum: 2
                  type: integer
                memory_size_mib:
                  minimum: 1024
                  maximum: 4096
                  type: integer
      additionalPrinterColumns:
        - name: Sockets
          type: integer
          jsonPath: .spec.num_sockets
          description: Number of sockets
        - name: vCPUsPerSocket
          type: integer
          jsonPath: .spec.num_vcpus_per_socket
          description: Number of vCPUs per socket
        - name: Memory
          type: integer
          jsonPath: .spec.memory_size_mib
          description: Memory Size in MiB
```

これを準備したKubernetesクラスタに適用すると、CRDとして適用されます。

```shell
[sho.uchida@C02Y41RDJHD3 kopf-nutanix] (main)$ kubectl get crds
NAME                 CREATED AT
ahvvms.nutanix.com   2021-11-11T14:40:16Z
```

## 2. カスタムコントローラの準備
kopfにおけるコントローラの書き方に沿ってPythonでカスタムコントローラを実装します。接続先のPrism Centralの情報はコントローラPodの環境変数から参照する実装としました。中身はいたってシンプルで、ahvvms.nutanix.comのカスタムリソースが作成された際にNutanixのv3APIを使用して仮想マシンを作成します。

```python:2_cc.py
import kopf
import requests
import urllib3
import json
import os
import logging
import sys

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

logger = logging.getLogger(__name__)
sh = logging.StreamHandler(sys.stdout)
sh.setLevel(logging.DEBUG)
logger.addHandler(sh)

# Prism
PRISM_HOST = os.getenv('PRISM_HOST')
PRISM_USER = os.getenv('PRISM_USER')
PRISM_PASS = os.getenv('PRISM_PASS')
PRISM_PORT = os.getenv('PRISM_PORT', 9440)
PRISM_CLUSTERNAME = os.getenv('PRISM_CLUSTERNAME')
PRISM_CLUSTERUUID = os.getenv('PRISM_CLUSTERUUID')

@kopf.on.create('nutanix.com', 'v1alpha1', 'ahvvm')
def create_fn(spec, **kwargs):

    vm_name = kwargs["body"]["metadata"]["name"]
    num_sockets = spec.get('num_sockets', 1)
    num_vcpus_per_socket = spec.get('num_vcpus_per_socket', 1)
    memory_size_mib = spec.get('memory_size_mib', 1024)

    url = f'https://{PRISM_HOST}:{PRISM_PORT}/api/nutanix/v3/vms'
    payload = f'{{ \
                    "spec":{{ \
                        "name":"{vm_name}", \
                        "resources":{{ \
                            "num_sockets":{num_sockets}, \
                            "num_vcpus_per_socket":{num_vcpus_per_socket}, \
                            "memory_size_mib":{memory_size_mib} \
                        }}, \
                        "cluster_reference":{{ \
                            "kind":"cluster", \
                            "name":"{PRISM_CLUSTERNAME}", \
                            "uuid":"{PRISM_CLUSTERUUID}" \
                        }}\
                    }}, \
                    "metadata":{{ \
                        "kind":"vm" \
                    }} \
                }}'
    try:
        response = session.post(url, data=payload)
        if(response.ok):
            logger.debug("OK")
        else:
            logger.error(f'An error occurred while connecting to {PRISM_HOST}.')
            logger.error(response.text)
    except Exception as ex:
        logger.error(f'An {type(ex).__name__} exception occurred while connecting to {PRISM_HOST}.\nArgument: {ex.args}.')
      
session = requests.Session()
session.auth = (PRISM_USER, PRISM_PASS)
session.verify = False 
session.headers.update({"Content-Type": "application/json", "Accept": "application/json", "cache-control": "no-cache"})
logger.debug("Session is created.")
```

このソースコードを使用し、pythonのベースイメージを使ってコンテナイメージをビルドします。

```Dockerfile:Dockerfile
FROM python:3.7
RUN mkdir /src
ADD 2_cc.py /src
ADD requirements.txt .
RUN pip3 install -r requirements.txt
CMD kopf run /src/2_cc.py --verbose
```

```shell
[sho.uchida@C02Y41RDJHD3 kopf-nutanix] (main)$ docker build -t (Dockerレポジトリ名)/kopf-nutanix:latest .
[sho.uchida@C02Y41RDJHD3 kopf-nutanix] (main)$ docker push (Dockerレポジトリ名)/kopf-nutanix:latest
```

## 3. カスタムコントローラの展開
2でビルド・プッシュしたコントローラのコンテナイメージを元に、コントローラをKubernetesクラスタ上に展開します。

```yaml:3_cc.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ahvvm-operator
  namespace: kopf-system
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      application: ahvvm-operator
  template:
    metadata:
      labels:
        application: ahvvm-operator
    spec:
      serviceAccountName: ahvvm-operator
      containers:
      - name: ahvvms
        image: (Dockerレポジトリ名)/kopf-nutanix:latest
        imagePullPolicy: Always
        env:
        - name: PRISM_HOST
          value: "Prism Centralのホスト名"
        - name: PRISM_USER
          value: "Prism Centralのユーザ名"
        - name: PRISM_PASS
          value: "Prism Centralのパスワード"
        - name: PRISM_PORT
          value: "Prism Centralのポート番号、たぶん9440"
        - name: PRISM_CLUSTERNAME
          value: "クラスタ名"
        - name: PRISM_CLUSTERUUID
          value: "クラスタUUID"
```

カスタムコントローラが展開出来ていることを確認します。

```shell
[sho.uchida@C02Y41RDJHD3 kopf-nutanix] (main)$ kubectl get all -n kopf-system
NAME                                  READY   STATUS    RESTARTS   AGE
pod/ahvvm-operator-5d78c4b94f-cj9p9   1/1     Running   0          20d

NAME                             READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/ahvvm-operator   1/1     1            1           20d

NAME                                        DESIRED   CURRENT   READY   AGE
replicaset.apps/ahvvm-operator-5d78c4b94f   1         1         1       20d

```

## 4. カスタムリソースの適用
では、CRDとカスタムコントローラが展開されましたので、CRDで定義されたモデルに則ったカスタムリソースを作成します。

```yaml:4_cr.yaml
apiVersion: nutanix.com/v1alpha1
kind: AhvVm
metadata:
  name: ahvvm-from-operator
spec:
  num_sockets: 2
  num_vcpus_per_socket: 1
  memory_size_mib: 2048
```

ahvvmのリソースを確認すると、新たに作成した**ahvvm-from-operator**のオブジェクトが出来ています。

```shell
[sho.uchida@C02Y41RDJHD3 kopf-nutanix] (main)$ kubectl get ahvvm
NAME                  SOCKETS   VCPUSPERSOCKET   MEMORY
ahvvm-from-operator   2         1                2048
```

コントローラのログ確認。

```shell
[sho.uchida@C02Y41RDJHD3 kopf-nutanix] (main)$ kubectl logs ahvvm-operator-5d78c4b94f-cj9p9 -n kopf-system
~~~略~~~
[2021-11-12 00:06:13,086] kopf.objects         [DEBUG   ] [default/ahvvm-from-operator] Creation is in progress: {'apiVersion': 'nutanix.com/v1alpha1', 'kind': 'AhvVm', 'metadata': {'annotations': {'kubectl.kubernetes.io/last-applied-configuration': '{"apiVersion":"nutanix.com/v1alpha1","kind":"AhvVm","metadata":{"annotations":{},"name":"ahvvm-from-operator","namespace":"default"},"spec":{"memory_size_mib":2048,"num_sockets":2,"num_vcpus_per_socket":1}}\n'}, 'creationTimestamp': '2021-11-12T00:06:12Z', 'generation': 1, 'managedFields': [{'apiVersion': 'nutanix.com/v1alpha1', 'fieldsType': 'FieldsV1', 'fieldsV1': {'f:metadata': {'f:annotations': {'.': {}, 'f:kubectl.kubernetes.io/last-applied-configuration': {}}}, 'f:spec': {'.': {}, 'f:memory_size_mib': {}, 'f:num_sockets': {}, 'f:num_vcpus_per_socket': {}}}, 'manager': 'kubectl', 'operation': 'Update', 'time': '2021-11-12T00:06:12Z'}], 'name': 'ahvvm-from-operator', 'namespace': 'default', 'resourceVersion': '636082', 'uid': '92cf13c0-c68f-4ef8-8d18-ade4aac450df'}, 'spec': {'memory_size_mib': 2048, 'num_sockets': 2, 'num_vcpus_per_socket': 1}}
[2021-11-12 00:06:13,087] kopf.objects         [DEBUG   ] [default/ahvvm-from-operator] Handler 'create_fn' is invoked.
[2021-11-12 00:06:13,090] urllib3.connectionpo [DEBUG   ] Resetting dropped connection: 10.42.9.39
[2021-11-12 00:06:13,806] urllib3.connectionpo [DEBUG   ] https://10.42.9.39:9440 "POST /api/nutanix/v3/vms HTTP/1.1" 202 None
[2021-11-12 00:06:13,807] __kopf_script_0__/sr [DEBUG   ] OK
OK
[2021-11-12 00:06:13,808] kopf.objects         [INFO    ] [default/ahvvm-from-operator] Handler 'create_fn' succeeded.
[2021-11-12 00:06:13,808] kopf.objects         [INFO    ] [default/ahvvm-from-operator] Creation is processed: 1 succeeded; 0 failed.
[2021-11-12 00:06:13,809] kopf.objects         [DEBUG   ] [default/ahvvm-from-operator] Patching with: {'metadata': {'annotations': {'kopf.zalando.org/last-handled-configuration': '{"spec":{"memory_size_mib":2048,"num_sockets":2,"num_vcpus_per_socket":1}}\n'}}}
[2021-11-12 00:06:13,939] kopf.objects         [DEBUG   ] [default/ahvvm-from-operator] Something has changed, but we are not interested (the essence is the same).
[2021-11-12 00:06:13,939] kopf.objects         [DEBUG   ] [default/ahvvm-from-operator] Handling cycle is finished, waiting for new changes.
```

そして肝心のAHV VMが作成されているかを確認してみると、めでたく指定されたスペックで仮想マシンが作成されています。
![スクリーンショット 2021-12-02 23.58.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134931/e1f781de-d70d-672c-baba-b0ffa862a9be.png)

# 終わりに
今回はVM作成という非常にシンプルなユースケースで手っ取り早く実装を行いましたのでOperatorがただのAPI変換マシーンになっています。今後、VMレプリカ数を制御し、障害時は新たな仮想マシンを構築する等、KubernetesのControllerが内部実行するReconciliation Loopの仕組みを利用したNutanixの運用自律化の構築に期待が出来る内容となっています。また、Operator Frameworkでの実装にもチャレンジしたいと思っています。以上！