Proxmox VEとTerraformでインターン生に仮想マシンを払い出す話

## はじめに

インターンシップを素晴らしい企業体験にするためには、課題に対して必要なリソースを素早く学生に提供する必要がありますよね。  
すぐにでも課題の開発を始めたいのに、渡したパソコンにVirtualBoxを入れて仮想マシンにOSをインストールしてもらって...なんてことをやってたら、ただでさえ限られた時間がどんどん減ってしまいます。

今回はProxmox VEとcloud-initを組み合わせて必要なリソースのVMを素早く提供する方法について、CLIとTerraformを使った2パターンをご紹介します。  
Proxmox VE自体については https://pve.proxmox.com/wiki/Main_Page を参照ください。

## 用意するもの

- Proxmox VE 7.2がインストールされたマシン

以降は、Proxmox VEで仮想マシンが起動できる状態になったことを前提に進行します。  
この記事では `7.2-14` 以降をベースに記載しています。

## 仮想マシンのテンプレートの準備

まずは、ベースとなる仮想マシンイメージを作成しましょう。  
今回は Ubuntu-22.04 のクラウドイメージを使用します。

### クラウドイメージの取得

```
wget https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img -O ubuntu-22.04-server-cloudimg-amd64.img
```

### ベースとなる仮想マシンの作成とテンプレート化

```
# Base VM configuration
qm create 9100 --net0 virtio,bridge=vmbr0
qm importdisk 9100 ubuntu-22.04-server-cloudimg-amd64.img local-lvm
qm set 9100 --name ubuntu-22.04a
qm set 9100 --scsihw virtio-scsi-pci --virtio0 local-lvm:vm-9100-disk-0
qm set 9100 --boot order=virtio0
qm set 9100 --ide2 local-lvm:cloudinit
qm set 9100 --nameserver 192.168.0.1 --searchdomain example.com
# Convert VM to VM Template
qm template 9100
```

ネットワーク設定は、管理者が把握しているという前提で、以下のように設定します。

* 接続先bridge: vmbr0
* subnet: 192.168.0.0/24
* gateway: 192.168.0.1
* nameserver: 192.168.0.1
* domain: example.com

ここまでで、ベースのVM TemplateがProxmox VE上に作成されました。

## 提供の作業フロー

それでは 4 vCPU/8GB Memory/120GB Disk の仮想マシンが要望されたと仮定して、それを提供してみましょう。  
作業フローは以下の通りです。

1. 利用者は以下の情報を管理者に知らせる
    * 必要な仮想マシンのサイズ（今回は 4 vCPU/8GB Memory/120GB Disk）
    * ログイン用のユーザー名(sample01)と公開鍵(sample01-rsakey.pub)
2. 管理者は受け取った情報から仮想マシンを作る
    * IPアドレスは管理者が把握しているという前提で `192.168.0.11` に設定します。
3. 管理者は仮想マシン作成後、利用者にアクセス先のIPアドレスを通知する
4. 完了

## 仮想マシンの提供（CLIを使うケース）

作成したテンプレートをクローンして、構成を変更して起動します。  
IPアドレスやログイン情報をcloud-initで設定できるところが非常に便利です。

```console
qm clone 9100 1001 --name sample01 --full --storage local-lvm
qm set 1001 --cores 4
qm set 1001 --memory 8192
qm resize 1001 virtio0 120G
qm set 1001 --ipconfig0 ip=192.168.0.11/24,gw=192.168.0.1
qm set 1001 --ciuser sample01 --sshkeys sample01-rsakey.pub
qm start 1001
```

あとは、使用者にIPアドレスを伝えて完了です。  
これで必要なリソースを持った仮想マシンを提供できました。

これなら、管理者は使用者のパスワードを知る必要も、ログイン後に初期パスワードの変更を依頼する必要もありませんね。

## 仮想マシンの提供（Terraformを使うケース）

Proxmox VEではTerraformを使うこともできます。  
早速 [Proxmox Provider](https://registry.terraform.io/providers/Telmate/proxmox/latest/docs) を使ってCLIと同じようなことができるか試してみましょう。

今回は管理者が実行するので、かなり緩めの設定をしていきます。  
細かくリソースプールや権限を設定して、管理者以外のユーザーが直接Terraformを使えるようにしても良いでしょう。

### APIトークンの取得

root権限をそのまま継承( `--privsep 0` )したAPIトークンを作成します。

```
# pvesh create /access/users/root@pam/token/qiita-sample --privsep 0
┌──────────────┬──────────────────────────────────────┐
│ key          │ value                                │
╞══════════════╪══════════════════════════════════════╡
│ full-tokenid │ root@pam!qiita-sample                │
├──────────────┼──────────────────────────────────────┤
│ info         │ {"privsep":"0"}                      │
├──────────────┼──────────────────────────────────────┤
│ value        │ d2f33ee3-bf19-4155-bd7c-e23a3f14522d │
└──────────────┴──────────────────────────────────────┘
```

このトークンを使用することで、root権限で好きなだけProxmox VEのAPIを実行できます。

### Terraformの記述

使用したTerraformのバージョンです。

```
$ terraform -version
Terraform v1.3.9
on linux_amd64
+ provider registry.terraform.io/telmate/proxmox v2.9.11
```

Terraformの記述例です。

```tf
variable username {}
variable public_key {}
variable cores {}
variable memory {}
variable disk_size {}
variable ip_address {}

terraform {
  required_providers {
    proxmox = {
      source  = "telmate/proxmox"
      version = "2.9.11"
    }
  }
}

provider "proxmox" {
  pm_api_token_id = "root@pam!qiita-sample"
  pm_api_token_secret = "d2f33ee3-bf19-4155-bd7c-e23a3f14522d"
  pm_api_url = "https://pve-sample:8006/api2/json"
  pm_tls_insecure = true
}

resource "proxmox_vm_qemu" "intern-vm" {
  name = "intern-vm-${var.username}"
  target_node = "pve-c01"
  clone = "ubuntu-22.04a"
  os_type = "cloud-init"
  boot = "order=virtio0"
  cores   = "${var.cores}"
  memory  = "${var.memory}"
  disk {
    storage = "local-lvm"
    type = "virtio"
    size = "${var.disk_size}G"
  }
  network {
    model = "virtio"
    bridge = "vmbr0"
    firewall = false
  }
  ipconfig0 = "ip=${var.ip_address}/24,gw=192.168.0.1"
  ciuser = "${var.username}"
  sshkeys = <<EOF
${var.public_key}
EOF

}
```

なお最新のTerraform providerは `2.9.13` ですが、API Token周りにバグがある [terraform-provider-proxmox/issues/703](https://github.com/Telmate/terraform-provider-proxmox/issues/703) ようなので `2.9.11` を使用しています。

作成したいVMの情報を記載した変数ファイルを作成します。

```
$ cat sample01.tfvars 
username = "sample01"
public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHZYaK+tKWpOVCxhKeTeoKLW/UoAOaj03QXPGrqY2JQm user@sample"
cores = 4
memory = 8192
disk_size = 120
ip_address = "192.168.0.11"
```

### Terraformの実行例

実行例です。

```
$ terraform apply -var-file sample01.tfvars

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # proxmox_vm_qemu.intern-vm will be created
  + resource "proxmox_vm_qemu" "intern-vm" {
      + additional_wait           = 0
      + agent                     = 0
      + automatic_reboot          = true
      + balloon                   = 0
      + bios                      = "seabios"
      + boot                      = "order=virtio0"
      + bootdisk                  = (known after apply)
      + ciuser                    = "sample01"
      + clone                     = "ubuntu-22.04a"
      + clone_wait                = 0
      + cores                     = 4
      + cpu                       = "host"
      + default_ipv4_address      = (known after apply)
      + define_connection_info    = true
      + force_create              = false
      + full_clone                = true
      + guest_agent_ready_timeout = 100
      + hotplug                   = "network,disk,usb"
      + id                        = (known after apply)
      + ipconfig0                 = "ip=192.168.0.11/24,gw=192.168.0.1"
      + kvm                       = true
      + memory                    = 8192
      + name                      = "intern-vm-sample01"
      + nameserver                = (known after apply)
      + numa                      = false
      + onboot                    = false
      + oncreate                  = true
      + os_type                   = "cloud-init"
      + preprovision              = true
      + reboot_required           = (known after apply)
      + scsihw                    = (known after apply)
      + searchdomain              = (known after apply)
      + sockets                   = 1
      + ssh_host                  = (known after apply)
      + ssh_port                  = (known after apply)
      + sshkeys                   = <<-EOT
            ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHZYaK+tKWpOVCxhKeTeoKLW/UoAOaj03QXPGrqY2JQm user@sample
        EOT
      + tablet                    = true
      + target_node               = "pve-c01"
      + unused_disk               = (known after apply)
      + vcpus                     = 0
      + vlan                      = -1
      + vmid                      = (known after apply)

      + disk {
          + backup             = 0
          + cache              = "none"
          + file               = (known after apply)
          + format             = (known after apply)
          + iops               = 0
          + iops_max           = 0
          + iops_max_length    = 0
          + iops_rd            = 0
          + iops_rd_max        = 0
          + iops_rd_max_length = 0
          + iops_wr            = 0
          + iops_wr_max        = 0
          + iops_wr_max_length = 0
          + iothread           = 0
          + mbps               = 0
          + mbps_rd            = 0
          + mbps_rd_max        = 0
          + mbps_wr            = 0
          + mbps_wr_max        = 0
          + media              = (known after apply)
          + replicate          = 0
          + size               = "120G"
          + slot               = (known after apply)
          + ssd                = 0
          + storage            = "local-lvm"
          + storage_type       = (known after apply)
          + type               = "virtio"
          + volume             = (known after apply)
        }

      + network {
          + bridge    = "vmbr0"
          + firewall  = false
          + link_down = false
          + macaddr   = (known after apply)
          + model     = "virtio"
          + queues    = (known after apply)
          + rate      = (known after apply)
          + tag       = -1
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

proxmox_vm_qemu.intern-vm: Creating...
proxmox_vm_qemu.intern-vm: Still creating... [10s elapsed]
proxmox_vm_qemu.intern-vm: Still creating... [20s elapsed]
proxmox_vm_qemu.intern-vm: Still creating... [30s elapsed]
proxmox_vm_qemu.intern-vm: Still creating... [40s elapsed]
proxmox_vm_qemu.intern-vm: Still creating... [50s elapsed]
proxmox_vm_qemu.intern-vm: Still creating... [1m0s elapsed]
proxmox_vm_qemu.intern-vm: Creation complete after 1m6s [id=pve-c01/qemu/103]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
```

もちろん、削除も `terraform destroy -var-file sample01.tfvars` で簡単です。

## おしまい

今回はProxmox VEとcloud-initにTerraformを組み合わせてVMを作る方法を書いてみました。  
大抵のことはパブリッククラウドサービスで何とかなるとはいえ、手元の機材を使いたいケースもありますので、上手く使ってみてください。