DEV Community

totegamma
totegamma

Posted on

Experimenting with Terraform in YAML

Why even try this

Terraform is one of the go-to tools for infrastructure as code.

However, once you start managing multiple environments (dev/prod, etc.), dealing with environment-specific differences becomes surprisingly tricky. Many people have looked into Terragrunt as a solution to this problem.

Also, while Terraform’s dedicated language HCL (HashiCorp Configuration Language) is fairly approachable, using constructs like count or for fluently requires some practice. And at the end of the day, it’s almost entirely a Terraform-specific DSL — not something that can be easily reused elsewhere.

That’s why, in this article, I want to try something different: defining a Terraform project using YAML and its expression evaluator yisp.

With YAML + yisp, you can freely define and compose modules in more flexible ways, and the same knowledge can be applied to other YAML-based domains like Kubernetes manifests.

Whether this is practical or not is still questionable — but it might open up new ways to handle environment-specific variations.


Terraform JSON Syntax Refresher

Terraform usually expects .tf files written in HCL, but you can also provide configuration in JSON format.

For example, the following HCL file:

terraform {
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "2.4.0"
    }
  }
}

provider "local" {}

resource "local_file" "hello1" {
  content  = "1st Hello, World!"
  filename = "hello1.txt"
}

resource "local_file" "hello2" {
  content  = "2nd Hello, World!"
  filename = "hello2.txt"
}
Enter fullscreen mode Exit fullscreen mode

Can be rewritten as JSON:

{
  "provider": {
    "local": {}
  },
  "resource": {
    "local_file": {
      "hello1": {
        "content": "1st Hello, World!",
        "filename": "hello1.txt"
      },
      "hello2": {
        "content": "2nd Hello, World!",
        "filename": "hello2.txt"
      }
    }
  },
  "terraform": {
    "required_providers": {
      "local": {
        "source": "hashicorp/local",
        "version": "2.4.0"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Save this as <filename>.tf.json, and Terraform will happily accept it with the usual terraform apply.


What is yisp?

yisp is a small tool that lets you embed and evaluate expressions inside YAML.

For instance, this YAML:

# index.yaml
mynumber: !yisp
  - add
  - 5
  - 3
Enter fullscreen mode Exit fullscreen mode

Evaluates to:

# result
mynumber: 8
Enter fullscreen mode Exit fullscreen mode

by simply running yisp build ..

yisp also supports importing multiple YAML files, merging objects, or even outputting JSON instead of YAML.

In this experiment, we’ll use yisp to make Terraform JSON easier to author in YAML, and to manage environment-specific differences through imports and patching.


First Steps

Here’s a simple Terraform definition written in YAML:

# tf.yaml
terraform:
  required_providers:
    local:
      source: hashicorp/local
      version: "2.4.0"

---
provider:
  local: {}

---
resource:
  local_file:
    hello1:
      content: "1st Hello, World!"
      filename: "hello1.txt"

---
resource:
  local_file:
    hello2:
      content: "2nd Hello, World!"
      filename: "hello2.txt"
Enter fullscreen mode Exit fullscreen mode

Then, in index.yaml, merge them into one object:

# index.yaml
!yisp
- lists.reduce
- - include
  - tf.yaml
- maps.merge
Enter fullscreen mode Exit fullscreen mode

Running yisp build . will output the same JSON structure as shown earlier.


Using a Makefile

Terraform commands don’t accept configuration via stdin — they require an actual file. To streamline the workflow, let’s use a Makefile that automatically generates the JSON before running Terraform:

rendered.tf.json:
    yisp build . -o json > rendered.tf.json

init: rendered.tf.json
    terraform init

plan: rendered.tf.json
    terraform plan

apply: rendered.tf.json
    terraform apply
Enter fullscreen mode Exit fullscreen mode

Now you can simply run make plan or make apply, and YAML will be converted to JSON on demand.


Modularizing for Environment Differences

Let’s go one step further and introduce modules to handle dev/prod variations.
Here’s the directory structure:

📁 .
├── 📁 base
│   ├── 📄 core.yaml
│   └── 📄 localfile.yaml
└── 📁 env
    ├── 📁 dev
    │   ├── 📄 index.yaml
    │   └── 📄 localfile.yaml
    └── 📁 prod
        ├── 📄 index.yaml
        └── 📄 localfile.yaml
Enter fullscreen mode Exit fullscreen mode

Core config (base/core.yaml)

# base/core.yaml
terraform:
  required_providers:
    local:
      source: hashicorp/local
      version: "2.4.0"

---
provider:
  local: {}
Enter fullscreen mode Exit fullscreen mode

Module definition (base/localfile.yaml)

# base/localfile.yaml
!yisp &main
- lambda
- [ props ]
- !quote
  resource:
    local_file:
      hello:
        content: !yisp
        - strings.format
        - "Hello: %s"
        - *props.name
        filename: "hello1.txt"
Enter fullscreen mode Exit fullscreen mode

Module usage (env/dev/localfile.yaml)

# env/dev/localfile.yaml
!yisp
- import
- ["localfile", "../../base/localfile.yaml"]

---
!yisp
- *localfile.main
- name: "alice"
Enter fullscreen mode Exit fullscreen mode

Final merge (env/dev/index.yaml)

# env/dev/index.yaml
!yisp
- lists.reduce
- - include
  - ../../base/core.yaml
  - localfile.yaml
- maps.merge
Enter fullscreen mode Exit fullscreen mode

Evaluating this gives us:

# output
terraform:
  required_providers:
    local:
      source: hashicorp/local
      version: "2.4.0"
provider:
  local: {}
resource:
  local_file:
    hello:
      content: "Hello: alice"
      filename: "hello1.txt"
Enter fullscreen mode Exit fullscreen mode

Here we get a local_file resource that writes "Hello: alice".
For production, you can simply adjust the parameters and reuse the same module.


Conclusion

In this article, we tried using yisp to define Terraform in YAML, while modularizing environment-specific differences.

It’s still uncertain whether this approach is practical in production, but it was an interesting discovery: just like Kubernetes’ kustomize helps manage manifest variations, we can experiment with similar ideas for Terraform.

If this piqued your interest, give it a spin on your own setup!

Top comments (0)