OCI Network Load Balancer: Layer 4 TCP/UDP Traffic Distribution with Terraform

The OCI Application Load Balancer terminates HTTP, inspects headers, routes by path, and applies WAF rules. That processing costs latency. When your workload is TCP-based, does not need HTTP inspection, or requires preserving the client source IP all the way to the backend, the Network Load Balancer is the correct choice. It operates at layer 4, passes packets without termination, and delivers lower latency with higher throughput.

When to Use the Network Load Balancer

Use the Network Load Balancer for non-HTTP workloads: Oracle Database listener on TCP 1521, gRPC with true pass-through, DNS over TCP and UDP, and any workload where the backend needs to see the real client IP. The Application Load Balancer always presents its own IP to the backend, not the client IP. The Network Load Balancer passes the original source IP through unchanged when source preservation is enabled.

Step 1: Provision the NLB

resource "oci_network_load_balancer_network_load_balancer" "production_nlb" {
  compartment_id = var.compartment_id
  display_name   = "production-nlb"
  subnet_id      = var.private_subnet_id

  is_private                     = true
  is_preserve_source_destination = true

  network_security_group_ids = [var.nlb_nsg_id]

  defined_tags = {
    "Operations.Environment" = "production"
    "Operations.ManagedBy"   = "terraform"
  }
}

output "nlb_ip_addresses" {
  value = oci_network_load_balancer_network_load_balancer.production_nlb.ip_addresses
}

Step 2: Backend Set for Oracle Database

resource "oci_network_load_balancer_backend_set" "oracle_db" {
  network_load_balancer_id = oci_network_load_balancer_network_load_balancer.production_nlb.id
  name                     = "oracle-db-backend-set"
  policy                   = "FIVE_TUPLE"
  is_preserve_source       = true

  health_checker {
    protocol           = "TCP"
    port               = 1521
    interval_in_millis = 10000
    timeout_in_millis  = 3000
    retries            = 3
  }
}

resource "oci_network_load_balancer_backend" "db_primary" {
  network_load_balancer_id = oci_network_load_balancer_network_load_balancer.production_nlb.id
  backend_set_name         = oci_network_load_balancer_backend_set.oracle_db.name
  name                     = "db-primary"
  ip_address               = var.db_primary_ip
  port                     = 1521
  weight                   = 100
  is_backup                = false
  is_drain                 = false
  is_offline               = false
}

resource "oci_network_load_balancer_backend" "db_standby" {
  network_load_balancer_id = oci_network_load_balancer_network_load_balancer.production_nlb.id
  backend_set_name         = oci_network_load_balancer_backend_set.oracle_db.name
  name                     = "db-standby"
  ip_address               = var.db_standby_ip
  port                     = 1521
  weight                   = 1
  is_backup                = true
  is_drain                 = false
  is_offline               = false
}

Step 3: Listeners for Oracle DB and DNS

resource "oci_network_load_balancer_listener" "oracle_db_tcp" {
  network_load_balancer_id = oci_network_load_balancer_network_load_balancer.production_nlb.id
  name                     = "oracle-db-tcp-1521"
  default_backend_set_name = oci_network_load_balancer_backend_set.oracle_db.name
  port                     = 1521
  protocol                 = "TCP"
}

resource "oci_network_load_balancer_backend_set" "dns_servers" {
  network_load_balancer_id = oci_network_load_balancer_network_load_balancer.production_nlb.id
  name                     = "dns-backend-set"
  policy                   = "THREE_TUPLE"

  health_checker {
    protocol           = "UDP"
    port               = 53
    interval_in_millis = 10000
    timeout_in_millis  = 3000
    retries            = 3
  }
}

resource "oci_network_load_balancer_listener" "dns_udp" {
  network_load_balancer_id = oci_network_load_balancer_network_load_balancer.production_nlb.id
  name                     = "dns-udp-53"
  default_backend_set_name = oci_network_load_balancer_backend_set.dns_servers.name
  port                     = 53
  protocol                 = "UDP"
}

resource "oci_network_load_balancer_listener" "dns_tcp" {
  network_load_balancer_id = oci_network_load_balancer_network_load_balancer.production_nlb.id
  name                     = "dns-tcp-53"
  default_backend_set_name = oci_network_load_balancer_backend_set.dns_servers.name
  port                     = 53
  protocol                 = "TCP"
}

Step 4: NSG Rules for Source IP Preservation

resource "oci_core_network_security_group_security_rule" "nlb_healthcheck_to_db" {
  network_security_group_id = var.db_nsg_id
  direction                 = "INGRESS"
  protocol                  = "6"
  source_type               = "CIDR_BLOCK"
  source                    = var.nlb_subnet_cidr

  tcp_options {
    destination_port_range { min = 1521; max = 1521 }
  }

  description = "Allow NLB health checks to Oracle DB"
}

# Critical: with source IP preservation, client IPs arrive at the backend
# Backend NSG must allow the CLIENT subnet, not just the NLB subnet
resource "oci_core_network_security_group_security_rule" "client_to_db_via_nlb" {
  network_security_group_id = var.db_nsg_id
  direction                 = "INGRESS"
  protocol                  = "6"
  source_type               = "CIDR_BLOCK"
  source                    = var.app_subnet_cidr

  tcp_options {
    destination_port_range { min = 1521; max = 1521 }
  }

  description = "Allow app tier connections to Oracle DB via NLB with source IP preserved"
}

Step 5: Health Check Alarm

resource "oci_monitoring_alarm" "nlb_unhealthy_backend" {
  compartment_id        = var.compartment_id
  display_name          = "nlb-unhealthy-backend"
  is_enabled            = true
  metric_compartment_id = var.compartment_id
  namespace             = "oci_nlb"
  query                 = "UnhealthyBackendServers[5m]{networkLoadBalancerId = 'NLB_OCID'}.max() > 0"
  severity              = "CRITICAL"
  pending_duration      = "PT5M"
  destinations          = [var.ops_notification_topic_id]
  body                  = "One or more backends on the production NLB are failing health checks. Check database listener status immediately."
}

Operational Notes

When is_preserve_source_destination = true, traffic arrives at the backend with the original client IP as the source. Backend NSG rules must allow the client subnet CIDR, not just the NLB subnet CIDR. This is the most common NLB configuration mistake: NSG rules allow the NLB IP, health checks pass, but client connections are dropped because the backend NSG does not permit the actual client source IP.

The FIVE_TUPLE policy hashes on source IP, source port, destination IP, destination port, and protocol. For Oracle Database connections this ensures the same client consistently reaches the same backend, maintaining session stickiness without application-layer session management. For stateless workloads, THREE_TUPLE or TWO_TUPLE are simpler and sufficient.

Regards,
Osama

#OCI #OracleCloud #NetworkLoadBalancer #Terraform #IaC #Networking #TechBlog #Oracle #PlatformEngineering #CloudNetworking #OracleCloudInfrastructure #Layer4 #LoadBalancing #HighAvailability #OracleDatabase #TCP #UDP #HealthChecks #NSG #DevOps

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.