HCL Code Generation
Terramate supports the generation of arbitrary HCL code referencing Terramate defined data.
The generated code can then be composed/referenced by any Terraform code inside a stack (or any other tool that uses HCL, like Packer).
HCL code generation is done using generate_hcl
blocks in Terramate configuration files.
The code may include:
- Blocks, sub blocks, etc
- Attributes initialized by literals
- Terramate Global references
- Terramate Metadata references
- Expressions using interpolation, functions, etc
- Dynamic blocks using the
tm_dynamic
block type
Anything you can do in Terraform can be generated using a generate_hcl
block. References to Terramate globals and metadata are evaluated, but any other reference is just transported to the generated code (partial evaluation).
Each generate_hcl
block requires a single label that is the path where the generated file will be saved. For more details about how code generation use labels check the Labels Overview) docs.
Inside the generate_hcl
block a content
block is required. All code inside content
is going to be used to generate the final HCL code. Any tm_dynamic block inside the content
block is going to be evaluated and expanded in the final HCL code.
Now lets jump to some examples. Lets generate backend and provider configurations for all stacks inside a project.
Given these globals defined on the root of the project:
globals {
backend_data = "backend_data"
provider_data = "provider_data"
provider_version = "0.6.6"
terraform_version = "1.1.3"
}
We can define the generation of a backend configuration for all stacks by defining a generate_hcl
block in the root of the project:
generate_hcl "backend.tf" {
content {
backend "local" {
param = global.backend_data
}
}
}
Which will generate code for all stacks, creating a file named backend.tf
on each stack:
backend "local" {
param = "backend_data"
}
To generate provider/terraform configuration for all stacks we can add in the root configuration:
generate_hcl "provider.tf" {
content {
provider "name" {
param = global.provider_data
}
terraform {
required_providers {
name = {
source = "integrations/name"
version = global.provider_version
}
}
}
terraform {
required_version = global.terraform_version
}
}
}
Which will generate code for all stacks, creating a file named provider.tf
on each stack:
provider "name" {
param = "provider_data"
}
terraform {
required_providers {
name = {
source = "integrations/name"
version = "0.6.6"
}
}
}
terraform {
required_version = "1.1.3"
}
tm_dynamic block
The tm_dynamic
is a special block type that can only be used inside the content
block of the generate_hcl
block. It's similar to Terraform dynamic blocks but supports partial evaluation of the expanded code.
The generate block's attributes can be provided by a content
block, an attributes
attribute, or even both if they don't conflict. When using the content
block, additional sub-blocks can be generated and nested tm_dynamic
blocks can be defined.
Example using the content
block:
globals {
values = ["a", "b", "c"]
}
generate_hcl "file.tf" {
content {
tm_dynamic "block" {
for_each = global.values
iterator = value
content {
attr = "index: ${value.key}, value: ${value.value}"
attr2 = not_evaluated.attr
}
}
}
}
Which generates a file.tf
file like this:
block {
attr = "index: 0, value: a"
attr2 = not_evaluated.attr
}
block {
attr = "index: 1, value: b"
attr2 = not_evaluated.attr
}
block {
attr = "index: 2, value: c"
attr2 = not_evaluated.attr
}
Additionally, a labels
attribute can be provided for generating the block's labels. Example:
globals {
values = ["a", "b", "c"]
}
generate_hcl "file.tf" {
content {
tm_dynamic "block" {
for_each = global.values
iterator = value
labels = ["some", "labels", value.value]
content {
key = value.key
value = value.value
}
}
}
}
which generates:
block "some" "labels" "a" {
key = 0
value = "a"
}
block "some" "labels" "b" {
key = 1
value = "b"
}
block "some" "labels" "c" {
key = 2
value = "c"
}
The labels
must evaluate to a list of strings, otherwise it fails.
The tm_dynamic
content block only evaluates the Terramate variables/functions, everything else is just copied as is to the final generated code.
The same goes when using attributes
:
globals {
values = ["a", "b", "c"]
}
generate_hcl "file.tf" {
content {
tm_dynamic "block" {
for_each = global.values
iterator = value
attributes = {
attr = "index: ${value.key}, value: ${value.value}"
attr2 = not_evaluated.attr
}
}
}
}
Also generates a file.tf
file like this:
block {
attr = "index: 0, value: a"
attr2 = not_evaluated.attr
}
block {
attr = "index: 1, value: b"
attr2 = not_evaluated.attr
}
block {
attr = "index: 2, value: c"
attr2 = not_evaluated.attr
}
The for_each
attribute is optional. If it is not defined then only a single block will be generated and no iterator will be available on block generation.
The tm_dynamic
block also supports an optional condition
attribute that must evaluate to a boolean. When not defined it is assumed to be true. If the condition
is false the tm_dynamic
block is ignored, including any of its nested tm_dynamic
blocks. No other attribute of the tm_dynamic
block is evaluated if the condition
is false, so it is safe to use it like this:
generate_hcl "file.tf" {
content {
tm_dynamic "block" {
for_each = global.values
condition = tm_can(global.values)
iterator = value
attributes = {
attr = "index: ${value.key}, value: ${value.value}"
attr2 = not_evaluated.attr
}
}
}
}
And if global.values
is undefined the block is just ignored.
Hierarchical Code Generation
HCL code generation can be defined anywhere inside a project, from a specific stack, which defines code generation only for the specific stack, to parent dirs or even the project root, which then has the potential to affect code generation to multiple or all stacks (as seen in the previous example).
There is no overriding or merging behavior for generate_hcl
blocks. Blocks defined at different levels with the same label aren't allowed, resulting in failure for the overall code generation process.
Conditional Code Generation
Conditional code generation is achieved by the use of the condition
attribute. The condition
attribute should always evaluate to a boolean. The file will be generated only if it evaluates to true.
If the condition
attribute is absent then it is assumed to be true.
Any expression that produces a boolean can be used, including references to globals and function calls. For example:
generate_hcl "file" {
condition = tm_length(global.list) > 0
content {
locals {
list = global.list
}
}
}
Will only generate the file for stacks that the expression tm_length(global.list) > 0
evaluates to true.
When condition
is false the content
block won't be evaluated.
Partial Evaluation
A partial evaluation strategy is used when generating HCL code. This means that you can generate code with unknown references/function calls and those will be copied verbatim to the generated code.
Lets assume we have a single global as Terramate data:
globals {
terramate_data = "terramate_data"
}
And we want to mix this Terramate references with Terraform references, like locals/vars/outputs/etc. All we have to do is define our generate_hcl
block like this:
generate_hcl "main.tf" {
content {
resource "myresource" "name" {
count = var.enabled ? 1 : 0
data = global.terramate_data
path = terramate.path
name = local.name
}
}
}
And it will generate the following main.tf
file:
resource "myresource" "name" {
count = var.enabled ? 1 : 0
data = "terramate_data"
path = "/path/to/stack"
name = local.name
}
The global.terramate_data
and terramate.path
references were evaluated, but the references to var.enabled
and local.name
were retained as is, hence the partial evaluation.
Function calls are also partially evaluated. Any unknown function call will be retained as is, but any function call starting with the prefix tm_
is considered a Terramate function and will be evaluated. Terramate function calls can only have as parameters Terramate references or literals.
For example, given:
generate_hcl "main.tf" {
content {
resource "myresource" "name" {
data = tm_upper(global.terramate_data)
name = upper(local.name)
}
}
}
This will be generated:
resource "myresource" "name" {
data = "TERRAMATE_DATA"
name = upper(local.name)
}
If one of the parameters of a unknown function call is a Terramate reference the value of the Terramate reference will be replaced on the function call.
This:
generate_hcl "main.tf" {
content {
resource "myresource" "name" {
data = upper(global.terramate_data)
name = upper(local.name)
}
}
}
Generates:
generate_hcl "main.tf" {
content {
resource "myresource" "name" {
data = upper("terramate_data")
name = upper(local.name)
}
}
}
Currently there is no partial evaluation of for
expressions. Referencing Terramate data inside a for
expression will result in an error (for
expressions with unknown references are copied as is).