OCI DNS Traffic Management: Failover, Geolocation, and Load Balancing Steering Policies with Terraform

A load balancer distributes traffic across instances in a single region. When that region has an outage, the load balancer goes down with it. DNS Traffic Management sits above the load balancer layer and distributes traffic across regions, data centers, or endpoints based on health, geography, or weight. It is the difference between a system that survives a regional failure and one that does not.

OCI DNS Traffic Management provides steering policies that control how DNS queries are answered based on rules you define. A failover policy routes all traffic to a primary endpoint and switches to a secondary only when the primary fails its health check. A geolocation policy routes users to the nearest region. A load balancing policy distributes traffic by weight across multiple endpoints. This post covers deploying all three patterns with Terraform and integrating health checks for automatic failover.

How OCI DNS Steering Policies Work

A steering policy has three components that work together. Answer pools contain the DNS records that can be returned: A records, CNAME records, or AAAA records pointing to your endpoints. Rules define the logic for selecting which answers to return for a given query. Health check monitors validate that endpoints in the answer pools are actually serving traffic before DNS returns them.

When a DNS query arrives, OCI evaluates the rules in order, filters out answers from unhealthy endpoints, and returns the surviving answers according to the policy type. If a health check fails for an endpoint, that endpoint is removed from the answer set until it recovers.

Step 1: IAM Policy

resource "oci_identity_policy" "dns_policy" {
  compartment_id = var.compartment_id
  name           = "dns-traffic-management-policy"
  description    = "Permissions to manage OCI DNS zones and steering policies"

  statements = [
    "Allow group ${var.ops_group_name} to manage dns in compartment id ${var.compartment_id}",
    "Allow group ${var.ops_group_name} to manage health-checkers in compartment id ${var.compartment_id}"
  ]
}

Step 2: Health Check Monitors

Health checks are the foundation of automatic failover. Without them, DNS continues returning a failed endpoint’s IP address and users get errors. Define health checks before creating steering policies so they are ready to attach.

resource "oci_health_checks_http_monitor" "primary_region_check" {
  compartment_id      = var.compartment_id
  display_name        = "primary-region-health-check"
  interval_in_seconds = 30
  is_enabled          = true
  protocol            = "HTTPS"
  port                = 443
  path                = "/health"
  method              = "GET"
  timeout_in_seconds  = 10

  targets = [var.primary_load_balancer_ip]

  vantage_point_names = [
    "goo-chs",
    "aws-lhr",
    "aws-sin"
  ]

  expected_response_code_group = ["2XX"]

  headers = {
    "User-Agent" = "OCI-HealthCheck/1.0"
    "Accept"     = "application/json"
  }

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

resource "oci_health_checks_http_monitor" "secondary_region_check" {
  compartment_id      = var.compartment_id
  display_name        = "secondary-region-health-check"
  interval_in_seconds = 30
  is_enabled          = true
  protocol            = "HTTPS"
  port                = 443
  path                = "/health"
  method              = "GET"
  timeout_in_seconds  = 10

  targets = [var.secondary_load_balancer_ip]

  vantage_point_names = [
    "goo-chs",
    "aws-lhr",
    "aws-sin"
  ]

  expected_response_code_group = ["2XX"]
}

The vantage_point_names list specifies the global locations from which OCI runs the health checks. Using multiple vantage points prevents a single network path issue from triggering a failover. An endpoint is considered unhealthy only when a majority of vantage points report failure. Three vantage points is a sensible minimum for production.

Step 3: Failover Steering Policy

A failover policy routes all traffic to the primary endpoint and only uses the secondary when the primary health check fails. This is the correct pattern for active-passive disaster recovery.

resource "oci_dns_steering_policy" "failover_policy" {
  compartment_id = var.compartment_id
  display_name   = "api-failover-policy"
  template       = "FAILOVER"
  ttl            = 30

  health_check_monitor_id = oci_health_checks_http_monitor.primary_region_check.id

  answers {
    name        = "primary-jeddah"
    rtype       = "A"
    rdata       = var.primary_load_balancer_ip
    pool        = "primary"
    is_disabled = false
  }

  answers {
    name        = "secondary-dubai"
    rtype       = "A"
    rdata       = var.secondary_load_balancer_ip
    pool        = "secondary"
    is_disabled = false
  }

  rules {
    rule_type     = "FILTER"
    description   = "Remove unhealthy answers"

    cases {
      answer_data {
        answer_condition = "answer.isHealthy"
        should_keep      = true
      }
    }

    default_answer_data {
      answer_condition = "answer.isHealthy"
      should_keep      = true
    }
  }

  rules {
    rule_type   = "PRIORITY"
    description = "Primary pool first, secondary as fallback"

    cases {
      answer_data {
        answer_condition = "answer.pool == 'primary'"
        value            = 1
      }
      answer_data {
        answer_condition = "answer.pool == 'secondary'"
        value            = 2
      }
    }

    default_answer_data {
      answer_condition = "answer.pool == 'primary'"
      value            = 1
    }

    default_answer_data {
      answer_condition = "answer.pool == 'secondary'"
      value            = 2
    }
  }

  rules {
    rule_type   = "LIMIT"
    description = "Return only 1 answer"
    count       = 1
  }

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

The TTL of 30 seconds means DNS resolvers cache the answer for 30 seconds. During a failover, clients that cached the primary IP continue using it for up to 30 seconds before their resolver fetches the updated answer pointing to the secondary. Set TTL based on your acceptable failover time. 30 seconds is a reasonable balance between cache efficiency and failover speed.

Step 4: Attach the Steering Policy to a DNS Zone

resource "oci_dns_zone" "primary_zone" {
  compartment_id = var.compartment_id
  name           = var.domain_name
  zone_type      = "PRIMARY"

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

resource "oci_dns_steering_policy_attachment" "api_attachment" {
  steering_policy_id = oci_dns_steering_policy.failover_policy.id
  zone_id            = oci_dns_zone.primary_zone.id
  domain_name        = "api.${var.domain_name}"
  display_name       = "api-failover-attachment"
}

The attachment links the steering policy to the specific subdomain api.yourdomain.com. When a resolver queries for this domain, OCI DNS applies the failover policy logic and returns the appropriate IP based on health check status.

Step 5: Geolocation Steering Policy

A geolocation policy routes users to the nearest region based on where their DNS resolver is located. Users in the Middle East resolve to the Jeddah region. Users in Europe resolve to Frankfurt. Users in Asia resolve to Singapore.

resource "oci_dns_steering_policy" "geolocation_policy" {
  compartment_id = var.compartment_id
  display_name   = "api-geolocation-policy"
  template       = "CUSTOM"
  ttl            = 60

  answers {
    name  = "me-jeddah"
    rtype = "A"
    rdata = var.jeddah_lb_ip
    pool  = "middle-east"
  }

  answers {
    name  = "eu-frankfurt"
    rtype = "A"
    rdata = var.frankfurt_lb_ip
    pool  = "europe"
  }

  answers {
    name  = "ap-singapore"
    rtype = "A"
    rdata = var.singapore_lb_ip
    pool  = "asia"
  }

  answers {
    name  = "us-ashburn"
    rtype = "A"
    rdata = var.ashburn_lb_ip
    pool  = "default"
  }

  rules {
    rule_type   = "FILTER"
    description = "Remove unhealthy endpoints"

    cases {
      answer_data {
        answer_condition = "answer.isHealthy"
        should_keep      = true
      }
    }

    default_answer_data {
      answer_condition = "answer.isHealthy"
      should_keep      = true
    }
  }

  rules {
    rule_type   = "PRIORITY"
    description = "Route by geography"

    # Middle East and Africa
    cases {
      case_condition = "query.client.geoKey matches 'ME' || query.client.geoKey matches 'AF'"
      answer_data {
        answer_condition = "answer.pool == 'middle-east'"
        value            = 1
      }
      answer_data {
        answer_condition = "answer.pool == 'europe'"
        value            = 2
      }
      answer_data {
        answer_condition = "answer.pool == 'default'"
        value            = 3
      }
    }

    # Europe
    cases {
      case_condition = "query.client.geoKey matches 'EU'"
      answer_data {
        answer_condition = "answer.pool == 'europe'"
        value            = 1
      }
      answer_data {
        answer_condition = "answer.pool == 'middle-east'"
        value            = 2
      }
      answer_data {
        answer_condition = "answer.pool == 'default'"
        value            = 3
      }
    }

    # Asia Pacific
    cases {
      case_condition = "query.client.geoKey matches 'AP'"
      answer_data {
        answer_condition = "answer.pool == 'asia'"
        value            = 1
      }
      answer_data {
        answer_condition = "answer.pool == 'default'"
        value            = 2
      }
    }

    # Default - North America and everything else
    default_answer_data {
      answer_condition = "answer.pool == 'default'"
      value            = 1
    }

    default_answer_data {
      answer_condition = "answer.pool == 'europe'"
      value            = 2
    }
  }

  rules {
    rule_type = "LIMIT"
    count     = 1
  }
}

Each case in the PRIORITY rule has a fallback priority list. If the preferred regional endpoint is unhealthy, the FILTER rule already removed it, and the PRIORITY rule falls through to the next available pool. A Middle East user whose Jeddah endpoint is down gets routed to the Europe pool rather than receiving a DNS NXDOMAIN response.

Step 6: Weighted Load Balancing Policy

A weighted policy distributes traffic across endpoints by percentage. Use this for canary deployments, gradual traffic migration between regions, or A/B testing at the DNS level.

resource "oci_dns_steering_policy" "weighted_policy" {
  compartment_id = var.compartment_id
  display_name   = "api-weighted-policy"
  template       = "LOAD_BALANCE"
  ttl            = 30

  health_check_monitor_id = oci_health_checks_http_monitor.primary_region_check.id

  answers {
    name  = "primary-endpoint"
    rtype = "A"
    rdata = var.primary_lb_ip
    pool  = "primary"
  }

  answers {
    name  = "canary-endpoint"
    rtype = "A"
    rdata = var.canary_lb_ip
    pool  = "canary"
  }

  rules {
    rule_type   = "FILTER"
    description = "Remove unhealthy endpoints"

    cases {
      answer_data {
        answer_condition = "answer.isHealthy"
        should_keep      = true
      }
    }

    default_answer_data {
      answer_condition = "answer.isHealthy"
      should_keep      = true
    }
  }

  rules {
    rule_type   = "WEIGHTED"
    description = "90% primary, 10% canary"

    cases {
      answer_data {
        answer_condition = "answer.pool == 'primary'"
        value            = 90
      }
      answer_data {
        answer_condition = "answer.pool == 'canary'"
        value            = 10
      }
    }

    default_answer_data {
      answer_condition = "answer.pool == 'primary'"
      value            = 90
    }

    default_answer_data {
      answer_condition = "answer.pool == 'canary'"
      value            = 10
    }
  }

  rules {
    rule_type = "LIMIT"
    count     = 1
  }
}

For a canary deployment, start with 5% weight on the canary endpoint. Monitor error rates and latency. Increase to 10%, then 25%, then 50% as confidence grows. If the canary shows problems, update the Terraform config to set canary weight to 0, apply, and 100% of traffic returns to the primary within the TTL window.

Step 7: Monitoring Failover Events

resource "oci_monitoring_alarm" "health_check_failing" {
  compartment_id        = var.compartment_id
  display_name          = "primary-region-health-check-failing"
  is_enabled            = true
  metric_compartment_id = var.compartment_id
  namespace             = "oci_healthchecks"
  query                 = "HTTP.isHealthy[5m]{monitorId = '${oci_health_checks_http_monitor.primary_region_check.id}'}.mean() < 1"
  severity              = "CRITICAL"
  pending_duration      = "PT5M"
  destinations          = [var.ops_notification_topic_id]
  body                  = "Primary region health check is failing. DNS failover to secondary region may be active. Investigate primary region load balancer and application status immediately."
}

resource "oci_events_rule" "dns_steering_change" {
  compartment_id = var.compartment_id
  display_name   = "dns-steering-policy-change-alert"
  is_enabled     = true

  condition = jsonencode({
    eventType = [
      "com.oraclecloud.dns.updatesteeringpolicy",
      "com.oraclecloud.dns.deletesteeringpolicy"
    ]
  })

  actions {
    actions {
      action_type = "ONS"
      is_enabled  = true
      topic_id    = var.ops_notification_topic_id
      description = "Alert ops team when DNS steering policy is modified"
    }
  }
}

Step 8: Validating DNS Routing

# Check current DNS resolution
dig api.yourdomain.com +short

# Check which OCI DNS name servers are authoritative
dig NS yourdomain.com +short

# Query directly against OCI DNS to bypass local resolver cache
dig @${OCI_NS1} api.yourdomain.com +short

# Test health check status via OCI CLI
oci health-checks http-probe-result list \
  --probe-configuration-id ${HEALTH_CHECK_ID} \
  --query 'data[0:5].{target:target, healthy:"is-healthy", time:"start-time", status:"status-code"}' \
  --output table

# Simulate a failover by disabling the primary answer in the steering policy
# (for testing only - re-enable immediately after)
oci dns steering-policy update \
  --steering-policy-id ${STEERING_POLICY_ID} \
  --answers '[{"name": "primary-jeddah", "rtype": "A", "rdata": "'${PRIMARY_IP}'", "pool": "primary", "isDisabled": true}, {"name": "secondary-dubai", "rtype": "A", "rdata": "'${SECONDARY_IP}'", "pool": "secondary", "isDisabled": false}]'

# Wait 30 seconds (TTL), then verify DNS returns the secondary IP
dig api.yourdomain.com +short

Combining Policies: Geolocation with Failover

Real production environments often need both geolocation and failover together. Route users to their nearest region, and if that region fails, fall back to the next nearest healthy region. OCI DNS achieves this by combining a FILTER rule that removes unhealthy endpoints with a PRIORITY rule that orders endpoints by geography. The filter runs first, eliminating unhealthy options. The priority rule then selects from the remaining healthy options in geographic order. A user in the Middle East whose primary Jeddah endpoint is down gets the Europe endpoint returned instead of an empty response.

Test the combined policy by temporarily disabling an answer in the steering policy, waiting for the TTL to expire, and verifying that DNS returns the expected fallback endpoint. Do this during off-peak hours and have the re-enable command ready to run immediately after validation.

Operational Notes

Health check intervals and failover TTL work together. If your health check interval is 30 seconds and your DNS TTL is 30 seconds, the worst-case failover time is roughly 90 seconds: 30 seconds for the health check to detect failure, plus 30 seconds for the TTL to expire on resolvers that cached the old answer, plus propagation time. Reducing either number shortens the failover window but increases DNS query volume and health check frequency.

Always test failover before you need it. Disable the primary endpoint in the steering policy, verify DNS switches to the secondary, verify your application actually works through the secondary, then re-enable the primary and verify traffic returns. Do this quarterly at minimum. A failover configuration that has never been tested is a configuration that may not work when you need it.

Keep steering policy changes in version control and apply them through Terraform. Manual console changes to DNS routing are not audited in the same way as Terraform applies, they are harder to roll back, and they create drift between your infrastructure code and the actual configuration. An OCI Events rule that alerts on manual console changes, as configured in Step 7, helps catch drift before it becomes a problem.

Regards,
Osama

#OCI #OracleCloud #DNS #TrafficManagement #Terraform #CloudNative #IaC #OracleCloudInfrastructure #PlatformEngineering #CloudArchitecture #HighAvailability #TechBlog #Oracle #GlobalLoadBalancing #DisasterRecovery #Geolocation #Failover #CloudNetworking #MultiRegion #DevOps

Leave a comment

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