Fetch a list of string from remote state - terraform

I have a VPC module that has the following outputs.
$ tf output
dev_cp_subnet_ids = [
tolist([
"subnet-0cb8b0a98205082d8",
"subnet-03964e7892b6a5336",
"subnet-0917a9e6d87918c87",
]),
]
vpc_id = "vpc-06f3520baa1199f6b"
I want to use the above values in another module i.e. terrafrom-eks
I first go ahead and declare the remote state config
data "terraform_remote_state" "vpc" {
backend = "http"
config = {
address = "..."
}
}
and then as part of the module call, do the following
module "eks" {
source = "terraform-aws-modules/eks/aws"
cluster_name = "build"
cluster_version = "1.22"
cluster_endpoint_private_access = true
cluster_endpoint_public_access = true
cluster_addons = {
coredns = {}
kube-proxy = {}
vpc-cni = {}
}
vpc_id = data.terraform_remote_state.vpc.outputs.vpc_id
subnet_ids = data.terraform_remote_state.vpc.outputs.dev_cp_subnet_ids
vpc_id is successfully fetched as I can see it in a tf plan
}
+ vpc_id = "vpc-06f3520baa1199f6b"
}
Plan: 41 to add, 0 to change, 0 to destroy.
but for dev_cp_subnet_ids, I get the following error
│ Error: Invalid value for module argument
│
│ on main.tf line 24, in module "eks":
│ 24: subnet_ids = data.terraform_remote_state.vpc.outputs.dev_cp_subnet_ids
│
│ The given value is not suitable for child module variable "subnet_ids"
│ defined at .terraform/modules/eks/variables.tf:53,1-22: incorrect list
│ element type: string required.
As per the documentation for subnet_ids, its of type list(string). My understanding is that the following output is in the format of list(string)
dev_cp_subnet_ids = [
tolist([
"subnet-0cb8b0a98205082d8",
"subnet-03964e7892b6a5336",
"subnet-0917a9e6d87918c87",
]),
]
or am I missing something here ?

The expression you showed at the end of your question has the following type:
tuple([list(string)])
That is: a single-element tuple whose element is itself a list of strings.
Because Terraform knows that subnet_ids expects a list of strings, it is first trying to automatically convert that to a list type, and producing a value of the following type as a result:
list(list(string))
Terraform then notices that the element type of this resulting list is list(string) rather than string as the argument requires, and so returns this error.
You can fix this by removing the outer brackets [ .. ] so that you are just directly assigning the tolist result, which will be a list of strings as expected:
dev_cp_subnet_ids = tolist([
"subnet-0cb8b0a98205082d8",
"subnet-03964e7892b6a5336",
"subnet-0917a9e6d87918c87",
]),

Related

Print output of an imported module

I want to run a debug session and since terraform does not have any functionality about viewing variable values when performing a plan, I am doing the following
locals {
policy = flatten([
for policy_name, policy_content in var.policy : [
for content in policy_content : [
for item in content : {
name = policy_name
capabilities = item
path = item
}
]
]
])
}
and
output "debug" {
value = local.policy
}
From the calling module:
module "policies" {
source = "./path/to/module/above"
for_each = var.policies
policy = each.value
}
output "debug" {
description = "Debug output"
value = module.policies[*].outputs.debug
}
This fails as follows:
│ on outputs.tf line 13, in output "debug":
│ 13: value = module.policies[*].outputs.debug
│
│ This object does not have an attribute named "outputs".
How can I work around that?
You can either output everything from policies:
output "debug" {
description = "Debug output"
value = module.policies
}
Or, use values() function to convert your policies map generated by for_each to a list, to which you can apply your splat expression
output "debug" {
description = "Debug output"
value = values(module.policies)[*].debug
}

Terraform Lifecycle Ignore changes

I am trying to apply lifecycle ignore_changes rule against parameter in resource resource "aws_servicecatalog_provisioned_product" as shown below.
resource "aws_servicecatalog_provisioned_product" "example" {
name = "example"
product_name = "Example product"
provisioning_artifact_name = "Example version"
provisioning_parameters {
key = "foo"
value = "bar"
}
provisioning_parameters {
key = "key2"
value = lookup(var.parameter_group, "key2", "test2")
}
provisioning_parameters {
key = "key3"
value = "test3"
}
tags = {
foo = "bar"
}
lifecycle {
ignore_changes = [
tags["foo"],
aws_servicecatalog_provisioned_product.provisioning_parameters.example["key2"]
]
}
}
variable parameter_group {
description = "Parameters map required for modules.
type = map(any)
default = {}
}
when i am running the plan i am getting below error
│ Error: Unsupported attribute
│
│ on modules/example_provision/main.tf line 28, in resource "aws_servicecatalog_provisioned_product" "example":
│ 28: aws_servicecatalog_provisioned_product.provisioning_parameters.example["key2"]
│
│ This object has no argument, nested block, or exported attribute named "aws_servicecatalog_provisioned_product".
I would like to ignore the changes made to this parameter value. The Ignore on tags is working fine but as soon as i add my second line which is aws_servicecatalog_provisioned_product.provisioning_parameters.example["key2"] the error starts to come in.
looking for suggestion/help here :)
Regards
ignore_changes can only ignore changes to the configuration of the same resource where it's declared, and so you only need to name the argument you wish to ignore and not the resource type or resource name:
lifecycle {
ignore_changes = [
tags["foo"],
provisioning_parameters,
]
}
The provisioning_parameters block type doesn't seem to be represented as a mapping (the provisioning_parameter blocks don't have labels in their headers) so you won't be able to refer to a specific block by its name.
However, the provider does declare it as being a list of objects and so I think you will be able to ignore a specific item by its index, where the indices are assigned in order of declaration. Therefore in your current example the one with key = "key2" will have index 1, due to being the second block where Terraform counts up from zero:
lifecycle {
ignore_changes = [
tags["foo"],
provisioning_parameters[1],
]
}

How to loop over a map and return value if it matches with a value from a list in Terraform

I have a map with variable names of a subnet and their ids
eg:
subnet_id = {
subnet-a="XXXX/subnet-a",
subnet-b="XXXX/subnet-b",
subnet-c="XXXX/subnet-c"
}
I have a list variable(subnet_variable_name) with the variable names of subnets (which is a value inside a map variable:network_rule)
eg:
network_rule = {
rule1 = {
subnet_variable_name = ["subnet-a","subnet-b"]
rule = []
}
}
How do I loop through subnet_id and return the values where the key matches with values in list subnet_variable_name ? I tried using the below for function but it is throwing an error.
resource "xxx" "xxx"{
for_each = var.network_rule
subnet_ids = {for value in each.value["subnet_variable_name"]: value => lookup(var.subnet_id, value, null)}
}
Error :
Error: Incorrect attribute value type
│ 7: subnet_ids = {for value in each.value["subnet_variable_name"]: value => lookup(var.subnet_id, value, null)}
│ ├────────────────
│ │ each.value["subnet_variable_name"] is list of string with 1 element
│ │ var.subnet_id is map of string with 2 elements
│
│ Inappropriate value for attribute "subnet_id": set of string required.
Edit: the result I want is for subnet_ids to be assigned ["XXXX/subnet-a","XXXX/subnet-b"]
You can use a condition with contains and keys functions:
resource "xxx" "xxx" {
for_each = var.network_rule
subnet_ids = [
for subnet in each.value.subnet_variable_name: subnet if contains(keys(var.subnet_id), subnet)
]
}
Update:
If you want the value of the subnet_id instead of the key, you can do the following:
resource "xxx" "xxx" {
for_each = var.network_rule
subnet_ids = [
for subnet in each.value.subnet_variable_name: var.subnet_id[subnet] if contains(keys(var.subnet_id), subnet)
]
}

Dynamic resources for_each output in terraform module

Terraform v1.0.0
Provider: aws v3.49.0
I created dynamic AWS subnets resources with a for_each from a module.
The resources creation is working fine, however being able to output dynamically created resources is not working and cannot find proper documentation for it.
The subnet module is
resource "aws_subnet" "generic" {
vpc_id = var.vpc_id
cidr_block = var.cidr_block
map_public_ip_on_launch = var.public_ip_on_launch
tags = {
Name = var.subnet_tag_name
Environment = var.subnet_environment
}
}
With simple module output defined
output "subnet_id" {
value = aws_subnet.generic.id
}
Then from root module, I am creating a for_each loop over a list variable to create multiple dynamic resources from the module
module "subnets" {
source = "../modules/networking/subnet"
for_each = var.subnets
vpc_id = "vpc-09d6d4c17544f3a49"
cidr_block = each.value["cidr_block"]
public_ip_on_launch = var.public_ip_on_launch
subnet_environment = var.subnet_environment
subnet_tag_name = each.value["subnet_tag_name"]
}
When I run this without defining outputs in the root module, things get created normally. The problem comes when I try to define the outputs
output "subnets" {
value = module.subnets.*.id
description = "Imported VPC ID"
}
It comes up with this error
│ Error: Unsupported attribute
│
│ on output.tf line 2, in output "subnets":
│ 2: value = module.subnets.*.id
│
│ This object does not have an attribute named "id".
I tried different output definitions. Would appreciate guidance on how to properly define outputs of instances dynamically created with a for_each module.
Per the Terraform documentation, the "splat" operator (*) can only be used with lists, and since you're using for_each your output will be a map.
You need to use map/list comprehension to achieve what you want.
For an output that is a map of key/value pairs (note that I've changed the output description to something that makes more sense):
output "subnets" {
value = {
for k, v in module.subnets:
k => v.subnet_id
}
description = "Subnet IDs"
}
For a list that only contains the subnet IDs:
output "subnets" {
value = [
for k, v in module.subnets:
v.subnet_id
]
description = "Subnet IDs"
}

how to fix terraform unpredict instance creation issue?

I'm getting the below error while running terraform plan and apply
on main.tf line 517, in resource "aws_lb_target_group_attachment" "ecom-tga":
│ 517: for_each = local.service_instance_map
│ ├────────────────
│ │ local.service_instance_map will be known only after apply
│
│ The "for_each" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will
│ be created. To work around this, use the -target argument to first apply only the resources that the for_each depends on.
My configuration file is as below
variable "instance_count" {
type = string
default = 3
}
variable "service-names" {
type = list
default = ["valid","jsc","test"]
}
locals {
helper_map = {for idx, val in setproduct(var.service-names, range(var.instance_count)):
idx => {service_name = val[0]}
}
}
resource "aws_instance" "ecom-validation-service" {
for_each = local.helper_map
ami = data.aws_ami.ecom.id
instance_type = "t3.micro"
tags = {
Name = "${each.value.service_name}-service"
}
vpc_security_group_ids = [data.aws_security_group.ecom-sg[each.value.service_name].id]
subnet_id = data.aws_subnet.ecom-subnet[each.value.service_name].id
}
data "aws_instances" "ecom-instances" {
for_each = toset(var.service-names)
instance_tags = {
Name = "${each.value}-service"
}
instance_state_names = ["running", "stopped"]
depends_on = [
aws_instance.ecom-validation-service
]
}
locals {
service_instance_map = merge([for env, value in data.aws_instances.ecom-instances:
{
for id in value.ids:
"${env}-${id}" => {
"service-name" = env
"id" = id
}
}
]...)
}
resource "aws_lb_target_group_attachment" "ecom-tga" {
for_each = local.service_instance_map
target_group_arn = aws_lb_target_group.ecom-nlb-tgp[each.value.service-name].arn
port = 80
target_id = each.value.id
depends_on = [aws_lb_target_group.ecom-nlb-tgp]
}
Since i'm passing count as var and its value is 3,i thought terraform will predict as it needs to create 9 instances.But it didn't it seems and throwing error as unable to predict.
Do we have anyway to by pass this by giving some default values for instances count prediction or for that local service_instance_map?
Tried try function but still no luck
Error: Invalid for_each argument
│
│ on main.tf line 527, in resource "aws_lb_target_group_attachment" "ecom-tga":
│ 527: for_each = try(local.service_instance_map,[])
│ ├────────────────
│ │ local.service_instance_map will be known only after apply
│
│ The "for_each" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will
│ be created. To work around this, use the -target argument to first apply only the resources that the for_each depends on.
My requirement got changed and now i have to create 3 instances in 3 subnets available in that region.I changed the locals as like below But same prediction issue
locals {
merged_subnet_svc = try(flatten([
for service in var.service-names : [
for subnet in aws_subnet.ecom-private.*.id : {
service = service
subnet = subnet
}
]
]), {})
variable "azs" {
type = list(any)
default = ["ap-south-1a", "ap-south-1b", "ap-south-1c"]
}
variable "private-subnets" {
type = list(any)
default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
}
resource "aws_instance" "ecom-instances" {
for_each = {
for svc in local.merged_subnet_svc : "${svc.service}-${svc.subnet}" => svc
}
ami = data.aws_ami.ecom.id
instance_type = "t3.micro"
tags = {
Name = "ecom-${each.value.service}-service"
}
vpc_security_group_ids = [aws_security_group.ecom-sg[each.value.service].id]
subnet_id = each.value.subnet
}
}
In your configuration you've declared that data "aws_instances" "ecom-instances" depends on aws_instance.ecom-validation-service. Since that other object won't exist yet on your first run, Terraform must therefore wait until the apply step to read data.aws_instances.ecom-instances because otherwise it would fail to honor the dependency you've declared, because aws_instance.ecom-validation-service wouldn't exist yet.
To avoid the error message you saw here, you need to make sure that for_each only refers to values that Terraform will know before any objects are actually created. Because EC2 assigns instance ids only once the instance is created, it's not correct to use an EC2 instance id as part of a for_each instance key.
Furthermore, there's no need for a data "aws_instances" block to retrieve instance information here because you already have the relevant instance information as a result of the resource "aws_instance" "ecom-validation-service" block.
With all of that said, let's start from your input variables and build things up again while making sure that we only build instance keys only from values we'll know during planning. The variables you have stay essentially the same; I've just tweaked the type constraints a little to match how we're using each one:
variable "instance_count" {
type = string
default = 3
}
variable "service_names" {
type = set(string)
default = ["valid", "jsc", "test"]
}
I understand from the rest of your example that you are intending to create var.instance_count instances for each distinct element of var.service_names. Your setproduct to produce all of the combinations of those is also good, but I'm going to tweak it to assign the instances unique keys that include the service name:
locals {
instance_configs = tomap({
for pair in setproduct(var.service_names, range(var.instance_count)) :
"${pair[0]}${pair[1]}" => {
service_name = pair[0]
}
})
}
This will produce a data structure like the following:
{
valid0 = { service_name = "valid" }
valid1 = { service_name = "valid" }
valid2 = { service_name = "valid" }
jsc0 = { service_name = "jsc" }
jsc1 = { service_name = "jsc" }
jsc2 = { service_name = "jsc" }
test0 = { service_name = "test" }
test1 = { service_name = "test" }
test2 = { service_name = "test" }
}
This matches the shape that for_each expects, so we can use it directly to declare nine aws_instance instances:
resource "aws_instance" "ecom-validation-service" {
for_each = local.instance_configs
instance_type = "t3.micro"
ami = data.aws_ami.ecom.id
subnet_id = data.aws_subnet.ecom-subnet[each.value.service_name].id
vpc_security_group_ids = [
data.aws_security_group.ecom-sg[each.value.service_name].id,
]
tags = {
Name = "${each.value.service_name}-service"
Service = each.value_service_name
}
}
So far this has been mostly the same as what you shared. But this is the point where I'm going to go in a totally different direction: rather than now trying to read back the instances this declared using a separate data resource, I'll just gather the same data directly from the aws_instance.ecom-validation-service resource. It's generally best for a Terraform configuration to either manage a particular object or read it, not both at the same time, because this way the necessary dependency ordering is revealed automatically be the references.
Notice that I included an extra tag Service on each of the instances to give a more convenient way to get the service name back. If you can't do that then you could get the same information by trimming the -service suffix from the Name tag, but I prefer to keep things direct where possible.
It seemed like your goal then was to have a aws_lb_target_group_attachment instance per instance, with each one connected to the appropriate target group based on the service name. Because that aws_instance resource has for_each set, aws_instance.ecom-validation-service in expressions elsewhere is a map of objects where the keys are the same as the keys in var.instance_configs. That means that value is also compatible with the requirements for for_each and so we can use it directly to declare the target group attachments:
resource "aws_lb_target_group_attachment" "ecom-tga" {
for_each = aws_instance.ecom-validation-service
target_group_arn = aws_lb_target_group.ecom-nlb-tgp[each.value.tags.Service].arn
port = 80
target_id = each.value.id
}
I relied on the extra Service tag from earlier to easily determine which service each instance belongs to in order to look up the appropriate target group ARN. each.value.id works here because each.value is always an aws_instance object, which exports that id attribute.
The result of this is two sets of instances that each have keys matching those in local.instance_configs:
aws_instance.ecom-validation-service["valid0"]
aws_instance.ecom-validation-service["valid1"]
aws_instance.ecom-validation-service["valid2"]
aws_instance.ecom-validation-service["jsc0"]
aws_instance.ecom-validation-service["jsc1"]
aws_instance.ecom-validation-service["jsc2"]
...
aws_lb_target_group_attachment.ecom-tga["valid0"]
aws_lb_target_group_attachment.ecom-tga["valid1"]
aws_lb_target_group_attachment.ecom-tga["valid2"]
aws_lb_target_group_attachment.ecom-tga["jsc0"]
aws_lb_target_group_attachment.ecom-tga["jsc1"]
aws_lb_target_group_attachment.ecom-tga["jsc2"]
...
Notice that all of these keys contain only information specified directly in the configuration, and not any information decided by the remote system. That means we avoid the "Invalid for_each argument" error even though each instance still has an appropriate unique key. If you were to add a new element to var.service_names or increase var.instance_count later then Terraform will also see from the shape of these instance keys that it should just add new instances of each resource, rather than renaming/renumbering any existing instances.

Resources