Creating and maintaining AWS security groups using Terraform has become even more accessible, thanks to community-built modules on Terraform Registry. Today I want to show you two particular features of one of the modules called Named Groups and Named Rules.
Named Groups allow you to create security groups with a known set of ingress and egress rules. Named Groups are available for 45+ applications, including HTTP, HTTPS, SSH, various databases, logging and monitoring tools, etc. For example, using the http-80
Named Group will create a security group with the standard ingress and egress rules for serving an application on HTTP.
Named Rules add some extra bit of magic to Named Groups. There are 110+ Named Rules available for known applications. Once you’ve created a security group, you can further customise the ingress and egress for that security group using Named Rules. For example, you can add the Named Rule for https-443-tcp to add support for HTTPS.
Using Named Groups and Named Rules allows you to define what you want in simple English instead of littering your Terraform with port numbers. As a consequence, Terraform projects become more comfortable to parse, review, and debug.
To get started, let’s first create a security group for an HTTP service.
Terraform Security Group Module
First, visit the Terraform Registry page and scroll down until you see the Featured Providers section.
Once there, select AWS. The next page will list all of the public modules available for the AWS provider. Scroll down the page until you see the Security Group module (the one with 5 million installs!). The module is very well documented with plenty of examples to get you started.
Creating a Security Group for an HTTP service
Let’s assume we have an HTTP service that requires port 80 open from all IP addresses. This service will also need to connect to the internet to download updates and packages. Using the Security Group module, we can implement these requirements by creating the following Terraform configuration:
module "sg-ayushsharma-in" {
source = "terraform-aws-modules/security-group/aws"
name = "sg-ayushsharma.in"
description = "HTTP security group."
vpc_id = module.vpc_ayushsharma_in.vpc_id
egress_with_cidr_blocks = [
{
from_port = 0
to_port = 65535
protocol = "all"
description = "Open internet"
cidr_blocks = "0.0.0.0/0"
}
]
ingress_with_cidr_blocks = [
{
from_port = 80
to_port = 80
protocol = "tcp"
description = "HTTP"
cidr_blocks = "0.0.0.0/0"
}
]
tags = {
env = "production"
project = "my-project"
owner = "ayushsharma.in"
}
}
Note: We’re using the VPC that we created earlier.
Once the file is in place, run terraform init
and terraform plan
to review the list of new resources.
Then, run terraform apply
and let the module do the rest!
module.sg-ayushsharma-in.aws_security_group.this_name_prefix[0]: Creating...
module.sg-ayushsharma-in.aws_security_group.this_name_prefix[0]: Creation complete after 3s [id=sg-0cbe42edd400b7ab3]
module.sg-ayushsharma-in.aws_security_group_rule.ingress_with_cidr_blocks[0]: Creating...
module.sg-ayushsharma-in.aws_security_group_rule.egress_with_cidr_blocks[0]: Creating...
module.sg-ayushsharma-in.aws_security_group_rule.ingress_with_cidr_blocks[0]: Creation complete after 1s [id=sgrule-305099671]
module.sg-ayushsharma-in.aws_security_group_rule.egress_with_cidr_blocks[0]: Creation complete after 3s [id=sgrule-1993722846]
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
Let’s check if our Terraform was successful using the AWS CLI:
> aws ec2 describe-security-groups --filter "Name=tag-key,Values=owner" "Name=tag-value,Values=ayushsharma.in"
{
"SecurityGroups": [
{
"Description": "HTTP security group.",
"GroupName": "mysg-ayushsharma.in-20200910202906131800000001",
"IpPermissions": [
{
"FromPort": 80,
"IpProtocol": "tcp",
"IpRanges": [
{
"CidrIp": "0.0.0.0/0",
"Description": "HTTP"
}
],
"Ipv6Ranges": [],
"PrefixListIds": [],
"ToPort": 80,
"UserIdGroupPairs": []
}
],
"OwnerId": "1234567890",
"GroupId": "sg-xxxxxxxxxx",
"IpPermissionsEgress": [
{
"IpProtocol": "-1",
"IpRanges": [
{
"CidrIp": "0.0.0.0/0",
"Description": "Open internet"
}
],
"Ipv6Ranges": [],
"PrefixListIds": [],
"UserIdGroupPairs": []
}
],
"Tags": [
{
"Key": "project",
"Value": "my-project"
},
{
"Key": "Name",
"Value": "mysg-ayushsharma.in"
},
{
"Key": "owner",
"Value": "ayushsharma.in"
},
{
"Key": "env",
"Value": "production"
}
],
"VpcId": "vpc-xxxxxxxxxx"
}
]
}
Using the HTTP Named Group
Let’s change things a bit and create the same security group above using a Named Group. In a new Terraform file, execute the following:
module "sg2-ayushsharma-in" {
source = "terraform-aws-modules/security-group/aws//modules/http-80"
name = "mysg2-ayushsharma.in"
description = "HTTP security group."
vpc_id = module.vpc_ayushsharma_in.vpc_id
egress_cidr_blocks = ["0.0.0.0/0"]
ingress_cidr_blocks = ["0.0.0.0/0"]
tags = {
env = "production"
project = "my-project"
owner = "ayushsharma.in"
}
}
Notice that in source
we’re using the path to a directory that contains our Named Group.
If you describe the above security group using the CLI, you will notice something different.
{
"SecurityGroups": [
{
"Description": "HTTP security group.",
"GroupName": "mysg2-ayushsharma.in-20200910204139314300000001",
"IpPermissions": [
{
"FromPort": 80,
"IpProtocol": "tcp",
"IpRanges": [
{
"CidrIp": "0.0.0.0/0",
"Description": "HTTP"
}
],
"Ipv6Ranges": [],
"PrefixListIds": [],
"ToPort": 80,
"UserIdGroupPairs": []
},
{
"IpProtocol": "-1",
"IpRanges": [],
"Ipv6Ranges": [],
"PrefixListIds": [],
"UserIdGroupPairs": [
{
"Description": "Ingress Rule",
"GroupId": "sg-xxxxxxxxxx",
"UserId": "1234567890"
}
]
}
],
"OwnerId": "1234567890",
"GroupId": "sg-xxxxxxxxxx",
"IpPermissionsEgress": [
{
"IpProtocol": "-1",
"IpRanges": [
{
"CidrIp": "0.0.0.0/0",
"Description": "All protocols"
}
],
"Ipv6Ranges": [
{
"CidrIpv6": "::/0",
"Description": "All protocols"
}
],
"PrefixListIds": [],
"UserIdGroupPairs": []
}
],
"Tags": [
{
"Key": "Name",
"Value": "mysg2-ayushsharma.in"
},
{
"Key": "env",
"Value": "production"
},
{
"Key": "project",
"Value": "my-project"
},
{
"Key": "owner",
"Value": "ayushsharma.in"
}
],
"VpcId": "vpc-xxxxxxxxxx"
}
]
}
Notice the following:
- First, a new ingress rule for the same security group is present to allow communication on all channels between services that share this security group.
- Second, an egress rule for IPv6 is also present. I missed this step while creating the security group manually in the previous section. Using the standard Named Groups ensures security groups are correct and consistent.
Using the HTTPS Named Rule
Now let’s add a rule for allowing traffic on HTTPS. We’re going to add a new ingress_rules
section to Terraform which will look like this:
module "sg2-ayushsharma-in" {
source = "terraform-aws-modules/security-group/aws//modules/http-80"
name = "mysg2-ayushsharma.in"
description = "HTTP security group."
vpc_id = module.vpc_ayushsharma_in.vpc_id
egress_cidr_blocks = ["0.0.0.0/0"]
ingress_cidr_blocks = ["0.0.0.0/0"]
ingress_rules = ["https-443-tcp"]
tags = {
env = "production"
project = "my-project"
owner = "ayushsharma.in"
}
}
Execute the above Terraform and describe the security group using the CLI. You’ll see the following:
{
"SecurityGroups": [
{
"Description": "HTTP security group.",
"GroupName": "mysg2-ayushsharma.in-20200910204139314300000001",
"IpPermissions": [
{
"FromPort": 80,
"IpProtocol": "tcp",
"IpRanges": [
{
"CidrIp": "0.0.0.0/0",
"Description": "HTTP"
}
],
"Ipv6Ranges": [],
"PrefixListIds": [],
"ToPort": 80,
"UserIdGroupPairs": []
},
{
"IpProtocol": "-1",
"IpRanges": [],
"Ipv6Ranges": [],
"PrefixListIds": [],
"UserIdGroupPairs": [
{
"Description": "Ingress Rule",
"GroupId": "sg-xxxxxxxxxx",
"UserId": "1234567890"
}
]
},
{
"FromPort": 443,
"IpProtocol": "tcp",
"IpRanges": [
{
"CidrIp": "0.0.0.0/0",
"Description": "HTTPS"
}
],
"Ipv6Ranges": [],
"PrefixListIds": [],
"ToPort": 443,
"UserIdGroupPairs": []
}
],
"OwnerId": "1234567890",
"GroupId": "sg-xxxxxxxxxx",
"IpPermissionsEgress": [
{
"IpProtocol": "-1",
"IpRanges": [
{
"CidrIp": "0.0.0.0/0",
"Description": "All protocols"
}
],
"Ipv6Ranges": [
{
"CidrIpv6": "::/0",
"Description": "All protocols"
}
],
"PrefixListIds": [],
"UserIdGroupPairs": []
}
],
"Tags": [
{
"Key": "Name",
"Value": "mysg2-ayushsharma.in"
},
{
"Key": "env",
"Value": "production"
},
{
"Key": "project",
"Value": "my-project"
},
{
"Key": "owner",
"Value": "ayushsharma.in"
}
],
"VpcId": "vpc-xxxxxxxxxx"
}
]
}
A new rule for HTTPS traffic is present in our security group. Right now it’s accepting traffic from all sources, but if you need to restrict it to another CIDR, you can modify the value of ingress_cidr_blocks
in the Terraform.
To summarise, the combination of Named Groups and Named Rules can save a lot of time and effort. With 45+ Named Groups and 110+ Named Rules the most common use-cases are already covered. And if your requirements are more fine-grained you can always use the regular module.
What’s next?
The AWS security group module above has 45+ different inputs that give you fine-grained control over configuration. There are other features in this module that I haven’t explored, like using computed values and support for VPC endpoint prefixes. Overall, there seem to be many possible combinations that might cover the large portion of security use-cases.
Anton Babenko and many contributors maintain this open-source module. If you found the module helpful, you can browse the list of open issues and contribute to the stability and popularity of this project. Once you’re comfortable with this module, you can get adventurous and create your own!.
Thanks for reading. And happy coding :)