Azure NAT Gateway: Saída de Internet Controlada com Terraform

Sem configuração explícita, uma subnet do Azure sai pra internet através de um mecanismo de SNAT dinâmico — o IP de saída não é fixo, e sob carga alta o número de portas SNAT disponíveis pode se esgotar, derrubando conexões novas silenciosamente. O Azure NAT Gateway resolve isso: toda a saída da subnet passa a usar um IP público fixo e conhecido, com muito mais portas SNAT disponíveis por padrão.

Neste segundo artigo da série artigos-network (depois do Azure Virtual Network Manager), a gente cria um Azure NAT Gateway com Terraform e testa de verdade: um Container Instance faz uma requisição de saída real, e o IP visto pela internet precisa bater exatamente com o IP do NAT Gateway.

Segurança: os comandos deste guia de Azure NAT Gateway com Terraform utilizam variáveis de ambiente para credenciais. Nunca insira IDs de subscription, senhas ou chaves diretamente nos comandos — use TF_VAR_*, um arquivo .tfvars fora do Git, ou o Azure Key Vault.

Sumário

Pré-requisitos

Antes de criar o Azure NAT Gateway com Terraform, você vai precisar do Azure CLI autenticado (az login), do Terraform >= 1.5 e de permissão de Network Contributor na subscription. O provider usado é o azurerm ~> 4.0.

az account show --query "{name:name, id:id}" -o table

Por que usar o Azure NAT Gateway

O SNAT dinâmico padrão do Azure aloca portas de saída sob demanda e reparte esse número entre todas as instâncias de uma subnet — em cenários de muitas conexões simultâneas (ex: uma aplicação que faz muitas chamadas HTTP de saída), é comum esgotar as portas disponíveis e começar a ver falhas de conexão intermitentes, difíceis de diagnosticar. O Azure NAT Gateway resolve isso de duas formas: garante um IP de saída fixo (essencial quando o serviço do outro lado exige allowlist de IP) e disponibiliza 64.000 portas SNAT por IP público associado — bem mais previsível que o esquema dinâmico padrão.

Rede: VNet e subnet delegada

Pra testar o Azure NAT Gateway de ponta a ponta, a subnet aqui é delegada a Azure Container Instances — assim conseguimos rodar o container de teste diretamente integrado à VNet, sem precisar de VM. O módulo reutilizável terraform-virtual-network-modules ganhou suporte a delegation por subnet nesta sessão, pra cobrir esse caso:

module "vnet" {
  source = "../../terraform-virtual-network-modules"

  name                = "vnet-natgw-blog-castilho"
  resource_group_name = module.rg.name
  location            = var.location
  address_space       = [var.vnet_address_space]

  subnets = [
    {
      key              = "natgw"
      name             = "snet-natgw"
      address_prefixes = [var.subnet_prefix]
      delegation = {
        name                       = "aci-delegation"
        service_delegation_name    = "Microsoft.ContainerInstance/containerGroups"
        service_delegation_actions = ["Microsoft.Network/virtualNetworks/subnets/action"]
      }
    },
  ]

  tags = var.tags
}

Criando o Azure NAT Gateway

O Azure NAT Gateway precisa de um IP público Standard associado e, separadamente, de uma associação com a subnet que vai usá-lo — no módulo novo terraform-nat-gateway-modules, isso vira uma chamada só, a partir do IP criado por terraform-public-ip-modules:

module "pip_natgw" {
  source = "../../terraform-public-ip-modules"

  name                 = "pip-natgw-blog-castilho"
  resource_group_name = module.rg.name
  location             = var.location

  tags = var.tags
}

module "nat_gateway" {
  source = "../../terraform-nat-gateway-modules"

  name                    = "natgw-blog-castilho"
  resource_group_name    = module.rg.name
  location                = var.location
  idle_timeout_in_minutes = 4
  public_ip_address_id   = module.pip_natgw.id
  subnet_ids              = [module.vnet.subnets["natgw"].id]

  tags = var.tags
}

A partir do momento em que essa associação existe, todo o tráfego de saída da subnet passa pelo Azure NAT Gateway automaticamente — não precisa configurar rota nenhuma nos recursos dentro dela.

Container Instance de teste

O container de teste roda uma vez (restart_policy = "Never"), faz um curl pra um serviço que devolve o IP de origem da requisição, e termina — sem custo de recurso parado. Vem do módulo terraform-aci-modules, estendido nesta sessão com subnet_ids/ip_address_type/commands (que ainda não existiam nele):

module "aci_test" {
  source = "../../terraform-aci-modules"

  name                 = "aci-natgw-test"
  resource_group_name = module.rg.name
  location             = var.location
  restart_policy       = "Never"
  ip_address_type      = "Private"
  subnet_ids           = [module.vnet.subnets["natgw"].id]

  container_name = "curl-egress-ip"
  image  = "mcr.microsoft.com/azure-cli:latest"
  cpu    = "0.5"
  memory = "0.5"

  ports = [
    { port = 80, protocol = "TCP" },
  ]

  commands = ["curl", "-s", "https://api.ipify.org", "--max-time", "10"]

  tags = var.tags

  depends_on = [module.nat_gateway]
}

terraform plan e apply

terraform init -backend-config=backend.hcl
terraform plan -out=tfplan.out
terraform apply tfplan.out

O ambiente completo do Azure NAT Gateway cria 8 recursos: Resource Group, VNet + subnet (com delegation), Public IP, NAT Gateway + 2 associações, e o Container Instance de teste. O NAT Gateway cobra por hora de recurso mais dados processados — pequeno pra um teste, mas não gratuito; vale destruir depois de validar.

Resource group com o Azure NAT Gateway e demais recursos criados
Resource group depois do apply: Container Instance, NAT Gateway, Public IP e VNet

Troubleshooting: dois bugs reais

O primeiro apply falhou logo na criação do container, com MissingIpAddressPorts. Um Container Instance com integração de VNet (subnet_ids preenchido) exige que pelo menos uma porta seja declarada no bloco ports do container — mesmo quando o container não precisa expor nada publicamente, como aqui. A correção foi simplesmente declarar uma porta qualquer (80/TCP) que nunca chega a ser usada de fato.

Error: unexpected status 400 (400 Bad Request) with error:
MissingIpAddressPorts: The ports in the 'ipAddress' of container group
'aci-natgw-test' cannot be empty.

Resolvido esse, o segundo erro veio da tentativa de puxar a imagem curlimages/curl do Docker Hub — um 409 RegistryErrorResponse repetido em várias tentativas. Esse é um problema real e conhecido: o Docker Hub aplica rate limit a pulls anônimos, e IPs de datacenter do Azure são compartilhados por muitos clientes, esbarrando nesse limite com frequência. A correção foi trocar pra uma imagem do Microsoft Container Registry (mcr.microsoft.com/azure-cli, que já vem com curl instalado) — sem esse limite de pull.

Confirmando o IP de saída

Depois do apply, o output devolve o IP público do NAT Gateway. A prova real de que o Azure NAT Gateway está funcionando é ler o log do container e comparar:

az container logs -g rg-blog-castilho-natgw-prod -n aci-natgw-test
# 52.240.57.108

terraform output nat_gateway_public_ip
# "52.240.57.108"

O IP que o serviço externo api.ipify.org viu como origem da requisição é exatamente o IP público do Azure NAT Gateway — não um IP dinâmico qualquer da plataforma. Essa é a validação que importa: não basta o apply ter funcionado, é preciso confirmar que o tráfego de fato sai pelo IP esperado.

Conclusão

O Azure NAT Gateway é uma das configurações mais simples de justificar em produção: resolve o problema real de esgotamento de portas SNAT e entrega um IP de saída fixo, sem exigir nenhuma mudança nos recursos que já estão na subnet. Os dois bugs deste artigo — porta obrigatória num Container Instance de VNet e rate limit do Docker Hub — não têm relação direta com o NAT Gateway em si, mas são exatamente o tipo de atrito real que aparece ao montar um ambiente de teste do zero, e vale documentar pra quem for reproduzir.

Próximo artigo da série artigos-network: Azure DDoS Protection Standard.

Referências oficiais

Interessado em saber mais sobre esse assunto?

Deixe uma resposta

Descubra mais sobre Blog do Castilho - Tecnologia | FinOps | DevOps | Cloud

Assine agora mesmo para continuar lendo e ter acesso ao arquivo completo.

Continuar lendo