我试着使用terraform-provider-esxi创建了一个虚拟机和网络

首先

我在无偿使用VMware ESXi上尝试使用Terraform创建多个VM并连接网络时,遇到了比我预想的更多困难,所以我将其作为备忘录记录下来。

2023年9月22日的备注
关于VMware ESXi的网络适配器分配问题,我们发现当多个适配器排列在一起时,并不保证网络适配器1会成为客户操作系统中编号最小的网卡。
这可能与P2V有关,或者是一种算法,用于避免填充PCI插槽。在客户操作系统中,网卡被分配的顺序是根据PCI地址的年轻程度,这将导致不匹配。
因此,下面提到的IP地址分配,在我们所预期的情况下无法按预期运行。我们希望能研究如何使客户操作系统和ESXi上的网络适配器排序一致,并在未来进行报告。

本次所做的事情

    • VMware ESXi上にベースのVMを用意

 

    • Terraformを使い、ベースVMをcloneして複数VMを作成

 

    • Terraformでportgroup, vswitchを作成し、VM間を接続

 

    cloud-initを使い固定IPアドレスをTerraform経由で設定

Git仓库

我创建了一个整理了示例代码的存储库。
https://github.com/iMasaruOki/terraform-provider-esxi-demo

我已将其更改为Apache-2.0许可证。请随意使用。

这次的执行环境

只是尝试了一下,所以不需要根据实际试验情况进行调整。

    • Terraform実行環境: Ubuntu 18.04LTS

 

    • VMware ESXi環境

Xeon Silver 4110 CPU @ 2.10GHzマシン (メモリ96GB, SSD240GB)
VMware ESXi 7.0U3

由於環境限制,我在Ubuntu 18.04LTS上進行了測試,但我認為在20.04LTS或22.04LTS上也會得到相同的結果。

准备基础虚拟机映像。

我会选择以下方式在中文中转述: 咱们尝试部署一个Debian 12的虚拟机。首先,使用virt-builder创建一个qcow2镜像。在此之前,请确保提前安装libguestfs-tools。

USERNAME=debian
PASSWORD=debian
sudo virt-builder debian-12 \
  --format qcow2 \
  --hostname debian \
  --root-password password:tekito \
  --install sudo \
  --install open-vm-tools \
 --install cloud-init \
  --install netplan \
  --install sysctl \
  --run-command 'useradd -m -G sudo,operator -s /bin/bash ${USERNAME}' \
  --run-command 'chage -M -1 ${USERNAME}' \
  --run-command 'echo ${USERNAME}:${PASSWORD} | chpasswd' \
  --edit '/etc/network/interfaces: s/ens3/enp192/' \
  --firstboot-command 'env DEBIAN_FRONTEND=noninteractive dpkg-reconfigure openssh-server' \
  -o debian12.qcow2
chown $USER.$GID debian12.qcow2

请把制作好的debian12.qcow2转换成VMDK格式,但在VMware ESXi中使用时需要做一点调整。可能是因为我的工作机是Ubuntu 18.04LTS版本太旧了,如果使用最新版本的qemu-img命令可能不需要特别指定参数。

qemu-img convert \
  -o adapter_type=lsilogic,subformat=streamOptimized,compat6 \
  -O vmdk \
  debian12.qcow2 debian12.vmdk

没使用这个选项创建的VMDK似乎是旧的磁盘类型,在将其传送到ESXi并附加到虚拟机时无法启动。由于出现了“磁盘类型2是某某”的错误消息,我们不知道该如何翻译,因此苦恼了一段时间。

将基础虚拟机部署到ESXi上。

使用数据存储浏览器将创建的debian12.vmdk文件导入到ESXi上。然后创建新的虚拟机。

    • 名前: debian12

OS: Linux – Debian11(64bit)
VCPU数やメモリは適宜
既存ハードディスク (debian12.vmdk) を指定

选择现有的硬盘映像并指定放入的VMDK文件。由于操作系统选项中还没有12,所以选择Debian11。

如果出现问题,请创建一个空的虚拟机并导出为OVF格式,然后尝试通过”导入”功能将OVF文件和debian12.vmdk文件导入。

调整了各种基础虚拟机设置。

正常启动后,手动连接到网络,并在克隆的一方设置所需的配置。

使用virt-builder安装的cloud-init,需要对文件进行编辑以支持通过VMware进行初始设置。

datasource_list: [VMware]

请将其写在某个地方,也许通过virt-builder的参数来编辑文件会更方便。

当准备工作完成后,请进行清洁。特别是/etc/network/interfaces.d/*,如果保留不变,将会跳过网络设置,因此请不要忘记删除。

sudo rm /etc/network/interfaces.d/50-clout-init
sudo rm -r /var/lib/cloud
sudo cloud-init clean --logs --machine-id

清理完成后,我会关机。如果一直运行,会导致克隆失败。

terraform-provider-esxi –> Terraform ESXi 供应商

HashiCorp公司提供的terraform-provider-vsphere只能通过付费版调用API。因为无法使用免费版,所以本次将使用支持免费版的terraform-provider-esxi,通过SSH方式进行操作设计。

公式在这里。GitHub上的源代码在这里。

只需在Terraform中声明,它会自动获取,无需进行git clone。

所需之物

    • terraform

 

    • ovftool

 

    VMware ESXi

请在工作机器上安装Terraform和OVF工具。
由于VMware ESXi是一个类似操作系统的超级管理程序,建议将其安装在容量适当的物理机上,而不是在应用程序中。特别是磁盘空间需要相对较多。

基本设置

创建一个合适的目录,将HCL文件放置在其中。

terraform {
  required_version = ">= 0.13"
  required_providers {
    esxi = {
      source  = "josenk/esxi"
      version = ">= 1.8.0"
    }
}

写下诸如此类的声明并声明使用,然后按照以下基本设置写下。

provider "esxi" {
    esxi_hostname = "ホスト名かIPアドレス"
    esxi_username = "操作権限のあるアカウント"
    esxi_password = "パスワード"
}

只需要在VM资源中写入,基本上就完成了。

resource "esxi_guest" "vm" {
  clone_from_vm = "debian12"
  guest_name = "debian12-clone"
  power = "on"
  disk_store = "datastore1"
  network_interfaces {
    virtual_network = "VM Network"
  }
}

请根据环境的需要适当更改数据存储。

ESXi的准备工作

在ESXi的一端,我们需要提前启用SSH。

部署

terraform init
terraform apply

通常情况下,我们会使用计划或验证来检查格式,但如果应用也有问题,就会发生错误,我们会忽略它。

在我试用的环境中,部署需要大约3分钟时间。您可以通过ESXi的WebUI调用控制台,尝试登录等操作。根据环境而定,可能会为网络适配器分配IP地址,这样您就可以立即从外部登录。

准备多个虚拟机

设计

我最终希望以这样的方式来思考。

.-------.
| HostA |10.0.0.1/8----.
`-------'              |
    |192.168.0.1/24    |管理用NW
    |                  |-------(VM Network)
    |192.168.0.2/24    |
.-------.              |
| HostB |10.0.0.2/8----+
`-------'              |
    |172.21.0.2/24     |
    |                  |
    |172.21.0.1/24     |
.-------.              |
| HostC |10.0.0.3/8---'
`-------'   

目前有一個NIC,這是用於管理並連接到VM網路。另外還需準備兩個NIC,並進行設定以連接到圖上所示的方式。

定义局部变量

在Terraform中,可以在locals中定义变量,所以我随便写了一些。我把它格式化为与网络配置相关的样式。我认为可以填写克隆源虚拟机的信息或进行扩展,但这次就这样吧。

2023/09/20更新:通过更改jinja_template的处理方式,我改变了格式,不需要多次写入NIC名称。
locals {
  eth_prefix = "ens"
  eth_start_num = 35
  hosts = {
    "HostA" = {
      "VM Network" = [ "10.0.0.1/8" ],
      "HostAB"     = [ "192.168.0.1/24" ]
    },
    "HostB" = {
      "VM Network" = [ "10.0.0.2/8" ],
      "HostAB"     = ["192.168.0.2/24" ],
      "HostBC"     = [ "172.21.0.2/24" ]
    },
    "HostC" = {
      "VM Network" = [ "10.0.0.3/8" ],
      "HostBC"     = [ "172.21.0.1/24" ]
    }
  }
}

请根据实际的虚拟机情况调整 NIC 名称(例如 ens35)。

在上述中,我們已經為網絡命名了一個名字。這個名字將在創建portgroup或vswitch時使用。但如果在循環中繼續提取值,就會出現重複。因此,我們還需要創建一個不重複的集合。

locals {
  private_network = [for nic in disinct(flatten([
    for host in values(local.hosts) : keys(host)
  ])): nic if nic != "VM Network"]
}

大致的解释是:

    • 内側のforでホストごとのネットワーク一覧からネットワーク名だけを取り出し

 

    • 構造化されてるのでフラットにして重複を取り除いて

 

    そこから`”VM Network”を取り除く

这就是这样。

出于说明的目的,我们将其分开了,但是如果您把它与locals.tf混合在一起也可以。

网络的定义

我会创建portgroup和vswitch。我会通过for_each循环来定义重复操作。这可能与常见的编程语言有点不同,所以开始时可能会感到困惑。

resource "esxi_vswitch" "vswitch" {
  for_each = toset(local.private_network)
  name = etch.key
}

resource "esxi_portgroup" "portgroup" {
  for_each = toset(local.private_network)
  name = each.key
  vswitch = each.key
}

多个虚拟机的定义

请使用for_each来完成这一部分。将网卡多次生成的地方使用dynamic进行操作。

2023/09/20修正:在使用for_each循环遍历dynamic network_interfaces时,也需要关注顺序,所以进行了修正。
resource "esxi_guest" "vm" {
  for_each = local.hosts
  clone_from_vm = "debian12"
  guest_name = each.key
  power = "on"
  disk_store = "datastore1"
  dynamic network_interfaces {
    for_each = toset(keys(each.value))
    content {
      virtual_network = network_interfaces.key
  }
}

部署

最好运行tellaform validate来确认没有任何写作错误。并且,为了避免每次都需要输入yes之类的操作,可以考虑添加参数。如果需要多次执行,可以从历史记录中调用,所以一开始可以接受较长的操作时间。

terraform apply -auto-approve

有时会等待几分钟。可能是由于portgroup和vswitch的依赖关系处理不够好,在创建portgroup时可能会出现错误。不要慌乱,不要惊慌失措,再执行一次就会成功。这可能也与ESXi服务器的能力有关。

分配 IP 地址

在这之前,我做得还算顺利,但在尝试分配IP地址时遇到了困难。

使用cloud-init进行IP地址分配

通过在资源“esxi_guest”中设置“guestinfo”,可以从Terraform向虚拟机传递用于cloud-init的初始配置。大致如下所示。尽管可以准备外部文件,但是当虚拟机数量增加时,管理变得非常困难,所以此处直接使用heredoc嵌入文本。

  guestinfo = {
    "userdata" ~ base64gzip("#cloud-config\n")
    "userdata.encoding" = "gzip+base64"
    "metadata" = <<EOT
instance-id: HostA
local-hostname: HostA
network:
  version: 2
  ethernets:
    ens36:
      addresses:
        - 192.168.0.2/24
EOT
    "metadata.encoding" = "gzip+base64"
  }

将此直接指定的文档进行冗长的书写即可解决问题,但是要从上方定义的local.hosts中获取需要一些技巧。因为网卡数量并不是固定的。

不写弯弯绕的事情,只写怎么实现的。

使用terraform-provider-jinja的数据源jinja_template

我正在寻找各种方法来解决这个问题,然后我找到了一个可以展开Jinja2模板的提供商,决定尝试使用它。

由于功能不足,无法完美书写,可能使用 “String Templates” 或 “templatefile()” 更直接些。稍后尝试修改。

这里是公式。
这里是源代码(GitHub)。

通过将其添加到terraform配置文件的上下文中,并声明提供者,可以使其可用。

terraform {
  required_version = ">= 0.13"
  required_providers {
    esxi = {
      source  = "josenk/esxi"
      version = ">= 1.8.0"
    }
    jinja = {
      source  = "NikolaLohinski/jinja"
      version = ">= 1.17.0"
    }
}
provider "esxi" {
    esxi_hostname = "ホスト名かIPアドレス"
    esxi_username = "操作権限のあるアカウント"
    esxi_password = "パスワード"
}

provider "jinja" {}

准备一个jinja2模板

我会准备一个文件。最初我考虑使用heredoc来内嵌写入,但是由于Terraform会自动解释并出现错误,所以我不得不分开来做。

为了将所有主机的所有网卡的IP地址信息放入这里,我们会嵌入分隔符。当对模板进行处理并返回字符串后,可以使用分隔符将其拆分,以便按照主机的顺序使用索引来提取信息,这就是所设的机制。

2023/09/20补记·修正
由于了解到数据也可以使用for_each,所以我进行了修改,以便能够生成每个主机的字符串。当所有VM具有相同的操作系统时,接口名称的使用方式也相同,所以为了避免每次都写入数据变得麻烦,我将其通用化。
instance-id: {{ host }}
local-hostname: {{ host }}
network:
  version: 2
  ethernets:
{%   set namespace(ns, count=0)
{%   for net in nets %}
    {{ eth_prefix }}{{ eth_start_num + loop.index - 1 }};
      addresses:
{%     for address in addressess[net] %}
        - {{ address }}
{%     endfor %}
{%   endfor %}
{% endfor %}

创建jinja_template数据

请指定模板和输入数据。

data "jinja_template" "metadata" {
  for_each = local.hosts
  template = "./metadata.j2"
  context {
    type = "json"
    data = format(<<EOT
{
  "eth_prefix": "%s",
  "eth_start_num": %d,
  "host":  "%s",
  "nets": %v,
  "addresses\":%v
}
EOT
,
    local.eth_prefix,
    local.eth_start_num,
    each.key,
    keys(each.value),
    each.value)
  }
}

我正在传递每个值为keys(each.value)的值。由于each.value本身是一个map,所以简单地循环无法按照指定的顺序进行,这导致了希望添加地址的NIC和实际添加的NIC不匹配的问题。为了防止这种情况发生,我传递了一个保证顺序的列表,以便按照指定的顺序进行处理。

这样做,您就可以从其他资源中提取字符串,通过data.jinja_template.metadata[each.key].result的方式进行描述。

使用guestinfo.metadata获取结果

我只会主要的元素。

  guestinfo = {
    metadata = data.jinja_template.metadata[each.key].result
  }

部署

完成后我会尝试执行terraform apply。

terraform apply

如果虚拟机数量多的话,所需时间就会相应增加。让我们耐心等待。

检查设定情况

我试着打开虚拟机的控制台并尝试登录。确认一下是否有分配到IP地址。

Debian GNU/Linux 12 HostA tty1
HostA login: debian
Password:
Linux nrm-controller 6.1.0-9-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.27-1 (2023-05-08) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Wed Sep 13 12:58:27 2023
debian@HostA:~$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: ens35: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 00:0c:29:98:45:e1 brd ff:ff:ff:ff:ff:ff
    altname enp2s3
    inet 10.0.0.1/8 brd 10.207.9.255 scope global ens35
       valid_lft forever preferred_lft forever
    inet6 fe80::20c:29ff:fe98:45e1/64 scope link 
       valid_lft forever preferred_lft forever
3: ens36: <BROADCAST,MULTICAST,UP,LOWER UP> mtu 1500 qdisc fq_codel state DOWN group default qlen 1000
    link/ether 00:0c:29:98:45:eb brd ff:ff:ff:ff:ff:ff
    altname enp2s4
    inet 192.168.0.1/24 brd 100.64.0.255 scope global ens36
       valid_lft forever preferred_lft forever
debian@HostA:~$ 

一切平安无事。喜庆喜庆。

故障排除

cloud-init没有正确反映传递的数据!

你可以在虚拟机端确认。如果没有设置主机名,就不会反映出来。

您可以在下方找到验证的信息。

vmware-rpctool "info-get guestinfo.metadata" | base64 --decode | gzip -d

可能是这里做得不够好才是问题所在。

vmware-rpctoolが入ってなければクローン元のベースVMにopen-vm-toolsを入れましょう。
空っぽの場合 /etc/cloud/cloud.cfg の datastore_list の設定を再確認。[VMware]になってますか?

Network:のversionが2の場合、それ以下のデータは何も考えずnetplanに渡されます。netplan入ってますか?
想定通りの値になってますか?

/etc/network/interfaces.dにゴミが残ってたりしませんか?
念のため vmware rpctool “info-get guestinfo.metadata.encoding”も確認を。

/var/log/cloud-init.logや/var/log/cloud-init-out.log でエラー箇所を確認します。[ERROR]とは限りません。

请注意一些细节。

如果执行过于频繁,似乎会显示以下消息。

Screenshot from 2023-09-14 04-39-25.png

我没有意识到它是什么时候开始运作的,也没注意到900秒解消了的时间,但平常它能够运转,但不知道为什么现在不正常。如果你觉得不对劲,请怀疑一下。

最后

Terraform的错误信息非常不友好。即使指定了TF_LOG来获取调试日志,但是大部分都是垃圾信息,关键的信息几乎没有显示出来。只能自己使用所谓的printf调试方法,而且还不能在任意位置显示任意值,非常痛苦。要想知道为什么会出现这个错误消息,以及那时候的值是什么,单单了解这些就已经是一种苦差事。如果有一个调试器的话,肯定会受到非常感激啊,可惜我没有那么多精力去开发。

广告
将在 10 秒后关闭
bannerAds