Managing network access in AWS is inseparable from security groups. Terraform provides several distinct mechanisms to create and manage security group rules, each with tradeoffs in predictability, organization, and team collaboration. This article covers the resource-level options, the recommended modern pattern, data-driven bulk management, and the module ecosystems that abstract rules into firewall-like policies.
Introduction
Security groups are stateful stateful firewalls at the instance level. In Terraform the surface area for defining their rules has evolved. The classic aws_security_group resource allows inline ingress and egress blocks, while aws_security_group_rule provides standalone rule resources. HashiCorp now advises the split rule resources aws_vpc_security_group_ingress_rule and aws_vpc_security_group_egress_rule as the best practice for new code. In parallel, community modules like terraform-aws-modules/security-group/aws and terraform-aws-sg offer higher level abstractions for rule sets.
Inline rules versus standalone rules
Terraform supports two ways to add rules to a security group:
- Inline rules: Rules defined with the
aws_security_groupterraform resource - Standalone rules: Rules defined separately using the
aws_security_group_ruleterraform resource
Note: Keep in mind that these are the two ways to add rules to a security group through Terraform, which ultimately correspond to the same thing on AWS.
Terraform advises that inline rules should not be used in conjunction with standalone rules. Doing so will cause a conflict of rule settings and will overwrite rules.
The practical implication is that a security group managed with inline ingress and egress blocks cannot be mixed with aws_security_group_rule resources targeting the same group. The plan will show destructive replacement of the inline set whenever a standalone rule is added.
The awssecuritygroup_rule resource
Provides a security group rule resource. Represents a single ingress or egress group rule, which can be added to external Security Groups.
NOTE on Security Groups and Security Group Rules: Terraform currently provides both a standalone Security Group Rule resource (a single ingress oregress rule), and a Security Group resource with ingress and egress rules defined in-line. At this time you cannot use a Security Group with in-line rules in conjunction with any Security Group Rule resources. Doing so will cause a conflict of rule settings and will overwrite rules.
Basic usage
hcl
resource "aws_security_group_rule" "allow_all" {
type = "ingress"
from_port = 0
to_port = 65535
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
prefix_list_ids = ["pl-12c4e678"]
security_group_id = "sg-123456"
}
The following arguments are supported:
| Argument | Required | Description |
|---|---|---|
| type | Required | The type of rule being created. Valid options are ingress (inbound) or egress (outbound). |
| cidr_blocks | Optional | List of CIDR blocks. Cannot be specified with sourcesecuritygroup_id. |
| ipv6cidrblocks | Optional | List of IPv6 CIDR blocks. |
| prefixlistids | Optional | List of prefix list IDs for allowing access to VPC endpoints. Only valid with egress. |
| from_port | Required | The start port (or ICMP type number if protocol is "icmp"). |
| protocol | Required | The protocol. If not icmp, tcp, udp, or all use the protocol number |
| securitygroupid | Required | The security group to apply this rule to |
A typical plan for adding an SSH rule to a known security group looks like:
```
Terraform will perform the following actions:
awssecuritygrouprule.allowsshfromvpc will be created
- resource "awssecuritygrouprule" "allowsshfromvpc" {
- cidr_blocks = [
- "172.31.0.0/16",
] - description = "Allow SSH from VPC"
- from_port = 22
- id = (known after apply)
- protocol = "tcp"
- securitygroupid = "sg-0d7749dea35961abc"
- securitygrouprule_id = (known after apply)
- self = false
- sourcesecuritygroup_id = (known after apply)
- to_port = 22
- type = "ingress"
}
Plan: 1 to add, 0 to change, 0 to destroy.
```
Run the terraform apply command to deploy the changes.
When using for_each to target multiple groups, Terraform will destroy the previous singular rule and create indexed instances:
```
awssecuritygrouprule.allowsshfromvpc will be destroyed
(because resource uses count or for_each)
- resource "awssecuritygrouprule" "allowsshfromvpc" {
- cidr_blocks = [
- "172.31.0.0/16",
] -> null - description = "Allow SSH from VPC" -> null
- from_port = 22 -> null
- id = "sgrule-2807208966" -> null
- protocol = "tcp" -> null
- securitygroupid = "sg-0d7749dea35961abc" -> null
- securitygrouprule_id = "sgr-05e047df70ca6e3e2" -> null
- self = false -> null
- to_port = 22 -> null
type = "ingress" -> null
}awssecuritygrouprule.allowsshfromvpc["sg-0438324f09abb192e"] will be created
resource "awssecuritygrouprule" "allowsshfromvpc" {
- cidr_blocks = [
- "172.31.0.0/16",
] - description = "Allow SSH from VPC"
- from_port = 22
- id = (known after apply)
- protocol = "tcp"
- securitygroupid = "sg-0438324f09abb192e"
- securitygrouprule_id = (known after apply)
- self = false
- sourcesecuritygroup_id = (known after apply)
- to_port = 22
- type = "ingress"
}
```
The plan should reflect the deletion of the previous rule and the creation of two new rules along with the output securitygroupids.
Managing multiple externally created security groups with for_each
When security groups are created outside Terraform, such as via the AWS console, you can still manage rules for them centrally. Create a security group in the console with a tag:
json
{
"managed-by" = "aws-console"
}
Instead of referring to each of these security groups individually, we can use the awssecuritygroups data resource and provide the tag value to refer to all of them at once.
Note: Previously, we used awssecuritygroup data source to refer to a single security group and now we are using awssecuritygroup(s) data source to refer to multiple security groups.
hcl
data "aws_security_groups" "security_groups_managed_by_aws_console" {
tags = {
"managed-by" = "aws-console"
}
}
The data source returns a list of matching security groups. That list can be fed into for_each on aws_security_group_rule to apply the same rule to every matched group without hardcoding IDs.
Recommended modern rule resources
There a bunch of ways that you can handle AWS Security Group rules in Terraform, including in-line rules with the awssecuritygroup resource or the old awssecuritygrouprule resource, but the Terraform community recommends using awsvpcsecuritygroupingressrule and awsvpcsecuritygroupegress_rule as a best practice.
The split rule resources decouple rule lifecycle from the security group resource itself, avoiding the conflict warning and allowing rules to be added, removed, or changed independently. This is especially important when rules reference other security groups or when you want fine-grained dependency graphs.
Organizing rules across application and database modules
For example, suppose you have an application with all of its resources and its security group defined in application.tf and you have a database with all of its resources and its security group defined in database.tf. And then suppose you need a rule which allows egress traffic from the app to the database, and you need a rule which allows ingress traffic to the database from application. It can be easy to place each rule in the “wrong” file and then six months later when you need to make a change you forgot which rule is in which file. Or if you have dozens of related rules in the same configuration, it can be annoying to give each rule a unique name that you’ll be able to remember later.
A pattern to avoid those kind of problems is to place rules where there is really only one logical place in your configuration that a given rule could possibly be placed, so it always gets placed correctly. One common approach is to co-locate ingress rules with the target resource and egress rules with the source resource, or to centralize all cross-service rules in a dedicated networking file keyed by source-target pairs.
Module abstractions for security groups
terraform-aws-modules/security-group/aws
Terraform module which creates EC2 security group within VPC on AWS.
hcl
module "security_group" {
source = "terraform-aws-modules/security-group/aws"
name = "example"
description = "Example security group"
vpc_id = "vpc-12345678"
ingress_rules = {
https = {
from_port = 443
ip_protocol = "tcp"
cidr_ipv4 = "10.0.0.0/16"
description = "HTTPS from internal"
}
self-all = {
ip_protocol = "-1"
referenced_security_group_id = "self"
description = "All traffic from members of this SG"
}
}
egress_rules = {
all = {
ip_protocol = "-1"
cidr_ipv4 = "0.0.0.0/0"
}
}
tags = {
Environment = "dev"
}
}
Each preset submodule under modules/ ships a curated set of ingress rules for a specific service (PostgreSQL, Consul, Cassandra, etc.). Use one when a security group serves a single service.
hcl
module "postgresql_security_group" {
source = "terraform-aws-modules/security-group/aws//modules/postgresql"
name = "postgresql"
description = "PostgreSQL access"
vpc_id = "vpc-12345678"
ingress_cidr_ipv4 = {
vpc = "10.0.0.0/16"
peer = "172.16.0.0/12"
}
}
To allow traffic between members of the security group created by this module, set referencedsecuritygroup_id = "self" on the rule.
terraform-aws-sg firewall-like policy
terraform-aws-sg is a Terraform module to create AWS EC2 Security Group from a firewall-like ruleset policy. This Terraform module deploys an EC2 Security Group into specified VPC with ingress/egress rules generated from a 'policy document' in plain text format.
Motivation for this module was to allow people that are not familiar with terraform (like Network and InfoSec guys) to be able to create/review Security Groups configurations without HCL in a way.
Example policy input:
IN TCP 80 Any_IPv4,Any_IPv6 - HTTP Inbound
IN TCP 443 0.0.0.0/0,::/0 - HTTPS Inbound
IN TCP 8005 {bastion_ip}/32 - Tomcat admin from Bastion
IN PING 0.0.0.0/0,::/0 - PING from Internet
OUT TCP 3306 {sg_db} - Outbound to MySql DB
OUT TCP 443 pl-02cd2c6b - DynamoDB Prefix List
The module translates the plain text into the corresponding aws_security_group and rule resources.
Comparison of approaches
| Approach | Rule placement | Mixing risk | Typical use case |
|---|---|---|---|
| Inline rules in awssecuritygroup | Within group resource | High conflict with standalone | Simple groups, single file |
| awssecuritygroup_rule | Standalone resource | Cannot mix with inline | External groups, fine-grained rules |
| awsvpcsecuritygroupingressrule / egressrule | Standalone split resources | Recommended best practice | New code, large rule sets |
| terraform-aws-modules/security-group/aws | Module maps | Abstracted | Team standard modules |
| terraform-aws-sg | Policy document | Abstracted | Network/InfoSec review |
Conclusion
Choosing the right security group rule strategy in Terraform is about lifecycle isolation and team ownership. Inline rules are convenient for small, static groups but become brittle when teams need to add rules independently. Standalone aws_security_group_rule resources solve the brittleness but still carry the inline vs standalone conflict. The current best practice is to adopt aws_vpc_security_group_ingress_rule and aws_vpc_security_group_egress_rule for new work, and to use data sources such as aws_security_groups with tags and for_each when you must manage rules for groups created outside Terraform.
Module layers add further value. The terraform-aws-modules/security-group/aws module provides opinionated ingress and egress rule maps and service-specific presets, while terraform-aws-sg lets non-HCL reviewers express rules in a firewall-like policy document. Together these approaches let you keep rules in one logical place, avoid accidental duplication, and maintain an auditable security posture as infrastructure scales.