Replacing Native NAT Gateways with Aviatrix Spoke Gateways

In this post I’m going to transfer the functionality of a couple of native NAT gateways to Aviatrix while preserving the NAT GWs IP addresses. If you need a refresh on AVX egress capabilities please take a look at:

AVX spoke gateways can be used as egress in a distributed model customizing the snat functionality.

Elastic IP (EIP)

An Elastic IP address is a static IPv4 address which is reachable from the internet. An Elastic IP address is allocated to your AWS account, and is yours until you release it.

NAT Gateways

A NAT gateway is a Network Address Translation (NAT) service providing internet access to instances in a private subnet. The NAT gateway replaces the source IP address of the instances with the IP address of the NAT gateway.

Aviatrix EIPs

Aviatrix Controller creates EIPs when creating gateways and applies tags as show below:

  • Backup
  • Aviatrix-Created-Resource
  • Type
  • Name
  • Controller

The gw EIP is used in several places such as security rules, syslog, netflow among others. The controller is responsible to manage it.

Using terraform:

resource "aws_eip" "aws_eip-nat" {
  count = 2
  lifecycle {
    ignore_changes = [tags, ]
  }
}

NAT Gateways Deployment

Two GWs are deployed on different AZs:

Using terraform:

resource "aws_nat_gateway" "aws_nat_gateway" {
  count         = var.aws_nat == true ? 2 : 0
  allocation_id = aws_eip.aws_eip-nat["${count.index}"].id
  subnet_id     = data.aws_subnet.aws_subnet_prefix["${count.index}"].id
}

Route table (private):

Using terraform:

resource "aws_route" "aws_route-nat" {
  count                  = var.aws_nat == true ? 2 : 0
  route_table_id         = data.aws_route_table.aws_route_table-private["${count.index}"].id
  destination_cidr_block = "0.0.0.0/0"
  nat_gateway_id         = aws_nat_gateway.aws_nat_gateway["${count.index}"].id
  depends_on = [
    aws_nat_gateway.aws_nat_gateway
  ]
}

Aviatrix Gateways Deployment

resource "aviatrix_spoke_gateway" "aviatrix_spoke_gateway-gateway" {
  depends_on = [
    aws_nat_gateway.aws_nat_gateway
  ]
  count                             = var.aws_nat == true ? 0 : 1
  cloud_type                        = "1"
  account_name                      = var.account
  gw_name                           = var.vpc_name
  vpc_id                            = data.aws_vpc.vpc_name.id
  vpc_reg                           = var.region
  gw_size                           = var.gw_size
  subnet                            = data.aws_subnet.aws_subnet_public-prefix[0].cidr_block
  ha_subnet                         = data.aws_subnet.aws_subnet_public-prefix[1].cidr_block
  ha_gw_size                        = var.gw_size
  manage_transit_gateway_attachment = false
  allocate_new_eip                  = false
  eip                               = aws_eip.aws_eip-nat[0].public_ip
  ha_eip                            = aws_eip.aws_eip-nat[1].public_ip
}

Once the gateways are provisioned we need to create the custom snat policy:

resource "aviatrix_gateway_snat" "aviatrix_gateway_snat-egress" {
  depends_on = [
    aviatrix_spoke_gateway.aviatrix_spoke_gateway-gateway
  ]
  count     = var.aws_nat == true ? 0 : 1
  gw_name   = aviatrix_spoke_gateway.aviatrix_spoke_gateway-gateway[0].gw_name
  snat_mode = "customized_snat"
  snat_policy {
    src_cidr    = data.aws_vpc.vpc_name.cidr_block
    src_port    = ""
    dst_cidr    = ""
    dst_port    = ""
    protocol    = "all"
    interface   = "eth0"
    connection  = "None"
    mark        = ""
    snat_ips    = aviatrix_spoke_gateway.aviatrix_spoke_gateway-gateway[0].private_ip
    snat_port   = ""
    exclude_rtb = ""
  }
}

resource "aviatrix_gateway_snat" "aviatrix_gateway_snat-egress-ha" {
  depends_on = [
    aviatrix_spoke_gateway.aviatrix_spoke_gateway-gateway
  ]
  count     = var.aws_nat == true ? 0 : 1
  gw_name   = aviatrix_spoke_gateway.aviatrix_spoke_gateway-gateway[0].ha_gw_name
  snat_mode = "customized_snat"
  snat_policy {
    src_cidr    = data.aws_vpc.vpc_name.cidr_block
    src_port    = ""
    dst_cidr    = ""
    dst_port    = ""
    protocol    = "all"
    interface   = "eth0"
    connection  = "None"
    mark        = ""
    snat_ips    = aviatrix_spoke_gateway.aviatrix_spoke_gateway-gateway[0].ha_private_ip
    snat_port   = ""
    exclude_rtb = ""
  }
}

The flat “apply route” takes care of creating the proper route to bring the egress traffic to the gateway:

The last step is to attach them to the transit:

resource "aviatrix_spoke_transit_attachment" "aviatrix_spoke_transit_attachment-gateway" {
  depends_on = [
    aviatrix_spoke_gateway.aviatrix_spoke_gateway-gateway
  ]
  count           = var.aws_nat == true ? 0 : 1
  spoke_gw_name   = aviatrix_spoke_gateway.aviatrix_spoke_gateway-gateway[0].gw_name
  transit_gw_name = var.transit_gw
}

Extras

Data:

data "aws_vpc" "vpc_name" {
  filter {
    name   = "tag:Name"
    values = [var.vpc_name]
  }
}

data "aws_subnets" "aws_subnets_public" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.vpc_name.id]
  }
  tags = {
    Name = "*public*"
  }
}
data "aws_subnet" "aws_subnet_public-prefix" {
  count = length(data.aws_subnets.aws_subnets_public.ids)
  id    = tolist(data.aws_subnets.aws_subnets_public.ids)[count.index]
}

data "aws_subnets" "aws_subnets_private" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.vpc_name.id]
  }
  tags = {
    Name = "*private*"
  }
}
data "aws_subnet" "aws_subnet_private-prefix" {
  count = length(data.aws_subnets.aws_subnets_private.ids)
  id    = tolist(data.aws_subnets.aws_subnets_private.ids)[count.index]
}


data "aws_route_table" "aws_route_table-private" {
  count     = length(data.aws_subnets.aws_subnets_private.ids)
  subnet_id = tolist(data.aws_subnets.aws_subnets_private.ids)[count.index]
}

The process is controlled by a variable (aws_nat) defined on the terraform.tfvars file:

controller_ip = ":)"
username      = "admin"
password      = ":)"
account    = ":)"
region     = "us-east-1"
vpc_name   = "other-apps-vpc"
transit_gw = "aws-us-east-1-transit"
gw_size    = "t3.small"
aws_nat    = false

Setting aws_nat to false triggers the migration while setting it to false fails back to the native NAT gateways. Because EIPs can be allocated only during the gateway creation, setting to aws_nat true will destroy the AVX gws. If EIPs don’t need to be repurposed the spoke gateways do not need to be destroyed:

resource "aviatrix_spoke_gateway" "aviatrix_spoke_gateway-gateway" {
  cloud_type                        = "1"
  account_name                      = var.account
  gw_name                           = var.vpc_name
  vpc_id                            = data.aws_vpc.vpc_name.id
  vpc_reg                           = var.region
  gw_size                           = var.gw_size
  subnet                            = data.aws_subnet.aws_subnet_public-prefix[0].cidr_block
  ha_subnet                         = data.aws_subnet.aws_subnet_public-prefix[1].cidr_block
  ha_gw_size                        = var.gw_size
  manage_transit_gateway_attachment = false
  allocate_new_eip                  = true
}

and can be attached independently of the NAT gateways existence:

resource "aviatrix_spoke_transit_attachment" "aviatrix_spoke_transit_attachment-gateway" {
  depends_on = [
    aviatrix_spoke_gateway.aviatrix_spoke_gateway-gateway
  ]
  spoke_gw_name   = aviatrix_spoke_gateway.aviatrix_spoke_gateway-gateway[0].gw_name
  transit_gw_name = var.transit_gw
}

References

https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html

https://docs.aviatrix.com/HowTos/nat_only_outbound_traffic.html?highlight=snat%20egress#nat-for-non-tunnel-bound-traffic

Leave a Reply