LoginSignup
6
0

More than 1 year has passed since last update.

HCLに対してプログラムから操作したい? hclwrite package を使ってみよう!

Last updated at Posted at 2022-12-21

本記事はterraform Advent Calendar 2022の21日目の記事です。

何を書いた記事か

みなさんTerraformしていますか?
Terraformされている方は、あの独特の言語 HCL について何かしら思うところがあるのではないでしょうか。
プログラムから扱いにくいとか。

そんなみなさんのために、HCLに対するなんらかの操作を可能にするpackageの基本的な使い方を紹介します。

HCLとは

HCLとは、 HashiCorp Configuration Language の略称で、その名の通りHashicorp社のツールを操作する際に利用されます。
と言っても筆者はTerraformでしか触ったことはありません。

HCLのsyntaxの詳細は下記に記載されていますが、ここでは後述の説明上重要な attribute block body の概念について説明します。

  • attribute
    • 特定の名前が付けられた値
  • block
    • {} で囲われた構造体。内部に子の body を持つ
    • type と label によって修飾される
  • body
    • attribute と body の集合体

よくわからないのでサンプルファイルを見て解説します。

sample.tf
resource "aws_instance" "app_server" {
  ami           = "ami-830c94e3"
  instance_type = "t2.micro"

  tags = {
    Name = "ExampleAppServerInstance"
  }
}

sample.tf というFileの中に、一番大きな body が定義されています。
その下に {} でまとまりが定義された block があります。 block は上述の通り type と label によって修飾されるのですが、この場合は resource が type に該当し、 "aws_instance" "app_server" が label に該当します。
label の数は type によって決まっており、 resource の場合は2つの label を取ると定義されています。 type によっては label を1つしかとらないもの(Terraformでは variable や provider など)や、全くとらないもの(Terraformでは terraform など)もあります。

resource block の中に、 ami や instance_type attribute が定義されています。
また、さらに tags block も定義されており、これらがまとめて resouce block の body となります。つまり、 File 以下でマトリョーシカ構造を取りえます。

File
└── body
    ├── attribute
    └── block
        └── body
            ├── attribute
            └── block

HCLをパースする際は、上記の構造になっていることを念頭に置く必要があります。

hclwriteとは

パッケージ概要

上述のように、HCLで定義されたファイルに対してプログラムから操作するには、何らかのパーサが必要となり、それがハードルを上げているものと思われます。
ここで本題の hclwrite というパッケージを紹介します。

HCLにはメインで利用されている hcl というパーサがあるのですが、 hclwrite の特徴は、既存のHCLファイルに対して、コメントや改行の配置といった、運用上不可された情報を失うことなくファイルに対する操作ができ、抽象化レベルが hcl とは異なっているところにあります。

運用で肥大化・複雑化したTerraformの定義ファイルを、プログラムによって何らかの自動操作をしたい、でもコメントや改行のような運用性・可読性を担保するための情報は残したい、そういったときに利用できるパッケージです。

できること

一例をお見せします。

例えば以下のような variables.tf ファイルがあったとします。

variables.tf
variable "foo" {
  type = string
}

variable "bar" {
  default = ["us-west-1a"]
}

# some comment
variable "baz" {
  default = 100
}

Terraformを運用しているとこういったvariableをまとめたtfファイルは発生しがちですが、ここに対して後からlinter(tflintなど)を適用しようとすると、以下の理由でlinterに怒られてしまうことがあります。

  • descriptionがない
  • typeがついていない変数がある

上記のファイルくらいの分量であれば問題ないのですが、実際にはこれが数百連なった(そっちの方が問題ですが)ファイルを運用しないといけないケースはあり得ます。
そんなとき、 label から description を自動生成し、 default から type を推測して自動的にある程度埋めてくれると相当リファクタが楽になるな、と思って作ったツールが以下です。

※突貫で作ったのでREADMEとかtestとか色々足りてません。参考程度に見てください。

先ほどの variables.tf をツールにかけると、以下のように改修されます。

variable "foo" {
  description = "Foo"
  type        = string
}

variable "bar" {
  description = "Bar"
  type        = list(string)
  default     = ["us-west-1a"]
}

# some comment
variable "baz" {
  description = "Baz"
  type        = number
  default     = 100
}

このように、コメントは残しつつ、自動で description と type を埋めてくれるので、あとは必要なところだけ手動で変更すればおkです。

これを実現するのに、 hclwrite の力を借りています。
以降で hclwrite の基本的な使い方に触れてみましょう。

触ってみよう

hclファイルの生成

まずはpackageのチュートリアルに記載されている、新規のファイル生成をそのまま試してみます。
サンプルコードを書いて動かすと

package main

import (
	"fmt"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/hclwrite"
	"github.com/zclconf/go-cty/cty"
)

func main() {
	f := hclwrite.NewEmptyFile()
	rootBody := f.Body()
	rootBody.SetAttributeValue("string", cty.StringVal("bar")) // this is overwritten later
	rootBody.AppendNewline()
	rootBody.SetAttributeValue("object", cty.ObjectVal(map[string]cty.Value{
		"foo": cty.StringVal("foo"),
		"bar": cty.NumberIntVal(5),
		"baz": cty.True,
	}))
	rootBody.SetAttributeValue("string", cty.StringVal("foo"))
	rootBody.SetAttributeValue("bool", cty.False)
	rootBody.SetAttributeTraversal("path", hcl.Traversal{
		hcl.TraverseRoot{
			Name: "env",
		},
		hcl.TraverseAttr{
			Name: "PATH",
		},
	})
	rootBody.AppendNewline()
	fooBlock := rootBody.AppendNewBlock("foo", nil)
	fooBody := fooBlock.Body()
	rootBody.AppendNewBlock("empty", nil)
	rootBody.AppendNewline()
	barBlock := rootBody.AppendNewBlock("bar", []string{"a", "b"})
	barBody := barBlock.Body()

	fooBody.SetAttributeValue("hello", cty.StringVal("world"))

	bazBlock := barBody.AppendNewBlock("baz", nil)
	bazBody := bazBlock.Body()
	bazBody.SetAttributeValue("foo", cty.NumberIntVal(10))
	bazBody.SetAttributeValue("beep", cty.StringVal("boop"))
	bazBody.SetAttributeValue("baz", cty.ListValEmpty(cty.String))

	fmt.Printf("%s", f.Bytes())
}

次のように hcl のファイルが出力されます。

string = "foo"

object = {
  bar = 5
  baz = true
  foo = "foo"
}
bool = false
path = env.PATH

foo {
  hello = "world"
}
empty {
}

bar "a" "b" {
  baz {
    foo  = 10
    beep = "boop"
    baz  = []
  }
}

ここで、冒頭で説明した attributeblock といった構造体に対してプログラムから操作している点に注目してサンプルコードを読んでみてください。
rootBody := f.Body() で生成した body インスタンスに対して、 SetAttributeValue で attribute を付与したり、 AppendNewBlock で block を作成したりしています。
また、 AppendNewline のように、可読性確保のために改行を付与してくれる点も hclwrite のユニークな点かと思います。

しかし、我々が行いたいのは新規にhclファイルを作成することではありません。それ自体は手で書いた方が早いです。

次に、実際に既存のhclファイルに対する操作の基本をやってみます。

hclファイルの操作

シンプルに、以下のようなhclファイルが存在するものとします

provider "aws" {
  region  = "us-west-2"
}

まずファイルを読み込んで、ファイルの中身を丸々 block に格納します。

// tfファイルを読み込み
filePath := "./sample.tf"
src, err := ioutil.ReadFile(filePath)
if err != nil {
    log.Fatal(err)
}

// tfファイルをParse
file, diags := hclwrite.ParseConfig(src, filePath, hcl.InitialPos)
if diags.HasErrors() {
    log.Fatal(err)
}

// ファイルの実態を取得
body := file.Body()

bodyにはattributeやblockがありますが、ここではblockが定義されているファイルであるとして、 body 内の block にアクセスします。
そして、 block の label や、 body、 body 内 attribute を取得します。

for _, block := range body.Blocks() {
    labels := block.Labels()
    blockBody := block.Body()
    attributes := blockBody.Attributes()
    fmt.Println(labels, blockBody, attributes)

Blocks()blockのsliceを返却します
Block構造体はこのような構成になっているので、 label や body に対してアクセスし、値を取得することができます。

print結果は以下です。

[aws]
&{{<nil> 0x140001108e0} map[0x14000113380:{}]}
map[region:0x140001131d0]

まだ直感的に扱える形にはなっていませんね。
Labels()Attributes はそれぞれ string[] と map[string]*Attribute を返却します(※Labels※Attributes)。

実際の attribute にアクセスするには GetAttribute を使って attribute 名を指定します。

value := blockBody.GetAttribute("region")
fmt.Println(value)

この結果出力される値は下記です。

&{{<nil> 0x14000110680} 0x14000112930 0x14000112960 0x14000112a20 0x14000112a50}

このままでは扱いづらいので、以下のようにしてByte変換したあとstringにキャストすると値を取得できます。

value := blockBody.GetAttribute("region").Expr().BuildTokens(nil).Bytes()
fmt.Println(string(value))
"us-west-2"

先に見せたサンプルコードでは、ここで取得した値に対して型推測を実施して type を埋めたり、 label から description を生成したりする処理を実装しています。

また、何らかの変換(以下では parseVariable() 内で実装)を噛ませた block を新しく生成し、 既存の block を削除 RemoveBlock して新しい block を負荷 AppendBlock することで、元のファイルに対する任意の操作を行うようにしています。

fixer.go
	for _, block := range body.Blocks() {
		new_block := parseVariable(block)

		body.RemoveBlock(block)
		body.AppendBlock(new_block)
		file.Body().AppendNewline() // blockごとに改行
	}

終わりに

Terraform運用していると、HCLに対してプログラムからなんらかの操作を行いたいことが出てくるよね、そういったときに hclwrite パッケージはいいぞ、という紹介でした。
ネット上に知見がそこまで転がっていないパッケージなので、みんなでどんどん触って実験してノウハウを共有していきたいです!面白い使い方あったら教えてください!

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