我试着使用terraform-provider-esxi创建了一个虚拟机和网络
首先
我在无偿使用VMware ESXi上尝试使用Terraform创建多个VM并连接网络时,遇到了比我预想的更多困难,所以我将其作为备忘录记录下来。
关于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中定义变量,所以我随便写了一些。我把它格式化为与网络配置相关的样式。我认为可以填写克隆源虚拟机的信息或进行扩展,但这次就这样吧。
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进行操作。
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地址信息放入这里,我们会嵌入分隔符。当对模板进行处理并返回字符串后,可以使用分隔符将其拆分,以便按照主机的顺序使用索引来提取信息,这就是所设的机制。
由于了解到数据也可以使用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]とは限りません。
请注意一些细节。
如果执行过于频繁,似乎会显示以下消息。
我没有意识到它是什么时候开始运作的,也没注意到900秒解消了的时间,但平常它能够运转,但不知道为什么现在不正常。如果你觉得不对劲,请怀疑一下。
最后
Terraform的错误信息非常不友好。即使指定了TF_LOG来获取调试日志,但是大部分都是垃圾信息,关键的信息几乎没有显示出来。只能自己使用所谓的printf调试方法,而且还不能在任意位置显示任意值,非常痛苦。要想知道为什么会出现这个错误消息,以及那时候的值是什么,单单了解这些就已经是一种苦差事。如果有一个调试器的话,肯定会受到非常感激啊,可惜我没有那么多精力去开发。