How to use locals to mimic dynamic variables (use case dynamic aws_security_group_rule)

Hi there ,
I am bit new to terraform experssions and would like to dyanamically create aws_security_group ingress rules using for each but I have hard time coming up with the right logic.

I have say 3 cases

  • case 1 : ssh only => port 22
  • case 2: ssh+ web => ports 22 , 80, 443
  • case 3 : ssh+web+ rdp => ports 22, 80, 443, 3389

I want to use for each to pick only the list of ports matching the case I choose (1 out of 3).

  • so I created a map expression for each case
    #SG rules
    variable "sg_mapping" {
      description = "mapping for sg rules "
      default = {
        "SSH" = "sg_ssh",
        "WEB" = "sg_web",
        "ALL" = "sg_all"
      }
    } 

# case 1
    variable "sg_ssh" {
        type = map
        default = {
            SSH = 22
        }
    }
# case 2
     variable "sg_web" {
            type = map
            default = {
                SSH = 22
                HTTP = 80
                HTTPS= 443
            }
        }

#case 3
 variable "sg_all" {
        type = map
        default = {
            SSH = 22
            RDP = 3389
            HTTP = 80
            HTTPS= 443
        }
    }     
  • I then thought of creating a variable that I can change to specify which case I am wishing to accomplish .
variable "sg_type"{
      default = "WEB" 
      }

I know there is no interpolation or any expression that wouln’t yeild a constant for variables in terraform .
so I thought I’d use locals but I am not familiar at all , hence the below is more a pseudo code than a code that is syntaxically correct .

locals {
  sg_rule = "${lookup(var.sg_mapping, var.sg_type)}"  
      }


resource "aws_security_group_rule" "terra_sg_rule" {
  for_each = var.${local.sg_rule}   # just want to select the right map list
  type              = "ingress"
  from_port         = each.value
  to_port           = each.value
  protocol          = "tcp"
  security_group_id = aws_security_group.some_test.id
  description = each.key
}                    
  • I know that we can’t call nested variables like the above as the result is not a constant and would have to be interpreted dynamically, but I also sense there is a way to load the right map by using a local variable . maybe even gather all the ports combination in one map (sub lists inside default maybe ).

Thanks in advance this is my first Topic so I appologise if I the format looks more like a draft.
Koss

Hi @brokedba,

I’m not sure if I’m following fully what you are aiming to do so for this first answer I’m going to focus only on the part of selecting one of the maps based on local.sg_rule.

As you mentioned, you can’t dynamically choose a variable like that, but you can dynamically index into a map and so you can make this work by constructing the necessary map first and then indexing it:

locals {
  sg_rule = var.sg_mapping[var.sg_type]

  rule_sets = {
    sg_ssh = var.sg_ssh
    sg_web = var.sg_web
    sg_all = var.sg_all
  }
}

resource "aws_security_group_rule" "terra_sg_rule" {
  for_each = local.rule_sets[local.sg_rule]

  # ...
}           

Hopefully that moves you a little closer to what you are looking for. If you have any follow-up questions from that, please let me know!

1 Like

Hi @apparentlymart , thank you for the quick reply.
I may have not explained clearly my goal by using those maps.
I wanted to replace my old vpc configuration that had a security group including hard coded inline ingress rules (fixed list of ports) by => an iteration of a dynamic block of type “aws_security_group_rule” using for_each function.

  • In addition I also want to have the posibility of choosing the list of ports to open through a variable (example : linux => 22 , webserver=>22/80/443, windows=> rdp/80/443) . Hence the 3 maps I listed in my OP.

I will start with your example , I feel I’m getting close :slight_smile: .

thank you again.
@brokedba

HI ,
Thank you so much that worked perfectly . the local variable made the concatenation of multiple variables in one possible .
local.rule_sets[local.sg_rule] ===> var.[var.sg_mapping[var.sg_type]

  • I now have a dynamic block for security_group rules that can change depending on the value I set sg_type which will apply the right set of ports.

  • I even found a way to output attributes of each of the rules (splat expression equivalent) using a for loop :slight_smile: .

    {for k,v in aws_security_group_rule.terra_sg_rule : k => v.to_port}
    { “HTTP” = 80
    “HTTPS” = 443
    “SSH” = 22}

I had another question if I may related to the maps I previously created for each case. Is it possible to merge all of these maps as subsets of a bigger map but still be able to select the right submap depending on the sg_type variable ?

  • I don’t know if the below is correct but It could look like this :
variable "main_sg" {
type = list(map(any))  
sg_ssh = {
            SSH = 22
        }
sg_web = {
                SSH = 22
                HTTP = 80
                HTTPS= 443
            }
sg_all = {
            SSH = 22
            RDP = 3389
            HTTP = 80
            HTTPS= 443
        }
}

It’s just to know if I can reduce the number of the lines further.

thanks again , I’d put 5 stars to your first answer already but there is no such thing in hashi-discuss :slight_smile: .
Cheers

I found how. If I wanted to consolidate all my maps in one and still call them using the local variable I would need to write it as below .

# all cases in one map 
    variable "main_sg" {
    default = {
    sg_ssh = {
       SSH = 22
             }, 
    sg_web = {  
       SSH = 22
       HTTP = 80
       HTTPS = 443 },
    sg_win { 
       RDP = 3389
       HTTP = 80
       HTTPS = 443 } 
                           }
}
 # Locals block  locals {
    sg_mapping = {           #  variable substitution within a variable
      SSH = var.main_sg.sg_ssh
      WEB = var.main_sg.sg_web
      WIN = var.main_sg.sg_win
  }
}

resource "aws_security_group_rule" "terra_sg_rule" {
  for_each = local.sg_mapping[var.sg_type] 

  # ...
}