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

本記事は[terraform Advent Calendar 2022](https://qiita.com/advent-calendar/2022/terraform)の21日目の記事です。

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

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

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

https://github.com/hashicorp/hcl

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

https://github.com/hashicorp/hcl/blob/main/hclsyntax/spec.md

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


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

```hcl: 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 というパッケージを紹介します。

https://pkg.go.dev/github.com/hashicorp/hcl/v2/hclwrite

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

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

## できること
一例をお見せします。

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

```hcl: 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 を推測して自動的にある程度埋めてくれると相当リファクタが楽になるな、と思って作ったツールが以下です。

https://github.com/kzk-maeda/hcl-lint-fixer

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

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

```hcl
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 を埋めてくれるので、あとは必要なところだけ手動で変更すればおｋです。

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

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

```go
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 のファイルが出力されます。

```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  = []
  }
}
```

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

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

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

## hclファイルの操作

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

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

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

```go
// 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 を取得します。

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

`Blocks()` は [blockのsliceを返却](https://pkg.go.dev/github.com/hashicorp/hcl/v2/hclwrite#Body.Blocks)します
Block構造体は[このような構成](https://github.com/hashicorp/hcl/blob/v2.15.0/hclwrite/ast_block.go#L8-L17)になっているので、 label や body に対してアクセスし、値を取得することができます。

print結果は以下です。

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

まだ直感的に扱える形にはなっていませんね。
`Labels()` や `Attributes` はそれぞれ `string[]` と　`map[string]*Attribute` を返却します（[※Labels](https://pkg.go.dev/github.com/hashicorp/hcl/v2/hclwrite#Block.Labels
)、[※Attributes](https://pkg.go.dev/github.com/hashicorp/hcl/v2/hclwrite#Body.Attributes)）。

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

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

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

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

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


```go
value := blockBody.GetAttribute("region").Expr().BuildTokens(nil).Bytes()
fmt.Println(string(value))
```

```
"us-west-2"
```

先に見せた[サンプルコード](https://github.com/kzk-maeda/hcl-lint-fixer)では、ここで取得した値に対して型推測を実施して type を埋めたり、 label から description を生成したりする処理を実装しています。

また、何らかの変換（以下では `parseVariable()` 内で実装）を噛ませた block を新しく生成し、　既存の block を削除　`RemoveBlock` して新しい block を負荷 `AppendBlock` することで、元のファイルに対する任意の操作を行うようにしています。
 
```go: 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 パッケージはいいぞ、という紹介でした。
ネット上に知見がそこまで転がっていないパッケージなので、みんなでどんどん触って実験してノウハウを共有していきたいです！面白い使い方あったら教えてください！