Terraform network module – Part 2

T

Hi, devops fans. Welcome to the 2d part of articles devoted to the terraform network module. Here we are going to deal with routing and security groups. At 1st part we have finished at routing tables to the subnets. Let’s continue from that point.

Lets open terraform/modules/network/routes.tf:

# PRIVATE

resource "aws_route_table" "private" {
  count = length(aws_subnet.private)

  vpc_id = aws_vpc.main.id

  tags = merge(local.common_tags, {
    Name = format("%s-private-%s",
      local.name_prefix,
      substr(strrev(element(data.aws_availability_zones.az.names, count.index)), 0, 1)
    )
  })
}

resource "aws_route_table_association" "private" {
  count = length(aws_subnet.private)

  subnet_id      = element(aws_subnet.private[*].id, count.index)
  route_table_id = element(aws_route_table.private[*].id, count.index)
}

resource "aws_route" "private_natgw" {
  count = local.natgw_count == 0 ? 0 : length(aws_route_table.private)

  route_table_id         = element(aws_route_table.private[*].id, count.index)
  destination_cidr_block = "0.0.0.0/0"

  nat_gateway_id = local.natgw_count == 0 ? 0 : (
  local.natgw_count == 1 ? aws_nat_gateway.ngw[0].id : element(aws_nat_gateway.ngw[*].id, count.index)
  )
}

# PUBLIC

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-public"
  })
}

resource "aws_route_table_association" "public" {
  count = length(aws_subnet.public)

  subnet_id      = element(aws_subnet.public[*].id, count.index)
  route_table_id = aws_route_table.public.id
}

resource "aws_route" "route_public" {
  route_table_id         = aws_route_table.public.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.igw.id
}

Here we create private route tables and then assign those tables to our private subnets. Then to the default route configuration we are adding additional aws_route_table_association related to our Network Address Translation (NAT) gateway. Public part looks almost the same except that in that case we are adding route to Internet Gateway (IGW).

And now security groups (SG). 

When we create a VPC, it comes with a default security group which we are going to preserve. Here is terraform code responsible for that (terraform/modules/network/sg-default.tf):

### DEFAULT - keep empty!

resource "aws_default_security_group" "default" {
  vpc_id = aws_vpc.main.id
}

But we are also going to add additional security groups for our VPC. We are doing it at the network module as we can associate a security group only with resources in the VPC for which it is created. For each security group, we add rules that control the traffic based on protocols and port numbers. There are separate sets of rules for inbound traffic and outbound traffic.

Here we are going to create 3 additional security groups that would be used in further articles. Let’s start from management security group. It is the most simple. We simply allow all outgoing traffic + allow to send ping and allow ssh connection. We will use that security group for our bastion in order to connect to our private network. The code related to management SG is located at terraform/modules/network/sg-management.tf:

resource "aws_security_group" "management" {
  description = "Management"
  name        = "Management"
  vpc_id      = aws_vpc.main.id

  lifecycle {
    create_before_destroy = true
  }

  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-management"
  })
}

### EGRESS

resource "aws_security_group_rule" "management_egress" {
  description = "Management Egress"
  type        = "egress"
  from_port   = 0
  to_port     = 65535
  protocol    = "all"

  cidr_blocks       = ["0.0.0.0/0"] # tfsec:ignore:AWS007
  security_group_id = aws_security_group.management.id
}

### ICMP

resource "aws_security_group_rule" "management_icmp" {
  description = "Management ICMP"
  type        = "ingress"
  from_port   = -1
  to_port     = -1
  protocol    = "icmp"

  cidr_blocks       = ["0.0.0.0/0"] # tfsec:ignore:AWS006
  security_group_id = aws_security_group.management.id
}

### SSH FROM MANAGEMENT IPS

resource "aws_security_group_rule" "management_ssh" {
  count = length(var.management_ips)

  description = values(var.management_ips)[count.index]
  type        = "ingress"
  from_port   = 22
  to_port     = 22
  protocol    = "tcp"
  cidr_blocks = [keys(var.management_ips)[count.index]]

  security_group_id = aws_security_group.management.id
}

Then we have a separate security group that would be used by OpenSearch cluster. It also looks rather simple. Again we allow all outgoing traffic but allow only ping operation + incoming traffic from management security group. Despite it looks almost the same as management group – it is preferable to have it as separate definition as in practice you may want to extend it – e.g you may add additional access to OpenSearch cluster for some application. The code related to elasticsearch (es) security group is located at terraform/modules/network/sg-es.tf:

resource "aws_security_group" "es" {
  description = "Elasticsearch"
  name        = "Elasticsearch"
  vpc_id      = aws_vpc.main.id

  lifecycle {
    create_before_destroy = true
  }

  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-elasticsearch"
  })
}

### EGRESS

resource "aws_security_group_rule" "es_egress" {
  description = "ElasticSearch Egress"
  type        = "egress"
  from_port   = 0
  to_port     = 65535
  protocol    = "all"

  cidr_blocks       = [aws_vpc.main.cidr_block]
  security_group_id = aws_security_group.es.id
}

### ICMP

resource "aws_security_group_rule" "es_icmp" {
  description = "ElasticSearch ICMP"
  type        = "ingress"
  from_port   = -1
  to_port     = -1
  protocol    = "icmp"

  cidr_blocks       = [aws_vpc.main.cidr_block]
  security_group_id = aws_security_group.es.id
}

### FROM MANAGEMENT

resource "aws_security_group_rule" "es_management" {
  description = "From Management"
  type        = "ingress"
  from_port   = 0
  to_port     = 65535
  protocol    = "tcp"

  security_group_id        = aws_security_group.es.id
  source_security_group_id = aws_security_group.management.id
}

There is also one more interesting security group which will be used within articles related to themes at how deploy an elasticsearch cluster at AWS using ECS or EC2 instances + docker swarm. I called that security group as application SG. Here you can see the same standard things. All outgoing traffic, allow ping and traffic from management security group. But in addition we also allow traffic from itself – to not break any traffic between EC2 inside our private network. There is also the possibility of direct access for some external application – at real practice it can be some CI/CD server or external monitoring service, or it can be some inner company vpn address – suppose that in real life you will probably need it anyway. But if not – you may remove it. Remember that is only the skeleton which you will have to adjust to your requirements anyway. I also left here some mockup in case you are going to use ALB (Application Load Balancer) as a solution for giving public access to the Elasticsearch or OpenSearch cluster. We are not going to use ALB within further articles related to theme “How to deploy HA Elasticsearch cluster at AWS” – but I thought it would be useful for some of you to have it as a prepared template. Ok, enough text – here is the code – terraform/modules/network/sg-app.tf:

resource "aws_security_group" "app" {
  description = "App"
  name        = "App"
  vpc_id      = aws_vpc.main.id

  lifecycle {
    create_before_destroy = true
  }

  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-app"
  })
}

### EGRESS

resource "aws_security_group_rule" "app_egress" {
  description = "App Egress"
  type        = "egress"
  from_port   = 0
  to_port     = 65535
  protocol    = "all"

  cidr_blocks       = ["0.0.0.0/0"] # tfsec:ignore:AWS007
  security_group_id = aws_security_group.app.id
}

### ICMP

resource "aws_security_group_rule" "app_icmp" {
  description = "App ICMP"
  type        = "ingress"
  from_port   = -1
  to_port     = -1
  protocol    = "icmp"

  cidr_blocks       = ["0.0.0.0/0"] # tfsec:ignore:AWS006
  security_group_id = aws_security_group.app.id
}

### FROM MANAGEMENT

resource "aws_security_group_rule" "app_management" {
  description = "From Management"
  type        = "ingress"
  from_port   = 0
  to_port     = 65535
  protocol    = "tcp"

  security_group_id        = aws_security_group.app.id
  source_security_group_id = aws_security_group.management.id
}

### FROM ALB

//resource "aws_security_group_rule" "app_alb" {
//  count = length(var.app_ports)
//
//  description = "From ALB"
//  type        = "ingress"
//  from_port   = element(var.app_ports, count.index)
//  to_port     = element(var.app_ports, count.index)
//  protocol    = "tcp"
//
//  security_group_id        = aws_security_group.app.id
//  source_security_group_id = aws_security_group.alb.id
//}

### FROM ITSELF

resource "aws_security_group_rule" "app_self" {
  description = "From itself"
  type        = "ingress"
  from_port   = 0
  to_port     = 65535
  protocol    = "all"

  self              = true
  security_group_id = aws_security_group.app.id
}

### DIRECT ACCESS

resource "aws_security_group_rule" "app_vpn" {
  for_each = var.app_direct_access["vpn"]

  description = each.value
  type        = "ingress"
  from_port   = 0
  protocol    = "all"
  to_port     = 65535

  cidr_blocks       = [each.key]
  security_group_id = aws_security_group.app.id
}

Ok, seems that article already appeared to be long. Let’s finish with network terraform module at the next and last 3d part, where will speak about outputs, variables and see how to use network module in practice. We will implement out terrafrom network module and see also how all added resources are looks from AWS console side. If you are interested in current theme, please, check my blog regularly or simply  subscribe to my newsletter. Alternatively, you may pass all material at once in convenient and fast way at my on-line course at udemy. Below is the link to the course. As the reader of that blog you are also getting possibility to use coupon for the best possible low price.


architecture AWS cluster cyber-security devops devops-basics docker elasticsearch flask geo high availability java machine learning opensearch php programming languages python recommendation systems search systems spring boot symfony