Create Terraform Resource References in Go with hclwrite
Tags: go, terraform, devops, programming
Published: 2022-03-22 06:50:26

In our [last post](https://dev.to/pdcommunity/write-terraform-files-in-go-with-hclwrite-2e1j) I introduced the [hclwrite](https://pkg.go.dev/github.com/hashicorp/hcl/v2/hclwrite) library from HashiCorp which allows you to write [HashiCorp Configuration Language (HCL)](https://www.terraform.io/docs/language/syntax/configuration.html) using [Go](https://go.dev/). In that article I created a basic HCL file that defined 150 [Business Service](https://registry.terraform.io/providers/PagerDuty/pagerduty/latest/docs/resources/business_service) resources in a PagerDuty account. Truth be told, I picked Business Services because they didn't require reference values from other resources, because I didn't know how to construct those resource references using hclwrite...until now.

In this post we're going to create HCL that defines a [PagerDuty Service](https://registry.terraform.io/providers/PagerDuty/pagerduty/latest/docs/resources/service). A service requires a reference to a [PagerDuty Escalation Policy](https://registry.terraform.io/providers/PagerDuty/pagerduty/latest/docs/resources/escalation_policy) which, in turn, requires a reference to a [PagerDuty Schedule](https://registry.terraform.io/providers/PagerDuty/pagerduty/latest/docs/resources/schedule). Each of these references are accomplished by using a [resource reference expression](https://www.terraform.io/language/expressions/references#resources). For example, the HCL for defining a service looks something like this, where the `escalation_policy` attribute is the ID of the escalation policy associated with the service. 

```hcl
resource "pagerduty_service" "me1" {
  name              = "Me Service 1"
  escalation_policy = pagerduty_escalation_policy.ep.id
}
```

We already know how to create new blocks and set primitive attribute values, as we covered those concepts in the [last post]((https://dev.to/pdcommunity/write-terraform-files-in-go-with-hclwrite-2e1j)). However, we need to first create a service resource block in order to set an `escalation_policy` attribute. That code for creating the service resource block looks something like this:

```go
service := rootBody.AppendNewBlock("resource", 
    []string{"pagerduty_service", "me1")})
serviceBody := service.Body()
serviceBody.SetAttributeValue("name", cty.StringVal("Me Service 1")))
```

Now we're ready to add the `escalation_policy` attribute to `serviceBody`, which is where things get interesting. To create the `name` attribute in the code above we used the `SetAttributeValue` and the `cty.StringVal` functions to set the string value. But, a resource reference is not a string.  It's essentially a variable. If you've been using Terraform for a while you might be thinking you could use the older expression syntax where you set the value as a string and wrapped expression with a `${ }`. The hclwrite library actually watches for this syntax and tries to escape it by adding an extra `$` to the front, like this `$${ }`. 

There are a couple of approaches that will work for creating these expressions. The first is to use `SetAttributeTraversal`. A [Traversal](https://pkg.go.dev/github.com/hashicorp/hcl/v2/hclwrite#Traversal) is designed to define a path of an expression. To define the `escalation_policy` as a Traversal it would look something like this:

```go
serviceBody.SetAttributeTraversal("escalation_policy", hcl.Traversal{
    hcl.TraverseRoot{
        Name: "pagerduty_escalation_policy",
    },
    hcl.TraverseAttr{
        Name: "ep",
    },
    hcl.TraverseAttr{
        Name: "id",
    },    
})
```
This creates a perfectly formatted expression of `pagerduty_escalation_policy.ep.id`.

The other way to create the necessary reference syntax, and the one I prefer, is to use `SetAttributeRaw`. This function requires you to define the entire expression manually as a series of [Tokens](). A Token is a struct containing `Type` and `Bytes` fields. The `Type` comes from the `hclsyntax.TokenType`. And `Bytes` is a `[]byte` with the value of the Token. In this case, where we're setting the `escalation_policy` value, the `Tokens` and subsequent `SetAttributeRaw` would look something like this:

```go
tokens := hclwrite.Tokens{
  {
    Type: hclsyntax.TokenIdent, 
    Bytes: []byte(`pagerduty_escalation_policy.ep.id`),
  },
}

serviceBody.SetAttributeRaw("escalation_policy", tokens)
```

There are a few reasons why I prefer using the `SetAttributeRaw` method. The first advantage you'll notice from this approach is that it takes less code than the Traversal. However, the real advantage comes when creating attributes that contain a list value. We do just that when defining a `pagerduty_schedule` resource. In the `layer` block of the schedule resource we set a list of `users`.

The HCL for the entire PagerDuty Schedule looks something like this:

```hcl
resource "pagerduty_schedule" "foo" {
  name      = "Me Schedule"
  time_zone = "America/Los_Angeles"

  layer {
    name                         = "This Shift"
    start                        = "2022-03-15T20:00:00-08:00"
    rotation_virtual_start       = "2022-03-15T20:00:00-08:00"
    rotation_turn_length_seconds = 86400
    users                        = [data.pagerduty_user.me.id]

    restriction {
      type              = "daily_restriction"
      start_time_of_day = "17:00:00"
      duration_seconds  = 54000
    }
  }
}
```

The Go code needed to create this `pagerduty_schedule` resource and `layer` blocks looks like this:

```go
// create schedule
sched := rootBody.AppendNewBlock("resource",
  []string{"pagerduty_schedule", "foo"})
schedBody := sched.Body()
schedBody.SetAttributeValue("name", cty.StringVal("Me Schedule"))
schedBody.SetAttributeValue("time_zone", cty.StringVal("America/Los_Angeles"))
schedBody.AppendNewline()
schedLayer := schedBody.AppendNewBlock("layer", nil)
schedLayerBody := schedLayer.Body()
schedLayerBody.SetAttributeValue("name", cty.StringVal("This Shift"))
schedLayerBody.SetAttributeValue("start", cty.StringVal("2022-03-15T20:00:00-08:00"))
schedLayerBody.SetAttributeValue("rotation_virtual_start", cty.StringVal("2022-03-15T20:00:00-08:00"))
```

Creating the Tokens for the `users` list looks extremely similar to the code we used above to create the `escalation_policy` reference. The difference now is that we wrap our `data.pagerduty_user.me.id` value in opening and closing square brackets, like so:

```go
userTokens := hclwrite.Tokens{
  {
    Type:  hclsyntax.TokenOBrack,
    Bytes: []byte(`[`),
  },
  {
    Type:  hclsyntax.TokenIdent,
    Bytes: []byte(`data.pagerduty_user.me.id`),
  },
  {
    Type:  hclsyntax.TokenCBrack,
    Bytes: []byte(`]`),
  },
}

schedLayerBody.SetAttributeRaw("users", userTokens)
```

Hopefully, you now have a better idea of how to use the `SetAttributeRaw` function when writing  resource references in HCL when using the `hclwrite` library. You can find all the code that was described in this article in the [Create Terraform Files in Go](https://github.com/stmcallister/create-terraform-files-go) repository on GitHub.