I'm new to terraform and could use some help please. I had some basic config to build a VPC and two subnets with instances. This ran successfully when I did a 'terraform apply'. Now running a terraform destroy and getting the error in the title. Even running terraform plan to see if anything has changed just throws the same error. The full error says
each.value is object with no attributes
This object does not have an attribute named "az".
I'm guessing there's something relating to the 'for_each' function i've not done right. But then i'm not sure how it applied successfully. I've checked and the resources created from the apply are still there.
main.tf
resource "aws_subnet" "iperf_subnet" {
vpc_id = aws_vpc.ireland_vpc.id
for_each = var.private_subnets
cidr_block = each.value.subnet
availability_zone = each.value.az
}
variables.tf
variable "private_subnets" {
type = map(object({}))
}
exercise.tfvars
private_subnets = {
host_a = {
az = "eu-west-1a"
subnet = "172.30.1.0/25"
}
host_b = {
az = "eu-west-1b"
subnet = "172.30.1.128/25"
}
}
You are specifying the type of the variable private_subnets to be map(object({})). Since the object does not have any attributes explicitly specified, Terraform will throw an error.
You should change this to map(map(string)) or to map(object({az = string, subnet = string})) if you want be more specific.
Related
I am creating an AWS VPC with a single public subnet in a brand-new Terraform project, consisting only of a main.tf file. In that file I am using two resource blocks, aws_vpc and aws_subnet. The second resource must be attached to the first using the vpc_id attribute. The value of this attribute is created only upon apply, so it cannot be hard-coded. How do I get the ID of the resource I just created, so I can use it in the subsequent block?
resource "aws_vpc" "my_vpc" {
cidr_block = "102.0.0.0/16"
tags = {
Name = "My-VPC"
}
}
resource "aws_subnet" "my_subnet" {
vpc_id = # what goes here?
cidr_block = "102.0.0.0/24"
tags = {
Name = "My-Subnet"
}
}
The docs give the example of data.aws_subnet.selected.vpc_id for vpc_id. The value of this appears to depend on two other blocks, variable and data. I am having a hard time seeing how to wire these up to my VPC. I tried copying them directly from the docs. Upon running terraform plan I get the prompt:
var.subnet_id
Enter a value:
This is no good; I want to pull the value from the VPC I just created, not enter it at the command prompt. Where do I specify that the data source is the resource that I just created in the previous code block?
I have heard that people create a separate file to hold Terraform variables. Is that what I should to do here? It seems like it should be so basic to get an ID from one resource and use it in the next. Is there a one-liner to pass along this information?
You can just call the VPC in the subnet block by referencing Terraform's pointer. Also, doing this tells Terraform that the VPC needs to be created first and destroyed second.
resource "aws_vpc" "my_vpc" {
cidr_block = "102.0.0.0/16"
tags = {
Name = "My-VPC"
}
}
resource "aws_subnet" "my_subnet" {
vpc_id = aws_vpc.my_vpc.id
cidr_block = "102.0.0.0/24"
tags = {
Name = "My-Subnet"
}
}
I want to pull the value from the VPC I just created,
You can't do this. You can't dynamically populate variables from data sources. But you could use local instead:
locals {
subnet_id = data.aws_subnet.selected.id
}
and refer to it as local.subnet_id.
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.
I have the code below in which I create vnets in for_each block:
provider "azurerm" {
features {}
}
variable "vnets" {
type = map(object({
name = string
address_space = list(string)
}))
default = {
"vnet1" = {
"name" = "vnet1",
"address_space" = ["10.0.0.0/16"]
},
"vnet2" = {
"name" = "vnet2",
"address_space" = ["10.1.0.0/16"]
}
}
}
resource "azurerm_resource_group" "vnets" {
name = "vnets"
location = "WestEurope"
}
resource "azurerm_virtual_network" "virtual_network" {
for_each = var.vnets
name = each.value.name
location = "West Europe"
resource_group_name = azurerm_resource_group.vnets.name
address_space = each.value.address_space
}
Everything works with the plan, virtual networks will be created,
but the problem is how to get to the created resources from the for_each block?
When I type the command to return the resources list:
terraform state list
Then I have the following output from console:
azurerm_resource_group.vnets
azurerm_virtual_network.virtual_network["vnet1"]
azurerm_virtual_network.virtual_network["vnet2"]
And when I want to use a vnet1 anywhere in the code using reference azurerm_virtual_network.virtual_network["vnet1"] then I'm getting an error.
For example, I want to view resource vnet1:
terraform state show azurerm_virtual_network.virtual_network["vnet1"]
Im getting such error:
Error parsing instance address: azurerm_virtual_network.virtual_network[vnet1]
This command requires that the address references one specific instance.
To view the available instances, use "terraform state list". Please modify
the address to reference a specific instance.
I tried the following commands to access a resource, but they don't work:
terraform state show azurerm_virtual_network.virtual_network["vnet1"]
terraform state show 'azurerm_virtual_network.virtual_network["vnet1"]'
terraform state show azurerm_virtual_network.virtual_network[vnet1]
terraform state show azurerm_virtual_network.virtual_network[0]
terraform state show azurerm_virtual_network.virtual_network.vnet1
Do you know how to solve it?
In addition, if you want to show a specific instance in the state file, you can use terraform state show 'azurerm_virtual_network.virtual_network[\"vnet1\"]'
I ran into this same issue with using modules. The quoted above is the correct answer and the following is how to perform it with a module:
terraform state show 'module.module_name[\"module_instance\"].resource.resource_name[\"resource_instance\"]
Example:
state show 'module.rsm_keyvault_solution_module_resource[\"development\"].azurerm_key_vault.keyvault_module_resource[\"rsm-playground-dev\"]'
In this case, you can use the values function to take a map and return a list containing the values of the elements in that map.
For example, to get the values of VNets in a map:
output "azurerm_vnets_names" {
value = values(azurerm_virtual_network.virtual_network)[*].name
}
Or get a specific VNet name like this:
output "azurerm_vnet1_name" {
value = values(azurerm_virtual_network.virtual_network)[0].name
}
In addition, if you want to show a specific instance in the state file, you can use
terraform state show 'azurerm_virtual_network.virtual_network[\"vnet1\"]'
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "2.33.0"
name = "${local.env_name}-vpc"
public_subnets = data.template_file.public_cidrsubnet[*].rendered
private_subnets = data.template_file.private_cidrsubnet[*].rendered
tags = merge(local.common_tags, { Name = "${local.env_name}-vpc" })
Creating is successful but only problem is tagging, for all the newly created using module vpc tag is "default-vpc"
I wanted to tag each resource separately, like private-subnet, public-subnet etc.
In case anyone else stumbles upon this question. It is now possible to supply public_subnet_tags as noted in the documentation for VPC in Terraform.
Create the subnet separately and assign it to the vpc
resource "aws_subnet" "main" {
vpc_id = module.vpc.vpc_id
cidr_block = "10.0.1.0/24"
tags = {
Name = "Main"
}
}
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}"
}