I am looking for a way to generate Terraform code based on JSON values.
Imagine I have a JSON file with the following structure:
{
"settings": [
{
"conf": [
{
"setting": "DeploymentPolicy",
"namespace": "aws:elasticbeanstalk:command",
"value": "AllAtOnce"
},
{
"setting": "BatchSize",
"namespace": "aws:elasticbeanstalk:command",
"value": "30"
},
{
"setting": "BatchSizeType",
"namespace": "aws:elasticbeanstalk:command",
"value": "Percentage"
}
]
}
]
}
What I want to do is the following:
Creating a working Terraform resource based on the JSON file values, e.g. a beanstalk environment like this:
resource "aws_elastic_beanstalk_environment" "app_prod" {
name = "${aws_elastic_beanstalk_application_version.app.name}-prod"
application = aws_elastic_beanstalk_application.app.name
solution_stack_name = data.aws_elastic_beanstalk_solution_stack.latest_linux_java.name
wait_for_ready_timeout = "10m"
version_label = aws_elastic_beanstalk_application_version.app.name
# Elastic beanstalk configuration
setting {
name = "DeploymentPolicy"
namespace = "aws:elasticbeanstalk:command"
value = "AllAtOnce"
}
setting {
name = "BatchSize"
namespace = "aws:elasticbeanstalk:command"
value = "30"
}
...
}
Therefore I have to create the settings block in HCL (Terraform configuration) based on the JSON values.
This means the JSON file above should result in:
setting {
name = "DeploymentPolicy"
namespace = "aws:elasticbeanstalk:command"
value = "AllAtOnce"
}
setting {
name = "BatchSize"
namespace = "aws:elasticbeanstalk:command"
value = "30"
}
setting {
name = "BatchSizeType"
namespace = "aws:elasticbeanstalk:command"
value = "Percentage"
}
As you can see, the structure of JSON and HCL is very similar, but not identical. See e.g. settings, conf, or setting instead of name in the JSON.
A possible approach would be to read the JSON values and store them in an array or a map. But I have no idea how I could generate valid HCL and inject it in the desired part of the resource. Furthermore I tried to use a template but Terraform does not support the looping functionality that I need to iterate over the settings.
To sum up:
Input is a JSON file that must be read
JSON contains settings (besides other information)
The number of settings can differ
Somehow I have to generate a settings block
Somehow I have to inject this settings blok in the resource
Does anyone have an idea how to do that? Any other approaches?
Thanks a lot!
Assuming that your JSON object were in a file called settings.json inside your module directory, you could do something like this:
locals {
environment_settings = jsondecode(file("${path.module}/settings.json")).settings[0].conf[0]
}
resource "aws_elastic_beanstalk_environment" "app_prod" {
name = "${aws_elastic_beanstalk_application_version.app.name}-prod"
application = aws_elastic_beanstalk_application.app.name
solution_stack_name = data.aws_elastic_beanstalk_solution_stack.latest_linux_java.name
wait_for_ready_timeout = "10m"
version_label = aws_elastic_beanstalk_application_version.app.name
dynamic "setting" {
for_each = local.environment_settings
content {
namespace = setting.value.namespace
name = setting.value.setting
value = setting.value.value
}
}
}
This special dynamic block is a sort of macro to create repeated setting blocks, each one correlating with one element of the collection given in for_each.
You can do whatever transformations of the input you need using Terraform's expression language in the locals block to ensure that the local.environment_settings value contains one element for each setting block you will generate, and then in the content nested block tell Terraform how to populate the setting arguments based on those element values.
Related
A JSON string string passes the jsonlint test.
response = [
{
"article" : {
"info" : {
"initial" : {
"articleIds" : [
"7461221587662919569"
],
}
},
"text" : "where they would 'transfer to' next.",
"lang" : "en",
}
},
{
"article" : {
"info" : {
"initial" : {
"articleIds" : [
"6613144915874808065"
],
}
},
"text" : "produto regional.",
"lang" : "pt"
}
}
]
However, after processing
require 'json'
file = File.read('/Users/main/jugg//article_samples.js')
data_hash = JSON.parse(file)
One is left with an array, whereas more frequently a hash with a name labels a subsequent array, where one works with that nomenclature such as response['data']
But in this case the array is not accessible via response[0]. How can this be considered as an array in order to process each individual element collection.each do |member|?
A curiosity: data_hash.class => NilClass
The response = ... code from article_samples.js is JavaScript, not JSON. This initializes a variable named response with a JavaScript array.
To use this as JSON, then rename the file to article_samples.json and remove response = from the file. The first line should start with [.
Now your second block of code should work just fine as long as the article_samples.json file is in the correct path.
On a side note, I suggest that you find a way to make the path more flexible. The way you have it currently hard coded is tied directly to your current machine's file system. This won't work if you want to run this code from another machine because the folder /Users/main/jugg probalby won't exist.
If this is a web server with ruby on rails, then one solution is to create an environment variable with the path where this file is stored.
I need to add a conditional when the variable is true, add a block of code in my JSON file but if this variable is false, I need it to do nothing
This is my main.tf
resource "grafana_dashboard" "dashboard_test" {
conficonfig_json = template_file("dashboard.json")
data_source = var.data_source
}
I need add this a block of code in my file JSON
{
"datasource": {
"type": "CloudWatch",
"uid": "${mystring}"
}
}
You should probably switch to using templatefile function [1]. In your example, you would then have:
resource "grafana_dashboard" "dashboard_test" {
config_json = templatefile("dashboard.json", {
mystring = "somevalue"
})
data_source = var.data_source
}
If you do not want to hardcode the value for the mystring variable, you could alternatively use a Terraform variable e.g., mystring = var.mystring. I would also avoid giving just the filename and change the block of code to look like this:
resource "grafana_dashboard" "dashboard_test" {
config_json = templatefile("${path.root}/dashboard.json", {
mystring = var.mystring
})
data_source = var.data_source
}
variable "mystring" {}
More information about using path-based variables is in [2].
[1] https://www.terraform.io/language/functions/templatefile
[2] https://www.terraform.io/language/expressions/references#filesystem-and-workspace-info
Hey team I’m having trouble finding in the documentation on how to add terraform variables in a JSON file,
I need inject this variable in this JSON,
In this JSON of this shape but not it works,
I did try with var and locals, I tried it with var and locals, but it does not work, it is by default
You could use templatefile function [1]:
locals {
mystring = "Test"
}
resource "grafana_dashboard" "metrics" {
config_json = templatefile("${path.root}/EC2.json.tpl", {
mystring = local.mystring
})
}
For this to work, you would have to change the JSON to be:
"datasource": {
"type": "CloudWatch"
"uid": "${mystring}"
}
The file with JSON data should also be renamed to EC2.json.tpl.
[1] https://www.terraform.io/language/functions/templatefile
To provision tag policies in an AWS organization, I need to build the JSON content from variables. Management of tag policies, scp, etc. shall be centralized, so changes can be applied everywhere: Renaming, adding, removing tags, etc.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
}
}
}
provider "aws" {
profile = "default"
region = "us-west-1"
}
The problem at hand I am facing is: How would I build the JSON object?
Example variable/ tag map:
# tag_policies.tf
variable "resource_tags" {
description = "Central resource tags"
type = list( object( {
name = string
tags = map(string)
} ) )
default = [
{
name = "Environment"
tags = {
prod = "crn::env:prod"
lab = "crn::env:lab"
dev = "crn::env:dev"
}
}
]
}
What I have tried so far is to use HCL template tags, but I end up with one , comma too much when iterating through the map of tag names. This works fine for the join() with the sub-map of tag names, but does not workout if I try to wrap the template markup. Why did I try this? Because I ran out of ideas.
# vars.tf
resource "aws_organizations_policy" "root-tag-policy" {
name = "RootTagPolicy"
type = "TAG_POLICY"
content = <<CONTENT
{
"tags": {
%{ for tag in var.resource_tags_env ~}
"${tag.name}": {
"tag_key": {
"##assign": "${tag.name}",
"##operators_allowed_for_child_policies": [ "##none" ]
},
"tag_value": { "##assign": [ "${join( ", ", values( tag.tags ) )}" ] }
},
%{ endfor ~}
}
}
CONTENT
}
The solution actually was quite simple: Iterate of the tags using a for expression and enclose it with curly braces { … } to return an object (=> returns tuples).
Finally jsonencode() cares about converting the HCL key = value syntax to proper JSON.
resource "aws_organizations_policy" "root-tag-policy" {
name = "RootTagPolicy"
type = "TAG_POLICY"
content = jsonencode( [ for key, tag in var.resource_tags: {
"${tag.name}" = {
"tag_key" = {
"##assign" = tag.name,
"##operators_allowed_for_child_policies" = [ "##none" ]
},
"tag_value" = { "##assign" = [ join( ", ", values( tag.tags ) ) ] }
}
} ] )
}
EDIT This still does not work, as I forgot that the whole JSON object needs to get wrapped inside a tags: {}.
kaiser's answer shows a good general approach: build a suitable data structure and then pass it to jsonencode to get a valid JSON string from it.
Here's an example that I think matches what the string template in the original question would've produced:
content = jsonencode({
tags = {
for tag in var.resource_tags_env : tag.name => {
tag_key = {
"##assign" = tag.name
"##operators_allowed_for_child_policies" = ["##none"]
}
tag_value = {
"##assign" = values(tag.tags)
}
}
}
})
I'm not familiar with the aws_organizations_policy resource type so I'm sorry if I got some details wrong here, but hopefully you can adapt the above example to generate the JSON data structure you need.
After reading #martin-atkins answer, I finally understood how the for works for objects and maps. The var before the => arrow actually is part of the resulting object. (This highly confused me as I compared it to other languages arrow functions and arguments.)
The first part of the process is to build a map of maps. The main reason is that I don't want to have a convention of a name key in a map of variables. This might lead to handling of conventions later on, what should be avoided at all costs as it is a possible trap if one does not pay close attention or is aware of it. So the key actually is the name now.
Data Structure
variable "resource_tags" {
description = "Central resource tags"
type = map(
map(string)
)
default = {
Environment = {
common = "grn::env:common"
prod = "grn::env:prod"
stage = "grn::env:stage"
dev = "grn::env:dev"
demo = "grn::env:demo"
lab = "grn::env:lab"
},
Foo = {
bar = "baz"
}
}
}
The content as JSON
After understanding that the key in { "tags": { … } } is just the part before the =>, I could reduce the final resource to the following block.
resource "aws_organizations_policy" "root-tag-policy" {
name = "RootTagPolicy"
description = "Tag policies, assigned to the root org."
type = "TAG_POLICY"
content = jsonencode({
tags = {
for key, tags in var.resource_tags : key => {
tag_key = {
"##assign" = key
"##operators_allowed_for_child_policies" = ["##none"]
}
tag_value = {
"##assign" = values( tags )
}
}
}
})
}
Quick test:
Add the following output statement after the resource block:
output "debug" {
value = aws_organizations_policy.tp_root-tag-policy.content
}
Now apply (or plan or refresh) just this resource. It's faster this way. Then output the built debug from the apply or refresh run.
$ terraform apply -target=aws_organizations_policy.root-tag-policy
…things happening…
$ terraform output debug | json_pp
ProTips:
Pipe the output of the output directly into json_pp or jq so you can read it.
Use jq . if you want validation on top. If you see the output, it means it's valid. Else you should receive 0 as response.
I have a terraform template file source.tpl - it's a json and it has to be JSON, because it's produced by python json library. This file has the following entry
[
{
"data": {
"address": "${NETWORK}",
"netmask": "${NETMASK}",
}
}
]
In my tf module, I render this template:
data "template_file" "source" {
template = "${file("${path.module}/source.tpl")}"
vars = {
NETWORK = element(split("/", "${var.cidr}"),0)
NETMASK = tonumber(element(split("/", "${var.cidr}"),1))
}
}
where cidr is a string - something like 10.1.1.0/24
In the rendered output I need NETMASK to be a number and NETWORK to be a string. I.e. it has to be something like:
data = {
address = "10.1.1.0"
netmask = 24
}
But I'm getting:
data = {
address = "10.1.1.0"
netmask = "24"
}
I.e. netmask is a string. How can I get rid of those quotes in terraform? Initial source.tpl should still have those quotes, because if I remove them - it becomes invalid JSON.
I understand the problem here, you're generating the template using a JSON library that cannot produce something like the following since it's invalid JSON, though this is what you want for the template to be
[
{
"data": {
"address": "${NETWORK}",
"netmask": ${NETMASK}
}
}
]
Might I recommend a little bit of preprocessing? For example
template = "${replace(file("${path.module}/source.tpl"), "\"$${NETMASK}\"", "$${NETMASK}")}"