Managing AWS Security Groups with Terraform is central to maintaining consistent network controls across dev, staging, and production. Security Groups are one of the core components of an AWS infrastructure, and when managed with Terraform, it brings advanced automation, control, and consistency to your cloud environment. The challenge is not just creating a security group, but deciding how to define its rules in a way that remains maintainable as the rule set grows.
This article covers the two primary rule models Terraform supports, how to structure standalone rules with aws_security_group_rule, the newer aws_vpc_security_group_ingress_rule and aws_vpc_security_group_egress_rule resources, module patterns with dynamic blocks, and practical organization strategies to avoid common pitfalls.
Inline Rules vs Standalone Rules
Terraform provides two ways to add rules to a security group, which ultimately correspond to the same thing on AWS.
- 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.
The choice affects plan diffs, modularity, and reuse. Inline rules are convenient for simple security groups with a handful of rules. For anything more complex, separate rule resources with for_each give you granular control and cleaner diffs.
Standalone Security Group Rules Example
Terraform enables you to do this via the aws_security_group_rule resource.
```hcl
resource "awssecuritygroup" "websg" {
name = "web-sg"
description = "Web Server SG"
vpcid = aws_vpc.main.id
}
resource "awssecuritygrouprule" "allowhttp" {
type = "ingress"
fromport = 80
toport = 80
protocol = "tcp"
cidrblocks = ["0.0.0.0/0"]
securitygroupid = awssecuritygroup.websg.id
}
resource "awssecuritygrouprule" "allowallegress" {
type = "egress"
fromport = 0
toport = 0
protocol = "-1"
cidrblocks = ["0.0.0.0/0"]
securitygroupid = awssecuritygroup.web_sg.id
}
```
This approach is modular and useful when reusing or dynamically creating rules via for_each or count.
Dynamic Rule Creation with for_each and count
When a team wants to open multiple ports for the same Security Group, a port list can be looped through with for_each:
```hcl
variable "ports" {
default = [80, 443, 8080]
}
resource "awssecuritygrouprule" "httprules" {
foreach = toset(var.ports)
type = "ingress"
fromport = each.value
toport = each.value
protocol = "tcp"
cidrblocks = ["0.0.0.0/0"]
securitygroupid = awssecuritygroup.web_sg.id
}
```
Using for_each to create multiple rules dynamically gives fine-grained control over each rule and produces cleaner Terraform plans than repeating blocks.
Environment-specific rules can be made dynamic with variables or workspaces. If you want to set a specific set of rules for different development environments, use variables or workspaces in Terraform to dynamically assign different CIDR blocks or ports.
hcl
variable "env" {
default = "dev"
}
Cross Security Group References
Use a Terraform awssecuritygroup_rule to allow inbound traffic from the app server’s SG ID.
hcl
source_security_group_id = aws_security_group.app_sg.id
This pattern avoids hard-coding CIDR ranges when traffic should be allowed only from another security group within the same VPC.
Restricting Egress for Compliance
Many configurations use 0.0.0.0/0 for all outbound. Consider restricting this to only the ports and destinations your application actually needs.
An organization needs to block all outbound traffic except for a specific IP range, so you can define the rules in Terraform Security Group.
hcl
egress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["198.51.100.0/24"]
}
Restricting egress reduces blast radius and supports compliance requirements.
Module Structure for Reuse
Modules allow you to extract and reuse Security Group configurations across different environments and teams.
Folder structure:
modules/
└── security_group/
├── main.tf
├── variables.tf
└── outputs.tf
main.tf inside module:
```hcl
resource "awssecuritygroup" "this" {
name = var.name
description = var.description
vpcid = var.vpcid
dynamic "ingress" {
foreach = var.ingressrules
content {
fromport = ingress.value.fromport
toport = ingress.value.toport
protocol = ingress.value.protocol
cidrblocks = ingress.value.cidrblocks
}
}
dynamic "egress" {
foreach = var.egressrules
content {
fromport = egress.value.fromport
toport = egress.value.toport
protocol = egress.value.protocol
cidrblocks = egress.value.cidrblocks
}
}
}
```
Modules promote reuse, simplify maintenance, and make it easier to manage security groups across environments like dev, staging, and production.
Managing Multiple Security Groups with Data Sources
Managing multiple security groups with for_each allows bulk operations on tagged resources.
Let us create another security group called security-group-managed-outside-terraform-2 using the AWS console with the tag shown below.
hcl
{
"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"
}
}
This enables rule creation against externally managed groups without hard-coding IDs.
Organization Patterns for Rule Placement
There are 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.
But when you are creating resources for each individual rule, it can sometimes be difficult to keep them organized.
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 that reduces this risk is to centralize rules so there is only one logical place in your configuration that a given rule could possibly be placed, so it always gets placed correctly.
Rule Resource Comparison
The following table summarizes the Terraform resources available for Security Group rules.
| Resource | Scope | Typical Use |
| awssecuritygroup | Inline rules | Simple groups with few rules |
| awssecuritygrouprule | Standalone rules | Modular reuse, foreach |
| awsvpcsecuritygroupingressrule | Per rule ingress | Recommended best practice |
| awsvpcsecuritygroupegressrule | Per rule egress | Recommended best practice |
For simple security groups with a handful of rules, inline rules work fine. For anything more complex, separate rule resources with for_each give you granular control and cleaner diffs. Wrapping everything in a module keeps your codebase consistent as it grows.
Common Issues and Pitfalls
Common pitfalls include cyclical dependencies, overlapping rules, improper CIDR formatting, or not updating state after manual changes in the AWS Console.
Additional issues observed in practice:
- That includes awssecuritygrouprule, awsvpcsecuritygroupingressrule, and awsvpcsecuritygroupegress_rule. Terraform will fight itself trying to manage them.
- Forgetting createbeforedestroy - Without this lifecycle rule, Terraform may try to delete the security group before creating a replacement, which fails if other resources still reference it.
- Overly permissive egress - Many configurations use 0.0.0.0/0 for all outbound. Consider restricting this to only the ports and destinations your application actually needs.
- Not using nameprefix - Using name instead of nameprefix prevents createbeforedestroy from working because security group names must be unique.
You can use the awssecuritygroup resource block in Terraform to define the name, description, VPC, and rule sets for your Security Group. Therefore, whether you need to automate a few specific ports or set rules for all of them, Terraform makes it easy to maintain the AWS infrastructure.
Basic Security Group Creation
How do I create a basic Security Group with Terraform?
You can use the awssecuritygroup resource block in Terraform to define the name, description, VPC, and rule sets for your Security Group.
What’s the benefit of using modules for Security Groups in Terraform?
Modules promote reuse, simplify maintenance, and make it easier to manage security groups across environments like dev, staging, and production.
Operational Workflow Example
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.
Conclusion
Security Group rule management in Terraform is about picking one approach per security group and sticking with it. Inline rules keep small configurations concise. Standalone rules with awssecuritygrouprule and foreach provide modularity and dynamic generation. The newer awsvpcsecuritygroupingressrule and awsvpcsecuritygroupegressrule resources are the recommended best practice for per-rule management.
Modules centralize variables for ingressrules and egressrules and allow reuse across dev, staging, and production. Data sources like awssecuritygroups enable bulk referencing of externally managed groups by tags.
The key is picking one approach per security group and sticking with it. Consistent naming, restricted egress, cross-SG references instead of open CIDRs, and lifecycle safeguards like createbeforedestroy and name_prefix prevent drift and deployment failures. Wrapping everything in a module keeps your codebase consistent as it grows.