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?