ESEN

DNS privado híbrido (on-premises ↔ nube)

On-premises resuelve las zonas privadas de la nube (Private Endpoints incluidos) y la nube resuelve el dominio corporativo, sin servidores DNS propios en la nube.

Problema

Con un enlace VPN o dedicado ya hay conectividad IP, pero los nombres no cruzan: on-premises no puede consultar el resolver interno de la nube y los workloads de la nube no conocen corp.example.com. La solución clásica son VMs con DNS en la nube, que hay que parchear y escalar. Los requisitos son: reenvío condicional en las dos direcciones, alta disponibilidad, que los spokes resuelvan sin configuración propia y sin loops de reenvío.

La topología es la misma en todas las nubes; lo que cambia es con qué se implementa cada rol.

Microsoft Azure

DNS privado híbrido (on-premises ↔ nube) — Azurereenvío condicionalresuelvecorp.example.comreenvía por VPNDNS on-premisesEntrada (on-prem → nube)DNS Private Resolver:inbound endpoint3 gotchas en este rolZona privada de la nubeAzure Private DNS1 gotcha en este rolWorkloads en la nubeSalida (nube → on-prem)DNS Private Resolver:outbound endpoint +forwarding ruleset5 gotchas en este rol

Implementación

Entrada (on-prem → nube)
DNS Private Resolver: inbound endpointIP estática en una subnet /28 dedicada y delegadaVenís de AWS: allá este rol es Route 53 Resolver inbound endpoint.Venís de GCP: allá este rol es Cloud DNS inbound server policy.
Zona privada de la nube
Azure Private DNSZona vinculada al hub; los spokes la resuelven por una regla al inbound endpointVenís de AWS: allá este rol es Route 53 private hosted zone. La zona vinculada a la VNet se consulta antes que el ruleset; en Route 53 una regla de forward le gana a la zona del mismo nombre.Venís de GCP: allá este rol es Cloud DNS private zone.
Salida (nube → on-prem)
DNS Private Resolver: outbound endpoint + forwarding rulesetRuleset linkeado a los spokes (no al hub)Venís de AWS: allá este rol es Route 53 Resolver outbound endpoint + regla FORWARD.Venís de GCP: allá este rol es Cloud DNS forwarding zone.

Modelo de costo

No escala a cero

Costo fijo aunque no haya tráfico:

  • Inbound endpoint (por hora)
  • Outbound endpoint (por hora)
  • Forwarding ruleset (por mes)

Los endpoints y el ruleset se cobran aunque no haya consultas, más un cargo por consulta. Sigue siendo más barato que mantener dos VMs de DNS con alta disponibilidad.

Piso mensual sin tráfico

USD 363/ mes

Con la configuración por default del módulo Terraform. Sirve para comparar órdenes de magnitud, no para presupuestar.

Precio de lista en eastus al 2026-10-24, en USD, sin descuentos ni impuestos.

⚖ Comparar SKUs: DNS híbrido (on-premises ↔ nube) →

Letra chica · 5 gotchas

Red● verificado hace 34 días◌ sin revisor externo

Cada endpoint necesita su propia subnet delegada, y el resolver queda atado a una VNet y una región

Inbound y outbound no pueden compartir subnet: cada uno va en una dedicada de /28 a /24, delegada a Microsoft.Network/dnsResolvers y sin ningún otro recurso. Hay un solo resolver por VNet, los rulesets solo se linkean a VNets de la misma región y no funciona con subnets IPv6 ni con VNets que tienen virtual network encryption. Reservá las dos /28 en el diseño del hub antes de que el espacio se llene.

⚑ Reportar desactualizado
Red● verificado hace 34 días◌ sin revisor externo

En Azure la zona privada vinculada gana sobre el ruleset; en AWS es al revés

Con el DNS de Azure en la VNet, primero se consultan las Private DNS zones vinculadas y recién si no hay match se evalúan las reglas del ruleset (gana el sufijo más largo). En Route 53 Resolver, una regla de forward le gana a una private hosted zone con el mismo nombre. Si migrás un diseño de "reenviar todo corp.example.com a on-prem" con una zona privada del mismo nombre, el comportamiento se invierte.

⚑ Reportar desactualizado
Confiabilidad● verificado hace 34 días◌ sin revisor externo

Linkear el ruleset al hub, con una regla que apunta a su propio inbound endpoint, arma un loop

El diseño típico manda la zona privada desde los spokes al inbound endpoint del hub. Si ese mismo ruleset también se linkea a la VNet del hub, las consultas del hub vuelven al hub y se arma un loop. Por eso el módulo linkea solo spokes. Si los workloads del hub necesitan resolver corp.example.com, usá un segundo ruleset sin la regla al inbound endpoint.

⚑ Reportar desactualizado
Confiabilidad● verificado hace 34 días◌ sin revisor externo

Una regla comodín "." tiene que resolver nombres públicos, y 168.63.129.16 no sirve como destino

Si reenviás todo con ".", el DNS destino tiene que resolver nombres públicos: varios servicios de Azure dependen de eso. Algunos dominios reservados de Azure se ignoran igual en la regla comodín. Además, 168.63.129.16 no se acepta como destino de una regla, y Microsoft desaconseja el comodín en una VNet con un gateway de ExpressRoute porque puede romper la conectividad de gestión.

⚑ Reportar desactualizado
Operación● verificado hace 34 días◌ sin revisor externo

DNS Private Resolver no es compatible con Azure Lighthouse ni con ExpressRoute FastPath

Si un partner administra el tenant del cliente con Lighthouse, no puede operar el resolver por esa vía: hace falta acceso directo en el tenant. Tampoco soporta FastPath de ExpressRoute ni linkear rulesets entre tenants. Para un MSP es una decisión de diseño de día uno, no un detalle de implementación.

⚑ Reportar desactualizado

Terraform

azurerm ~> 5.8✓ fmt · validate · tflint · checkov · terraform testGitHub ↗
main.tf
# DNS privado híbrido — Azure
#
# DNS Private Resolver en el hub: inbound endpoint para on-premises, outbound
# endpoint + forwarding ruleset para corp.example.com, y una regla que manda la
# zona privada al inbound endpoint para que los spokes la resuelvan sin links.

locals {
  # Las reglas usan nombres terminados en punto.
  onprem_fqdn = "${trimsuffix(var.onprem_domain, ".")}."
  zone_fqdn   = "${trimsuffix(var.private_zone_name, ".")}."
}

resource "azurerm_resource_group" "this" {
  name     = "rg-${var.name}"
  location = var.location
  tags     = var.tags
}

resource "azurerm_virtual_network" "hub" {
  name                = "vnet-${var.name}"
  resource_group_name = azurerm_resource_group.this.name
  location            = azurerm_resource_group.this.location
  address_space       = [var.hub_address_space]
  tags                = var.tags
}

# Una subnet por endpoint, delegada y sin otros recursos (gotcha: resolver-dedicated-subnets).
resource "azurerm_subnet" "inbound" {
  name                 = "snet-dns-inbound"
  resource_group_name  = azurerm_resource_group.this.name
  virtual_network_name = azurerm_virtual_network.hub.name
  address_prefixes     = [var.inbound_subnet_prefix]

  delegation {
    name = "dns-resolver"
    service_delegation {
      name    = "Microsoft.Network/dnsResolvers"
      actions = ["Microsoft.Network/virtualNetworks/subnets/join/action"]
    }
  }
}

resource "azurerm_subnet" "outbound" {
  name                 = "snet-dns-outbound"
  resource_group_name  = azurerm_resource_group.this.name
  virtual_network_name = azurerm_virtual_network.hub.name
  address_prefixes     = [var.outbound_subnet_prefix]

  delegation {
    name = "dns-resolver"
    service_delegation {
      name    = "Microsoft.Network/dnsResolvers"
      actions = ["Microsoft.Network/virtualNetworks/subnets/join/action"]
    }
  }
}

resource "azurerm_network_security_group" "resolver" {
  name                = "nsg-${var.name}-dns"
  resource_group_name = azurerm_resource_group.this.name
  location            = azurerm_resource_group.this.location
  tags                = var.tags

  # UDP y TCP: las respuestas grandes caen a TCP.
  security_rule {
    name                       = "dns-from-onprem"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "*"
    source_address_prefixes    = var.onprem_cidrs
    source_port_range          = "*"
    destination_address_prefix = var.inbound_subnet_prefix
    destination_port_range     = "53"
  }
}

resource "azurerm_subnet_network_security_group_association" "inbound" {
  subnet_id                 = azurerm_subnet.inbound.id
  network_security_group_id = azurerm_network_security_group.resolver.id
}

resource "azurerm_subnet_network_security_group_association" "outbound" {
  subnet_id                 = azurerm_subnet.outbound.id
  network_security_group_id = azurerm_network_security_group.resolver.id
}

resource "azurerm_private_dns_resolver" "this" {
  name                = "dnspr-${var.name}"
  resource_group_name = azurerm_resource_group.this.name
  location            = azurerm_resource_group.this.location
  virtual_network_id  = azurerm_virtual_network.hub.id
  tags                = var.tags
}

# IP estática: es la que se carga en los forwarders condicionales on-premises.
resource "azurerm_private_dns_resolver_inbound_endpoint" "this" {
  name                    = "in-${var.name}"
  private_dns_resolver_id = azurerm_private_dns_resolver.this.id
  location                = azurerm_private_dns_resolver.this.location
  tags                    = var.tags

  ip_configurations {
    subnet_id                    = azurerm_subnet.inbound.id
    private_ip_allocation_method = "Static"
    private_ip_address           = cidrhost(var.inbound_subnet_prefix, 4)
  }
}

resource "azurerm_private_dns_resolver_outbound_endpoint" "this" {
  name                    = "out-${var.name}"
  private_dns_resolver_id = azurerm_private_dns_resolver.this.id
  location                = azurerm_private_dns_resolver.this.location
  subnet_id               = azurerm_subnet.outbound.id
  tags                    = var.tags
}

# --- Zona privada (vinculada solo al hub; el inbound endpoint la resuelve) ------

resource "azurerm_private_dns_zone" "cloud" {
  name                = var.private_zone_name
  resource_group_name = azurerm_resource_group.this.name
  tags                = var.tags
}

resource "azurerm_private_dns_zone_virtual_network_link" "hub" {
  name                 = "hub"
  private_dns_zone_id  = azurerm_private_dns_zone.cloud.id
  virtual_network_id   = azurerm_virtual_network.hub.id
  registration_enabled = false
  tags                 = var.tags
}

# --- Reglas de reenvío -----------------------------------------------------------

resource "azurerm_private_dns_resolver_dns_forwarding_ruleset" "this" {
  name                                       = "dnsfrs-${var.name}"
  resource_group_name                        = azurerm_resource_group.this.name
  location                                   = azurerm_resource_group.this.location
  private_dns_resolver_outbound_endpoint_ids = [azurerm_private_dns_resolver_outbound_endpoint.this.id]
  tags                                       = var.tags
}

resource "azurerm_private_dns_resolver_forwarding_rule" "onprem" {
  name                      = "onprem"
  dns_forwarding_ruleset_id = azurerm_private_dns_resolver_dns_forwarding_ruleset.this.id
  domain_name               = local.onprem_fqdn
  enabled                   = true

  dynamic "target_dns_servers" {
    for_each = var.onprem_dns_servers
    content {
      ip_address = target_dns_servers.value
      port       = 53
    }
  }
}

# Los spokes resuelven la zona privada vía el inbound endpoint del hub.
resource "azurerm_private_dns_resolver_forwarding_rule" "cloud_zone" {
  name                      = "cloud-zone"
  dns_forwarding_ruleset_id = azurerm_private_dns_resolver_dns_forwarding_ruleset.this.id
  domain_name               = local.zone_fqdn
  enabled                   = true

  target_dns_servers {
    ip_address = azurerm_private_dns_resolver_inbound_endpoint.this.ip_configurations[0].private_ip_address
    port       = 53
  }
}

# Solo spokes: linkear el hub, con una regla que apunta a su propio inbound
# endpoint, genera un loop (gotcha: ruleset-hub-loop).
resource "azurerm_private_dns_resolver_virtual_network_link" "spoke" {
  for_each = var.spoke_vnet_ids

  name                      = "spoke-${each.key}"
  dns_forwarding_ruleset_id = azurerm_private_dns_resolver_dns_forwarding_ruleset.this.id
  virtual_network_id        = each.value
}
variables.tf
variable "name" {
  description = "Prefijo para nombrar los recursos (ej. \"dns-hub\")."
  type        = string

  validation {
    condition     = can(regex("^[a-z0-9-]{3,20}$", var.name))
    error_message = "name: 3-20 caracteres, minúsculas, números o guiones."
  }
}

variable "location" {
  description = "Región del hub. El resolver y los ruleset links solo funcionan con VNets de la misma región."
  type        = string
  default     = "eastus2"
}

variable "hub_address_space" {
  description = "Espacio de direcciones de la VNet hub."
  type        = string
  default     = "10.10.0.0/16"
}

variable "inbound_subnet_prefix" {
  description = "Subnet dedicada al inbound endpoint (/28 a /24)."
  type        = string
  default     = "10.10.0.0/28"

  validation {
    condition     = tonumber(split("/", var.inbound_subnet_prefix)[1]) >= 24 && tonumber(split("/", var.inbound_subnet_prefix)[1]) <= 28
    error_message = "inbound_subnet_prefix: entre /28 y /24 (gotcha: resolver-dedicated-subnets)."
  }
}

variable "outbound_subnet_prefix" {
  description = "Subnet dedicada al outbound endpoint (/28 a /24)."
  type        = string
  default     = "10.10.0.16/28"

  validation {
    condition     = tonumber(split("/", var.outbound_subnet_prefix)[1]) >= 24 && tonumber(split("/", var.outbound_subnet_prefix)[1]) <= 28
    error_message = "outbound_subnet_prefix: entre /28 y /24 (gotcha: resolver-dedicated-subnets)."
  }
}

variable "private_zone_name" {
  description = "Zona privada de Azure que on-premises tiene que resolver (puede ser una privatelink.*)."
  type        = string
  default     = "azure.example.internal"
}

variable "onprem_domain" {
  description = "Dominio corporativo que se reenvía a on-premises."
  type        = string
  default     = "corp.example.com"
}

variable "onprem_dns_servers" {
  description = "IPs de los DNS on-premises (alcanzables por VPN/ExpressRoute)."
  type        = list(string)

  validation {
    condition     = length(var.onprem_dns_servers) >= 1 && length(var.onprem_dns_servers) <= 6 && !contains(var.onprem_dns_servers, "168.63.129.16")
    error_message = "onprem_dns_servers: 1 a 6 IPs, y 168.63.129.16 no se acepta como destino de una regla."
  }
}

variable "onprem_cidrs" {
  description = "Rangos on-premises que consultan el inbound endpoint."
  type        = list(string)
}

variable "spoke_vnet_ids" {
  description = "VNets spoke (misma región) que usan el ruleset. No incluyas el hub: el ruleset reenvía la zona privada al inbound endpoint del hub."
  type        = map(string)
  default     = {}
}

variable "tags" {
  description = "Tags comunes."
  type        = map(string)
  default     = {}
}
outputs.tf
output "inbound_endpoint_ip" {
  description = "IP para los forwarders condicionales on-premises (zona privada → esta IP)."
  value       = azurerm_private_dns_resolver_inbound_endpoint.this.ip_configurations[0].private_ip_address
}

output "hub_vnet_id" {
  description = "VNet hub (a peerear con los spokes y conectar a on-premises)."
  value       = azurerm_virtual_network.hub.id
}

output "ruleset_id" {
  description = "Forwarding ruleset para linkear más spokes de la misma región."
  value       = azurerm_private_dns_resolver_dns_forwarding_ruleset.this.id
}
versions.tf
terraform {
  required_version = ">= 1.7.0"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.8"
    }
  }
}

provider "azurerm" {
  features {}

  # subscription_id se toma de ARM_SUBSCRIPTION_ID.
}

Amazon Web Services

DNS privado híbrido (on-premises ↔ nube) — AWSreenvío condicionalresuelvecorp.example.comreenvía por VPNDNS on-premisesEntrada (on-prem → nube)Route 53 Resolverinbound endpoint4 gotchas en este rolZona privada de la nubeRoute 53 privatehosted zone2 gotchas en este rolWorkloads en la nubeSalida (nube → on-prem)Route 53 Resolveroutbound endpoint +regla FORWARD2 gotchas en este rol

Implementación

Entrada (on-prem → nube)
Route 53 Resolver inbound endpointENIs en dos AZs; SG con UDP y TCP 53 desde on-premisesVenís de Azure: allá este rol es DNS Private Resolver: inbound endpoint.Venís de GCP: allá este rol es Cloud DNS inbound server policy.
Zona privada de la nube
Route 53 private hosted zoneAsociada a la VPC del inbound endpointVenís de Azure: allá este rol es Azure Private DNS.Venís de GCP: allá este rol es Cloud DNS private zone.
Salida (nube → on-prem)
Route 53 Resolver outbound endpoint + regla FORWARDRegla asociada a cada VPC; a otras cuentas con AWS RAMVenís de Azure: allá este rol es DNS Private Resolver: outbound endpoint + forwarding ruleset. La regla FORWARD le gana a una private hosted zone con el mismo nombre; en Azure la zona vinculada gana sobre el ruleset.Venís de GCP: allá este rol es Cloud DNS forwarding zone.

Modelo de costo

No escala a cero

Costo fijo aunque no haya tráfico:

  • 4 ENIs de Resolver (2 inbound + 2 outbound) por hora

El costo fijo lo ponen las ENIs de los endpoints; las consultas reenviadas se cobran por millón. Las consultas que resuelve el resolver de la VPC sin pasar por un endpoint no se cobran.

Piso mensual sin tráfico

USD 365/ mes

Con la configuración por default del módulo Terraform. Sirve para comparar órdenes de magnitud, no para presupuestar.

Precio de lista en us-east-1 al 2026-10-24, en USD, sin descuentos ni impuestos.

⚖ Comparar SKUs: DNS híbrido (on-premises ↔ nube) →

Letra chica · 5 gotchas

Red● verificado hace 34 días◌ sin revisor externo

Reenviar desde on-premises a la IP .2 de la VPC no está soportado

AWS indica que reenviar consultas DNS privadas a cualquier dirección base del CIDR de la VPC + 2 desde on-premises u otros servidores DNS de VPC no está soportado y puede dar resultados inestables. En su lugar, AWS recomienda usar un inbound endpoint de Resolver.

⚑ Reportar desactualizado
Red● verificado hace 34 días◌ sin revisor externo

Una regla de forward le gana a una private hosted zone con el mismo nombre

Si en una VPC hay una regla FORWARD para corp.example.com y también una private hosted zone corp.example.com, gana la regla. Entre reglas gana la más específica, y con una regla SYSTEM excluís un subdominio del reenvío.

⚑ Reportar desactualizado
Costo● verificado hace 34 días◌ sin revisor externo

Cada IP de un endpoint es una ENI que se paga por hora, y el mínimo son dos por endpoint

Route 53 Resolver cobra por ENI por hora, además de las consultas, y cada endpoint necesita al menos dos IPs en AZs distintas. Entrada más salida son cuatro ENIs facturando las 24 horas aunque no haya una sola consulta. En GCP la inbound policy y las forwarding zones no tienen cargo por hora. Si tenés varias cuentas, centralizá los endpoints en el hub y compartí las reglas con RAM en lugar de repetirlos.

⚑ Reportar desactualizado
Confiabilidad● verificado hace 34 días◌ sin revisor externo

Son 10.000 QPS por IP, pero con connection tracking pueden ser 1.500

Cada IP de un endpoint procesa hasta 10.000 consultas UDP por segundo. Si las reglas restrictivas del security group fuerzan connection tracking (o las consultas pasan por un NLB), el inbound endpoint puede bajar a 1.500 QPS por IP. Este módulo limita el origen a los rangos on-premises, así que contá con el número bajo: monitoreá la métrica por ENI y agregá IPs cuando pase el 50%.

⚑ Reportar desactualizado

Terraform

aws ~> 6.67✓ fmt · validate · tflint · checkov · terraform testGitHub ↗
main.tf
# DNS privado híbrido — AWS
#
# Route 53 Resolver en la VPC hub: inbound endpoint para on-premises, outbound
# endpoint + regla FORWARD para corp.example.com, y la private hosted zone
# asociada a la VPC del inbound endpoint.

locals {
  # TCP además de UDP: respuestas grandes (DNSSEC, muchos registros) caen a TCP.
  dns_protocols = ["udp", "tcp"]
}

resource "aws_security_group" "resolver" {
  name        = "${var.name}-resolver"
  description = "Route 53 Resolver endpoints: DNS desde y hacia on-premises"
  vpc_id      = var.vpc_id
}

resource "aws_vpc_security_group_ingress_rule" "dns_from_onprem" {
  for_each = { for pair in setproduct(var.onprem_cidrs, local.dns_protocols) : "${pair[0]}-${pair[1]}" => pair }

  security_group_id = aws_security_group.resolver.id
  description       = "DNS desde on-premises (${each.value[1]})"
  cidr_ipv4         = each.value[0]
  ip_protocol       = each.value[1]
  from_port         = 53
  to_port           = 53
}

resource "aws_vpc_security_group_egress_rule" "dns_to_onprem" {
  for_each = { for pair in setproduct(var.onprem_cidrs, local.dns_protocols) : "${pair[0]}-${pair[1]}" => pair }

  security_group_id = aws_security_group.resolver.id
  description       = "DNS hacia on-premises (${each.value[1]})"
  cidr_ipv4         = each.value[0]
  ip_protocol       = each.value[1]
  from_port         = 53
  to_port           = 53
}

# Cada IP es una ENI que se cobra por hora (gotcha: resolver-endpoints-fixed-cost).
resource "aws_route53_resolver_endpoint" "inbound" {
  name               = "${var.name}-inbound"
  direction          = "INBOUND"
  security_group_ids = [aws_security_group.resolver.id]

  dynamic "ip_address" {
    for_each = var.endpoint_subnet_ids
    content {
      subnet_id = ip_address.value
    }
  }
}

resource "aws_route53_resolver_endpoint" "outbound" {
  name               = "${var.name}-outbound"
  direction          = "OUTBOUND"
  security_group_ids = [aws_security_group.resolver.id]

  dynamic "ip_address" {
    for_each = var.endpoint_subnet_ids
    content {
      subnet_id = ip_address.value
    }
  }
}

resource "aws_route53_resolver_rule" "onprem" {
  name                 = "${var.name}-onprem"
  domain_name          = var.onprem_domain
  rule_type            = "FORWARD"
  resolver_endpoint_id = aws_route53_resolver_endpoint.outbound.id

  dynamic "target_ip" {
    for_each = var.onprem_dns_servers
    content {
      ip   = target_ip.value
      port = 53
    }
  }
}

resource "aws_route53_resolver_rule_association" "this" {
  for_each = merge({ hub = var.vpc_id }, var.spoke_vpc_ids)

  name             = "${var.name}-${each.key}"
  resolver_rule_id = aws_route53_resolver_rule.onprem.id
  vpc_id           = each.value
}

# Tiene que estar asociada a la VPC del inbound endpoint para que on-premises la resuelva.
resource "aws_route53_zone" "cloud" {
  #checkov:skip=CKV2_AWS_38:DNSSEC signing no aplica a private hosted zones.
  #checkov:skip=CKV2_AWS_39:El query logging se configura a nivel VPC (Resolver query logging), fuera del alcance del patrón.
  name = var.private_zone_name

  vpc {
    vpc_id = var.vpc_id
  }
}
variables.tf
variable "name" {
  description = "Prefijo para nombrar los recursos (ej. \"dns-hub\")."
  type        = string

  validation {
    condition     = can(regex("^[a-z0-9-]{3,20}$", var.name))
    error_message = "name: 3-20 caracteres, minúsculas, números o guiones."
  }
}

variable "region" {
  description = "Región del hub."
  type        = string
  default     = "us-east-1"
}

variable "vpc_id" {
  description = "VPC hub (la que tiene la VPN o el Direct Connect)."
  type        = string
}

variable "endpoint_subnet_ids" {
  description = "Subnets para las ENIs de los endpoints, en al menos dos AZs distintas."
  type        = list(string)

  validation {
    condition     = length(var.endpoint_subnet_ids) >= 2 && length(var.endpoint_subnet_ids) <= 6
    error_message = "endpoint_subnet_ids: entre 2 y 6 (Resolver exige al menos dos IPs por endpoint)."
  }
}

variable "onprem_cidrs" {
  description = "Rangos on-premises que consultan el inbound endpoint y donde viven los DNS destino."
  type        = list(string)
}

variable "onprem_dns_servers" {
  description = "IPs de los DNS on-premises."
  type        = list(string)

  validation {
    condition     = length(var.onprem_dns_servers) >= 1 && length(var.onprem_dns_servers) <= 6
    error_message = "onprem_dns_servers: entre 1 y 6 (máximo de target IPs por regla)."
  }
}

variable "onprem_domain" {
  description = "Dominio corporativo que se reenvía a on-premises."
  type        = string
  default     = "corp.example.com"
}

variable "private_zone_name" {
  description = "Private hosted zone que on-premises tiene que resolver."
  type        = string
  default     = "aws.example.internal"
}

variable "spoke_vpc_ids" {
  description = "VPCs adicionales (misma cuenta y región) que usan la regla de reenvío. Para otras cuentas, compartila con AWS RAM."
  type        = map(string)
  default     = {}
}

variable "tags" {
  description = "Tags comunes."
  type        = map(string)
  default     = {}
}
outputs.tf
output "inbound_endpoint_ips" {
  description = "IPs para los forwarders condicionales on-premises (se conocen después del apply)."
  value       = [for ip in aws_route53_resolver_endpoint.inbound.ip_address : ip.ip]
}

output "resolver_rule_id" {
  description = "Regla de reenvío, para compartir con AWS RAM a otras cuentas."
  value       = aws_route53_resolver_rule.onprem.id
}
versions.tf
terraform {
  required_version = ">= 1.7.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.67"
    }
  }
}

provider "aws" {
  region = var.region

  default_tags {
    tags = var.tags
  }
}