Dynamic resources for_each output in terraform module - terraform

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"
}

Related

Terraform - Can't access attributes on a primitive-typed value (string) when trying to add multiple disks

I'm a total newbie to Terraform and loving it so far. However, I'm a bit stuck with the below. I'm trying to add a disk to multiple machines using a dynamic block, but I'm getting the error 'Can't access attributes on a primitive-typed value (string)' whenever I run terraform plan. The config for my compute engine instance/disk looks like this:
resource "google_compute_instance" "vm_instance" {
count = 2
name = "test-instance${count.index + 1}"
machine_type = "e2-micro"
labels = {
"environment" = var.environment
}
boot_disk {
initialize_params {
image = var.image
}
}
network_interface {
# A default network is created for all GCP projects
network = "default"
}
lifecycle {
ignore_changes = [
resource_policies,
metadata,
attached_disk
]
}
}
resource "google_compute_disk" "default" {
for_each = toset(google_compute_instance.vm_instance.*.id)
name = each.value.name
type = "pd-ssd"
labels = {
environment = "dev"
}
physical_block_size_bytes = 4096
}
resource "google_compute_attached_disk" "default" {
for_each = google_compute_instance.vm_instance.*.id
disk = google_compute_disk.default[each.key].id
instance = each.key
}
It looks like the plan picks up two instances of the VM as expected, but Terraform is unable to access any of its attributes...
│ Error: Unsupported attribute
│
│ on main.tf line 50, in resource "google_compute_disk" "default":
│ 50: name = each.value.id
│ ├────────────────
│ │ each.value is "projects/blah/zones/europe-west2-a/instances/test-instance2"
Can anyone advise on where I'm going wrong, please? Thanks
If you use for_each over a set of string, you don't get access to properties, so this won't work.
resource "google_compute_disk" "default" {
for_each = toset(google_compute_instance.vm_instance.*.id)
name = each.value.name
# ...
}
If you want to use each of those id's, what you need is the set key.
resource "google_compute_disk" "default" {
for_each = toset(google_compute_instance.vm_instance.*.id)
name = each.key
# ...
}
Also, you're better off not using the Legacy Splat Expressions, and will find the code more intuitive I suspect formed something like this:
resource "google_compute_disk" "default" {
for_each = google_compute_instance.vm_instance
name = each.value.id
# ...
}

Unable to Create Terraform Resource Group when using modules

I am optimizing my terraform code by using modules. When i create a resource group module it works perfectly well but it creates two resource groups
i.e.
Temp-AppConfiguration-ResGrp
Temp-AppServices-ResGrp
instead it should only create
Temp-AppConfiguration-ResGrp
Code Resourcegroup.tf.
resource "azurerm_resource_group" "resource" {
name = "${var.environment}-${var.name_apptype}-ResGrp"
location = var.location
tags = {
environment = var.environment
}
}
output "resource_group_name" {
value = "${var.environment}-${var.name_apptype}-ResGrp"
}
output "resource_group_location" {
value = var.location
}
Variable.tf
variable "name_apptype" {
type = string
default = "AppServices"
}
variable "environment" {
type = string
default = "Temp"
}
variable "location" {
type = string
default = "eastus"
}
Main.tf
module "resourcegroup" {
source = "../Modules"
name_apptype = "AppConfiguration"
}
I want to pass name_apptype in main.tf when calling resource group module. So that i don't need to update variable.tf every time.
Any suggestions
where i am doing wrong. Plus i am also unable to output the value, i need it so that i could pass resource group name in the next module i want to create.
Thanks
You need to do that in the Main.tf
module "resourcegroup" {
source = "../Modules"
name_apptype = "AppConfiguration"
}
module "resourcegroup-appservices" {
source = "../Modules"
name_apptype = "AppServices"
}
These create a 2 resources groups with the values that you need, additionally you can remove the default value from the name_apptype variable.
If you want to create with the same module both resource groups you need to use count to iterate over an array of names

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.

Terraform : Using for_each in module

I am using terraform version 0.14.3.
I have a module for creating an Azure Network Interface Card, as below:
resource "azurerm_network_interface" "nic" {
name = var.nic_name
location = var.location
resource_group_name = var.rg_name
ip_configuration {
name = var.ipconfig_name
subnet_id = var.subnet_id
private_ip_address_allocation = "Dynamic"
}
}
Its output is defined as :
output "nic_id" {
value = azurerm_network_interface.nic.id
}
I am calling this module in this parent module:
module "NIC" {
source = "./NIC"
for_each = var.nics
nic_name = each.value.nic_name
location = "eastus2"
rg_name = "abc-test-rg"
ipconfig_name = each.value.ipconfig_name
subnet_id = <subnet_id>
}
output "nic_ids" {
value = [for k in module.NIC.nic_id : k.id]
}
The NIC values are defined as below:
nics = {
nic1 = {
nic_name = "abc-nic-1"
ipconfig_name = "nic-1-ipconfig"
}
}
I want to loop around the NIC output IDs, and want them displayed.
When I run above code, I get below error in terraform plan :
Error: Unsupported attribute
on main.tf line 15, in output "nic_ids":
15: value = [for k in module.NIC.nic_id : k.id]
|----------------
| module.NIC is object with 1 attribute "nic1"
This object does not have an attribute named "nic_id".
How do I get around it ?
Your module "NIC" block has for_each set, and so the module.NIC symbol elsewhere in the module is a mapping from instance keys to output objects, rather than just a single output object as for a singleton module.
Terraform's error message is attempting to draw attention to that with the following message:
module.NIC is object with 1 attribute "nic1"
Notice that nic1 here is a key from your var.nics, and not one of the output values defined in your module.
Assuming that the nic_id output you showed here is the only one defined in that module, the module.NIC value would be shaped something like this:
{
nic1 = {
nic_id = "eni-e5aa89a3"
}
}
It sounds like you instead want to produce a value shaped like this:
{
nic1 = "eni-e5aa89a3"
}
If so, a suitable expression to get that result would be the following:
output "nic_ids" {
value = { for k, nic in module.NIC : k => nic.nic_id }
}
The above means: produce a mapping with one element for each instance of the NIC module, whose key is the module instance key and whose value is the nic_id output value.
Alternatively, if it doesn't matter which id belongs to which instance then you could produce an unordered set of ids, like this:
output "nic_ids" {
value = toset([for nic in module.NIC : nic.nic_id])
}
In this case the for expression only defines a local symbol nic, which represents the module instance object, because it doesn't do anything with the instance key. The toset here is to represent that the IDs are not in any particular order: that isn't strictly necessary but I think it's a good practice to make sure that any other Terraform code depending on that value doesn't inadvertently depend on the current arbitrary ordering of the ids, which might change in future if you add or remove elements in var.nics.

Terraform output defined are empty [duplicate]

I'm trying to setup some IaC for a new project using Hashicorp Terraform on AWS. I'm using modules because I want to be able to reuse stuff across multiple environments (staging, prod, dev, etc.)
I'm struggling to understand where I have to set an output variable within a module, and how I then use that in another module. Any pointers to this would be greatly appreciated!
I need to use some things created in my VPC module (subnet IDs) when creating EC2 machines. My understanding is that you can't reference something from one module in another, so I am trying to use an output variable from the VPC module.
I have the following in my site main.tf
module "myapp-vpc" {
source = "dev/vpc"
aws_region = "${var.aws_region}"
}
module "myapp-ec2" {
source = "dev/ec2"
aws_region = "${var.aws_region}"
subnet_id = "${module.vpc.subnetid"}
}
dev/vpc simply sets some values and uses my vpc module:
module "vpc" {
source = "../../modules/vpc"
aws_region = "${var.aws_region}"
vpc-cidr = "10.1.0.0/16"
public-subnet-cidr = "10.1.1.0/24"
private-subnet-cidr = "10.1.2.0/24"
}
In my vpc main.tf, I have the following at the very end, after the aws_vpc and aws_subnet resources (showing subnet resource):
resource "aws_subnet" "public" {
vpc_id = "${aws_vpc.main.id}"
map_public_ip_on_launch = true
availability_zone = "${var.aws_region}a"
cidr_block = "${var.public-subnet-cidr}"
}
output "subnetid" {
value = "${aws_subnet.public.id}"
}
When I run terraform plan I get the following error message:
Error: module 'vpc': "subnetid" is not a valid output for module "vpc"
Outputs need to be passed up through each module explicitly each time.
For example if you wanted to output a variable to the screen from a module nested below another module you would need something like this:
child-module.tf
output "child_foo" {
value = "foobar"
}
parent-module.tf
module "child" {
source = "path/to/child"
}
output "parent_foo" {
value = "${module.child.child_foo}"
}
main.tf
module "parent" {
source = "path/to/parent"
}
output "main_foo" {
value = "${module.parent.parent_foo}"
}

Resources