Getting started
tf.libsonnet
In this section we cover installing tf.libsonnet
and getting started with writing your first Terraform Jsonnet code.
Target Terraform module
In this guide, we will work on using tf.libsonnet
to generate the equivalent JSON configuration for the following
Terraform code:
terraform {
required_providers {
null = {
source = "hashicorp/source"
version = "~> 3.0"
}
}
}
variable "trigger_id" {}
resource "null_resource" "this" {
triggers = {
id = var.trigger_id
}
}
output "this_id" {
value = null_resource.this.id
}
Installing Jsonnet and tf.libsonnet/core
To get started using tf.libsonnet
, you need to first install Jsonnet and
jsonnet-bundler (pkg manager). Since tf.libsonnet
is a pure
Jsonnet library for generating Terraform code, we can use the official tools for executing the code.
Once you have jsonnet
and jb
installed, you can start adding the tf.libsonnet
libraries to your project. We will
start with the core library, and later on switch to using the dedicated provider
library.
Run the following to install tf.libsonnet/core
with jb
:
jb init
jb install github.com/tf-libsonnet/core@v0.0.2
Adding a null_resource
Now that tf.libsonnet/core
is installed, let’s start building the Terraform module!
The core
library contains utilities for generating the base Terraform blocks in your document. You can see the list of
exported functions in the reference docs. We will start with
generating the null_resource
block by using
withResource function.
In your editor, create a file named main.tf.jsonnet
and add the following:
local tf = import 'github.com/tf-libsonnet/core/main.libsonnet';
tf.withResource('null_resource', 'this', {})
The above code imports the tf.libsonnet/core
library and uses it to generate a single null_resource
resource block
in the final document.
You can generate the corresponding Terraform code using the jsonnet
CLI:
jsonnet -J ./vendor -c -o out/main.tf.json main.tf.jsonnet
Here is an explanation of the arguments we are passing to jsonnet
:
-J ./vendor
adds thevendor
directory created byjb
to the library path. This ensures thatjsonnet
can findtf.libsonnet
when resolving theimport
calls.-o out/main.tf.json
tellsjsonnet
where to store the resulting JSON file.-c
tellsjsonnet
create the output directories.main.tf.jsonnet
is the file we want to compile withjsonnet
.
You should see the compiled Terraform code in the ./out/main.tf.json
file, which should look like the following:
{
"resource": {
"null_resource": {
"this": { }
}
}
}
You can now execute terraform
against the compiled code. Run the following to try it out!
cd out
terraform init
terraform apply
Adding a variable and binding triggers
Let’s augment the null_resource
with a trigger. The triggers
attributes allows you to control when the
null_resource
should be regenerated.
Use the withVariable function to add a variable,
and link it to the triggers
attribute:
local tf = import 'github.com/tf-libsonnet/core/main.libsonnet';
tf.withVariable('trigger_id')
+ tf.withResource('null_resource', 'this', {
triggers: {
trigger_id: '${var.trigger_id}',
},
})
When compiled, the resulting code should look like the following:
{
"resource": {
"null_resource": {
"this": {
"triggers": {
"trigger_id": "${var.trigger_id}"
}
}
}
},
"variable": {
"trigger_id": { }
}
}
In this way, you can use the attrs
parameter of the withResource
function to set the different attributes of the
resulting Terraform block.
Adding the required_providers block
The above code works as is, but in production you will want to ensure you are version locking your providers. You can do
this by adding the required_providers
Terraform block. In tf.libsonnet
, this block can be added with the
withProvider function.
Update your main.tf.jsonnet
file with the following:
local tf = import 'github.com/tf-libsonnet/core/main.libsonnet';
tf.withProvider('null', {}, src='hashicorp/null', version='~>3.0')
+ tf.withVariable('trigger_id')
+ tf.withResource('null_resource', 'this', {
triggers: {
trigger_id: '${var.trigger_id}',
},
})
Recompile the code, and inspect the resulting Terraform JSON. It should now include the required_providers
block to
version lock the null
provider:
{
"provider": {
"null": [
{ }
]
},
"resource": {
"null_resource": {
"this": {
"triggers": {
"trigger_id": "${var.trigger_id}"
}
}
}
},
"terraform": {
"required_providers": {
"null": {
"source": "hashicorp/null",
"version": "~>3.0"
}
}
},
"variable": {
"trigger_id": { }
}
}
Adding the output reference
Let’s complete the module by outputing the null_resource
ID. To do this, we will take advantage of the self reference
generator injected by the withResource
function. As indicated in the withResource function
docs, every resource injects a self reference
to the _ref
attribute.
To be able to use the self reference links, you need to bind the resource to a local
reference. Update your code with
the following:
local tf = import 'github.com/tf-libsonnet/core/main.libsonnet';
local o =
tf.withProvider('null', {}, src='hashicorp/null', version='~>3.0')
+ tf.withVariable('trigger_id')
+ tf.withResource('null_resource', 'this', {
triggers: {
trigger_id: '${var.trigger_id}',
},
})
+ tf.withOutput(
'this_id',
o._ref.null_resource.this.get('id'),
);
// Don't forget to declare the local var `o` as the final object for the document!
o
Note the o._ref.null_resource.this.get('id')
. When compiled, this generates the Terraform interpolation to reference
the id
field of the this
instance of the null_resource
resource (${null_resource.this.id}
). You can of course
replace that with the raw string, but using the _ref
reference ensures that you are referencing resources that exist
in the document. In essence, it provides a compile time assertion as opposed to run time assertion (e.g., you can
validate before the code is generated, vs checking during a terraform validate
call).
This will generate the final JSON we want for our module:
{
"output": {
"this_id": {
"value": "${null_resource.this.id}"
}
},
"provider": {
"null": [
{ }
]
},
"resource": {
"null_resource": {
"this": {
"triggers": {
"trigger_id": "${var.trigger_id}"
}
}
}
},
"terraform": {
"required_providers": {
"null": {
"source": "hashicorp/null",
"version": "~>3.0"
}
}
},
"variable": {
"trigger_id": { }
}
}
Using provider specific libraries
Using the core
library allows you to generate arbitrary Terraform code, but with real world use cases, it is better to
have some type safety. Jsonnet is not a statically typed language so you won’t get full static type safety like you do
with TypeScript, but you can get some limited form of type safety by using the provider specific libraries in
tf.libsonnet
. These libraries export every resource and data source that is supported by the provider as Jsonnet
functions. Using these libraries can give you compile time guarantees for the references and attributes.
For example, if you had misspelled triggers
in the attribute object, jsonnet
will compile the code down to
JSON, but terraform
will not be happy with it. With the library functions, triggers
is an explicit function
parameter so jsonnet
will complain if you mistype the name.
Refer to the Supported Providers page for the list of officially maintained provider libraries.
Let’s shift this check left by using the tf.libsonnet/hashicorp-null
library to generate the resource. Update the
code to reference the null
provider library:
local tf = import 'github.com/tf-libsonnet/core/main.libsonnet';
// NOTE: ideally we can bind `null`, but `null` is a reserved word so we use `tfnull` as an alternative.
local tfnull = import 'github.com/tf-libsonnet/hashicorp-null/main.libsonnet';
local o =
tf.withProvider('null', {}, src='hashicorp/null', version='~>3.0')
+ tf.withVariable('trigger_id')
+ tfnull.resource.new(
'this',
triggers={
trigger_id: '${var.trigger_id}',
},
)
+ tf.withOutput(
'this_id',
o._ref.null_resource.this.get('id'),
);
o
Before you can compile the code, you will need to make sure the library is available to Jsonnet. Install the library with jb
:
jb install github.com/tf-libsonnet/hashicorp-null@v0.0.3
Finally, run jsonnet
to generate the compiled code and verify your results. It should look exactly the same as it was
prior to using the null.resource.new
function.
Note that this is where the true strengths of the compiler pattern emerges. This switch to tf.libsonnet/hashicorp-null
is considered a refactor of your Jsonnet code. You are modifying the Jsonnet code without changing the resulting
Terraform. This means that you can be confident that you didn’t change any behavior as long as the generated code is
equivalent: you don’t need to run terraform validate
or terraform plan
to check! To drive this, you can output the
code to a different directory and then run a diff
to verify there are no changes. Or in real world scenarios, you can
rely on git
and GitOps, where you only commit the resulting Terraform files if there are any changes.
Where to go from here
Learn more about the Jsonnet language:
Get help on the tf.libsonnet GitHub Discussion.