Terraformで破壊可能なProxmox VMを管理する
この記事はUEC Advent Calendar 2024の16日目の記事です。
昨日はへるくんの「SMFのバイナリを読んでみる」でした。今年のアドカレで結構バイナリを読む記事を見るのですが、流行っているのですかね…?(困惑)
UEC 2 Advent Calendar 2024の方ではかいとからあげさんの「コンビニのフライドチキンおいしいランキング」でした。私は期間限定という言葉に弱いタイプなので、ローソンのやつは是非食べてみたいですね。
はじめに
逸般の誤家庭を運用されている皆さんのような方の多くは、Proxmoxのような仮想化ハイパーバイザを利用しているかと思います。VM上でサービスを運用するメリットとして、簡単に目的の環境だけを破壊したり構築したりできることが挙げられます。私も、Proxmoxを使い始めた時、この点に魅力を感じていました。
しかし、環境が複雑になってきた時にクラスターを間違えて破壊してしまうなどすると、頑張って構築した環境が全破壊されてしまい、再構築が非常にめんどくさいです。これが1OSでまとまっていてくれればインストールスクリプトを書いておけばいいのですが、複数のVMを管理している場合、それも難しいです。
そこで、今回は自宅鯖をIaCで管理する試みを紹介します。
この記事で紹介すること
- TerraformでVMを作成する
- Terragruntでstateを管理する
- CloudinitでVMを初期設定する
- Cloudinitでインストールスクリプトを使う
この記事で紹介しないこと
- Terraformの基本
- ProxmoxのLXCの管理
- Ansible
Terraform
VM管理自動化の第1ステップとして、terraformを使用してVMを作成します。terraformには非公式でproxmox用のproviderが提供されています。
これを使うとterraformのresourceとしてVMを作成できます。
Terragrunt
今回はterraformのコード管理にterragruntを使用します。特に今回受けたいterragruntの恩恵は次の通りです。
- S3のstate管理
- variableの管理
- environmentsの並列管理
特に環境について、terraformで管理する1サービスあたり、1環境という方針で管理します。そういった場合に、terragruntのrun-all機能で全環境を一括で管理できる点が重宝します。
terragruntの設定
以上を踏まえた上で、次のようなディレクトリ構成にします。
.
├── environments
├── modules
└── terragrunt
├── config
│ ├── main.tf.tftpl
│ ├── provider.tf.tftpl
│ ├── terraform.tfvars.tftpl
│ └── variables.tf.tftpl
└── terragrunt.hcl
terragrunt.hcl
の内容は次の通りです。
locals {
region = "ap-northeast-1"
env = basename(path_relative_to_include())
}
generate "provider" {
path = "provider.tf"
if_exists = "overwrite"
contents = file("./config/provider.tf.tftpl")
}
remote_state {
backend = "s3"
generate = {
path = "backend.tf"
if_exists = "overwrite"
}
config = {
bucket = "【S3のbucket名 (作成する必要なし)】"
key = "${local.env}/terraform.tfstate"
region = "${local.region}"
encrypt = true
}
}
generate "variables" {
path = "variables.tf"
if_exists = "overwrite"
contents = "${file("./config/variables.tf.tftpl")}\n${file("./config/${local.env}/variables.tf.tftpl")}"
}
generate "tfvars" {
path = "terraform.tfvars"
if_exists = "overwrite"
contents = "${file("./config/terraform.tfvars.tftpl")}\n${file("./config/${local.env}/terraform.tfvars.tftpl")}"
}
また、config
ディレクトリの配下には、次のような内容を持つファイルを配置します。
-
main.tf.tftpl
- 共通で使いたいterraformのコード
-
provider.tf.tftpl
- 共通で使いたいproviderの設定
-
terraform.tfvars.tftpl
- 共通で使いたいterraformの変数の内容
-
variables.tf.tftpl
- 共通で使いたいterraformの変数の定義
ここに新しい環境を生やしたい場合は、次のようにファイルを追加します。(tailscaleという環境を追加する場合)
environments
└── tailscale
├── cloudinit.tf
├── locals.tf
├── tailscale.sh.tftpl
└── terragrunt.hcl
環境内のterragrunt.hcl
の内容は次の通りです。
include {
path = "../../terragrunt/terragrunt.hcl"
}
加えて、terragrunt/config
ディレクトリには、tailscale
ディレクトリを追加し、次のようなファイルを配置します。
terragrunt/config
└── tailscale
├── terraform.tfvars.tftpl
└── variables.tf.tftpl
それぞれ、各環境で使いたいterraformの変数の内容と定義を記述します。
terragruntの実行
以上の設定を済ました上で、environments
ディレクトリに移動し、次のコマンドを実行します。
terragrunt run-all init
すると、terraform環境の初期化が始まります。具体的には次のような処理が走ります。
- S3バケット作成 (無ければ)
-
terragrunt.hcl
で指定したファイルを環境ディレクトリ内に生成 - 通常のterraformの初期化処理
Cloudinit
ここからresourceを作成していけばVMの作成ができますが、VMを作成して満足する人は少ないと思います。通常はここからサービスのインストールや実行に必要な初期設定を行います。ここからはTerraformを使用して、Cloudinitによる初期設定までを自動化していきます。(本当はAnsibleも使いたいところですが、難しくて諦めました。)
Cloudinitを普通の方法で使うやり方は紹介しないので各自調べてみてください。
今回はalmalinuxのcloudimageからテンプレートVMを作成した状態を前提とします。
proxmox providerでのcloudinitの設定
proxmox providerは標準でcloudinitをサポートしています。cloudinitでOSの初期化設定を記述するための、cloud-configファイルを適用するには、cicustom
にファイルを指定することで可能です。
ただしcloud-configファイルは事前にproxmoxのホストに配置しておく必要があるため、次のようなresourceを作成しておきましょう。
resource "terraform_data" "cloud_init_config_files" {
connection {
type = "ssh"
user = var.pm_ssh_user
password = var.pm_ssh_pass
host = var.pm_host
}
provisioner "file" {
source = local_file.cloudconfig_file.filename
destination = "/var/lib/vz/snippets/${var.cloud_init_user_data}"
}
}
ちなみに私は次のようなcloud-configファイルを使っています。
#cloud-config
package_update: true
package_upgrade: true
hostname: ${hostname}
manage_etc_hosts: true
fqdn: ${hostname}.${domain}
user: ${user}
password: ${password}
ssh_authorized_keys:
${ssh_keys}
packages:
${packages}
chpasswd:
expire: False
timezone: Asia/Tokyo
locale: ja_JP.UTF-8
keyboard:
layout: jp
cloud-configとインストールスクリプトの共存
通常の方法では、cloudinitの設定ファイルとしてcloud-configかインストールスクリプトの片方しか利用できません。しかし、mime/multipartを使うことで、複数の設定ファイルを適用することができます。
external providerでシェル芸をやることでmime/multipartファイルの作成を自動化します。
data "external" "mime_multipart" {
program = [
"bash",
"-c",
"echo {} | jq '. |= .+{\"content\": \"'`write-mime-multipart ${local_file.cloud_init_user_data.filename}:text/cloud-config ${var.script}:text/x-shellscript | base64 -w0`'\"}'"
]
}
resource "local_file" "multipart_file" {
content = base64decode(data.external.mime_multipart.result["content"])
filename = "${path.root}/files/mime_multipart"
}
module化
ここまでProxmox VMにcloudinitを適用するまでの手順を自動化してきましたが、設定項目が非常に多かったかと思います。これをmoduleにまとめておくことで、各環境で設定を再利用できるようになります。
以下のようにmoduleを作成しました。
modules/cloudinit/main.tf
terraform {
required_providers {
external = {
source = "hashicorp/external"
version = "~> 2"
}
http = {
source = "hashicorp/http"
version = "~> 3"
}
}
}
data "http" "ssh_keys" {
url = "https://github.com/${var.github_user}.keys"
}
resource "local_file" "cloud_init_user_data" {
content = templatefile("${coalesce(var.cloud_init_user_path, path.module)}/${var.cloud_init_user_data}.tftpl", {
hostname = var.hostname
domain = var.domain
user = var.user
password = data.external.mkpasswd.result["content"]
ssh_keys = replace(data.http.ssh_keys.response_body, "/(.+)/", " - \"$1\"")
packages = terraform_data.packages.input
})
filename = "${path.root}/files/${var.cloud_init_user_data}"
}
data "external" "mime_multipart" {
program = [
"bash",
"-c",
"echo {} | jq '. |= .+{\"content\": \"'`write-mime-multipart ${local_file.cloud_init_user_data.filename}:text/cloud-config ${try("${var.script}:text/x-shellscript", "")} | base64 -w0`'\"}'"
]
}
data "external" "mkpasswd" {
program = [
"bash",
"-c",
"echo {} | jq '. |= .+{\"content\": \"'`mkpasswd --method=SHA-512 --rounds=50000 ${var.pass}`'\"}'"
]
}
resource "terraform_data" "packages" {
input = replace(join("\n", var.packages), "/(.+)/", " - \"$1\"")
}
resource "local_file" "multipart_file" {
content = base64decode(data.external.mime_multipart.result["content"])
filename = "${path.root}/files/mime_multipart"
}
resource "terraform_data" "cloud_init_config_files" {
connection {
type = "ssh"
user = var.pm_ssh_user
password = var.pm_ssh_pass
host = var.pm_host
}
provisioner "file" {
source = local_file.multipart_file.filename
destination = "/var/lib/vz/snippets/${var.cloud_init_user_data}"
}
}
次のように使用します。
resource "local_file" "install_script" {
content = templatefile("${path.root}/tailscale.sh.tftpl", {
tailscale_key = var.tailscale_key
subnet_routes = local.subnet_routes
})
filename = "${path.root}/files/tailscale.sh"
}
module "cloudinit" {
depends_on = [local_file.install_script]
source = "../../modules/cloudinit"
clone = "template-almalinux"
gateway = "172.24.0.1"
ip = "172.25.0.5/14"
hostname = local.hostname
vmid = 105
vm_name = local.hostname
target_node = "americano"
pm_host = "172.24.8.3"
onboot = true
script = local_file.install_script.filename
}
みなさんの環境に合わせてカスタマイズしてみてください。
おわりに
terraformは設定項目が多くDRYしにくいので、積極的にterragruntやmoduleを活用していきたいですね。
明日はいずりなさんの「パスワード管理の面倒くささをどうにかしましょう」です。楽しみですね。
Discussion