KubeVirtとKubernetesオブジェクトの連携

[KubeVirt](https://kubevirt.io/)は、Kubernetes上でVirtual Machineを作成する技術です。
OpenShift Container Platformに組み込まれており、[OpenShift Virtualization](https://access.redhat.com/documentation/ja-jp/openshift_container_platform/4.5/html-single/openshift_virtualization/index)と呼ばれています。
Apple社がKubeCon+CloudNativeCon Europe 2020[^1]にてKubeVirtを利用していることを紹介していました。

**「Kubernetes上でVMを作る？」**
パブリッククラウド/プライベートクラウドを利用されている方はクラウドのIaaSを使ってVMを作ることが多いかと思います。そもそもManagedなKaaSを使っている方の多くは、Kubernetesクラスタ自体はVMのNodeで構成されている場合が多いかと思います。そのためKubernetes上でVMを作る必要性を感じることは少ないかもしれません。
ベアメタルでKubernetesクラスタを使っているが、どうしてもVMでないとサポートされない製品を稼働させる、とか、Legacyなシステムでコンテナ化が難しいものの移行先が欲しい、といったユースケースの場合、KubeVirtが候補として挙がってくるのかもしれません。

KubeVirtは大変興味深い技術です。
ググると、KubeVirtを使ってみたという情報はすぐ見つかります。しかしKubernetes上で稼働している点に注目し、Kubernetesのオブジェクトとどう連携できるのか？それによってVMのオペレーションがどう便利になるのか？という点に着目した情報はあまりありませんでした。
この記事では、具体的に以下に注目して掘り下げてみたいと思います。

- ConfigMap/Secret連携
- Service連携
- Ingress連携
- VMのメトリクス

## 環境
KubeVirtはVM上でもEmulateすることができるので、お手軽に試用することができます。
今回はminikube(hyperkit)上でVMを作りたいと思います。

- minikube 1.15.1
- Kubernetes 1.19.4
- kubevirt v0.34.0
- macOS Catalina 10.15.17

## セットアップ
KubeVirtはminikubeのaddonに組み込まれており、今回はそちらを利用します。
minikube上で、VMを起動するため、メモリをデフォルトより増やしておきます。
また、後ほどIngressも試すので、`nginx ingress controller`を有効にします。

```
$ minikube start --memory=6g
$ minikube addons enable kubevirt 
$ minikube addons enable ingress
```

KubeVirtのコンポーネントがすべて`Running`になったか確認します。

```
$ kubectl get po --namespace kubevirt
NAME                               READY   STATUS    RESTARTS   AGE
virt-api-7dd54c49d8-7gx9d          1/1     Running   0          3m19s
virt-api-7dd54c49d8-sj882          1/1     Running   0          3m19s
virt-controller-65d54bb555-6ddmv   1/1     Running   0          2m34s
virt-controller-65d54bb555-jrwn8   1/1     Running   0          2m34s
virt-handler-zf4zs                 1/1     Running   0          2m34s
virt-operator-57f89f688f-47p92     1/1     Running   0          3m58s
virt-operator-57f89f688f-766ps     1/1     Running   0          3m58s
```

KubeVirtのVMを操作するCLI `virtctl`は`kubectl plugins`で提供されているので、今回はそちらを利用します。

```
$ kubectl krew install virt      
$ kubectl plugin list
The following compatible plugins are available:

/Users/XXXX/.krew/bin/kubectl-virt
```

後ほどConfigMap, Secretとの連携を試すので、適当なConfigMap, Secretを作成します。

```
cat << EOF | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
  name: sample-configmap
data:
  setup.sh: |
    #!/bin/bash
    echo "Do something!"
---
apiVersion: v1
kind: Secret
metadata:
  name: sample-secret
type: Opaque
stringData:
  secret.yaml: |-
    secret: "important"
EOF
```

## VMの作成

さっそくVMを作りますが、今回はゲストOSとしてKubeVirtがメンテナンスしている`fedora`を使います。
以下のManifestを使います。
今回CloudinitでSecret, ConfigMap [^2] のDiskをマウント、パスワードを`fedora`にしています。
Manifestの中身が気になる方は、[公式ドキュメント](https://kubevirt.io/user-guide/#/creation/creating-virtual-machines)を読まれるとよいかと思います。

```:testvm.yaml
apiVersion: kubevirt.io/v1alpha3
kind: VirtualMachine
metadata:
  name: testvm
spec:
  running: false
  template:
    metadata:
      labels:
        kubevirt.io/size: small
        kubevirt.io/domain: testvm
    spec:
      domain:
        devices:
          disks:
            - name: containerdisk
              disk:
                bus: virtio
            - name: cloudinitdisk
              disk:
                bus: virtio
            - name: configmap-disk
              disk: {}
              serial: CVLY623300HK240D
            - name: secret-disk
              disk: {}
              serial: DVLY623300HK240D               
          interfaces:
          - name: default
            bridge: {}
        resources:
          requests:
            memory: 4096M
      networks:
      - name: default
        pod: {}
      volumes:
        - name: containerdisk
          containerDisk:
            image: kubevirt/fedora-cloud-container-disk-demo
        - name: cloudinitdisk
          cloudInitNoCloud:
            userData: |-
              #cloud-config
              password: fedora
              chpasswd: { expire: False }
              bootcmd:
                - "mkdir /mnt/configmap"
                - "mount /dev/$(lsblk --nodeps -no name,serial | grep CVLY623300HK240D | cut -f1 -d' ') /mnt/configmap"
                - "mkdir /mnt/secret"
                - "mount /dev/$(lsblk --nodeps -no name,serial | grep DVLY623300HK240D | cut -f1 -d' ') /mnt/secret"
        - configMap:
            name: sample-configmap 
          name: configmap-disk
        - secret:
            secretName: sample-secret
          name: secret-disk
```
```
$ kubectl apply -f testvm.yaml
# VM(testvm)起動
$ kubectl virt start testvm
$ kubectl get vmi
NAME     AGE   PHASE     IP            NODENAME
testvm   60s   Running   172.17.0.12   minikube
```
`vmi(Virtual Machine Instance)`の`PHASE`が`Running`になるまで待ち、Console接続をします。
Console接続中も、Fedoraのセットアップが終わるまで多少時間がかかります。
しばらく待ちましょう。

```
$ kubectl virt console testvm
Successfully connected to testvm console. The escape sequence is ^]
testvm login: fedora
Password:          <- fedoraです
```
`testvm.yaml`で指定した場所に`ConfigMap`と`Secret`がMountできているか確認します。

```
[fedora@testvm ~]$ ls /mnt/*
/mnt/configmap:
setup.sh

/mnt/secret:
secret.yaml
[fedora@testvm ~]$ cat /mnt/secret/secret.yaml
secret: "important"[fedora@testvm ~]$
```
ConfigMapもSecretもMountされていました。Kubernetesのオブジェクトがそのまま使えるのは便利ですね。
`Ctrl + ]` でConsoleを抜けます。

Kubernetes自体のConfigMap,SecretをPodでファイルマウントする場合は、動的に更新が反映されますが、KubeVirtのVMでは作成時の状態が維持されます。つまり動的に反映されません。
一括で設定変更したり、証明書を差し替えたりといったことができると大変便利だな、と期待していたのですが、残念ながらそのような使い方には対応していません。

## クラスタ外からVMにssh
続いてクラスタ外からVMにsshできるようにします。
今回はクラスタ外(Mac)からVMの22番ポートにsshできるようにType=NodePortで公開します。もちろん、Type=Loadbalancer, ClusterIPも使用可能です。
以下の`virtctl`コマンドでも操作できるのですが、KubernetesのServiceで操作する方が個人的にはわかりやすいので、Serviceを作ります。

```
$ kubectl virt expose vmi testvm --name=vm1-ssh --port=27017 --target-port=22 --type=NodePort
```
のちほど、80番ポートを使ってnginxを稼働させるので、設定しておきます。

```
$ cat << EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  name: vm-service
spec:
  externalTrafficPolicy: Cluster
  ports:
  - port: 27017
    protocol: TCP
    targetPort: 22
    name: ssh
  - port: 27013
    protocol: TCP
    targetPort: 80
    name: http
  selector:
    kubevirt.io/domain: testvm
  type: NodePort
EOF

$ kubectl get service
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                           AGE
kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP                           5h23m
vm-service   NodePort    10.108.41.198   <none>        27017:30722/TCP,27013:30981/TCP   5h16m
$ ssh fedora@`minikube ip` -p 30722
The authenticity of host '[192.168.64.23]:30722 ([192.168.64.23]:30722)' can't be established.
ECDSA key fingerprint is SHA256:8ALbFD6Exh9DDywwAeTVEpYThohTAsYkDMT1StBVYhc.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '[192.168.64.23]:30722' (ECDSA) to the list of known hosts.
fedora@192.168.64.23's password:
[fedora@testvm ~]$
```
のちほど、Podからの通信を確認するために、nginxをインストール・起動します。

```
[fedora@testvm ~]$ sudo dnf install -y nginx
[fedora@testvm ~]$ sudo nginx
[fedora@testvm ~]$ curl localhost
…省略
```

## Pod-VM間の通信
デフォルトでは、VMはPodのネットワークとLinux bridgeで接続されます。
他にも[Multusを使いNICを追加したり、IP Masqueradeする](https://kubevirt.io/user-guide/#/creation/interfaces-and-networks)ようにしたりできるようです。

まず、Pod to VM です。

```
$ kubectl get vmi
NAME     AGE   PHASE     IP           NODENAME
testvm   20m   Running   172.17.0.2   minikube

$ kubectl run -it --rm curl --image curlimages/curl sh
/ $ curl 172.17.0.2
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <title>Test Page for the HTTP Server on Fedora</title>
  <meta http-equiv="Content-Type" content="text/html; charset=us-ascii" />
  <style type="text/css">
…省略
```
問題なく疎通できてますね。
続いてVM to Podです。

```
$ kubectl run -it --rm nginx --image nginx 
$ kubectl get po -o wide
NAME                         READY   STATUS    RESTARTS   AGE   IP            NODE       NOMINATED NODE   READINESS GATES
nginx                        1/1     Running   0          10s   172.17.0.12   minikube   <none>           <none>
virt-launcher-testvm-ft8pv   2/2     Running   0          23m   172.17.0.2    minikube   <none>           <none>

$ ssh fedora@`minikube ip` -p 30722 curl 172.17.0.12
fedora@192.168.64.23's password:
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   612  100   612    0     0  22666      0 --:--:-- --:--:-- --:--:-- 36000
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
…省略
```
問題なく疎通できています。

## Ingress連携
さて、Serviceも動き、Pod-VM間通信も問題ないということは、Ingress連携もできるわけですね。

```
$ cat << EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  - http:
      paths:
      - path: /testpath
        pathType: Prefix
        backend:
          service:
            name: vm-service
            port: 
              number: 80
EOF
$ kubectl get ingress.v1.networking.k8s.io
NAME      CLASS    HOSTS   ADDRESS         PORTS   AGE
ingress   <none>   *       192.168.64.23   80      5h36m
$ open https://192.168.64.23/testpath 
```
![スクリーンショット 2020-12-16 15.42.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100376/a507ef40-6b3d-727c-2a7d-e5a0e43f8e7f.png)

いきました。これでVMの前段でTLS終端をしたり、hostnameを設定したり色々できますね。
いやー、冬なのに熱いですね。

## VMのメトリクス
KubeVirtのアプリケーション自体のメトリクスはもちろんですが、VMのメトリクスもデフォルトで取得することが可能です。
`Namespace:kubevirt` `Pod:virt-handler` `Port:8443`で取得することができます。より詳細な情報はこちら[^3] [^4]。

```
$ kubectl get po -o wide --namespace kubevirt
NAME                               READY   STATUS    RESTARTS   AGE   IP            NODE       NOMINATED NODE   READINESS GATES
virt-api-7dd54c49d8-7gx9d          1/1     Running   0          63m   172.17.0.8    minikube   <none>           <none>
virt-api-7dd54c49d8-sj882          1/1     Running   0          63m   172.17.0.9    minikube   <none>           <none>
virt-controller-65d54bb555-6ddmv   1/1     Running   0          62m   172.17.0.10   minikube   <none>           <none>
virt-controller-65d54bb555-jrwn8   1/1     Running   0          62m   172.17.0.11   minikube   <none>           <none>
virt-handler-zf4zs                 1/1     Running   0          62m   172.17.0.7    minikube   <none>           <none>
virt-operator-57f89f688f-47p92     1/1     Running   0          63m   172.17.0.4    minikube   <none>           <none>
virt-operator-57f89f688f-766ps     1/1     Running   0          63m   172.17.0.5    minikube   <none>           <none>

$ kubectl run -it --rm curl --image curlimages/curl sh
If you don't see a command prompt, try pressing enter.
/ $ curl --insecure https://172.17.0.7:8443/metrics
```
以下は対象のVMのメトリクス(CPU利用率)だけ抜粋したものです。labelに`testvm`が含まれており、対象のVMを識別することができます。

```
# HELP kubevirt_vmi_vcpu_seconds Vcpu elapsed time.-:-- --:--:-- 29742
# TYPE kubevirt_vmi_vcpu_seconds gauge
kubevirt_vmi_vcpu_seconds{id="0",kubernetes_vmi_label_kubevirt_io_domain="testvm",kubernetes_vmi_label_kubevirt_io_nodeName="minikube",kubernetes_vmi_label_kubevirt_io_size="small",name="testvm",namespace="default",node="minikube",state="running"} 1064
# HELP kubevirt_vmi_vcpu_wait_seconds vcpu time spent by waiting on I/O
# TYPE kubevirt_vmi_vcpu_wait_seconds gauge
kubevirt_vmi_vcpu_wait_seconds{id="0",kubernetes_vmi_label_kubevirt_io_domain="testvm",kubernetes_vmi_label_kubevirt_io_nodeName="minikube",kubernetes_vmi_label_kubevirt_io_size="small",name="testvm",namespace="default",node="minikube"} 0
…省略
```
VMにNode Exporterを設定せずとも、デフォルトでメトリクスが出力されているのは大変ありがたい。
既存のメトリクスモニタリングシステムと連携できますね。


## Cleanup
vmオブジェクトを削除してもいいのですが、メモリも変更したので、minikubeごと壊してしまいます。

```
$ minikube delete
$ kubectl krew uninstall virt 
```

## 所感
今回、KubeVirtで作成したVMがKubernetesオブジェクトをどう活用できるか、という点に注目して触ってみました。
結果として、思ったよりも多くKubernetesオブジェクトと連携することができ、VM-Pod間の通信、ConfigMapやSecretのマウント、Service、Ingressとの連携ができました。
さらにデフォルトでVMのMetricsも取得することができたのは、非常に便利です。
今回は触れていませんが、この他にもPVCやServiceAccountのマウントが可能です。

一方、試してみて残念だった点は、Configmap, Secretの動的な更新に対応できないことです。おそらくProjected Service Account Tokenにも対応できていないでしょう。PVにも制限がありそうです。

私は普段の業務でTerraformでIaaSを操作し、VMやLBをセットアップしていますが、その体験がKubeVirtにおいては、`terraform=kubectl`に、`Remote Storage=etcd`に。そんな印象を受けました。（terraform自体にもKubernetes Providerがあるので、そのあたりはなんとも言い難いのですが。。）

ところで、KubeVirtを仮に本番で安定稼働させるには悩ましい点がいっぱいです。
作成したVMの性能検証、Kubernetes・KubeVirtのバージョンアップ、Kubernetesクラスタ全体のコンピュートリソースの管理、CNI、実際の開発者が持つ権限、Kubernetesクラスタ間のVMの通信など、たくさんの検討箇所があります。Kubernetesクラスタの管理・操作そのものに長ける必要もありますし、そもそもKubernetesでVMを稼働させる前に、Podで稼働させられるようチャレンジした方がよいのでは？ということもあるでしょう。

色々考えさせられますが、非常に野心的な技術であることには間違いありません。
少し古い記事ですが、IstioとKubeVirtを連携するblog[^5]が公開されていました。
このようなクラウドネイティブな技術と簡単に結合できるようになると、これまでのVMのオペレーションからさらに進化したオペレーションが生まれるのかもしれません。
今後さらに踏み込んで、KubeVirtとKubernetesとの連携がさらに強化され、便利なツールなど提供されるとよいな、と期待しています。

[^1]: https://kccnceu20.sched.com/event/ZevE/kubevirt-intro-using-kubevirt-to-run-vms-at-scale-fabian-deutsch-red-hat-marcus-sorensen-apple
[^2]: https://kubevirt.io/user-guide/#/creation/disks-and-volumes?id=configmap
[^3]: https://kubevirt.io/2019/More-about-Kubevirt-metrics.html
[^4]: https://kubevirt.io/user-guide/#/installation/monitoring?id=metrics-about-virtual-machines
[^5]: https://kubevirt.io/2018/Run-Istio-with-kubevirt.html