AWS Security Group Rules in Terraform: Inline, Standalone, and Module Patterns

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_group terraform resource
  • Standalone rules: Rules defined separately using the aws_security_group_rule terraform 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.

Sources

  1. Spacelift Blog
  2. JWR Blog
  3. terraform-aws-sg ReadTheDocs
  4. W3Cub Terraform AWS Security Group Rule
  5. terraform-aws-modules/terraform-aws-security-group

Related Posts