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
- Por que usar o Azure NAT Gateway
- Rede: VNet e subnet delegada
- Criando o Azure NAT Gateway
- Container Instance de teste
- terraform plan e apply
- Troubleshooting: dois bugs reais
- Confirmando o IP de saída
- Conclusão
- Referências oficiais
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.

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
- Azure NAT Gateway overview — Microsoft Learn
- NAT Gateway resource — Microsoft Learn
- azurerm_nat_gateway — Terraform Registry
- Docker Hub pull rate limits
Interessado em saber mais sobre esse assunto?