variables.tf
variable "teams" {
type = map(any)
default = {}
}
input_value:
teams = {
{
team_id = "abc"
role_names = ["owner"]
},
{
team_id = "bcd"
role_names = ["read", "write"]
}
}
}
main.tf:
resource "mongodbatlas_project" "project" {
name = "testing"
org_id = "123456"
dynamic "teams" {
for_each = var.teams
content {
id = teams.value.team_id
names = [teams.value.role_names]
}
}
}
I have been trying the above code and it is not working. Is there an easier way to assign nested team value to the variable?
The teams variable does not seem to be correct for me and there are syntax errors (e.g. extra }in teams). I think it should be list, not map:
variable "teams" {
type = list(any)
default = []
}
and then
teams = [
{
team_id = "abc"
role_names = ["owner"]
},
{
team_id = "bcd"
role_names = ["read", "write"]
}
]
Then your resource could be:
resource "mongodbatlas_project" "project" {
name = "testing"
org_id = "123456"
dynamic "teams" {
for_each = toset(var.teams)
content {
id = teams.value.team_id
names = teams.value.role_names
}
}
}
When using dynamic blocks the iterator is called same as the block name.
Related
I am trying to output gcp project information by doing something like this :
output "projects" {
value = tomap({
for project_name in ["project_1", "project_2", "project_3"] :
project_name => tomap({
id = google_project."${project_name}".id
number = google_project."${project_name}".number
})
})
description = "Projects"
}
Or like this :
output "projects" {
value = tomap({
for_each = toset([google_project.project_1,google_project.project_2])
id = each.key.id
number = each.key.number
})
description = "Projects"
}
Is it at all possible to use resource names this way? Do I have to specify every resource by duplicating code?
E.g.
output "projects" {
value = tomap({
project_1 = tomap({
id = google_project.project_1.id
number = google_project.project_1.number
})
project_2 = tomap({
id = google_project.project_2.id
number = google_project.project_2.number
})
project_3 = tomap({
id = google_project.project_3 .id
number = google_project.pproject_3 .number
})
})
description = "Projects"
}
EDIT : declared resources.
In main.tf projects 1 to 3 are declared the same way.
resource "google_project" "project_3" {
name = var.projects.project_3.name
project_id = var.projects.project_3.id
folder_id = google_folder.parent.name
billing_account = data.google_billing_account.acct.id
auto_create_network = false
}
in variables.tf
variable "projects" {
type = map(object({
name = string
id = string
}))
}
in variables.tfvars
projects = {
project_1= {
name = "project_1"
id = "project_1-12345"
}
project_2= {
name = "project_2"
id = "project_2-12345"
}
project_3= {
name = "project_2"
id = "project_2-12345"
}
}
I misunderstood your question originally. I see now that you want to reference a resource by a variable name. No you cannot do that. But your setup here doesn't really make sense, and seems more complex than it needs to be.
Consider if these options would improve your setup.
locals {
projects = { # This is equivalent to your input.
project_1 = {
name = "project_1"
id = "project_1-12345"
}
project_2 = {
name = "project_2"
id = "project_2-12345"
}
project_3 = {
name = "project_3"
id = "project_3-12345"
}
}
}
resource "google_project" "this" {
for_each = local.projects
name = each.key # or each.value.name / don't really need name
project_id = each.value.id
folder_id = google_folder.parent.name
billing_account = data.google_billing_account.acct.id
auto_create_network = false
}
output "projects_from_input" {
description = "You can of course, just use the input."
value = local.projects
}
output "projects_explicit_values" {
description = "Alternatively, if you need a subset of resource values."
value = { for k, v in google_project.this : k => {
name = v.name
id = v.project_id
} }
}
output "complete_resources" {
description = "But you can just output the complete resource."
value = google_project.this
}
I edited my initial answer after seeing the Terraform resource that creates a project. The need is a way to get a resource name in the output bloc with interpolation.
I think if a single resource is used to create all the projets instead of one resource per projet, it's easier to expose this resource in the output bloc.
For example you can configure projects metadata information from a json file, or directly a local variable or a var if needed :
Example for a json file and local variable
mymodule/resource/projects.json :
{
"projects": {
"project_1": {
"id": "project_1",
"number": "23333311"
},
"project_2": {
"id": "project_2",
"number": "33399999"
}
}
}
Then retrieve projects as a variable from locals.tf file :
mymodule/locals.tf :
locals {
projects = jsondecode(file("${path.module}/resource/projects.json"))["projects"]
}
Create your projects in a single resource with a foreach :
resource "google_project" "projects" {
for_each = local.projects
name = each.key
project_id = each.value["id"]
folder_id = google_folder.parent.name
billing_account = data.google_billing_account.acct.id
auto_create_network = false
}
Expose the projects resource in an output.tf file :
output "projects" {
value = google_project.projects
description = "Projects"
}
The same principle can be done with a var instead of local variable.
I have defined a variable tenants which is a map:
variable tenants {
type = map
default = {
tenant1 = {
name = "Tenant1"
},
tenant2 = {
name = "Tenant2"
}
}
}
In the root file, I call a module tenant as follows:
module "tenant" {
source = "./modules/tenant"
for_each = var.tenants
tenant = each.value.name
}
The child module tenant looks as follows:
resource "mso_tenant" "tenant" {
name = var.tenant
}
And I have defined an output in the child module with:
output "mso_tenant" {
value = mso_tenant.tenant
In the output file under root, I do the following to print all tenants:
output "tenant_names" {
value = { for p in sort(keys(var.tenants)) : p => module.tenant[p].mso_tenant.name }
}
So far so good, all the above works.
Now I have another variable called schemas, also a map. A tenant can have multiple schema's. So I have defined the following
variable schemas {
type = map
default = {
schema1 = {
name = "Schema1",
template_name = "Template1",
tenant = <refer to tenant1> <==== refer to tenant module
},
schema2 = {
name = "Schema2"
template_name = "Template2",
tenant = <refer to tenant2> <==== refer to tenant module
},
schema3 = {
name = "Schema3"
template_name = "Template3",
tenant = <refer to tenant1> <==== refer to tenant module
},
}
}
In the main.tf file under root I want to do the following:
module "schema" {
source = "./modules/schema"
for_each = var.schemas
name = each.value.name
template_name = each.value.template_name
tenant_id = each.value.tenant
}
How could I reference the respective tenants in either the schema variable or else directly in the schema module?
Update:
Tried solution 1:
In variables file, I passed the tenant as follows:
schema1 = {
name = "Schema1",
template_name = "Template1",
tenant = module.tenant["tenant1"].mso_tenant
}
Error: Variables not allowed
Tried solution 2:
module "tenant" {
source = "./modules/tenant"
for_each = var.tenants
tenant = each.value.name
}
module "schema" {
source = "./modules/schema"
for_each = var.schemas
name = each.value.name
template_name = each.value.template_name
tenant_id = module.tenant[each.value.tenant].mso_tenant
}
Resulting in following error:
on main.tf line 30, in module "schema":
30: tenant_id = module.tenant[each.value.tenant].mso_tenant
|----------------
| each.value is object with 2 attributes
This object does not have an attribute named "tenant".
If you want to refer to the tenant resources of tenant1 you can use module.tenant["tenant1"].mso_tenant and assign this directly to the schema or in the input variables of the second module.
As you are using module for_each module.tenant is a map of objects keyed by the tenant_id. Each object consists of all the outputs of your tenant child module.
REFACTORED:
root variable definitions:
variable tenants {
type = map
default = {
tenant1 = {
name = "Tenant1"
},
tenant2 = {
name = "Tenant2"
}
}
}
variable schemas {
type = map
default = {
schema1 = {
name = "Schema1",
template_name = "Template1",
tenant_id = "tenant1"
},
schema2 = {
name = "Schema2"
template_name = "Template2",
tenant_id = "tenant2"
},
schema3 = {
name = "Schema3"
template_name = "Template3",
tenant_id = "tenant1"
},
}
}
calling the modules like this in the root:
module "tenant" {
source = "./modules/tenant"
for_each = var.tenants
tenant = each.value.name
}
module "schema" {
source = "./modules/schema"
for_each = var.schemas
name = each.value.name
template_name = each.value.template_name
tenant = module.tenant[each.value.tenant_id].mso_tenant
}
root output definitions could look like:
output "tenant_names" {
value = { for p in sort(keys(var.tenants)) : p => module.tenant[p].mso_tenant.name }
}
output "schemas" {
value = module.schema
}
this leads to the following outputs:
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
schemas = {
"schema1" = {
"schema" = {
"name" = "Schema1"
"template_name" = "Template1"
"tenant" = {
"name" = "Tenant1"
}
}
}
"schema2" = {
"schema" = {
"name" = "Schema2"
"template_name" = "Template2"
"tenant" = {
"name" = "Tenant2"
}
}
}
"schema3" = {
"schema" = {
"name" = "Schema3"
"template_name" = "Template3"
"tenant" = {
"name" = "Tenant1"
}
}
}
}
tenant_names = {
"tenant1" = "Tenant1"
"tenant2" = "Tenant2"
}
schema module used to simulate:
variable "name" {
type = string
}
variable "template_name" {
type = string
}
variable "tenant" {
type = any
}
output "schema" {
value = {
name = var.name
template_name = var.template_name
tenant = var.tenant
}
}
tenant module used to simulate:
variable "tenant" {
type = string
}
locals {
mso_tenant = {
tenant = {
name = var.tenant
}
}
}
output "mso_tenant" {
value = local.mso_tenant.tenant
}
hope in the full context this is easier and more clear ;)
I am trying to generate attributes dynamically in terraform 13. I've read through the docs but I can't seem to get this to work:
Given the following terraform:
#main.tf
locals {
secrets = {
secret1 = [
{
name = "user",
value = "secret"
},
{
name = "password",
value = "password123"
}
],
secret2 = [
{
name = "token",
value = "secret"
}
]
}
}
resource "kubernetes_secret" "secrets" {
for_each = local.secret
metadata {
name = each.key
}
data = {
[for name, value in each.value : name = value]
}
}
I would expect the following resources to be rendered:
resource "kubernetes_secret" "secrets[secret1]" {
metadata {
name = "secret1"
}
data = {
user = "secret"
password = "password123"
}
}
resource "kubernetes_secret" "secrets[secret2]" {
metadata {
name = "secret2"
}
data = {
token = "secret"
}
}
However I just get the following error:
Error: Invalid 'for' expression
on ../../main.tf line 96, in resource "kubernetes_secret" "secrets":
96: [for name, value in each.value : name = value]
Extra characters after the end of the 'for' expression.
Does anybody know how to make this work?
The correct syntax for generating a mapping using a for expression is the following:
data = {
for name, value in each.value : name => value
}
The above would actually be totally redundant, because it would produce the same value as each.value. However, because your local value has a list of objects with name and value attributes instead of maps from name to value, so to get a working result we'd either need to change the input to already be a map, like this:
locals {
secrets = {
secret1 = {
user = "secret"
password = "password123"
}
secret2 = {
token = "secret"
}
}
}
resource "kubernetes_secret" "secrets" {
for_each = local.secrets
metadata {
name = each.key
}
# each.value is already a map of a suitable shape
data = each.value
}
or, if the input being a list of objects is important for some reason, you can project from the list of objects to the mapping like this:
locals {
secrets = {
secret1 = [
{
name = "user",
value = "secret"
},
{
name = "password",
value = "password123"
}
],
secret2 = [
{
name = "token",
value = "secret"
}
]
}
}
resource "kubernetes_secret" "secrets" {
for_each = local.secrets
metadata {
name = each.key
}
data = {
for obj in each.value : obj.name => obj.value
}
}
Both of these should produce the same result, so which to choose will depend on what shape of local value data structure you find most readable or most convenient.
I'm trying to create a module for the aws_wafv2_web_acl resource and I can't figure out how to add multiple 'excluded_rule' blocks inside a dynamic block. Is this possible? Here is the resource:
resource "aws_wafv2_web_acl" "web-acl" {
name = var.name
description = ""
scope = "REGIONAL"
default_action {
allow {}
}
dynamic "rule" {
for_each = var.rules
content {
name = rule.value["name"]
priority = rule.value["priority"]
override_action {
count {}
}
statement {
managed_rule_group_statement {
name = rule.value["name"]
vendor_name = "AWS"
excluded_rule {
name = "excluded rule"
}
}
}
visibility_config {
cloudwatch_metrics_enabled = false
sampled_requests_enabled = false
metric_name = rule.value["name"]
}
}
}
visibility_config {
cloudwatch_metrics_enabled = false
sampled_requests_enabled = false
metric_name = "webaclmetric"
}
}
And here are the variables being passed:
name = "test"
rules = [
{"name": "AWSManagedRulesLinuxRuleSet", "priority": 0, "exclusions": "LFI_QUERYARGUMENTS,LFI_URIPATH"},
{"name": "AWSManagedRulesWindowsRuleSet", "priority": 1, "exclusions": "PowerShellCommands_Set1_QUERYARGUMENTS"}
]
it is possible. You may want to take a look at the terraform module I have written for WafV2 web acl -> https://github.com/umotif-public/terraform-aws-waf-webaclv2
Going back to your question you can solve it with the following block:
dynamic "excluded_rule" {
for_each = length(lookup(managed_rule_group_statement.value, "excluded_rule", {})) == 0 ? [] : toset(lookup(managed_rule_group_statement.value, "excluded_rule"))
content {
name = excluded_rule.value
}
}
and then you can pass in the following into your module
managed_rule_group_statement = {
name = "AWSManagedRulesCommonRuleSet"
vendor_name = "AWS"
excluded_rule = [
"SizeRestrictions_QUERYSTRING",
"SizeRestrictions_BODY",
"GenericRFI_QUERYARGUMENTS"
]
}
So I have created a google_bigquery module to create datasets and set access.
The module iterates over a map of list of maps. It uses the each.key to create the datasets then iterates over the list of maps to create the dynamic access.
The module works as in:
It has no errors nor warning
It deploys the resources
It populates the remote statefile appropriately.
The issue is that everytime I ran terraform it wants to re-apply the same changes, over and over again.
Clearly something is not right but not sure what.
here is the code
main.tf
locals {
env = basename(path.cwd)
project = basename(abspath("${path.cwd}/../.."))
project_name = coalesce(var.project_name, format("%s-%s", local.project, local.env))
}
data "google_compute_zones" "available" {
project = local.project_name
region = var.region
}
provider "google" {
project = local.project_name
region = var.region
version = "~> 2.0" #until 3.0 goes out of beta
}
terraform {
required_version = ">= 0.12.12"
}
resource "google_bigquery_dataset" "main" {
for_each = var.datasets
dataset_id = upper("${each.key}_${local.env}")
location = var.region
delete_contents_on_destroy = true
dynamic "access" {
for_each = flatten([ for k, v in var.datasets : [
for i in each.value : {
role = i.role
user_by_email = i.user_by_email
group_by_email = i.group_by_email
dataset_id = i.dataset_id
project_id = i.project_id
table_id = i.table_id
}]])
content {
role = lookup(access.value,"role", "")
user_by_email = lookup(access.value,"user_by_email","")
group_by_email = lookup(access.value,"group_by_email","")
view {
dataset_id = lookup(access.value,"dataset_id","")
project_id = lookup(access.value,"project_id","")
table_id = lookup(access.value,"table_id", "")
}
}
}
access {
role = "READER"
special_group = "projectReaders"
}
access {
role = "OWNER"
group_by_email = "Group"
}
access {
role = "OWNER"
user_by_email = "ServiceAccount"
}
access {
role = "WRITER"
special_group = "projectWriters"
}
}
variables.tf
variable "region" {
description = ""
default = ""
}
variable "env" {
default = ""
}
variable "project_name" {
default = ""
}
variable "owner_group" {
description = ""
default = ""
}
variable "owner_sa" {
description = ""
default = ""
}
variable "datasets" {
description = "A map of objects, including dataset_isd abd access"
type = map(list(map(string)))
}
terraform.tfvars
datasets = {
dataset01 = [
{
role = "WRITER"
user_by_email = "email_address"
group_by_email = ""
dataset_id = ""
project_id = ""
table_id = ""
},
{
role = ""
user_by_email = ""
group_by_email = ""
dataset_id ="MY_OTHER_DATASET"
project_id ="my_other_project"
table_id ="my_test_view"
}
]
dataset02 = [
{
role = "READER"
user_by_email = ""
group_by_email = "group"
dataset_id = ""
project_id = ""
table_id = ""
},
{
role = ""
user_by_email = ""
group_by_email = ""
dataset_id ="MY_OTHER_DATASET"
project_id ="my_other_project"
table_id ="my_test_view_2"
}
]
}
So the problem is that the dynamic block (the way I wrote it) can generate this output
+ access {
+ role = "WRITER"
+ special_group = "projectWriters"
+ view {}
}
this is applied, no errors, but it will want to re-apply it over and over
The issue seems to be that the provider API response doesn't include the empty view{}
Any suggestion how I could make the view block conditional on the values of it being not null?
I fixed the problem. I changed the module slightly and the variable type.
I have split the roles and the views into their own lists of maps within the parent map of datasets.
There are conditionals in each block so the dynamic block is only applied if the roles exists or views exists.
Also realized the dynamic block was iterating on the wrong iterator.
The dynamic block was iterating on var.datasets which was causing the permissions assigned to each dataset to be applied to all datasets. So now it has been changed to iterate on each.value (from the resource for_each).
Here is the new code that works
MAIN.TF
resource "google_bigquery_dataset" "main" {
for_each = var.datasets
dataset_id = upper("${each.key}_${local.env}")
location = var.region
delete_contents_on_destroy = true
dynamic "access" {
for_each = flatten([for i in each.value : [
for k, v in i : [
for l in v :
{
role = l.role
user_by_email = l.user_by_email
group_by_email = l.group_by_email
special_group = l.special_group
}]
if k == "roles"
]])
content {
role = access.value["role"]
user_by_email = access.value["user_by_email"]
group_by_email = access.value["group_by_email"]
special_group = access.value["special_group"]
}
}
dynamic "access" {
for_each = flatten([for i in each.value : [
for k, v in i : [
for l in v :
{
dataset_id = l.dataset_id
project_id = l.project_id
table_id = l.table_id
}]
if k == "views"
]])
content {
view {
dataset_id = access.value["dataset_id"]
project_id = access.value["project_id"]
table_id = access.value["table_id"]
}
}
}
}
VARIABLES.TF
variable "datasets" {
description = "A map of objects, including datasets IDs, roles and views"
type = map(list(map(list(map(string)))))
default = {}
}
continued....
Terraform.tfvars
datasets = {
dataset01 = [
{
roles = [
{
role="WRITER"
user_by_email="email_address"
group_by_email=""
special_group=""
}
]
views = [
{
dataset_id="MY_OTHER_DATASET"
project_id="my_other_project"
table_id="my_test_view"
}
]
}
]
dataset02 = [
{
roles = [
{
role="READER"
user_by_email=""
group_by_email="group"
special_group=""
}
]
views=[
{
dataset_id="MY_OTHER_DATASET"
project_id="my_other_project"
table_id="my_test_view_2"
}
]
}
]
}