0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

terraform-provider-esxiを使ってVMとネットワークを作ってみた

Last updated at Posted at 2023-09-13

はじめに

無償で使えるVMware ESXiの上にTerraformをつかってVMを複数作り、ネットワークをつなげるというのをやってみたところ、思ってたよりも手こずったので、備忘録として残しておきます。

2023/09/22注記
VMware ESXiのネットワークアダプタの割り当てですが、複数並べたときにネットワークアダプタ1がゲストOSで一番若い番号のNICになるとは限らないことがわかりました。

P2Vの関係なのか、PCIスロットを詰めないように配置するアルゴリズムのようです。ゲストOSではPCIアドレスの若い順にNICが割り当てられるようで、これは不一致になるはずですわ。

よって下記の後半に書かれているIPアドレス割当は、残念ながら想定していたとおりに動作しません。どうやってゲストOSとESXi上のネットワークアダプタとの並びを一致させられるか、今後調査して報告できればと思います。

今回やったこと

  • VMware ESXi上にベースのVMを用意
  • Terraformを使い、ベースVMをcloneして複数VMを作成
  • Terraformでportgroup, vswitchを作成し、VM間を接続
  • cloud-initを使い固定IPアドレスをTerraform経由で設定

Git repo

例示したコードをまとめたリポジトリを作りました。
https://github.com/iMasaruOki/terraform-provider-esxi-demo

Apache-2.0 Licenseにしてみました。ご自由にどうぞ。

今回の実行環境

これでやってみたというだけなので、実際に試される場合に合わせる必要はありません。

  • 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でも同じだと思います。

ベースVMイメージの準備

デプロイするVMは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と古すぎるので、新しいOSのqemu-imgだと指定不要かもしれません。

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

これをつけずに作ったVMDKはディスクタイプが古いらしく、ESXiに持っていってVMにくっつけても起動に失敗します。エラーメッセージが「ディスクタイプ2は云々」と出てくるので訳がわからず、しばらく悩みました。

ベースVMをESXiにデプロイ

作成したdebian12.vmdkをデータストアブラウザを使ってESXi上に放り込みます。そして新規仮想マシンの作成。

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

既存ハードディスクイメージを選択して放り込んだVMDKファイルを指定します。OSの選択肢まだ12がないようなのでDebian11で。

なんかうまくいかないようなら、空のVM作ってOVFエクスポートして、「インポートする」でOVFファイルとdebian12.vmdkを放り込んでみてください。

ベースVMいろいろ調整

ふつうに起動して手動でネットワークなどつないで、cloneした側に引き継いでほしい設定をしていきます。

virt-builderでインストールしたcloud-initですが、VMware経由での初期設定に対応させるにはファイルの編集が必要でした。

/etc/cloud/cloud.cfg
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

掃除が終わったらシャットダウンしておきます。動きっぱなしだとcloneに失敗します。

terraform-provider-esxi

HashiCorp公式から提供されるterraform-provider-vsphereは有償版でのみ提供されるAPIを呼び出します。むしょうばんではつかえませんので、今回はAPIを呼ばずssh経由で操作する設計のため無償版に対応しているterraform-provider-esxiを使います。

公式はこちら
ソースコード(GitHub)はこちら

Terraformで宣言すれば拾ってくるので、git cloneする必要はありません。

必要なもの

terraformovftoolは作業マシンにインストールしておいてください。
VMware ESXiはアプリケーションでなくOS相当のハイパーバイザですので、それなりの容量の物理マシンにインストールすることをおすすめします。とくにディスクはわりと必要です。

基本設定

適当なディレクトリを作って、そこにHCLファイルを置いていきます。

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

などと書いて利用を宣言して、下記のように基本設定を書きます。

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

でもってVMリソースを書けばひとまず完成です。

vm.tf
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

ふつうはplanとかvalidateで書式のチェックをするのですが、applyでもおかしければエラーになるので省いちゃいます。

試した環境では、デプロイに3分くらいかかりました。ESXiのWebUIからコンソールを呼び出して、ログインなどしてみてください。環境によってはNICにIPアドレスが振られるので、すぐ外からログインできるかもしれません。

複数VMを用意する

設計

最終的に、こんなふうにしたいと考えます。

.-------.
| 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 Networkにぶらさげます。他に2つずつNICを用意し、図のようにつなげるよう設定を用意します。

local variablesの定義

Terraformではlocalsの中に変数を定義できますので、てきとーに書いてみます。ネットワーク設定に特化した書式にしました。clone元VMの情報を埋めたりとか拡張できるとは思いますが、今回はこれで。

2023/09/20修正: jinja_template側の処理を変えて、NIC名を何度も書かずに済む書式に変更しました。

locals.tf
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とか)は実際のVMに合わせてください。

上記ではネットワークに名前をつけました。これはportgroupやvswitchを作る際の名前として使う予定です。が、このままループで値を引っ張り出すと重複してしまします。そこで、重複しない集合も作っておきます。

private.tf
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で繰り返しを定義します。一般的なプログラミング言語とはちょっと違うので最初は戸惑うかもしれません。

network.tf
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
}

複数VMの定義

こちらもfor_eachで。NICを複数生やすところはdynamicを使います。

2023/09/20修正: dynamic network_interfacesfor_eachでまわすときもオーダーを気にする必要があったので修正しました。

vm.tf
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アドレス割当

resource "esxi_guest"guestinfoを設定することで、TerraformからVMに向かってcloud-init用初期設定を流し込むことができます。おおよそ下記のような感じです。外部ファイルも用意できますがVMの数が増えると管理が大変なので、ここでは直接テキストを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から持ってくるには工夫が必要でした。なぜならNICの本数が一定ではなかったからです。

紆余曲折は省いて、どうやって実現したかだけ書きます。

terraform-provider-jinjaのdatasource jinja_templateを使う

どうにかできないかあれこれ探していたのですが、Jinja2テンプレートを展開するproviderがあったので使ってみることにしました。

※追記: --String Templatestemplatefile()を使うほうが素直かもしれません。あとで書き直してみます。--機能が足りなくてうまく書けませんでした。

公式はこちら
ソースコード(GitHub)はこちら

terraform コンテキストに追加し、providerを宣言することで使えるようになります。

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

provider "jinja" {}

jinja2テンプレートを用意する

1ファイル用意します。最初はheredocを使いインラインで書こうと思ったのですが、Terraformが勝手に解釈してエラーでご機嫌斜めになるのでやむなく分離しました。

ここに全ホストの全NICのIPアドレス情報を押し込めるため、デリミタを埋め込みます。テンプレート処理されて戻ってくる文字列をデリミタでsplitすると、ホストの並び順になるのでindexを使って取り出せるという仕掛けです。

2023/09/20追記・修正
dataでもfor_eachが使えることがわかったので、うまくホストごとの文字列を生成するように書き換えました。全VMが同じOSだとインタフェース名の付き方も同じなので、いちいちデータに書くのがしんどいので共通化します。

metadata.j2
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データを作る

テンプレートと入力データを指定します。

metadata.tf
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であるため、単純にfor`を回すと順序が記述どおりとならないため、アドレスを付与したいNICと実際に付与されるNICの不一致が発生していまいます。これを防ぐために順女性が保証されるリストを渡して記述順に処理されるようにしました。

こうしておくと、他リソースからdata.jinja_template.metadata[each.key].resultと記述して文字列をとりだせます。

guestinfo.metadataで結果を取り込む

他の要素は省きます。

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

デプロイする

完成したらterraform apply してみます。

terraform apply

VMの数が多いとその分時間がかかります。のんびり待ちましょう。

設定状況の確認

ためしにVMのコンソールを開いてログインしてみます。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に渡したはずのデータが反映されない!

VM側で確認できます。ホスト名が設定されていなければ、反映されていません。

渡される情報は下記で確認できます。

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

おそらくここがちゃんとできていないのが問題です。

  • vmware-rpctoolが入ってなければクローン元のベースVMにopen-vm-toolsを入れましょう。
  • 空っぽの場合 /etc/cloud/cloud.cfgdatastore_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デバッグをやるしかなくて、しかも任意の場所で任意の値を表示することもできないので苦痛が大きいです。なぜそのエラーメッセージなのか、そのときそこの値は何なのか、それを把握するだけで一苦労。デバッガでも作ったらめちゃくちゃ感謝されそうだなあと思いつつ、そんな気力はわかないのでした。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?