はじめに
terraform でリソース管理している中で、「特定の HCL ファイル内の一部の variable だけ具体的なリテラルで置き換えたい…」と思うことがありました。
そこで HCL ファイル中の変数をリテラルで置き換えるツールを作ってみました。
ツールを作成するに当たっては、 hashicorp 公式のライブラリである、 github.com/hashicorp/hcl/v2
を使ったのですが、
- HCL の構成要素の具体的な例や関係性がぱっとわからなかった
- HCL の変数の書き換えに関する記事が見つけられなかった
というつらみがあったため、備忘録も兼ねて記事にすることにしました。
本記事では、HCL の構成要素について、自分なりの解釈を具体例を交えた解説を行い、それを踏まえて HCL の変数をリテラルに書き換えた方法について説明します。
使用したライブラリ
ツールの作成に当たり、使用したライブラリは以下のとおりです。
-
github.com/hashicorp/hcl/v2
: v2.18.0github.com/hashicorp/hcl/v2/hclwrite
github.com/hashicorp/hcl/v2/hclsyntax
-
github.com/zclconf/go-cty/cty
: v1.14.1
hclwrite は HCL の組み立てを行うライブラリで、hclsyntax は HCL の構成要素をプログラムで扱いやすい形で提供するライブラリです。
HCL の書き換えだけなら hclwrite だけで完結できるのですが、今回は既存の変数を加工したかったため、hclsyntax を使用しました。
cty は hcl のリテラル値を扱う際に使用されます。
HCL の構成要素
まずは、ライブラリが HCL をどのような構成要素に分解しているのかについて説明します。
hclwrite, hclsyntax では、HCL の構成要素を同じ名前で定義しています。
ここでは、それぞれの構成要素について、以下の HCL を例に説明します。
resource "google_service_account" "default" {
account_id = "my-custom-sa"
display_name = "Custom SA for VM Instance"
}
resource "google_compute_instance" "default" {
name = "my-instance"
machine_type = "n2-standard-${var.cpus}"
zone = "us-central1-a"
tags = ["foo", "bar"]
boot_disk {
initialize_params {
image = "debian-cloud/debian-11"
labels = {
my_label = "value"
}
}
}
metadata_startup_script = file(var.startup_script_path)
service_account {
# Google recommends custom service accounts that have cloud-platform scope and permissions granted via IAM Roles.
email = google_service_account.default.email
scopes = ["cloud-platform"]
}
}
File
その名の通り、1つの HCL ファイルです。
HCL の読み書きをする際に使用するため、HCL の操作を行う場合は、このインスタンスを起点として操作を行うことになります。
例えば 既存の HCL ファイルを読み込むには、次のようにして File インスタンスを生成します。
hclwrite
import (
"os"
"github.com/hashicorp/hcl/v2/hclwrite"
)
path := "hoge.hcl"
content, err := os.ReadFile(path)
writeFile := hclwrite.ParseConfig(content, path, hcl.Pos{}) // hclwrite.File
hclsyntax
import (
"os"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
path := "hoge.hcl"
content, err := os.ReadFile(path)
syntaxFile := hclsyntax.ParseConfig(content, path, hcl.Pos{}) // hcl.File
同じように読み込むことができますが、hclsyntaxの場合は、hclsyntax.File
ではなく、hcl.File
として読み込むことになるため、hclsyntax.Body
として扱うためにはキャストが必要な点には注意が必要です。
子要素
- Body
writeFileBody := writeFile.Body() // hclwrite.Body
syntaxFileBody := syntaxFile.Body.(*syntax.Body) // Cast: hcl.Body -> hclsyntax.Body
Body
File や後述する Block 中の構成要素の塊です。
親要素が File であれば、Body は HCL ファイル全体の記述を指すことになりますし、親要素が上記の例における boot_disk
の Block であれば Body は以下のとおりになります。
initialize_params {
image = "debian-cloud/debian-11"
labels = {
my_label = "value"
}
}
子要素
- Blocks (= []Block)
writeBlocks := writeBody.Blocks() // []hclwrite.Block
syntaxBlocks := syntaxBody.Blocks // []hclsyntax.Block
- Attributes (= []Attribute)
writeAttributes := writeBody.Attributes() // []hclwrite.Attribute
syntaxAttributes := syntaxBody.Attributes // []hclsyntax.Attribute
Block
公式の syntax で説明されている Block に相当します。
上記の例では、
resource "google_service_account" "default" {
account_id = "my-custom-sa"
display_name = "Custom SA for VM Instance"
}
や
service_account {
# Google recommends custom service accounts that have cloud-platform scope and permissions granted via IAM Roles.
email = google_service_account.default.email
scopes = ["cloud-platform"]
}
が Block です。
子要素
- Body
for _, writeBlock := range writeBlocks {
writeBlockBody := writeBlock.Body() // hclwrite.Body
}
for _, syntaxBlock := range syntaxBlocks {
syntaxBlockBody := syntaxBlock.Body // hclsyntax.Body
}
Attribute
リソースの1つのパラメータを指定する記述が Attribute です。
上記のhcl中の例としては、
name = "my-instance"
tags = ["foo", "bar"]
email = google_service_account.default.email
などが Attribute に当たります。
子要素
- Expression
for _, writeAttribute := range writeAttributes {
writeExpression := writeAttribute.Expr() // hclwrite.Expression
}
for _, syntaxAttribute := range syntaxAttributes {
syntaxExpression := syntaxAttribute.Expr // hclsyntax.Expression
}
Expression
Expression は Attribute の右辺値の構成要素です。
単純な例としては、
name = "my-instance"
における、"my-instance"
や、
email = google_service_account.default.email
における、google_service_account.default.email
が該当します。
より複雑なケースとして、Expression が Expression の構成要素となるものもあります。
例えば、
machine_type = "n2-standard-${var.cpus}"
において、"n2-standard-${var.cpus}"
が Attribute 直下の Expression であるのに加え、 "n2-standard-"
と var.cpus
はそれぞれ "n2-standard-${var.cpus}"
を構成する Expression に当たります。
別な例としては、
metadata_startup_script = file(var.startup_script_path)
において、Attribute 直下の Expression が file(var.startup_script_path)
であり、子要素に Expression var.startup_script_path
を持ちます。
子要素 (hclsyntax only)
- Traversal
// hclwrite.Expression can't access Traversal
syntaxTraversals := syntaxExpression.Variables // []hcl.Traversal
- cty.Value
// hclwrite.Expression can't access cty.Value
syntaxValues := syntaxExpression.Values(nil) // []cty.Value
- Expression
hclsyntax には様々な Expression の実装が定義されており、LiteralValueExpressionのように Expression を子に持たないものもあれば、TemplateExpr のように子に持つものもおり、そのフィールド名も実装によってまちまちである。
Traversal
リファレンス には
A Traversal is a description of traversing through a value through a series of operations such as attribute lookup, index lookup, etc.
とあるが、扱う上ではとりあえず変数だと思っておけばよさそう。
例としては、google_service_account.default.email
や、var.cpus
など。
cty.Value
"my-instance"
などの何らかのリテラル。
HCL の書き換え
続いて本題となる HCL ファイル中の変数をリテラルで置換する方法について説明します。
変数の置換は、単純には次の手順で実現することになります。
- 置き換えたい変数を定義した
map[string]cty.Value
を使って、hcl.EvalContext
インスタンスを作成する。 - 作成した
hcl.EvalContext
を使って、hclsyntax.Expression.Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics)
で、変数の値を展開したあとのリテラルを生成する -
hclwrite.Body.SetAttributeValue(name string, val cty.Value) *Attribute
で、生成したリテラルで Attribute を置換する
具体的な実装例を以下に示します。
import (
"os"
"github.com/hashicorp/hcl/v2/hcl"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/zclconf/go-cty/cty"
)
... // Read HCL file to convert as hcl.File and hclwrite.File
// Read .tfvars file as HCL file
path := "fuga.tfvars"
content, err := os.ReadFile(path)
tfvarsFile := hclsyntax.ParseConfig(content, path, hcl.Pos{})
// Parse variables
tfvarsAttrs, _ := tfvarsFile.Body.JustAttributes()
tfvars := make(map[string]cty.Value)
for key, attr := range tfvarsAttrs {
value, _ := attr.Expr.Value(nil)
tfvars[name] = key
}
// Use tfvars as var.* variables
ctx := hcl.EvalContext {
Variables: map[string]cty.Value {
"var": cty.ObjectVal(tfvars)
}
}
...
// var syntaxAttrs []hclsyntax.Attribute
// var writeBody hclwrite.Body
for name := range syntaxAttrs {
value, _ := syntaxAttrs[name].Expr.Value(&ctx)
writeBody.setAttributeValue(name, value)
}
...
// var writeFile hclwrite.File
// Output file
os.WriteFile("converted.hcl", writeFile.Bytes(), 0644)
終わりに
hcl/v2 ライブラリにおける HCL ファイルの構成要素と、hclライブラリを使った変数の書き換え方法について説明しました。
みなさんもぜひ hcl/v2 ライブラリを使って terraform の定義ファイルをいじって遊んでみてください ^^