Terraform data structures: object and list of object issue - amazon-web-services

I'm trying to use terraform to create some aws resource.
Here is my issue:
I'm creating some variables, so I can access them from the resources.
Here is the variables.tf file content:
variables.tf
variable "zones" {
type = "list"
default = ["us-east-1a", "us-east-1b"]
}
variable "init" {
type = object({
vpc-id=list(string),
public-subnet=string,
aws_region=string,
ami=string,
vpc-sec-group= list(string)
})
param = {
vpc-id = ["vpc-1111111"]
public-subnet = "subnet-98e4567"
aws_region = "${element(var.zones,0)}"
ami = "ami-09d95fab7fff3776c",
vpc-sec-group = ["sg-d60bf3f5"]
}
}
variable "instances" {
type = list(object({ type=string, count=string, tags=map(string) }))
t2-micro ={
type = "t2.micro"
count = 4
tags = { Name = "Test T2"}
}
m4-large ={
type = "m4-large"
count = 2
tags = { Name = "Test M4"}
}
}
My plan is to use these variable to create some ec2 instances as so
Here is ec2.tf
ec2.tf
resource "aws_instance" "Test-T2" {
type = lookup(var.insts["t2-micro"],"type")
ami = lookup(var.init.ami["ami"],var.init.aws_region["aws_region"] )
count = lookup(var.insts["t2-micro"],"count")
tags = lookup(var.insts["t2-micro"],"tags")
key_name = aws_key_pair.terraform-demo.key_name
vpc_security_group_ids = toset(lookup(var.init, "vpc-sec-group"))
subnet_id = lookup(var.init.params["public-subnet"])
}
ISSUE
When I execute
terraform init
I get the following error:
Error: Unsupported argument
on variables.tf line 26, in variable "instances":
26: t2-micro ={
An argument named "t2-micro" is not expected here.
Error: Unsupported argument
on variables.tf line 32, in variable "instances":
32: m4-large ={
An argument named "m4-large" is not expected here.
Terraform has initialized, but configuration upgrades may be needed.
Terraform found syntax errors in the configuration that prevented full
initialization. If you've recently upgraded to Terraform v0.12, this may be
because your configuration uses syntax constructs that are no longer valid,
and so must be updated before full initialization is possible.
Could someone please help me to correct those errors?
More details and some actions I have taken
I have tried different ways of creating the variables to the best of my knowledge and following the Terraform documentation
to no avail.
I'm just emulating what would be a Python:
list of objects( list of dictionaries)
and
an object
Here is my version of terraform
terraform -v
Terraform v0.12.26
+ provider.aws v2.65.0
some more details
I'm using the latest version of Visual Studio Code 1.45.1 with the Terraform module HashiCop 1.4.0 for "Syntax highlighting, linting, formatting, and validation for Hashicorp's Terraform"

You need to separate your variable declarations and assignments. Those cannot co-exist within the same block.
Your declaration within the variables.tf would look like (with some fixes and cleanup):
variable "instances" {
type = list(object({
type = string # removed commas since you specified object type
count = number # fixed from string type
tags = map(string)
}))
}
Your variable assignment should be moved to a .tfvars file. Customarily this file would be named terraform.tfvars:
instances = [ # you specified a list so we add the proper syntax here
{ # you specified an object, so we remove the keys and retain the values
type = "t2.micro"
count = 4
tags = { "Name" = "Test T2"} # you specified map(string) so Name becomes string
},
{
type = "m4-large"
count = 2
tags = { "Name" = "Test M4"}
}
]
and that will be a valid input variable with proper assignment given your declaration. That will fix your error.

Related

Terraform local variables dependancy in for_each statement

I'm using following local variables to pass map of account_id and account_type to my child module in order to do to create patch manager resources based on account_type from my child module.
locals {
org_sub_accounts_map = zipmap(module.accounts.account_id, module.accounts.tags_all.*.AccountType)
org_sub_accounts = [for k, v in local.org_sub_accounts_map : {
id = k
type = v
}
]
}
module "ssm_patch_manager" {
source = "../../../../modules/aws/cloudformation/stacksets"
accounts = local.org_sub_accounts
account_exception_list = var.account_exception_list
regions = var.region_list
stackset_name = "SSM-PatchManager"
template = "ssm_patch_manager"
parameters = var.patch_manager_default_params
parameter_overrides = var.patch_manager_params_overrides
stackset_admin_role_arn = module.stackset_admin_role.role_arn
depends_on = [module.accounts]
}
local.org_sub_accounts is something like this:
org_sub_accounts = [
{
"id" = "111111111111"
"type" = "Dev"
},
{
"id" = "222222222222"
"type" = "Prod"
},
{
"id" = "33333333333"
"type" = "Dev"
}
]
This works fine with all the existing AWS accounts as terraform aware of the accounts IDs. Now the problem is, when I'm creating a new AWS account from module.accounts, and running the terraform plan, I get below error:
Error: Invalid for_each argument
on ../../../../modules/aws/cloudformation/stacksets/main.tf line 25, in resource "aws_cloudformation_stack_set_instance" "stack":
25: for_each = {
26: for stack_instance in local.instance_data : "${stack_instance.account}.${stack_instance.region}" => stack_instance if contains(var.account_exception_list, stack_instance.account) == false
27: }
├────────────────
│ local.instance_data will be known only after apply
│ var.account_exception_list is list of string with 1 element
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.
I understand this is clearly because terraform doesn't know the account_id when evaluating the locals variables. Can anyone suggest something here to resolve this issue?
Please note, this is already implemented solution. But we came to know after sometime when we try to create a new account. Therefore, any suggestion without major structure changes in the code would be really helpful.
Update
instance_data is a resource block in child module.
locals {
instance_data = flatten([
for account in var.accounts : [
for region in var.regions : {
account = account.id
type = try(length(account.type), 0) > 0 ? account.type : "default" # To support legacy var.account input, which account 'type' key is not passed.
region = region
}
]
])
resource "aws_cloudformation_stack_set_instance" "stack" {
for_each = {
for stack_instance in local.instance_data : "${stack_instance.account}.${stack_instance.region}" => stack_instance if contains(var.account_exception_list, stack_instance.account) == false
}
account_id = each.value.account
region = each.value.region
parameter_overrides = lookup(var.parameter_overrides, each.value.type, null) # To handle different parameters based on 'AccountType' Tag in sub accounts
stack_set_name = aws_cloudformation_stack_set.stackset.name
}
Finally came up with below solution. Just posting this here if anyone needs to reference in future.
data "aws_organizations_resource_tags" "account" {
count = length(module.organization.non_master_accounts.*.id)
resource_id = module.organization.non_master_accounts.*.id[count.index]
}
# “for_each” value in child module depends on resource attributes (AWS account_id) that cannot be determined until terraform apply, so Terraform cannot predict how many instances will be created.
# Because of this, we use `aws_organizations_resource_tags` data source to create stacksets, instead of module outputs.
locals {
org_sub_accounts = [for account in data.aws_organizations_resource_tags.account : {
id = account.id
type = try(length(account.tags.AccountType), 0) > 0 ? account.tags.AccountType : "default" # In case AccountType tag missing in existing/invited accounts.
}
]
}
# Output in Organization module
output "non_master_accounts" {
value = aws_organizations_organization.root.non_master_accounts
}

How to join terraform subnet variable

variable.tf
variable "private_subnets" {
type = list
default = ["subnet-abc1,subnet-abc2"]
}
main.tf
resource "aws_db_subnet_group" "rds_subnet_group" {
name = var.cluster_name
subnet_ids = "${var.private_subnets}"
tags = {
Name = var.cluster_name,
environment = var.environment
}
}
This the present code i want use varable.tf for subnet this is way i want achive like this subnet-abc1,subnet-abc2
Considering that your variable has a wrongly defined default value, the first thing to fix is that:
variable "private_subnets" {
type = list(string)
default = ["subnet-abc1", "subnet-abc2"]
}
The way you are currently defining the default value, i.e, ["subnet-abc1,subnet-abc2"], is a list, however, it is a list with one element. Each element in a list of strings needs to start and end with double quotes, i.e. "some value". You can read more about lists in [1].
Then, you would just need to fix the code in the main.tf to look like this:
resource "aws_db_subnet_group" "rds_subnet_group" {
name = var.cluster_name
subnet_ids = var.private_subnets
tags = {
Name = var.cluster_name,
environment = var.environment
}
}
The syntax for subnets in the main.tf file is the old terraform syntax, so this will work without double quotes and ${}.
[1] https://developer.hashicorp.com/terraform/language/expressions/types#list
i have sort issue with split this is the answer
variable.tf
variable "private_subnets" {
default = "subnet-abc1,subnet-abc2"
}
main.tf
resource "aws_db_subnet_group" "rds_subnet_group" {
name = var.cluster_name
subnet_ids = "${split(",", var.private_subnets)}"
tags = {
Name = var.cluster_name,
environment = var.environment
}

How to call default list variables in terraform module

I want to call list variables from below code. But, It is throwing error instead after mentioning the default value in variables.tf
Terraform Service Folder (/root/terraform-ukg-smtp).
main.tf
module "google_uig" {
source = "/root/terraform-google-vm/modules/compute_engine_uig"
depends_on = [
module.google_vm
]
project = var.project
count = var.num_instances
zone = var.zone == null ? data.google_compute_zones.available.names[count.index % length(data.google_compute_zones.available.names)] : var.zone
name = "apoc-uig-${random_integer.integer[count.index].result}"
instances = element((module.google_vm[*].google_instance_id), count.index)
named_ports = var.named_ports
}
variables.tf
variable "named_ports" {
description = "Named name and named port"
type = list(object({
port_name = string
port_number = number
}))
default = [{
port_name = "smtp"
port_number = "33"
}]
}
Terraform Core Folder (/root/terraform-google-vm/modules/compute_engine_uig).
main.tf
# Instance Group
resource "google_compute_instance_group" "google_uig" {
count = var.num_instances
project = var.project
zone = var.zone
name = var.name
instances = var.instances
dynamic "named_port" {
for_each = var.named_ports != null ? toset([1]) : toset([])
content {
name = named_port.value.port_name
port = named_port.value.port_number
}
}
}
variables.tf
variable "named_ports" {
description = "Named name and named port"
type = list(object({
port_name = string
port_number = number
}))
default = null
}
ERROR
╷
│ Error: Unsupported argument
│
│ on main.tf line 66, in module "google_uig":
│ 66: port_number = each.value["port_number"]
│
│ An argument named "port_number" is not expected here.
The error actually lies in the file /root/terraform-google-vm/modules/compute_engine_uig/main.tf, which you have not added to your question. But from the error message, I think to know what is wrong.
The resource google_compute_instance_group.google_uig in compute_engine_uig/main.tf should look like this:
resource "google_compute_instance_group" "google_uig" {
other_keys = other_values
dynamic "named_port" {
for_each = var.named_ports
content {
name = named_port.value.name
port = named_port.value.port
}
}
}
From the error message, it seems that you have written
name = named_ports.value.name
i.e., with a plural s instead of
name = named_port.value.name
in the content block.
If this doesn't solve it, please add the file that throws the error.
Edit from 30.05.2022:
Two more problems are now visible:
You set for_each = var.named_ports != null ? toset([1]) : toset([]), which is not correct. You have to iterate over var.named_ports (as I have written above), not over a set containing the number 1. Just copy it word by word from the code above.
Additionaly, you have defined the type of port_number in your variable named_ports as "number", but you have given a string "33". This may be fine for terraform since it does a lot of conversion in the background, but better change it too.

This object does not have an attribute named "this_rds_cluster_master_username"

Trying to upgrade Aurora version to latest one. Before upgrading terraform plan is working fine. Once I did upgrading I'm getting:
Error: Unsupported attribute
on main.tf line 50, in locals:
50:
"master_username" = module.db.this_rds_cluster_master_username
module.db is a object, known only after apply
This object does not have an attribute named "this_rds_cluster_master_username".
I tried replacing module.db with variables var.this_rds_cluster_master_username and it worked, but I want to make changes in output file not with variables. Any assistance would really appreciated.
output.tf
output "this_rds_cluster_master_username" {
value = module.db.this_rds_cluster_master_username
description = "The master username."
}
main.tf
locals {
rds_cluster_master_creds = {
"master_username" = module.db.this_rds_cluster_master_username
"master_password" = module.db.this_rds_cluster_master_password
}
}
How to modify output module?
#module db
module "db" {
source = "terraform-aws-modules/rds/aws"
version = "5.2.0"
engine = "aurora-postgressql"
engine_mode = "serveless"
engine_version = null
db_subnet_group_name = aws_db_subnet_group.rds_isolated.name
vpc_id = local.vpc_id
deletion_protection = true
}
#locals
locals {
rds_cluster_master_creds = {
"master_username" = module.db.this_rds_cluster_master_username
"master_password" = module.db.this_rds_cluster_master_password
}
}
Module terraform-aws-modules/rds/aws does not have output called this_rds_cluster_master_username. Instead it is called db_master_password. So instead of the following:
module.db.this_rds_cluster_master_username
you should be using
module.db.db_instance_username
Same for this_rds_cluster_master_password.

Applying tags to instances created with for each Terraform

I have multiple EC2 instances created using for each. Each instance is being deployed into a different subnet. I am getting an error when trying to apply tags to each instance being deployed. Any advice would be helpful. Below is the code for my tags and instances:
resource "aws_instance" "private" {
for_each = aws_subnet.private
ami = var.ec2_amis[var.region]
instance_type = var.tableau_instance
key_name = aws_key_pair.tableau.key_name
subnet_id = each.value.id
tags = {
Name = var.ec2_tags[each.key]
}
}
variable "ec2_tags" {
type = list(string)
default = [
"PrimaryEC2",
"EC2Worker1",
"EC2Worker2"
]
}
Error
Error: Invalid index
on vpc.tf line 21, in resource "aws_instance" "private":
21: Name = var.ec2_tags[each.key]
|----------------
| each.key is "3"
| var.ec2_tags is list of string with 3 elements
The given key does not identify an element in this collection value.
I had this code working earlier, not sure what happened. I made a change to the AMI it spins up, but I don't see why that could have an effect on tags. Any advice would be helpful.
UPDATE
I have updated the resource with the following locals block and dynamic block within my "aws_instance" "private" code:
locals {
private_instance = [{
name = "PrimaryEC2"
},
{
name = "EC2Worker1"
},
{
name = "EC2Worker2"
}]
}
dynamic "tags" {
for_each = local.private_instance
content {
Name = tags.value.name
}
}
Error
Error: Unsupported block type
on vpc.tf line 28, in resource "aws_instance" "private":
28: dynamic "tags" {
Blocks of type "tags" are not expected here.
Any advice how to fix would help. Thanks!
If you want to make your tags dynamic, you could create them as follows:
tags = {
Name = each.key == "0" ? "PrimaryEC2" : "EC2Worker${each.key}"
}
You would use it as follows (assuming everything else is OK):
resource "aws_instance" "private" {
for_each = aws_subnet.private
ami = var.ec2_amis[var.region]
instance_type = var.tableau_instance
key_name = aws_key_pair.tableau.key_name
subnet_id = each.value.id
tags = {
Name = each.key == "0" ? "PrimaryEC2" : "EC2Worker${each.key}"
}
}
The code uses conditional expression. It works as follows.
If each.key is equal to "0" (i.e., first instance being created) then its tag will be "PrimaryEC2". All remaining instances will be tagged: "EC2Worker1", "EC2Worker2", "EC2Worker3" and so on for as many subnets there are.
One possible cause of this errors is that the aws_subnet.private variable is longer then the list of ec2 tags which would result in an error when the index 3 is used on your ec2_tags list looking for the 4th (nonexistent element).