Creating AWS Resources with Terraform: AWS Security Groups

Sagar Kharab
4 min readAug 31, 2020

In last post we created Private and DB Subnets which were hidden from direct inbound internet access but could connect to internet via NAT Gateway. Now we will create security groups in order to allow traffic to AWS instances based on firewall rules.

Security groups are to act as virtual firewalls which controls the traffic coming to EC2 instances. When we launch any instance, we can add upto 5 security groups. They works on instance level and not subnet level. For Each secutiry group we can add rules which decide the traffic depending on protocal and port. Security groups also filter traffic based on source i.e. CIDR block, IP, another security group etc. Security groups are stateful — if you send a request from your instance, the response traffic for that request is allowed to flow in regardless of inbound security group rules unlike NACL. We will create three different type of groups i.e. open to public access, open to VPC traffic and DB security groups. Security group rules based on ingress and egress. Ingress is inbound traffic to security group and egress is outbound traffic.

Let’s create main.tf for security groups.

We will also be using outputs from the VPC module e.g. VPC id, Subnet ids etc.

locals {
vpc_cidr = var..vpc_cidr
vpc_id
= var.vpc_id
public_subnets
= var.public_subnets
private_subnets
= var.private_subnets

nginx_service
= {
ingress = [
{
description = "Nginx"
cidr_blocks
= ["0.0.0.0/0"]
from_port = 80
to_port = 80
protocol = "tcp"
},
{
description = "Nginx_Secure"
cidr_blocks
= ["0.0.0.0/0"]
from_port = 443
to_port = 443
protocol = "tcp"
},
{
description = "SSH"
cidr_blocks
= ["0.0.0.0/0"]
from_port = 22
to_port = 22
protocol = "tcp"
}
]
egress = [
{
description = "Internet open egress"
from_port
= 0
to_port = 0
protocol = "-1"
cidr_blocks
= ["0.0.0.0/0"]
}
]
}
rds_cluster = {
ingress = [
{
description = "Rds instances security groups"
cidr_blocks
= [local.vpc_cidr]
from_port = 3306
to_port = 3306
protocol = "tcp"
}
]
egress =[]
}
internal = {
ingress = [
{
description = "Internal Api Port"
cidr_blocks
= [local.vpc_cidr]
from_port = 80
to_port = 80
protocol = "tcp"
},
{
description = "Internal SSH Port"
cidr_blocks
= [local.vpc_cidr]
from_port = 22
to_port = 22
protocol = "tcp"
},
{
description = "Internal Api Port HTTPS"
cidr_blocks
= [local.vpc_cidr]
from_port = 443
to_port = 443
protocol = "tcp"
}
]
egress = [
{
description = "Rds Client"
cidr_blocks
= [local.vpc_cidr]
from_port = 3306
to_port = 3306
protocol = "tcp"
}
]
}
}


module "sg_public" {
source = "./aws-sg-module"
vpc_cidr
= local.vpc_cidr
vpc_id
= local.vpc_id
ingress
= local.nginx_service.ingress
egress
= local.nginx_service.egress
name
= "Public security group"
description
= "Public secuirty group"
}


module "sg_rds" {
source = "./aws-sg-module"
vpc_cidr
= local.vpc_cidr
vpc_id
= local.vpc_id
ingress
= local.rds_cluster.ingress
egress
= local.rds_cluster.egress
name
= "RDS security group"
description
= "RDS secuirty group"
}

module "sg_internal" {
source = "./aws-sg-module"
vpc_cidr
= local.vpc_cidr
vpc_id
= local.vpc_id
ingress
= local.internal.ingress
egress
= local.internal.egress
name
= "Private security group"
description
= "Private secuirty group"
}

We also need a variables.tf

#####################################################
# Variables
#####################################################

variable "region" {
type = string
default = "eu-west-1"
description
= "(optional) describe your variable"
}


variable "vpc_cidr" {
type = string
}
variable "vpc_id" {
type = string
}
variable "public_subnets" {
type = list
}
variable "private_subnets" {
type = list
}

Here we declared three security groups modules with different ingress and egress config. Now since these are modules, we would need to create a folder named aws-sg-module with below files

  • sg.tf
locals {
valid_ingress = [
for rule in var.ingress:
rule
if length(rule.cidr_blocks) > 0
]
valid_egress = [
for rule in var.egress:
rule
if length(rule.cidr_blocks) > 0
]
}


resource "aws_security_group" "security_group" {
vpc_id = var.vpc_id
name
= var.name
description
= var.description

dynamic "ingress"
{
for_each = local.valid_ingress
content
{
from_port = ingress.value.from_port
to_port
= ingress.value.to_port
protocol
= ingress.value.protocol
cidr_blocks
= ingress.value.cidr_blocks
description
= ingress.value.description
}
}
dynamic "egress" {
for_each = local.valid_egress
content
{
from_port = egress.value.from_port
to_port
= egress.value.to_port
protocol
= egress.value.protocol
cidr_blocks
= egress.value.cidr_blocks
description
= egress.value.description
}
}
lifecycle {
create_before_destroy = true
ignore_changes
= [
name
]
}
}
  • variables.tf
variable "vpc_id" {
type = string
description = "vpc_id"
}

variable "vpc_cidr" {
type = string
description = "vpc_cidr"
}

variable "name" {
type = string
description = "Name of Security group"
}

variable "description" {
type = string
description = "Description of Security group"
}

variable "ingress" {
type = any
description = "Ingress block"
}


variable "egress" {
type = any
description = "Egress block"
}
  • output.tf
output "id" {
value = aws_security_group.security_group.id
}

There it is. Now we have 3 types of security groups created for Each AZ, tied to subnets accordingly with restricted access.

--

--

Sagar Kharab

Software Developer by profession. Chef by chance. Runner by choice. Sums it all.