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"
}
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"
}
}
}
}
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
Evaluates to:
# result
mynumber: 8
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"
Then, in index.yaml
, merge them into one object:
# index.yaml
!yisp
- lists.reduce
- - include
- tf.yaml
- maps.merge
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
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
Core config (base/core.yaml
)
# base/core.yaml
terraform:
required_providers:
local:
source: hashicorp/local
version: "2.4.0"
---
provider:
local: {}
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"
Module usage (env/dev/localfile.yaml
)
# env/dev/localfile.yaml
!yisp
- import
- ["localfile", "../../base/localfile.yaml"]
---
!yisp
- *localfile.main
- name: "alice"
Final merge (env/dev/index.yaml
)
# env/dev/index.yaml
!yisp
- lists.reduce
- - include
- ../../base/core.yaml
- localfile.yaml
- maps.merge
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"
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)