WAF for all ALBs in an account using terraform - amazon-web-services

This is my waf.tf terraform file:
resource "aws_wafv2_web_acl" "waf_acl-dev" {
name = "waf_log4j_Protections-dev"
description = "WAFv2 for dev"
scope = "REGIONAL"
default_action {
allow {}
}
rule {
name = "AWSManagedRulesKnownBadInputsRule"
priority = 1
override_action {
count {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesKnownBadInputsRuleSet"
vendor_name = "AWS"
# Excluding all these leaves only Log4JRCE
excluded_rule {
name = "Host_localhost_HEADER"
}
excluded_rule {
name = "PROPFIND_METHOD"
}
excluded_rule {
name = "ExploitablePaths_URIPATH"
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "AWSManagedRulesKnownBadInputsRule"
sampled_requests_enabled = true
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "AWSManagedKnownBadInputsRule"
sampled_requests_enabled = true
}
}
variable "lb_arn" {
type = string
default = ""
}
data "aws_lb" "all_alb" {
tags = {
Environment = "Dev"
}
# arn = var.lb_arn
# for_each = data.aws_lb.all_alb
# id = each.value
}
output "all_alb" {
value = data.aws_lb.all_alb
}
resource "aws_wafv2_web_acl_association" "waf_acl-association-dev" {
for_each = data.aws_lb.all_alb.arn
resource_arn = each.value
web_acl_arn = aws_wafv2_web_acl.waf_acl-dev.arn
}
My objective is to create a WAF with rules (that portion works fine), and attach multiple ALBs to the WAF using tags on each ALB. Im running it inside a teamcity loop that loops into multiple AWS accounts (That is out of scope). Each account can have one or multiple ALBs with the tag provided. When I run this code I get the following error:
Error: Search returned 2 results, please revise so only one is returned
17:39:18 │
17:39:18 │ with data.aws_lb.all_alb,
17:39:18 │ on xa-waf-inow.tf line 49, in data "aws_lb" "all_alb":
17:39:18 │ 49: data "aws_lb" "all_alb" {
I also tried a few other alternatives like putting [0] at the end of line 49 but Im still getting hit with some syntax error or the other. Can someone please help?Thanks
Edit:
I also tried for_each in data block:
data "aws_lb" "all_alb" {
for_each = {
tags = {
Environment = "Dev"
}
}
arn = each.key
}
output "all_alb" {
value = data.aws_lb.all_alb
}
resource "aws_wafv2_web_acl_association" "waf_acl-association-dev" {
# for_each = data.aws_lb.all_alb.arn
resource_arn = data.aws_lb.all_alb[each.key]
web_acl_arn = aws_wafv2_web_acl.waf_acl-dev.arn
}
But got this error:
Error: Reference to "each" in context without for_each
18:24:01 │
18:24:01 │ on xa-waf-inow.tf line 65, in resource "aws_wafv2_web_acl_association" "waf_acl-association-dev":
18:24:01 │ 65: resource_arn = data.aws_lb.all_alb[each.key]
18:24:01 │
18:24:01 │ The "each" object can be used only in "module" or "resource" blocks, and
18:24:01 │ only when the "for_each" argument is set.

I was able to get this to work for myself. My issue was a mixture of lists and sets. I believe I can change my variable to a set, and not have to deal with any sets here, but I know the below works as is.
VARIABLES.TF
variable "list_of_alb" {
type = list(string)
}
MAIN.TF
list_of_alb = [
"${terraform.workspace}-unique-1",
"${terraform.workspace}-unique-2"
]
DATA.TF - the problem child
data "aws_lb" "main" {
for_each = toset( var.list_of_alb )
name = each.value
}
WAF.TF
resource "aws_wafv2_web_acl_association" "main" {
for_each = toset(var.list_of_alb)
resource_arn = data.aws_lb.main[each.value].arn
web_acl_arn = aws_wafv2_web_acl.main.arn
}

The aws_lb data source must return only one result. You can't change that, as this is how it was destined.
If you want to return multiple ALBs you have two choices:
Use for_each with the data source. This way your data source will run for each id of your alb. This means that you have to provide the alb ids as an input variable.
Or, create your own custom data source. Since this is fully custom code that you have to write, it can overcome any limitations of TF's build in data sources.

Related

Terraform error when applying only a certain tag

I'm trying to add the tag "Project" to the resource "aws_ec2_transit_gateway_vpc_attachment" to make it match the rest of the resources in this project, but when I do so Terraform throws an error.
Relevant Terraform code:
resource "aws_ec2_transit_gateway" "tgw" {
description = "TGW for inter-region connectivity"
auto_accept_shared_attachments = "disable"
default_route_table_association = "disable"
default_route_table_propagation = "disable"
dns_support = "enable"
tags = {
"Name" = "labs tgw",
"Terraform" = "true",
"Project" = var.project_id
}
}
## Create new TGW Attachments
resource "aws_ec2_transit_gateway_vpc_attachment" "tgw_attachments" {
subnet_ids = aws_subnet.tgw[*].id
transit_gateway_id = aws_ec2_transit_gateway.tgw.id
vpc_id = aws_vpc.vpc.id
dns_support = "enable"
transit_gateway_default_route_table_association = "false"
transit_gateway_default_route_table_propagation = "false"
tags = {
"Name" = "labs_tgw_attachment",
"Terraform" = "true",
#"Project" = var.project_id
}
}
## Accept attachment if it hasn't already been accepted
data "aws_ec2_transit_gateway_attachment" "attachment_data" {
transit_gateway_attachment_id = aws_ec2_transit_gateway_vpc_attachment.tgw_attachments.id
}
resource "aws_ec2_transit_gateway_vpc_attachment_accepter" "accept" {
count = (data.aws_ec2_transit_gateway_attachment.attachment_data.state == "pendingAcceptance") ? 1 : 0
transit_gateway_attachment_id = aws_ec2_transit_gateway_vpc_attachment.tgw_attachments.id
depends_on = [
aws_ec2_transit_gateway_vpc_attachment.tgw_attachments
]
}
The purpose of the data block and count is due to terraform throwing an error that the attachment is in an inconsistent state after it's already been accepted, so it checks to see if the attachment is in "pendingAcceptance" state, and builds the accepter only if that's true.
Currently, the attachment is accepted, everything else is working as expected, but if I uncomment "Project" = var.project_id, then I get this error:
Error: Invalid count argument
│
│ on modules/main.tf line 74, in resource "aws_ec2_transit_gateway_vpc_attachment_accepter" "accept":
│ 74: count = (data.aws_ec2_transit_gateway_attachment.attachment_data.state == "pendingAcceptance") ? 1 : 0
│
│ The "count" 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 count depends on.
I've tried adding the tags with merge, like so:
tags = merge({
"Name" = "labs tgw attachment",
"Terraform" = "true"
},
{
"Project" = var.project_id
}
)
}
But got the same error, presumably since that formatting isn't required and doesn't really change the way the tags are evaluated.

Terraform.tfvars parsing lists for variables.tf file

I've got a couple of variables set in my variables.tf files which are of type list and I am trying to find the correct syntax on my .tfvars file
variables.tf
variable "subnet_cidrs" {
type = list(string)
description = "A list of Subnets CIDR's - Should consist of minimum 2"
}
variable "aws_region" {
type = string
description = "Region where all AWS Resources will be created"
}
variable "az" {
type = list(string)
description = "Availability-Zones - Should match numbers of CIDRs given and AWS Region"
}
terraform.tfvars
subnet_cidrs = ["192.168.1.100", "192.168.4.100"]
aws_region = "eu-central-1"
az = ["eu-central-1a", "eu-central-1b"]
A typical output i'll receive is :
│ Error: Incorrect attribute value type
│
│ on ..\ec2\ec2.tf line 21, in resource "aws_subnet" "public":
│ 21: availability_zone = var.az
│ ├────────────────
│ │ var.az is a list of dynamic
│
│ Inappropriate value for attribute "availability_zone": string required.
When brackets are removed for example : az = "eu-central-1a", "eu-central-1b"
It will return the following instead Argument definitions must be separated by newlines, not commas. An argument definition must end with a newline.
Edit
ec2.tf
I've made this as short as possible and included only the variables
# Main VPC
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
tags = {
Name = "vpc-${var.name_prefix}"
}
}
# Two Subnets in different AZ - Public IP on launch
resource "aws_subnet" "public" {
count = length(var.subnet_cidrs)
cidr_block = var.subnet_cidrs
availability_zone = var.az
tags = {
Name = "subnet-${var.name_prefix}-${count.index}"
}
}
resource "aws_security_group" "ec2_sg" {
name = var.ec2_name
...
tags = {
Name = "EC2-SG-${var.name_prefix}"
}
}
# VM Key pair
resource "aws_key_pair" "auth" {
key_name = var.key_pair_name
public_key = file("~/.ssh/${var.ssh_file_name}.pub")
}
# EC2 Instance within 2 AZ's
resource "aws_instance" "ec2" {
count = length(var.subnet_cidrs)
tags = {
Name = "ubuntu-${var.name_prefix}-${count.index}"
}
}
Based on the variable definition and the way subnet resource is created, the following change is required:
resource "aws_subnet" "public" {
count = length(var.subnet_cidrs)
cidr_block = var.subnet_cidrs
availability_zone = var.az[count.index] # <---- using count.index here
tags = {
Name = "subnet-${var.name_prefix}-${count.index}"
}
}
This is needed because otherwise the problem will occur as availability_zone requires a single string value and passing only var.az will be an entire list. Using the count.index with the var.az will fetch a single value as is required.

getting error while using list(string) data type in terraform module

I am trying to create 2 subnets in aws (with terraform) by passing 2 values in single variable.
Getting below error while executing "terraform validate" command
Please guide me how to correctly define list(string) variable data type in terraform module and correctly use it.
│ Error: Invalid value for input variable
│
│ on usage-test.tf line 11, in module "vpc_module":
│ 11: subnet_cidr_block = ["10.0.0.0/24","10.0.1.0/24"]
│
│ The given value is not suitable for module.vpc_module.var.subnet_cidr_block declared at vpc/var-test.tf:21,1-29: string required.
╵
╷
│ Error: Invalid value for input variable
│
│ on usage-test.tf line 12, in module "vpc_module":
│ 12: subnet_az = ["ap-south-1a","ap-south-1b"]
│
│ The given value is not suitable for module.vpc_module.var.subnet_az declared at vpc/var-test.tf:25,1-21: string required.
╵
refer terraform files below:-
variable.tf:
variable "subnet_cidr_block" {
type = list(string)
}
variable "subnet_az" {
type = list(string)
}
main.tf:
resource "aws_subnet" "mysubnet_public" {
vpc_id = aws_vpc.myvpc.id
cidr_block = var.subnet_cidr_block
availability_zone = var.subnet_az
map_public_ip_on_launch = "true"
depends_on = [aws_internet_gateway.mygw]
}
usage.tf
provider "aws" {
region = "ap-south-1"
}
module "vpc_module" {
source = "./vpc"
vpc_cider_block = "10.0.0.0/16"
vpc_name = "myvpc"
route_table_name = "myrt"
subnet_cidr_block = ["10.0.0.0/24","10.0.1.0/24"]
subnet_az = ["ap-south-1a","ap-south-1b"]
# subnet_cidr_block = "10.0.0.0/24"
# subnet_az = "ap-south-1a"
# subnet_public_name = "mysubnet_public"
sg_mgmt_name = "mysg_mgmt"
}
Well, the error is pretty clear. You cannot use a list of strings, rather a single string value, as the provider documentation also shows [1]:
resource "aws_subnet" "main" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24" # <---- A single string value, not a list of strings
tags = {
Name = "Main"
}
}
As a hint for the future: the argument is singular, i.e., cidr_block so that usually means it's a single value.
[1] https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/subnet#basic-usage
Thank you #Marko E for your suggestion
after some research found solution for this issue, refer below code.:-
main.tf
#below code is for creating multiple subnets
resource "aws_subnet" "mysubnet_public" {
count = length(var.public_subnet_cidr)
vpc_id = aws_vpc.myvpc.id
cidr_block = element(var.public_subnet_cidr,count.index)
availability_zone = element(var.azs,count.index)
map_public_ip_on_launch = true
tags = {
Name = "Subnet-${count.index+1}"
}
}
#below code is for associating above created multiple subnets to route table
resource "aws_route_table_association" "myroutetableassociation_public" {
count = length(var.public_subnet_cidr)
subnet_id = element(aws_subnet.mysubnet_public[*].id, count.index)
route_table_id = aws_route_table.myroutetable_public.id
}
output.tf
output "mysubnet_public" {
description = "List of IDs of public route tables"
value = aws_subnet.mysubnet_public[*].id
}
output "myroutetableassociation_public" {
value = aws_route_table_association.myroutetableassociation_public[*].id
}
variable.tf
variable "public_subnet_cidr" {
type = list
}
variable "azs" {
type = list
}
usage.tf
provider "aws" {
region = "ap-south-1"
}
module "vpc_module" {
source = "./vpc"
vpc_name = "myvpc"
public_subnet_cidr = ["10.0.0.0/24", "10.0.1.0/24"]
azs = ["ap-south-1a", "ap-south-1b"]
}

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.

How to check, if Terraform will create resource

I use Terraform + AWS Lambda in my workflow, and i want to call some lambda with ARNs of my existent instances during every plan or apply action. I use this structure:
locals {
...
centralized_nodes = {...}
tags_map = {...}
backup_map = {
for node in keys(local.centralized_nodes):
node => aws_instance.centralized_node[node].arn
if can(local.tags_map[node]["backup"]) && !(can(aws_instance.centralized_node[node].root_block_device.0.tags["backup"]))
}
...
}
resource "aws_instance" "centralized_node" {
...
for_each = local.centralized_nodes
...
}
data "aws_lambda_invocation" "lambda_backup" {
for_each = local.backup_map
function_name = "lambdafunc"
input = jsonencode({
"resources" = [each.value]
})
}
it worked fine until i try to add node description object to centralized_nodes map and create new instance. When i add this, terraform shows me error during planning:
Error: Invalid for_each argument
│
│ on resources.tf line 223, in data "aws_lambda_invocation" "lambda_backup":
│ 223: for_each = local.backup_map
│ ├────────────────
│ │ local.backup_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.
Now i'm looking for terraform check if resource exists or will be created. i've tried adding arguments to map generator to exclude uncreated instances (it's OK in my workflow) like that:
if can(local.tags_map[node]["backup"]) && !(can(aws_instance.centralized_node[node].root_block_device.0.tags["backup"])) && can(aws_instance.centralized_node[node].arn)
But it doesn't work, terraform thinks it can reach ARN value for uncreated instance, but later.
Please, help me find the way to avoid such error.
Seems that your local.centralized_nodes is not pre-defined by rather generated automatically. If so, you can't use it with for_each.
The keys of the map (or all the values in the case of a set of strings) must be known values, or you will get an error message that for_each has dependencies that cannot be determined before apply, and a -target may be needed.
You can try converting your code to use count, as count does not have such a limitation. But then, count has its own issues which may prevent you from achieving your goals anyway.
I've found the only one way to get check if resources like instances and volumes already exist. It is using aws_instance and aws_instances datasources.
This is my solution:
locals {
...
centralized_nodes = {...}
tags_map = {...}
instance_name_root_volume_tags_map = {
for name, value in {for id, value in data.aws_instance.instance_id_arn_map : value.tags["Name"] => value}:
name => data.aws_ebs_volume.volume_instance_id_map[value.id].tags
}
backup_map = {
for name, value in {for id, value in data.aws_instance.instance_id_arn_map : value.tags["Name"] => value}:
name => value.arn
if !can(local.instance_name_root_volume_tags_map[name]["backup"]) && can({for node, value in local.tags_map: value["Name"] => value}[name]["backup"]) )
}
...
}
resource "aws_instance" "centralized_node" {
...
for_each = local.centralized_nodes
...
}
data "aws_lambda_invocation" "lambda_backup" {
for_each = local.backup_map
function_name = "lambdafunc"
input = jsonencode({
"resources" = [each.value]
})
}
data "aws_instances" "existant_instances_array" {
filter {
name = "tag:Name"
values = [for tagmap in local.hiera_tags_map : tagmap["Name"]]
}
}
data "aws_instance" "instance_id_arn_map" {
for_each = toset(data.aws_instances.existant_instances_array.ids)
instance_id = each.value
}
data "aws_ebs_volume" "volume_instance_id_map" {
for_each = data.aws_instance.instance_id_arn_map
filter {
name = "attachment.instance-id"
values = [each.value.id]
}
filter {
name = "attachment.device"
values = ["/dev/sda1"]
}
}