I am creating a security group that has some standard ingress rules. I also want to add additional ingress rules based on a variable.
variable "additional_ingress" {
type = list(object({
protocol = string
from_port = string
to_port = string
cidr_blocks = list(string)
}))
default = []
}
resource "aws_security_group" "ec2" {
name = "my-sg"
description = "SG for ec2"
vpc_id = data.aws_vpc.this.id
egress {
to_port = 0
from_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
protocol = "tcp"
from_port = 22
to_port = 22
cidr_blocks = ["10.0.0.0/8"]
}
# rdp
ingress {
protocol = "tcp"
from_port = 3389
to_port = 3389
cidr_blocks = ["10.0.0.0/8"]
}
# additional ingress rules
ingress {
for_each = var.additional_ingress
protocol = each.value.protocol
from_port = each.value.from_port
to_port = each.value.to_port
cidr_blocks = each.value.cidr_blocks
}
}
I am getting error
A reference to "each.value" has been used in a context in which it
unavailable, such as when the configuration no longer contains the
value in its "for_each" expression. │ Remove this reference to
each.value in your configuration to work around this error.
How do I add ingress rules based on variable
This is most easily managed with the aws_security_group_rule resource and the for_each meta-argument:
resource "aws_security_group_rule" "ec2" {
for_each = var.additional_ingress
type = each.value.type
from_port = each.value.from_port
to_port = each.value.to_port
protocol = each.value.protocol
cidr_blocks = each.value.cidr_blocks
security_group_id = aws_security_group.ec2.id
}
Note that the variable declaration for additional_ingress is missing the type key in its object constructor definition, so that would need to be added:
variable "additional_ingress" {
type = list(object({
type = string
...
}))
default = []
}
You can use dynamic blocks like this:
dynamic "ingress" {
for_each = var.additional_ingress
protocol = ingress.value.protocol
from_port = ingress.value.from_port
to_port = ingress.value.to_port
cidr_blocks = ingress.value.cidr_blocks
}
Provided the additional_ingress is an object (map) of ingress entries.
Of course, I would advise to use aws_security_group_rule resource, but if this is already live project, I understand if you'd want to stay with inline rules. Have fun!
How can I Use Loops to dynamically add acl rules ? I want to as example:
variable "protocol" will get all protocol values form the list of the objects [protocol1, protocol2, protocol3].
My code structure is :
-- Dev
-- main.tf
-- vars.tf
-- modules
-- acl
-- ressources.tf
This is my vars.tf
variable "acl_rules" {
type = list (object({
protocol = string
rule_no = number
action = string
cidr_block = string
from_port = number
to_port = number
}))
default = [
{ protocol = "tcp", rule_no = 200, action = "allow", cidr_block = "10.3.0.0/18", from_port = 443, to_port = 443 },
{ protocol = "udp", rule_no = 100, action = "allow", cidr_block = "10.3.0.0/18", from_port = 54, to_port = 54 },
{ protocol = "http", rule_no = 300, action = "allow", cidr_block = "10.3.0.0/18", from_port = 80, to_port = 80 }
]
}
This is my main.tf
# ACL for public subnet
module "acl" {
source = "../modules/acl"
vpc_id = module.vpc.vpcId
pub_snId_aza = element(module.pub-sn.snId[*], 0)
for_each = [for rule_obj in var.acl_rules :{
protocol = var.rule_obj.protocol
rule_no = var.rule_obj.rule_no
action = var.rule_obj.action
cidr_block = var.rule_obj.cidr_block
from_port = var.rule_obj.from_port
to_port = var.rule_obj.to_port
}
]
}
I'm implementing a security group modules such that it will create security group rules by taking & filtering cidr & source_security_group_id to create a security group rule.
The current module configuration.
securty_group_module.tf
resource "aws_security_group" "this" {
name = var.name
description = var.description
vpc_id = var.vpc_id
revoke_rules_on_delete = var.revoke_rules_on_delete
}
## CIDR Rule
resource "aws_security_group_rule" "cidr_rule" {
count = length(var.security_group_rules)
type = var.security_group_rules[count.index].type
from_port = var.security_group_rules[count.index].from_port
to_port = var.security_group_rules[count.index].to_port
protocol = var.security_group_rules[count.index].protocol
cidr_blocks = var.security_group_rules[count.index].cidr_block
description = var.security_group_rules[count.index].description
security_group_id = aws_security_group.this.id
}
## Source_security_group_id Rule
resource "aws_security_group_rule" "source_sg_id_rule" {
count = length(var.security_group_rules)
type = var.security_group_rules[count.index].type
from_port = var.security_group_rules[count.index].from_port
to_port = var.security_group_rules[count.index].to_port
protocol = var.security_group_rules[count.index].protocol
source_security_group_id = var.security_group_rules[count.index].source_security_group_id
description = var.security_group_rules[count.index].description
security_group_id = aws_security_group.this.id
}
main.tf
module "sample_sg" {
source = "./modules/aws_security_group"
name = "test-sg"
vpc_id = "vpc-xxxxxx"
security_group_rules = [
{ type = "ingress", from_port = 22, to_port = 22, protocol = "tcp", cidr_block = [var.vpc_cidr], description = "ssh" },
{ type = "ingress", from_port = 80, to_port = 80, protocol = "tcp", cidr_block = [var.vpc_cidr], description = "http" },
{ type = "ingress", from_port = 0, to_port = 0, protocol = "-1", source_sg_id = "sg-xxxx", description = "allow all" }
{ type = "egress", from_port = 0, to_port = 0, protocol = "-1", source_sg_id = "sg-xxxx", description = "allow all" }
]
}
So, the problem statement here is when I call the security group rules in the module with the above list of maps, it should check if it is source_sg_id or cidr.
Then filter those maps & pass it to respective resources in the module.
Ex:
module ""{
...
security_group_rules = [
{ type = "ingress", from_port = 22, to_port = 22, protocol = "tcp", cidr_block = [var.vpc_cidr], description = "ssh" },
{ type = "ingress", from_port = 0, to_port = 65535, protocol = "-1", source_sg_id = "sg-xxxx", description = "allow all" }
]
}
These rules should be looked up & pass the first one to CIDR rule & second one to Source_security_group_id rule.
I'm thinking of making it as below
locals {
sid_rules = some_function{var.security_group_rules, "source_security_group_id"}
cidr_rules = some_function{var.security_group_rules, "cidr"}
}
resource "aws_security_group_rule" "cidr_rule" {
count = count(local.cidr_rules)
....
cidr_blocks = local.cidr_rules[count.index].cidr_block
....
}
resource "aws_security_group_rule" "sid_rule" {
count = count(local.sid_rules)
....
source_security_group_id = local.sid_rules[count.index].source_sg_id
....
}
So, I'm looking for a way to filter the maps from list based on a key
I have tried lookup but was no help in case of list of string.
I figured out a clever way to do this.
Let's say I am trying to filter only the pets that are cats kind = "cat" from a list of pets.
variable "pets" {
type = list(object({
name = string
kind = string
}))
default = [
{
name = "Fido"
kind = "dog"
},
{
name = "Max"
kind = "dog"
},
{
name = "Milo"
kind = "cat"
},
{
name = "Simba"
kind = "cat"
}
]
}
First convert the list of pets to a map pets_map of pets using the index tostring(i) as the key.
This will be used in step 3 to lookup the filtered pets.
locals {
pets_map = { for i, pet in var.pets : tostring(i) => pet }
}
Next create a filtered list of the keys that respectively matches the condition pet.kind == "cat"
by looping over the keys in the pets_map and setting the respective keys that do not match to an
empty string. Then compact the list which removes the empty strings from the list.
locals {
cats_keys = compact([for i, pet in local.pets_map : pet.kind == "cat" ? i : ""])
}
Loop over the filtered keys cats_keys and lookup the respective pet from the pets_map. Now you
have the filtered list of pets that are cats kind = "cat".
locals {
cats = [for key in local.cats_keys : lookup(local.pets_map, key)]
}
You can now access the cats with local.cats, which will give you the following map.
{
name = "Milo"
kind = "cat"
},
{
name = "Simba"
kind = "cat"
}
Below is the full example.
variable "pets" {
type = list(object({
name = string
kind = string
}))
default = [
{
name = "Fido"
kind = "dog"
},
{
name = "Max"
kind = "dog"
},
{
name = "Milo"
kind = "cat"
},
{
name = "Simba"
kind = "cat"
}
]
}
locals {
pets_map = { for i, pet in var.pets : tostring(i) => pet }
cats_keys = compact([for i, pet in local.pets_map : pet.kind == "cat" ? i : ""])
cats = [for key in local.cats_keys : lookup(local.pets_map, key)]
}
Consider creating another module to handle the rules, and setting the security group resources inside that module.
module "security_groups" {
count = length(var.security_group_rules)
source_sg_id_rule = var.security_group_rules[count.index].source_sg_id_rule
}
Then, in the new module, use a count statement as a test to create optional items:
resource "aws_security_group_rule" "source_sg_id_rule" {
count = length(var.source_sg_id_rule) == 0 ? 0 : 1
type = var.type
from_port = var.from_port
to_port = var.to_port
protocol = var.protocol
source_security_group_id = var.source_security_group_id
description = var.description
security_group_id = var.security_group_id
}
This will create the resources as an array of one or zero items, and drop any lists of zero.
Thanks for the response #dan-monego.
I sorted it out with single module itslef.
Following is the module file.
aws_sg_module.tf
# Security group
##########################
resource "aws_security_group" "this" {
name = var.name
description = var.description
vpc_id = var.vpc_id
revoke_rules_on_delete = var.revoke_rules_on_delete
tags = merge(
{
"Name" = format("%s", var.name)
},
local.default_tags,
var.additional_tags
)
}
resource "aws_security_group_rule" "cidr" {
count = var.create ? length(var.cidr_sg_rules) : 0
type = var.cidr_sg_rules[count.index].type
from_port = var.cidr_sg_rules[count.index].from
to_port = var.cidr_sg_rules[count.index].to
protocol = var.cidr_sg_rules[count.index].protocol
cidr_blocks = var.cidr_sg_rules[count.index].cidr
description = var.cidr_sg_rules[count.index].description
security_group_id = local.this_sg_id
}
resource "aws_security_group_rule" "source_sg" {
count = var.create ? length(var.source_sg_rules) : 0
type = var.source_sg_rules[count.index].type
from_port = var.source_sg_rules[count.index].from
to_port = var.source_sg_rules[count.index].to
protocol = var.source_sg_rules[count.index].protocol
source_security_group_id = var.source_sg_rules[count.index].source_sg_id
description = var.source_sg_rules[count.index].description
security_group_id = local.this_sg_id
}
resource "aws_security_group_rule" "self" {
count = var.create ? length(var.self_sg_rules) : 0
self = true
type = var.source_sg_rules[count.index].type
from_port = var.source_sg_rules[count.index].from
to_port = var.source_sg_rules[count.index].to
protocol = var.source_sg_rules[count.index].protocol
description = var.source_sg_rules[count.index].description
security_group_id = local.this_sg_id
}
Call it using following module block.
security_groups.tf
module "stack_sg" {
source = "./modules/aws_security_group"
name = "stack-sg"
vpc_id = module.network.vpc_id
cidr_sg_rules = [
{ type = "ingress", from = 80, to = 80, protocol = "tcp", cidr = [module.network.vpc_cidr], description = "http" },
{ type = "egress", from = 0, to = 65535, protocol = "-1", cidr = ["0.0.0.0/0"], description = "allow all " }
]
source_sg_rules = [
{ type = "ingress", from = 0, to = 65535, protocol = "tcp", source_sg_id = module.alb_sg.sg_id, description = "alb" }
]
}
In order to filter a list of maps using a specific key value. You can use the following simple statement:
Assuming:
key is map's key you're filtering on
val is the value of the key
list is the original list of maps
element([
for element in list) : env
if element.key == "val"
], 0)
the result of the above statement will be a map.
Given:
[{foo: "...", baz: "..."}, ...]
And you want the baz of some element in this list, use:
element([
for o in [{"foo"="kick","baz"="5"},{"foo"="bar","baz"="100"}] :
o if o.foo == "bar"
],0).baz
In Terraform, how can access the values from the variable below?
variable "egress_rules" {
type = list(object({
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
}))
default = [
{
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
]
}
I tried:
resource "aws_security_group_rule" "egress" {
security_group_id = aws_security_group.new.id
type = "ingress"
for_each = var.egress_rules
from_port = each.value.from_port
to_port = each.value.to_port
protocol = each.value.protocol
cidr_blocks = each.value.cidr_blocks
}
But got this error:
Error: Invalid for_each argument
What is the correct way to reference this variable?
for_each will not work with a list of maps. You have to convert it to a map. This is commonly done through a for expression:
resource "aws_security_group_rule" "egress" {
security_group_id = aws_security_group.new.id
type = "ingress"
for_each = { for idx, rule in var.egress_rules: idx => rule }
from_port = each.value.from_port
to_port = each.value.to_port
protocol = each.value.protocol
cidr_blocks = each.value.cidr_blocks
}
to set multiple properties of a resource you could use a map of objects like this:
variable "egress_rules" {
type = map(object({
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
}))
}
Your variable definition would be:
egress_rules = {
{
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
The updated resource definition would be:
resource "aws_security_group_rule" "egress" {
security_group_id = aws_security_group.new.id
type = "egress"
for_each = var.egress_rules
from_port = each.value["from_port"]
to_port = each.value["to_port"]
protocol = each.valuep["protocol"]
cidr_blocks = each.value["cidr_blocks"]
}
Adding to above answer from #Marcin
If you want to use single object instead of list then you can directly access without for_each
Instead of declaring it as list of objects; use single object as shown below
variable "egress_rules" {
type = object({
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
})
default = ({
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}}
}
Then access in your resource like
resource "aws_security_group_rule" "egress" {
security_group_id = aws_security_group.new.id
from_port = var.egress_rules.from_port
to_port = var.egress_rules.from_port
protocol = var.egress_rules.protocol
cidr_blocks = var.egress_rules.cidr_blocks
}
The value of google_compute_subnetwork.subnetwork.secondary_ip_range looks like this:
[
{
ip_cidr_range = 10.1.0.0/16,
range_name = my-range
}
]
I can't figure out how to loop over that, this doesn't work:
resource "aws_security_group_rule" "sdfsdfsdf" {
count = "${length(data.google_compute_subnetwork.mysubnetwork.secondary_ip_range)}"
type = "ingress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["${data.google_compute_subnetwork.mysubnetwork.secondary_ip_range[count.index]}.ip_cidr_range}"]
}
the usage of count.index is in this document:
https://www.terraform.io/docs/configuration-0-11/interpolation.html#element-list-index-
element(aws_subnet.foo.*.id, count.index)
So your code can be changed to
resource "aws_security_group_rule" "sdfsdfsdf" {
count = "${length(data.google_compute_subnetwork.mysubnetwork.secondary_ip_range)}"
type = "ingress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["${element(data.google_compute_subnetwork.mysubnetwork.secondary_ip_range.*.ip_cidr_range, count.index)}"]
}