Hub-and-Spoke en OCI con DRG: routing inter-VCN centralizado
Cuando una organización empieza a desplegar varias líneas de negocio en OCI, lo primero que choca es la conectividad entre VCNs. La opción…

Hub-and-Spoke en OCI con DRG: routing inter-VCN centralizado
Cuando una organización empieza a desplegar varias líneas de negocio en OCI, lo primero que choca es la conectividad entre VCNs. La opción rápida es hacer Local Peering uno contra uno, pero ese modelo se rompe en cuanto aparecen tres, cuatro o diez VCNs: la matriz de peerings explota y nadie quiere mantener esas rutas a mano.
La respuesta enterprise en OCI es el Dynamic Routing Gateway (DRG) v2 y un patrón Hub-and-Spoke: un router central conecta todas las VCNs, las rutas se propagan de forma declarativa y la transitividad se controla con un único flag.
Este artículo recorre, módulo por módulo, un repositorio Terraform que monta exactamente esa arquitectura, con bastión en el hub, dos spokes aislados y un interruptor para pasar de transit a isolated sin destruir nada.
Repositorio: terraform-oci-drg-hub-spoke
El problema real: por qué hub-and-spoke en OCI no es “como en AWS”
Si vienes de AWS, probablemente esperas reutilizar el patrón clásico de Transit Gateway + egress centralizado vía NAT del hub. En OCI eso no funciona directamente.
La razón es concreta: la route table asociada a un NAT Gateway en OCI solo acepta IPs privadas como target. No acepta DRG, no acepta otro NAT, no acepta IGW. Para centralizar el egress a internet en OCI necesitas un NVA intermedio (OCI Network Firewall, una VM Linux con IP forwarding o un firewall de marketplace).
Por eso este lab adopta el patrón OCI-nativo:
- Cada VCN (hub y spokes) tiene su propio NAT Gateway local para egress a internet.
- El DRG queda reservado exclusivamente para tráfico inter-VCN.
- El NAT en OCI es gratuito, así que multiplicarlo no tiene coste (a diferencia de AWS, donde cada NAT son ~$32/mes).
Con ese principio claro, la arquitectura se vuelve simple.
Arquitectura de la solución
Tres VCNs conectadas por un DRG central:
Internet
│
▼
+-----------------------+
| HUB VCN |
| 10.0.0.0/16 |
| Public Subnet |
| 10.0.1.0/24 |
| [Bastion] |
| IGW | NAT | SGW |
| Private Subnet |
| 10.0.2.0/24 |
+-----------|-----------+
│
+-------+-------+
| DRG |
| hub_drgrt |
| spokes_drgrt |
+--+---------+--+
│ │
+------------+ +------------+
│ │
+-------+--------+ +---------+-------+
| SPOKE A VCN | | SPOKE B VCN |
| 10.1.0.0/16 | | 10.2.0.0/16 |
| Private + NAT | | Private + NAT |
| [vm-spoke-a] | | [vm-spoke-b] |
└────────|───────┘ └─────────|───────┘
Internet Internet
Componentes OCI y su propósito:
- VCN Hub (10.0.0.0/16): contiene el bastion en una subred pública, una privada para servicios compartidos, IGW (entrada SSH), NAT (egress del hub) y Service Gateway (acceso privado a servicios Oracle).
- VCN Spoke A (10.1.0.0/16) y Spoke B (10.2.0.0/16): una subred privada cada una con NAT local. Sin IGW. Sin acceso público.
- DRG (Dynamic Routing Gateway) v2: router lógico regional. Soporta hasta 300 VCNs + on-prem. Es gratuito en OCI.
- DRG Attachments: vínculo entre el DRG y cada VCN. Cada attachment apunta a una DRG Route Table.
- DRG Route Tables: dos tablas custom (
hub_drgrtyspokes_drgrt). Definen qué rutas conoce cada attachment. - DRG Route Distributions: políticas que importan rutas a las tablas del DRG según el
DRG_ATTACHMENT_IDde origen. Aquí vive el control real de la transitividad. - NAT Gateway por VCN: egress a internet local en cada VCN.
- Security Lists + NSGs: doble capa de firewall (en OCI ambas aplican simultáneamente).
- VCN Flow Logs: logging de tráfico para observabilidad y troubleshooting.
Modos de routing
El comportamiento inter-VCN se controla con una sola variable:
Modo spoke_a ↔ spoke_b spoke ↔ hub spoke → internet transit vía DRG vía DRG NAT local isolated vía DRG NAT local
Cambiar de modo no toca VMs ni VCNs: solo modifica drg_route_distribution_statement. El cambio es declarativo, idempotente y reversible.
Análisis del repositorio
Estructura
terraform-oci-drg-hub-spoke/
├── main.tf # Orquesta los 3 VCNs, el DRG y las VMs
├── drg.tf # Recurso DRG en root
├── variables.tf # Variables del root module
├── outputs.tf # Outputs (IDs, IPs, route tables)
├── provider.tf # Provider oracle/oci
├── versions.tf # Restricciones de versión
├── terraform.tfvars # Valores concretos (compartment, modo)
├── oci-lab.pub # Clave SSH pública del bastion
└── modules/
├── vcn/
│ ├── vcn.tf # Recurso oci_core_vcn
│ ├── subnets.tf # Subredes pública (hub) y privadas
│ ├── gateways.tf # IGW, NAT, SGW
│ ├── routes.tf # Route tables de VCN (rutas hacia DRG)
│ ├── nsg.tf # Network Security Groups
│ ├── sl.tf # Security Lists
│ ├── flow_logs.tf # Log group + VCN Flow Logs
│ ├── variables.tf
│ └── outputs.tf
├── drg/
│ ├── attachments.tf # Attachments hub, spoke-a, spoke-b
│ ├── route_tables.tf # hub_drgrt, spokes_drgrt
│ ├── route_distributions.tf # Lógica transit/isolated
│ ├── variables.tf
│ └── outputs.tf
└── compute/
├── vm.tf # Instancia VM.Standard.E4.Flex
├── user_data.yaml # cloud-init mínimo
├── data.tf # Availability domains
├── variables.tf
└── outputs.tf
Organización
- Root module: orquesta. No declara recursos de red salvo el propio DRG. Llama a tres instancias del módulo
vcn, una vez al módulodrgy tres veces al módulocompute(bastion + 1 VM por spoke). - Módulo
vcn: parametrizado conrole = "hub" | "spoke". El hub recibe IGW + SGW + subred pública, los spokes solo subred privada + NAT. Mismo módulo, comportamiento condicional concount. - Módulo
drg: encapsula attachments, route tables y route distributions. Toda la lógica del modotransit/isolatedvive aquí, controlada porcounten los statements. - Módulo
compute: genérico. No sabe si es bastion o vm-spoke. Recibesubnet_id,nsg_idyassign_public_ipy deja al root decidir.
Un detalle de diseño importante: el oci_core_drg se crea en el root, no dentro del módulo drg. Esto evita un ciclo de dependencia entre los módulos vcn (que necesitan el drg_id para sus route rules) y drg (que necesita los vcn_id para sus attachments).
Archivos clave
provider.tf y versions.tf
# provider.tf
provider "oci" {
config_file_profile = "DEFAULT"
}
# versions.tf
terraform {
required_version = ">= 1.5"
required_providers {
oci = {
source = "oracle/oci"
version = "~> 5.0"
}
}
}
Provider OCI oficial v5.x, autenticación vía ~/.oci/config con profile DEFAULT. Sin secretos en el código.
drg.tf (root)
resource "oci_core_drg" "this" {
compartment_id = var.compartment_ocid
display_name = "${var.project_name}-drg"
freeform_tags = var.tags
}
El DRG está aquí, fuera del módulo, justamente para romper el ciclo con las VCNs.
main.tf — composición del hub y los spokes
El root invoca el módulo vcn tres veces. Lo interesante son los remote_cidrs: la lista de redes que cada VCN debe alcanzar vía DRG.
module "hub_vcn" {
source = "./modules/vcn"
vcn_name = "hub"
role = "hub"
vcn_cidr = var.hub_vcn_cidr
drg_id = oci_core_drg.this.id
remote_cidrs = [var.spoke_a_vcn_cidr, var.spoke_b_vcn_cidr]
# ...
}
module "spoke_a_vcn" {
source = "./modules/vcn"
vcn_name = "spoke-a"
role = "spoke"
vcn_cidr = var.spoke_a_vcn_cidr
drg_id = oci_core_drg.this.id
remote_cidrs = var.spoke_isolation_mode == "transit" ? [
var.hub_vcn_cidr,
var.spoke_b_vcn_cidr,
] : [var.hub_vcn_cidr]
# ...
}
El truco: en modo transit, Spoke A conoce el CIDR del hub y el CIDR de Spoke B. En modo isolated, solo el del hub. La misma variable controla tanto las rutas de la VCN como las distribution statements del DRG.
modules/vcn/routes.tf — rutas de cada VCN
resource "oci_core_route_table" "spoke_private_rt" {
count = var.role == "spoke" ? 1 : 0
compartment_id = var.compartment_ocid
vcn_id = oci_core_vcn.this.id
display_name = "${var.project_name}-${var.vcn_name}-private-rt"
# Egress a internet por el NAT LOCAL del spoke
route_rules {
destination = "0.0.0.0/0"
destination_type = "CIDR_BLOCK"
network_entity_id = oci_core_nat_gateway.nat.id
}
# Trafico hacia otros spokes y hacia el hub via DRG
dynamic "route_rules" {
for_each = var.remote_cidrs
content {
destination = route_rules.value
destination_type = "CIDR_BLOCK"
network_entity_id = var.drg_id
}
}
freeform_tags = var.tags
}
La regla por defecto (0.0.0.0/0) apunta al NAT local del spoke. El resto de CIDRs (los remote_cidrs) apunta al DRG. Esa es la decisión clave: internet por NAT, inter-VCN por DRG.
modules/drg/route_distributions.tf — el cerebro del routing
Aquí es donde el modo transit vs isolated se materializa.
# Hub importa rutas de los dos spokes (siempre)
resource "oci_core_drg_route_distribution_statement" "hub_import_spoke_a" {
drg_route_distribution_id = oci_core_drg_route_distribution.hub_import.id
action = "ACCEPT"
priority = 10
match_criteria {
match_type = "DRG_ATTACHMENT_ID"
drg_attachment_id = oci_core_drg_attachment.spoke_a.id
}
}
# Spokes importan rutas del Hub (siempre)
resource "oci_core_drg_route_distribution_statement" "spokes_import_hub" {
drg_route_distribution_id = oci_core_drg_route_distribution.spokes_import.id
action = "ACCEPT"
priority = 10
match_criteria {
match_type = "DRG_ATTACHMENT_ID"
drg_attachment_id = oci_core_drg_attachment.hub.id
}
}
# Spokes importan rutas del OTRO spoke SOLO en modo transit
resource "oci_core_drg_route_distribution_statement" "spokes_import_a" {
count = var.isolation_mode == "transit" ? 1 : 0
drg_route_distribution_id = oci_core_drg_route_distribution.spokes_import.id
action = "ACCEPT"
priority = 20
match_criteria {
match_type = "DRG_ATTACHMENT_ID"
drg_attachment_id = oci_core_drg_attachment.spoke_a.id
}
}
El patrón es limpio: el count condicional decide si los spokes importan o no las rutas del otro spoke. No hay que destruir nada para cambiar de modo, solo añadir o quitar dos statements.
modules/drg/attachments.tf — quién va a qué tabla
resource "oci_core_drg_attachment" "hub" {
drg_id = var.drg_id
display_name = "${var.project_name}-hub-attach"
drg_route_table_id = oci_core_drg_route_table.hub_drgrt.id
network_details {
type = "VCN"
id = var.hub_vcn_id
}
}
resource "oci_core_drg_attachment" "spoke_a" {
drg_id = var.drg_id
display_name = "${var.project_name}-spoke-a-attach"
drg_route_table_id = oci_core_drg_route_table.spokes_drgrt.id
# ...
}
Dos tablas: hub_drgrt para el hub, spokes_drgrt compartida por ambos spokes. Esa separación es lo que permite tratar a los spokes como un grupo y aplicarles políticas distintas que al hub.
variables.tf — el interruptor
variable "spoke_isolation_mode" {
type = string
default = "transit"
validation {
condition = contains(["transit", "isolated"], var.spoke_isolation_mode)
error_message = "spoke_isolation_mode debe ser 'transit' o 'isolated'."
}
}
Una sola variable controla todo el comportamiento inter-spoke. Con validación, para evitar typos.
Implementación
Prerrequisitos
- Terraform
>= 1.5 - OCI CLI configurado en
~/.oci/configcon profileDEFAULT - Compartment con permisos para crear VCN, DRG, Compute y Logging
- Par de claves SSH
1. Generar las claves SSH
ssh-keygen -t rsa -b 4096 -f ./oci-lab -N ""
2. Configurar terraform.tfvars
compartment_ocid = "ocid1.compartment.oc1..xxxx"
project_name = "hubspoke"
spoke_isolation_mode = "transit"
3. Verificar la imagen Ubuntu en tu región
El image_ocid por defecto apunta a us-ashburn-1. Si trabajas en otra región:
oci compute image list \
--compartment-id <tenancy_ocid> \
--operating-system "Canonical Ubuntu" \
--operating-system-version "22.04" \
--shape "VM.Standard.E4.Flex" \
--sort-by TIMECREATED --sort-order DESC \
--query 'data[0].id' --raw-output
Y en terraform.tfvars:
image_ocid = "ocid1.image.oc1.<tu-region>.aaaaaaa..."
4. Desplegar
terraform init
terraform plan
terraform apply -auto-approve
El apply toma entre 5 y 7 minutos. El DRG y sus attachments son la parte más lenta.
5. Validar la conectividad
Desde el bastion al spoke A:
BASTION_IP=$(terraform output -raw bastion_public_ip)
scp -i ./oci-lab ./oci-lab ubuntu@$BASTION_IP:~/.ssh/oci-lab
ssh -i ./oci-lab ubuntu@$BASTION_IP
# Dentro del bastion
VM_A=$(terraform output -raw vm_spoke_a_private_ip)
ssh -i ~/.ssh/oci-lab ubuntu@$VM_A
Spoke A → Spoke B (depende del modo):
# Desde vm-spoke-a
ssh -i ~/.ssh/oci-lab ubuntu@<ip_privada_vm_spoke_b>
Modo Resultado transit conecta vía DRG isolated timeout
Inspeccionar rutas dinámicas del DRG:
oci network drg-route-rule list \
--drg-route-table-id $(terraform output -raw spokes_drgrt_id) \
--output table
En transit verás rutas del hub + spoke A + spoke B. En isolated, solo las del hub.
6. Cambiar de modo sin destruir
# Edita terraform.tfvars: spoke_isolation_mode = "isolated"
terraform apply -auto-approve
Solo cambian los drg_route_distribution_statement. Las VCNs, las VMs y los attachments quedan intactos. Eso es lo que hace este patrón especialmente útil en producción.
Casos de uso reales
Aislamiento entre líneas de negocio. Cada unidad de negocio (finanzas, RRHH, datos) en su propia VCN. El hub centraliza bastion, herramientas de observabilidad y servicios compartidos. En modo isolated ninguna unidad ve a las demás, pero todas pueden ser administradas desde el hub.
Zonificación dev/qa/prod en una misma región. Tres spokes (uno por entorno) conectados al mismo hub. Cambiar a isolated durante una ventana de cambio garantiza que prod no recibe tráfico lateral desde dev.
Migración progresiva desde Local Peering. Si ya tienes pares de VCNs con LPG, puedes migrarlas a este modelo manteniendo direccionamientos. El DRG soporta cientos de attachments; los LPG se vuelven inviables a partir de la quinta o sexta VCN.
Conexión a on-prem vía IPSec o FastConnect. El mismo DRG admite attachments de tipo IPSec o Virtual Circuit. El día que aparezca el on-prem, se añade el attachment y se ajustan las route distributions sin tocar las VCNs existentes.
Auditoría regulatoria. Un único punto (spokes_drgrt) concentra qué CIDRs ven los spokes. La política se audita leyendo dos archivos Terraform, no recorriendo route tables de cinco VCNs distintas.
Conclusión
El patrón hub-and-spoke con DRG v2 en OCI no es una copia del de AWS. La diferencia clave es el egress: en OCI cada VCN sale a internet por su propio NAT porque la RT del NAT solo acepta IPs privadas, y el DRG queda exclusivamente para tráfico inter-VCN. Una vez asumida esa decisión, el modelo se vuelve más simple y barato que su equivalente en AWS (NAT gratuito, DRG gratuito).
Este repositorio muestra el patrón completo en menos de 30 archivos: módulos reutilizables, un único interruptor para transit/isolated, y cambios de routing sin tocar VMs ni VCNs. Es la base sobre la que se construyen arquitecturas multi-región, conexiones a on-prem y políticas de aislamiento por línea de negocio.
El siguiente paso natural es añadir un cuarto attachment IPSec contra on-prem, o un attachment RPC contra otra región. El modelo no cambia: añades el attachment, actualizas la route distribution, aplicas. Eso es lo que hace que valga la pena pagar el coste inicial de aprender DRG v2.
Repositorio: github.com/jorgesalcedoeng/terraform-oci-drg-hub-spoke
메타데이터
- post_id
- 7c13efc7c271
- slug
- hub-and-spoke-en-oci-con-drg-routing-inter-vcn-centralizado-7c13efc7c271
- url
- https://medium.com/@jorge.salcedo.eng/hub-and-spoke-en-oci-con-drg-routing-inter-vcn-centralizado-7c13efc7c271
- canonical_url
- https://medium.com/@jorge.salcedo.eng/hub-and-spoke-en-oci-con-drg-routing-inter-vcn-centralizado-7c13efc7c271
- author_url
- https://medium.com/@jorge.salcedo.eng
- status
- ok
- fetched_at
- 2026-06-14 11:28:49