In the previous article, I talked about the reasons why I adopted a state file as the foundation for the framework I am developing.
In today’s article, we will see in practice how to manipulate Terraform state files using Golang.
Before we begin, make sure you have Go installed on your machine. You can obtain Go at https://golang.org/dl/. Additionally, you will need to have Terraform installed on your system. You can find installation instructions at https://www.terraform.io/downloads.html.
Preparing the Setup
To start, I will create a new directory for the Go project and then initialize a new Go module.
mkdir edit-terraform
cd edit-terraform
go mod init edit-terraform
This will create a go.mod file in the directory.
Next, we will create a directory called “test” and add the source.tf file where we will add the Terraform code.
# test/source.tf
variable "name" {
type = string
default = "World"
}
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "2.4.0"
}
}
}
resource "local_file" "foo" {
content = "Hello, ${var.name}"
filename = "${path.module}/hello.txt"
}
As we can see, the above code creates a file called “hello.txt” with the content “Hello, “ concatenated with the variable “name”. By default, the value of this variable is set to “World”. Therefore, when we execute the command “terraform init” (to initialize Terraform) and “terraform plan” (to plan the changes), we will have the following output:
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# local_file.foo will be created
+ resource "local_file" "foo" {
+ content = "Hello, World"
+ content_base64sha256 = (known after apply)
+ content_base64sha512 = (known after apply)
+ content_md5 = (known after apply)
+ content_sha1 = (known after apply)
+ content_sha256 = (known after apply)
+ content_sha512 = (known after apply)
+ directory_permission = "0777"
+ file_permission = "0777"
+ filename = "./hello.txt"
+ id = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform
apply" now.
This output shows the changes that are being made to the application state.
Opening the file
Now we will create the state.go file inside the state directory, and we will start working on the code responsible for manipulating the Terraform file.
Let’s create a function called Open that will receive the path to the Terraform file to be opened and return a pointer to an object representing the file’s contents.
// state/state.go
package state
import (
"errors"
"io/ioutil"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclwrite"
)
func Open(filepath string) (*hclwrite.File, error) {
content, err := ioutil.ReadFile(filepath)
if err != nil {
return nil, err
}
file, diags := hclwrite.ParseConfig(content, filepath, hcl.Pos{Line: 1, Column: 1})
if diags.HasErrors() {
err := errors.New("an error occurred")
if err != nil {
return nil, err
}
}
return file, nil
}
Let’s take a closer look at the code above.
content, err := ioutil.ReadFile(filepath)
if err != nil {
return nil, err
}
The first thing we do is read the desired Terraform file using the built-in library provided by the language.
file, diags := hclwrite.ParseConfig(content, filepath, hcl.Pos{Line: 1, Column: 1})
Next, we use hclwrite to convert the file into a Go data structure. This library is officially provided by HashiCorp.
Manipulating the file
Let’s also add the UpdateDefaultValue
method, which will take three parameters: the object representing the file, the variable name, and the new value.
// state/state.go
package state
import (
"errors"
"io/ioutil"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclwrite"
)
...
func UpdateDefaultValue(file *hclwrite.File, name string, value string) bool {
for _, block := range file.Body().Blocks() {
labels := block.Labels()
if block.Type() == "variable" && len(labels) > 0 && name == labels[0] {
if block.Body().GetAttribute("default") != nil {
block.Body().SetAttributeValue("default", cty.StringVal(value))
return true
}
}
}
return false
}
The above code scans through the body of the file, searching for variables that match the desired pattern. If a match is found, it replaces the value of that variable in the file with the provided value.
Writing the File
Lastly, let’s add a function to save the edited content back to the file. To do this, we will pass the file path and the file object to the function.
// state/state.go
package state
import (
"errors"
"io/ioutil"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclwrite"
)
...
func Save(filename string, file *hclwrite.File) error {
if err := ioutil.WriteFile(filename, file.Bytes(), 0644); err != nil {
return err
}
return nil
}
As we can see, once again we use ioutil
to write the new content to the file.
Testing
Now let’s create the main.go
file and use the package we just created.
package main
import (
"fmt"
"example/state"
)
func main() {
var filename = "tests/example.tf"
file, err := state.Open(filename)
if err != nil {
fmt.Println(err)
}
ok := state.UpdateDefaultValue(file, "name", "You")
if !ok {
err := state.Save(filename, file)
if err != nil {
fmt.Printf("Erro ao salvar as alterações no arquivo %s: %s\n", filename, err)
}
}
fmt.Printf("File Updated!\n", filename)
}
As we can see, the process is quite simple. We open the file, edit a variable, and then save the file. Now we can execute our code and check the changes.
go run main.go
File Updated!
If we open the contents of the test/source.tf
file, we will notice that the file has been updated on line 3, and where there was previously the value World
, now there is the value You
.
variable "name" {
type = set(string)
default = "You"
}
Now I’m going to update my code to change the value of the name
variable and perform the same test. And if the plan
command is executed again, we can see that the state is being planned with the new value.
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# local_file.foo will be created
+ resource "local_file" "foo" {
+ content = "Hello, You"
+ content_base64sha256 = (known after apply)
+ content_base64sha512 = (known after apply)
+ content_md5 = (known after apply)
+ content_sha1 = (known after apply)
+ content_sha256 = (known after apply)
+ content_sha512 = (known after apply)
+ directory_permission = "0777"
+ file_permission = "0777"
+ filename = "./hello.txt"
+ id = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform
apply" now.