【OpenShift Virtualization】仮想マシンにGitOpsを求めるのは間違っているだろうか

# 出落ち
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/0e491b43-4c24-44b8-d480-5dc2a90a6ac5.png)

# 間違っていない
アリストテレス的三段論法にて示します。
- KubeVirt / OpenShift Virtualizationでは、仮想マシン（VM）をKubernetesの上で扱うことができる。
- 故にVMをKubernetesのリソースとしてYAMLファイルで表現できる。
- よってVMのGitOpsは寧ろ推進されるべき営みである。

# 各種CustomResourceについて
KubeVirt / OpenShift Virtualizationの中で仮想マシンを管理する上で必要な`CustomResourceDefinition`について以下、軽く説明します。

## `DataVolume`CRD
仮想マシンのルートボリューム（`rootdisk`）を定義するCRDです。今回使うYAMLを示します。

```vm-datavolume.yaml
apiVersion: cdi.kubevirt.io/v1beta1
kind: DataVolume
metadata:
  name: fedora-datavolume
spec:
  source: #OSイメージの在り処を指定します。今回はURL直指定です。
    http:
      url: https://download.fedoraproject.org/pub/fedora/linux/releases/40/Cloud/x86_64/images/Fedora-Cloud-Base-Generic.x86_64-40-1.14.qcow2
  pvc: #永続ボリュームのスペックをPVCの書式に従って記載します
    accessModes:
      - ReadWriteMany
    volumeMode: Block
    storageClassName: ocs-storagecluster-ceph-rbd-virtualization
    resources:
      requests:
        storage: 30Gi
```
聞き慣れないリソースなので難しいと感じてしまうかもしれませんが、中身を見るととても簡単です。`DataVolume`では主に2つの要素を定義します。

ひとつは`source`です。ゲストマシンのOSイメージの在り処（取得先）を指定します。今回は[Fedoraプロジェクトがホームページで公開](https://fedoraproject.org/cloud/download)している「KVM仮想環境向けのイメージ（QCOW2形式）」のダウンロードURLを直接指定します。

::: note
QCOW2形式についての詳細は[こちら](https://docs.redhat.com/ja/documentation/red_hat_virtualization/4.2/html/technical_reference/qcow2)を御覧ください
:::

`source`は今回の場合は`http.url`としていますが、既にDeploy済みの仮想マシンの`DataVolume`から作成された`PersistentVolumeClaim`を指定したり、仮想マシンのスナップショットを指定したりもできます。
ちなみに、`source`部分だけを更に`DataSource`というCRDで外だしして記載することも可能ですが、今回はやりません。

次に`pvc`です。これは仮想マシンの永続ボリューム要求を`PersistentVolumeClaim`の書式そのままを記載します。`storageClass`や`accessModes`（仮想マシンをLive Migrationさせたい場合は`ReadWriteMany`でのProvisioningが必須です）等を指定します。

## `VirtualMachine` CRD
Kubernetes上の仮想マシンインスタンス（VMI）は、CustomResourceDefinitionである、文字通り`VirtualMachine`によって定義されます。コンテナアプリケーションでいうところの`Deployment`に近しい存在かと私個人は理解しており、`VirtualMachine`から`virt-launcher`（VMが起動しているコンテナを内包する`Pod`）等のリソースが作成されます。

今回使うManifestを以下に示します。

```vm-example.yaml
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
  name: vm-example
  labels:
    app: vm-example
    os.template.kubevirt.io/fedora: 'true'
spec:
  running: true #仮想マシン作成時の挙動を指定します。trueにすると作成後、自動的に起動します。
  template:
    metadata:
      labels:
        app: vm-example #VMに付与するlabelです。Serviceと紐づける為に付与しておきます
        kubevirt.io/domain: vm-example
    spec:
      accessCredentials: #SSH公開鍵との紐づけを設定します
        - sshPublicKey:
            propagationMethod:
              noCloud: {}
            source:
              secret: #public-key.yamlというSecretファイルに公開鍵情報を盛り込んでいます
                secretName: public-key
      domain:
        cpu: #仮想マシンにアロケーションするCPUリソースを定義します
          cores: 1
          sockets: 1
          threads: 1
        devices: #仮想マシンに接続する仮想デバイスを定義します
          disks:
            - disk:
                bus: virtio
              name: rootdisk #rootdiskとしてVirtIOデバイスを指定します
            - disk:
                bus: virtio
              name: cloudinitdisk #仮想マシン起動時の設定（cloud-init）を実行する為のディスクを定義します
          interfaces:
            - name: eth0 #Podに最初から付与されているvNIC（eth0）の名称です。任意のものに変更できます
              masquerade: {} #eth0は必ずmasqueradeに指定してください。
              macAddress: '02:c7:36:00:00:2b' #eth0に指定したいMACアドレスを記載します
              model: virtio
        memory: #仮想マシンにアロケーションするメモリを定義します
          guest: 2Gi
      hostname: vm-example
      networks: #仮想マシンインスタンスのvNICが接続するNWの定義を記載します
        - name: eth0 #eth0という名称のvNICをKubernetesのPod Network(デフォルトのOverlay Network）に参加させます。これは必須の設定です
          pod: {}
      volumes:
        - name: rootdisk #rootdiskとしてDataVolume名を参照しています
          dataVolume:
            name: vm-datavolume
        - name: cloudinitdisk #cloud-init設定内容を実行するディスク
          cloudInitNoCloud: #cloud-initの書式に従って記載された内容を実行します
            secretRef: #cloud-init設定内容をuserdata-secret.yamlに記載しています
              name: userdata-secret
```

`spec.template.spec.volumes`で2つのボリューム（仮想ディスク）を指定しています。`rootdisk`は先程示した`DataVolume`そのものになります。`cloudinitdisk`は仮想マシン起動時の初期設定（cloud-init）をディスクとしてマウントするものです。中にはユーザ/パスワード情報を含むcloud-initで設定可能な情報を記載しますので、センシティブな情報です。`VirtualMachine`YAML自体に記載することも可能ですが、別にSecretファイルを用意したほうがよさそうです。なお、今回の設定内容は以下のとおりです。

```yaml
   #cloud-config
   user: cloud-user
   password: cloud-password
   chpasswd:
      expire: false
   ssh_pwauth: true
   runcmd:
     - sudo yum install httpd -y
     - sudo systemctl start httpd.service
```
Fedora OSのユーザ/パスワード情報を記載しています。また、以下の記事でも触れているのですが、起動時に特定のコマンドを実行しています。今回はApacheをインストールし、Webサーバの起動をしています。

:::note
[[OpenShift Virtualization] 追加料金無しでRHEL立て放題、起動と同時にSubscription Managerに自動登録してみた](https://qiita.com/masaki-oomura/items/37dd4008e1b6d230954a)
:::

この内容を`userdata-secret.yaml`に記載しています。

```userdata-secret.yaml
kind: Secret
apiVersion: v1
metadata:
  name: userdata-secret
data:
  userData: I2Nsb3VkLWNvbmZpZw0KdXNlcjogY2xvdWQtdXNlcg0KcGFzc3dvcmQ6IGNsb3VkLXBhc3N3b3JkDQpjaHBhc3N3ZDoNCiAgZXhwaXJlOiBmYWxzZQ0Kc3NoX3B3YXV0aDogdHJ1ZQ0KcnVuY21kOg0KICAtIHN1ZG8geXVtIGluc3RhbGwgaHR0cGQgLXkNCiAgLSBzdWRvIHN5c3RlbWN0bCBzdGFydCBodHRwZC5zZXJ2aWNl
  #以下のcloud-configをBASE64エンコードしたものを`userData`に記載します
    #cloud-config
   #user: cloud-user
   #password: cloud-password
   #chpasswd:
      #expire: false
   #ssh_pwauth: true
   #runcmd:
     #- sudo yum install httpd -y
     #- sudo systemctl start httpd.service
```

:::note
cloud-initについては[この記事](https://qiita.com/yamada-hakase/items/40fa2cbb5ed669aaa85b)が大変参考になりました！
:::

# 用意しているYAML一覧
以下の通りの構成としています。

```
root/
 ├ app-of-apps/
 | └ application.yaml  
 ├ applications/
 | ├ namespace-application.yaml
 | ├ configs-application.yaml
 | ├ datavolume-application.yaml
 | └ virualmachine-application.yaml
 └ manifest/
   ├ namespace/
   | └ vm-namespace.yaml
   ├ configs/
   | ├ public-key
   | ├ userdata-secret.yaml
   | ├ vm-route.yaml
   | └ vm-service.yaml
   ├ datavolume/
   | └ vm-datavolume.yaml
   └ virtualmachine/
     └ vm-example.yaml
```

複数のManifestを順序を意識してApplyしたい（例：まずは`NameSpace`のManifestからApplyしたい、`DataVolume`を`VirtualMachine`よりも先にDeployしたい 等）ので、「app of apps pattern」を取り入れています。親`Application`から子`Application`をsync-waveの順序でApplyしていく様に設計しています。これにより、Manifestの適用順序をコントロールします。

以下に全てのファイルの中身を貼ります。
まずは親/子`Application`です。

```app-of-apps/application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: app-of-apps
  namespace: openshift-gitops
spec:
  destination:
    namespace: openshift-gitops
    server: 'https://kubernetes.default.svc'
  source:
    path: applications
    repoURL: >-
      https://<your_git_repositry_url>
    targetRevision: main
  project: default
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
```

```applications/namespace-application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: namespace
  namespace: openshift-gitops
  annotations:
    argocd.argoproj.io/sync-wave: "1"
spec:
  destination:
    namespace: vm-example
    server: 'https://kubernetes.default.svc'
  source:
    path: manifest/namespace
    repoURL: >-
      https://<your_git_repositry_url>
    targetRevision: main
  project: default
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - ApplyOutOfSyncOnly=true
```
```applications/configs-application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: configs
  namespace: openshift-gitops
  annotations:
    argocd.argoproj.io/sync-wave: "2"
spec:
  destination:
    namespace: vm-example
    server: 'https://kubernetes.default.svc'
  source:
    path: manifest/configs
    repoURL: >-
      https://<your_git_repositry_url>
    targetRevision: main
  project: default
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - ApplyOutOfSyncOnly=true
```
```applications/datavolume-application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: datavolume
  namespace: openshift-gitops
  annotations:
    argocd.argoproj.io/sync-wave: "3"
spec:
  destination:
    namespace: vm-example
    server: 'https://kubernetes.default.svc'
  source:
    path: manifest/datavolume
    repoURL: >-
      https://<your_git_repositry_url>
    targetRevision: main
  project: default
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - ApplyOutOfSyncOnly=true
```
```applications/virualmachine-application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: virtualmachine
  namespace: openshift-gitops
  annotations:
    argocd.argoproj.io/sync-wave: "4"
spec:
  destination:
    namespace: vm-example
    server: 'https://kubernetes.default.svc'
  source:
    path: manifest/virtualmachine
    repoURL: >-
      https://gitlab.com/masaki-oomura/openshift-virt-argocd.git
    targetRevision: main
  project: default
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - ApplyOutOfSyncOnly=true
```

次に各Manifestです。これらはステートレスなリソースを生成します。

```manifest/namespace/vm-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: vm-example
  labels:
    argocd.argoproj.io/managed-by: openshift-gitops #Argo CDインスタンス（openshift-gitops)から管理できる為に必要なlabelです
```
```manifest/configs/public-key.yaml
kind: Secret
apiVersion: v1
metadata:
  name: public-key
data:
  key: <公開鍵（id_rsa.pub）の中身をBASE64エンコードしたもの>
type: Opaque
```
```manifest/configs/userdata-secret.yaml
kind: Secret
apiVersion: v1
metadata:
  name: userdata-secret
data:
  userData: I2Nsb3VkLWNvbmZpZw0KdXNlcjogY2xvdWQtdXNlcg0KcGFzc3dvcmQ6IGNsb3VkLXBhc3N3b3JkDQpjaHBhc3N3ZDoNCiAgZXhwaXJlOiBmYWxzZQ0Kc3NoX3B3YXV0aDogdHJ1ZQ0KcnVuY21kOg0KICAtIHN1ZG8geXVtIGluc3RhbGwgaHR0cGQgLXkNCiAgLSBzdWRvIHN5c3RlbWN0bCBzdGFydCBodHRwZC5zZXJ2aWNl
  #以下のcloud-configをBASE64エンコードしたものを`userData`に記載します
    #cloud-config
   #user: cloud-user
   #password: cloud-password
   #chpasswd:
      #expire: false
   #ssh_pwauth: true
   #runcmd:
     #- sudo yum install httpd -y
     #- sudo systemctl start httpd.service
```
```manifest/configs/vm-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: vm-service
spec:
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  selector: 
    app: vm-example
  clusterIP: None
```
```manifest/configs/vm-route.yaml
kind: Route
apiVersion: route.openshift.io/v1
metadata:
  name: vm-route
spec:
  to:
    kind: Service
    name: vm-service
  port:
    targetPort: 80
```
Fedora VMにApacheをインストールしてWebサーバとしてインターネット公開するためには、`Service`と`Route`が必要ですので、それも一緒にApplyしてしまいます。

最後に`DataVolume`と`VirtualMachine`です。こちらは上述の再掲。

```manifest/datavolume/vm-datavolume.yaml
apiVersion: cdi.kubevirt.io/v1beta1
kind: DataVolume
metadata:
  name: fedora-datavolume
spec:
  source: #OSイメージの在り処を指定します。今回はURL直指定です。
    http:
      url: https://download.fedoraproject.org/pub/fedora/linux/releases/40/Cloud/x86_64/images/Fedora-Cloud-Base-Generic.x86_64-40-1.14.qcow2
  pvc: #永続ボリュームのスペックをPVCの書式に従って記載します
    accessModes:
      - ReadWriteMany
    volumeMode: Block
    storageClassName: ocs-storagecluster-ceph-rbd-virtualization
    resources:
      requests:
        storage: 30Gi
```

```manifest/virtualmachine/vm-example.yaml
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
  name: vm-example
  labels:
    app: vm-example
    os.template.kubevirt.io/fedora: 'true'
spec:
  running: true #仮想マシン作成時の挙動を指定します。trueにすると作成後、自動的に起動します。
  template:
    metadata:
      labels:
        app: vm-example #VMに付与するlabelです。Serviceと紐づける為に付与しておきます
        kubevirt.io/domain: vm-example
    spec:
      accessCredentials: #SSH公開鍵との紐づけを設定します
        - sshPublicKey:
            propagationMethod:
              noCloud: {}
            source:
              secret: #public-key.yamlというSecretファイルに公開鍵情報を盛り込んでいます
                secretName: public-key
      domain:
        cpu: #仮想マシンにアロケーションするCPUリソースを定義します
          cores: 1
          sockets: 1
          threads: 1
        devices: #仮想マシンに接続する仮想デバイスを定義します
          disks:
            - disk:
                bus: virtio
              name: rootdisk #rootdiskとしてVirtIOデバイスを指定します
            - disk:
                bus: virtio
              name: cloudinitdisk #仮想マシン起動時の設定（cloud-init）を実行する為のディスクを定義します
          interfaces:
            - name: eth0 #Podに最初から付与されているvNIC（eth0）の名称です。任意のものに変更できます
              masquerade: {} #eth0は必ずmasqueradeに指定してください。
              macAddress: '02:c7:36:00:00:2b' #eth0に指定したいMACアドレスを記載します
              model: virtio
        memory: #仮想マシンにアロケーションするメモリを定義します
          guest: 2Gi
      hostname: vm-example
      networks: #仮想マシンインスタンスのvNICが接続するNWの定義を記載します
        - name: eth0 #eth0という名称のvNICをKubernetesのPod Network(デフォルトのOverlay Network）に参加させます。これは必須の設定です
          pod: {}
      volumes:
        - name: rootdisk #rootdiskとしてDataVolume名を参照しています
          dataVolume:
            name: vm-datavolume
        - name: cloudinitdisk #cloud-init設定内容を実行するディスク
          cloudInitNoCloud: #cloud-initの書式に従って記載された内容を実行します
            secretRef: #cloud-init設定内容をuserdata-secret.yamlに記載しています
              name: userdata-secret
```

では早速、以下の要件を満たすOpenShiftクラスタにて、`application.yaml`を適用してみます。
- OpenShift Virtualizationが利用できる
- OpenShift Data Foundationが利用できる
- OpenShift GitOpsが利用できる

:::note
Red Hatのサポート対象外ではありますが、[こちらの記事](https://qiita.com/masaki-oomura/items/62ae108115d6164f9e42)のROSA（Red Hat OpenShift Service on AWS）で上記が実現可能です。
:::

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/8109770d-33b9-3bd2-f07c-aa2e7524498d.png)

app of appsの親`Application`を貼っつけて「CREATE」をクリック。
しばらく待つと全ての`Application`のステータスが`Healthy`になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/24908318-f9ca-2b90-1099-178b278c9e55.png)

OpenShiftのコンソール画面でDeployされた仮想マシンを確認してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/f5b0dc35-cb3d-7fe1-25e9-5e8d73eabd2a.png)

無事にDeployできました。「リソース」タブからRouteのURLにアクセスしてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/7bf61691-afd5-9392-5514-2d90d6dea7bf.png)

期待通り、Feroda VMがWebサーバとして公開されています。最後にVMにSSHしてみます。

```sh
 ~ % virtctl -n vm-example ssh cloud-user@vm-example --identity-file=~/.ssh/id_rsa
[cloud-user@vm-example ~]$ 
```
SSHで接続できました。

:::note
仮想マシンへのSSHについては[こちら](https://qiita.com/masaki-oomura/items/a1453307d005e4656479#%E3%82%BF%E3%83%BC%E3%83%9F%E3%83%8A%E3%83%AB%E3%81%8B%E3%82%89vm%E3%81%ABssh%E6%8E%A5%E7%B6%9A%E3%81%99%E3%82%8B)を参照ください
:::

# おわりに
この様に、仮想マシンのDeployがGitOpsで実現できるようになりました。今回は`NameSpace`内に単一のVMアプリケーションのみをDeployしましたが、GitOpsを活用できるということは、コンテナアプリケーションも混在してDeployすることが可能になります。KubeVirt / OpenShift Virtualizationによって、アプリケーションがコンテナで作られていても、VMであっても、一体的にリソース管理・Deployし、Immutable Infrastructureを推進できます。