コマンド集 / OpenTofu
OpenTofu コマンド集
OpenTofu で Cloudflare・Technitium・Proxmox のファイアウォールを宣言し、tofu plan を実機との差分検査にする日々の流れ、取り込み、差分の読み方、提供者ごとの罠、状態の扱いを引ける。
iac/ に Cloudflare(DNS・Access アプリ・SSH/RDP の対象・トンネルの ingress)、Technitium(lab.example.jp の A/CNAME)、Proxmox VE のファイアウォール、WARP のマネージドネットワークを .tf で宣言し、tofu plan を「repo と実機の差分検査」として使う環境向け。差分ゼロが正常で、出たら「実機が変わった」か「宣言を変えた」のどちらか。提供者は cloudflare/cloudflare 5.x、bartei/technitium 1.0.x、bpg/proxmox 0.113.x、hashicorp/external。
日々の流れ#
資格情報を環境変数に載せる#
値は 1Password から環境変数に載せるだけで、表示しない。.tf にも state にも秘密を書かない。
export CLOUDFLARE_API_TOKEN="$(op read op://<vault>/<item>/credential)"
export TF_VAR_technitium_api_token="$(op read op://<vault>/<item>/credential)"
export PROXMOX_VE_ENDPOINT="https://<IP>:8006/"
export PROXMOX_VE_API_TOKEN="$(op read op://<vault>/<item>/username)=$(op read op://<vault>/<item>/credential)"
source iac/env.sh
⚠️ PROXMOX_VE_API_TOKEN は user@realm!tokenid=<secret> の形。username 欄と credential 欄を = で繋ぐ。echo $CLOUDFLARE_API_TOKEN のような確認をしない。
初期化・整形・検証#
cd iac
tofu init
tofu fmt -recursive
tofu validate
⚠️ プロバイダを足したり版を上げたりしたら tofu init -upgrade。.terraform.lock.hcl は repo に置き、.terraform/ は置かない。
差分を見る・当てる#
tofu plan
tofu plan -out=plan.tfplan && tofu apply plan.tfplan
tofu apply
⚠️ apply は Cloudflare / Technitium / PVE の FW を実際に書き換える。公開名を増やす変更は、Entra の redirect URI を人が直した後に apply する(先に当てると OIDC が壊れる)。
状態を読む#
一覧と 1 件#
tofu state list
tofu state list | grep cloudflare_zero_trust_access_application
tofu state show cloudflare_zero_trust_tunnel_cloudflared_config.<名前>
⚠️ state show は属性を全部出す。sensitive = true の変数以外はマスクされないので、出力をそのまま貼らない。
全体と出力値#
tofu show
tofu show -json | jq '.values.root_module.resources[] | {address, type}'
tofu output
tofu output -json
取り込み#
実機の一覧から .tf と import 台本を作る#
最初の取り込みは、API で実機の一覧を JSON に落とし、そこから .tf と imports.sh を生成した。一度だけ。 以後は .tf を手で直し、生成台本を二度と回さない。
python3 gen.py
bash imports.sh
bash imports-proxmox.sh
tofu plan
⚠️ 一覧の JSON(_inventory.json)は ID の塊なので repo に置かない。M365 の MX / SPF / DKIM / 検証 TXT は持たない(メールの経路を tofu で壊せる状態にしない)。Workers のルートも持たない(権限が別トークンで、プロバイダは 1 つのトークンしか持てない)。
import の ID 形式(提供者ごと)#
文書に無い形式は試して確かめるしかなかった。
tofu import cloudflare_dns_record.<名前> "<zone_id>/<record_id>"
tofu import cloudflare_zero_trust_access_application.<名前> "accounts/<account_id>/<app_id>"
tofu import cloudflare_zero_trust_access_infrastructure_target.<名前> "<account_id>/<target_id>"
tofu import cloudflare_zero_trust_tunnel_cloudflared_config.<名前> "<account_id>/<tunnel_id>"
tofu import technitium_record.<名前> "lab.example.jp::<FQDN>::A::<IP>"
tofu import proxmox_virtual_environment_cluster_firewall.cluster cluster
tofu import proxmox_virtual_environment_cluster_firewall_security_group.<名前> <グループ名>
tofu import proxmox_virtual_environment_firewall_options.<名前> container/<node>/<ctid>
tofu import proxmox_virtual_environment_firewall_rules.<名前> vm/<node>/<vmid>
⚠️ Access アプリは accounts/ の接頭辞が要るが、infrastructure target とトンネル設定には要らない。PVE の alias(lab = ラボの CIDR)は取り込みの ID 形式が文書に無いので持たず、規則が名前で参照するだけにしている。
import ブロックで設定を生成する#
1 件ずつ手で書くより、import ブロックから生成させるほうが属性の綴りを間違えない。
import {
to = cloudflare_dns_record.example
id = "<zone_id>/<record_id>"
}
tofu plan -generate-config-out=generated.tf
⚠️ 生成された .tf は既定値まで全部書き出す。読める形に削ってから取り込む。
差分の読み方#
取り込み直後の「更新」はプロバイダの正規化#
取り込んだ直後に更新が 29 件出たが、中身は既定値の補完だった。宣言側で明示して消す。
resource "cloudflare_zero_trust_access_application" "example" {
enable_binding_cookie = false
options_preflight_bypass = false
allowed_idps = []
# ...
}
resource "technitium_record" "example" {
overwrite = false # 既存レコードを黙って上書きしない
# ...
}
⚠️ Access の infrastructure 型は policies を id 参照できず、connection_rules 込みで inline に書く。取り込み直後の 1 回だけ apply が要る(中身は同じ)。
期待どおりの差分#
WARP のマネージドネットワークは TLS 証明書の指紋で判定する。指紋は Let’s Encrypt の更新で必ず変わるので、external データソースが openssl で実物を読む。更新後に plan に差分が出るのは正常で、apply で追随する。
data "external" "web_fp" {
program = ["sh", "-c", "printf '{\"sha256\":\"%s\"}' \"$(echo | openssl s_client -connect <IP>:8006 2>/dev/null | openssl x509 -noout -fingerprint -sha256 | cut -d= -f2 | tr -d : | tr A-F a-f)\""]
}
⚠️ hashicorp/tls の tls_certificate は sha1 しか出さない(4.4.1 実測)。
無視したい属性#
実機側で勝手に動く属性は ignore_changes で黙らせる。
lifecycle {
ignore_changes = [comment]
}
⚠️ 黙らせた属性は差分検査から外れる。「差分ゼロ」の意味が変わるので、理由をコメントに残す。
リソース名を変える#
公開名の改名などでリソース名を変えると、素のままでは destroy + create になる。moved で state を追随させる。
moved {
from = cloudflare_dns_record.pve_cname
to = cloudflare_dns_record.proxmox01_cname
}
tofu state mv cloudflare_dns_record.pve_cname cloudflare_dns_record.proxmox01_cname
⚠️ plan に -/+(replace)が出たら止まる。DNS や Access アプリの作り直しは、その瞬間に無認証で晒すか名前が消えるかのどちらか。
提供者ごとの罠#
Cloudflare(v5)#
v5 はリソース名も属性の形も v4 と別物。ネストは config = { ingress = [ ... ] } のようなオブジェクト。
resource "cloudflare_zero_trust_tunnel_cloudflared_config" "main" {
account_id = local.account_id
tunnel_id = "<tunnel_id>"
config = {
ingress = [
{ hostname = "<公開ホスト名>", service = "https://node1.lab.example.jp:8006" },
{ hostname = "<公開ホスト名>", service = "https://localhost:53443", origin_request = { no_tls_verify = true } },
{ service = "http_status:404" },
]
}
}
⚠️ catch-all は末尾。ingress は丸ごと置換なので、1 本消すつもりで全部消さない。provider "cloudflare" {} は CLOUDFLARE_API_TOKEN を読むだけで、トークンは 1 つしか持てない。DNS の Edit をゾーン限定にしたトークンで、Workers のルートは触れない。
Technitium(bartei/technitium)#
OpenTofu のレジストリに無いので Terraform のレジストリを明示する。文書に反して TECHNITIUM_* の環境変数を読まない(1.0.1 実測)。
technitium = { source = "registry.terraform.io/bartei/technitium", version = "~> 1.0" }
variable "technitium_api_token" { type = string, sensitive = true }
provider "technitium" {
server_url = "https://<IP>:53443"
api_token = var.technitium_api_token
skip_tls_verify = true
}
⚠️ 同じ名前に A が複数あるときは値ごとに別リソース。overwrite = false を明示しないと差分が出続ける。Technitium はクラスタで設定を複製するので、片系にだけ当てる段階投入はできない。
Proxmox VE(bpg/proxmox)#
firewall_options の既定が radv = true / ndp = false で、PVE の既定(radv=0 / ndp=1)と逆。
resource "proxmox_virtual_environment_firewall_options" "g<ctid>" {
node_name = "node1"
container_id = <ctid>
enabled = true
input_policy = "ACCEPT"
output_policy = "ACCEPT"
dhcp = false
ipfilter = false
macfilter = true
ndp = true
radv = false
log_level_in = "nolog"
log_level_out = "nolog"
}
⚠️ 黙って apply するとゲストの IPv6 の挙動が変わる。 PVE の既定を全部明示してから当てる。apply で PVE が .fw を書き直し、先頭のコメント行は消える(規則のコメントは残る)。API トークンの役割は FW と監査だけにしておき、VM を持つときに足す。VM/LXC 定義は既定値が多く取り込むと差分が出やすいので、触る必要が出たものから。
状態の保護#
この repo での置き方(正直に)#
terraform.tfstate と terraform.tfstate.backup はプライベートな repo にコミットしている。復旧の媒体が repo だけなので、state を repo の外に置くと復旧できない。秘密はプロバイダの認証を環境変数で渡しているため state に入らない。
コミットする terraform.tfstate / terraform.tfstate.backup / .terraform.lock.hcl / *.tf
無視する .terraform/ / _inventory.json / terraform.tfstate.*.backup
⚠️ state には秘密は無いが、ゾーン ID・アカウント ID・トンネル ID・アプリ ID・内部 IP が全部入っている。repo を公開に変えた瞬間に全部出る。 公開先へ写すときは state と _inventory.json と imports.sh を必ず外す。
手術の前に控えを取る#
state mv / state rm / import の前に、その時点の state を別名で残す。
cp terraform.tfstate "terraform.tfstate.$(date +%F).backup"
tofu state pull > "state-$(date +%F).json"
⚠️ 日付つきの .backup は .gitignore で無視される。残したいなら名前を変えるか、git の履歴に頼る。
困ったとき#
ロックが残った#
ローカル state なので、前の tofu が異常終了したときだけ起きる。他に動いているプロセスが無いことを確かめてから外す。
tofu force-unlock <LOCK_ID>
差分が出た#
「実機が変わった」なら .tf を実機に合わせるか、apply で repo に合わせる。どちらが正しいかは変えた理由で決まるので、先に作業記録の日付を引く。
tofu plan -no-color | grep -E '^\s*[~+-]'
tofu plan -target=cloudflare_dns_record.<名前>
⚠️ -target は「その 1 件だけ」を当てるための道具で、常用しない。残りの差分が見えなくなる。
1 件だけ作り直す・忘れる#
tofu apply -replace=cloudflare_zero_trust_access_application.<名前>
tofu state rm cloudflare_dns_record.<名前>
⚠️ taint は非推奨で -replace を使う。state rm は実機を消さず state から忘れるだけ。もう一度持ちたければ import し直す。
apply の後で公開経路のログインが壊れた#
tofu の差分は Cloudflare 側しか見ていない。Entra の redirect URI・PVE の OIDC レルム・Technitium の SSO の返り先は tofu の外にある。公開名を足す/改名する変更は、先に人がそちらを直してから apply する。