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
}
}
}
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"
}
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"]
}
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
}
}
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 = [...]
}
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"]
}
Happy learning, and if you have any suggestions on improving the code above, please feel free to leave a comment :)
Top comments (2)
How do you reference server1 security group id in server2? Is it possible?
This is very useful and creative! Very impressive!!