How to Terraform multiple security group with varying configuration

oksanah

Oksana Horlock

Posted on September 23, 2022

How to Terraform multiple security group with varying configuration

Recently I had to work on standardizing security configuration for some servers. These were created manually, as were all security groups associated with them.

We wanted to ensure that we knew exactly what ports were open for which server, and ported the configuration of the security groups to Terraform with a view to removing the old security groups and applying Terraform changes to create new ones.

This blog post explains how to create several security groups with varying configuration.

Firstly, I put all the configuration into variables.tf file :

variable "config" {
   default = {
    "server1" = {
       ports  = [
        {
          from = 3000
          to = 3000
          source="0.0.0.0/0"
        },
        {
          from = 3000
          to = 3000
          source="::/0"
        },
         {
          from = 25
          to = 25
          source="0.0.0.0/0"
        },
        {
          from = 587
          to = 587
          source ="0.0.0.0/0"
        },
        {
          from = 1433
          to = 1433
          source="sg-1234"
        },
        {       
          from = 0
          to = 65535
          source= "1.2.3.4/32"
        }
      ]
    },
     "server2" = {
      ports = [       
         {
          from = 2001
          to = 2001
          source="0.0.0.0/0"
        },
        {
          from = 2001
          to = 2001
          source="::/0"
        },
         {
          from = 24001
          to = 24001
          source="0.0.0.0/0"
        },
        {
          from = 24001
          to = 24001
          source="::/0"
        },
        {
          from = 1433
          to = 1433
          source="sg-1234"
        }
      ]
    }
     "server3" = {
        ports = null
    }, 
     "server4" = {
        ports = {
          from = 1433
          to = 1433
          source="sg-1234"
        },
     },
     "server5" = {
      ports = null
     }
   } 
 }
Enter fullscreen mode Exit fullscreen mode

This variable contains ports/ranges that are open only on some instances.
The traffic source of port 1433 includes the ID of an already existing security group.

At the beginning I tried to create 2 types of elements in the map: one for ports and another one for the range of ports but then realised it's easier to put everything into one type of element.

By default, each server has ports 80 and 443 open to all traffic, and port 3389 (Remote Desktop) open from a specific IP.

After creating the variable with configuration for each server, I defined a security group for each server using Terraform for_each meta argument. The name and tags of each security group created in this way contain the name of the server so that it's easily identifiable:

resource "aws_security_group" "server_access_sg" {
  for_each = var.config
  name = "${each.key}-sg"
  description = "The security group for ${each.key}"
  vpc_id = data.aws_vpc.default.id

  tags = {
    "Server" = "${each.key}"
    "Provider" = "Terraform"
  }
Enter fullscreen mode Exit fullscreen mode

Within each of the security groups I also used in-line ingress block to create security group rules:

ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "http"
    cidr_blocks = ["0.0.0.0/0", "::/0"]
  }

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "https"
    cidr_blocks = ["0.0.0.0/0", "::/0"]
  }
Enter fullscreen mode Exit fullscreen mode

Next, for each of the ports I created a dynamic ingress block using the splat expression, which is basically a simplified version of the for_each loop.

dynamic "ingress" {
  for_each = each.value.ports[*]
  content {
    from_port   =  ingress.value.from
    to_port     =  ingress.value.to
    protocol    = "tcp"
    cidr_blocks = ingress.value.from != 1433 ? [ ingress.value.source] : null 
    ipv6_cidr_block = ingress.value.source=="::/0" ? [ingress.value.source] : null
    security_groups =   ingress.value.from == 1433 ? [ ingress.value.source] : null 
  }
}
Enter fullscreen mode Exit fullscreen mode

Please note that this is a nested loop, and it's looping on the elements of each.value element of the first loop. For example, all the configuration inside the squiggly brackets for server1 is the value.

"server1" = {
      ports = [...]
}
Enter fullscreen mode Exit fullscreen mode

Because it's a nested loop, and "each" is used to refer to the elements of the parent loop, in order to populate the values, we use "ingress".

Another thing worth pointing out is the conditional creation of cidr_blocks/security_groups attributes. In this case security_groups argument needs to be created only when the rule for port 1433 is being defined. Therefore I set the attribute to null when one of the arguments is not needed.

Finally, I created an egress rule to allow all outgoing traffic for each security group:

egress {
    from_port   = 0
    to_port     = 0
    protocol    = -1
    cidr_blocks = ["0.0.0.0/0", "::/0"]
  } 
Enter fullscreen mode Exit fullscreen mode

Happy learning, and if you have any suggestions on improving the code above, please feel free to leave a comment :)

💖 💪 🙅 🚩
oksanah
Oksana Horlock

Posted on September 23, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related