LoginSignup
0
0

hcl v2 パッケージを使って HCL の var.* をリテラルに置き換えてみた

Posted at

はじめに

terraform でリソース管理している中で、「特定の HCL ファイル内の一部の variable だけ具体的なリテラルで置き換えたい…」と思うことがありました。
そこで HCL ファイル中の変数をリテラルで置き換えるツールを作ってみました。

ツールを作成するに当たっては、 hashicorp 公式のライブラリである、 github.com/hashicorp/hcl/v2 を使ったのですが、

  • HCL の構成要素の具体的な例や関係性がぱっとわからなかった
  • HCL の変数の書き換えに関する記事が見つけられなかった

というつらみがあったため、備忘録も兼ねて記事にすることにしました。

本記事では、HCL の構成要素について、自分なりの解釈を具体例を交えた解説を行い、それを踏まえて HCL の変数をリテラルに書き換えた方法について説明します。

使用したライブラリ

ツールの作成に当たり、使用したライブラリは以下のとおりです。

  • github.com/hashicorp/hcl/v2: v2.18.0
    • github.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 ファイル中の変数をリテラルで置換する方法について説明します。

変数の置換は、単純には次の手順で実現することになります。

  1. 置き換えたい変数を定義した map[string]cty.Value を使って、 hcl.EvalContextインスタンスを作成する。
  2. 作成した hcl.EvalContextを使って、hclsyntax.Expression.Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) で、変数の値を展開したあとのリテラルを生成する
  3. 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 の定義ファイルをいじって遊んでみてください ^^

参考資料

0
0
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
0