If you have 10s of security group rules, it’s not effective to use aws_security_group_rule directly. Here I use a map and feed it to a module.
Sometimes, it is necessary to create security groups with many ingress and egress rules. For me, putting the rules in a map makes it easier to read and modify. First, let’s go through my main.tf. security-groups is a list of maps. Each map contains the security group name, description, and a list of rules. My main.tf tells the module to create 2 security groups.
The rules list of lists requires a rule id. It can be anything unique, so I use r1, r2, etc. Without the id, terraform process the list randomly. Next time when I run terraform, even if there is no change, terraform will try to destroy existing rules and create new ones because the list is ordered differently.
module bast-sg {
source = "../../modules/compute/security_groups"
security-groups = [
{
name = "WebAccess"
description = "Allow web access"
rules = [
["r1", "tcp", "172.20.0.0/16", "80", "80", "ingress", "HTTP"],
["r2", "tcp", "172.20.0.0/16", "443", "443", "ingress", "HTTPS"],
]
},
{
name = "SysadminAccess"
description = "Allow rdp access from sysadmin"
rules = [
["r1","tcp", "192.168.100.101/32", "3389", "3389", "ingress", "RDP"],
["r2", "tcp", "192.168.200.0/24", "3389", "3389", "ingress", "RDP"],
["r3","-1", "0.0.0.0/0", "0", "0", "egress", "Outbound"]
]
}
]
tags = local.default-tags
vpc-id = "vpc-12345678"
}
Next, let’s go through my module. The aws_security_group resource loops through the list and create security groups with description.
resource "aws_security_group" "sg" {
count = length(var.security-groups)
name = var.security-groups[count.index].name
description = var.security-groups[count.index].description
vpc_id = var.vpc-id
tags = merge(
var.tags,
{Name = var.security-groups[count.index].name}
)
}
Then flatten the rules list into a local rules variable. sg_key is generated, but rule_key is set using the rule id. Using the local rules variable, I can create aws_security_group_rule using the for_each loop.
// see https://www.terraform.io/docs/configuration/functions/flatten.html
locals {
rules = flatten([
for sg_key, sg in var.security-groups : [
for rule_key, rule in sg.rules : {
sg_key = sg_key
rule_key = rule[0]
sg_name = sg.name
protocol = rule[1]
cidr_blocks = rule[2]
from_port = rule[3]
to_port = rule[4]
type = rule[5]
description = rule[6]
}
]
])
}
resource "aws_security_group_rule" "rules" {
for_each = {
for rule in local.rules : "${rule.sg_key}.${rule.rule_key}" => rule
}
security_group_id = matchkeys(aws_security_group.sg.*.id, aws_security_group.sg.*.name, [each.value.sg_name])[0]
protocol = each.value.protocol
source_security_group_id = substr(each.value.cidr_blocks,0,2) == "sg" ? each.value.cidr_blocks : null
cidr_blocks = substr(each.value.cidr_blocks,0,2) != "sg" ? [each.value.cidr_blocks] : null
from_port = each.value.from_port
to_port = each.value.to_port
type = each.value.type
description = each.value.description
}
It’s a bit difficult to explain the transformation. Essentially, when terraform runs, it will generate rules [0.r1], [0.r2], etc. I also examine the value of cidr_blocks. If it starts with sg, then source_security_group_id is set. Otherwise, it will set the cidr_block parameter.
Give it a try and let me know if you think of a better way. Perhaps read the rules from a csv?
![]()