eqixiacEquinix Infrastructure as Code

Disaster recovery across regions

Primary and recovery metros with redundant cloud on-ramps, BGP failover and a failover test.

Scenario#

Production runs in Ashburn with AWS us-east-1, and the recovery site is Dallas with AWS us-east-2. The recovery objectives are a recovery time of 15 minutes for the network and replication that keeps the data loss below 5 minutes. The network must fail over without a manual change, and the recovery path must be tested every quarter.

Failure domains#

FailureMitigationAdds
One virtual connection or one provider devicePrimary and secondary connection in a redundancy group, to two provider locationsOne connection
One Fabric port or Fabric deviceTwo ports on different devicesOne port, one cross connect
One Cloud RouterTwo routers in the metroOne router fee
One IBXPorts in two IBXs of the metroA second cage or a metro connect
One metro or cloud regionA second metro with its own on-ramps, as belowThe whole second site

Architecture#

Metro DC (Ashburn), primaryMetro DA (Dallas), recoveryProduction cageAS 65100FCR DCprimaryAWS us-east-12 x Direct ConnectRecovery cageAS 65100FCR DAstandbyAWS us-east-22 x Direct Connectprimary + secondaryprimary + secondarybackbone IP_VC 1Greplication over the AWS backbone
Each metro has its own Cloud Router and its own on-ramp to the nearest AWS region. A backbone connection joins the routers. BGP path attributes make Ashburn primary.

Replication between the two AWS regions stays on the AWS network. The Fabric backbone carries site-to-site traffic and is the recovery path when one metro's on-ramp is lost.

Step 1: placement and metro loss#

The optimizer's resilience objective measures the share of the design that survives the loss of the metro whose loss removes the most.

Java
import com.eqixiac.equinix.core.enums.MetroCode;
import com.eqixiac.equinix.design.optimizer.enums.OptimizationStrategy;
import com.eqixiac.equinix.design.optimizer.enums.RedundancyTier;
import com.eqixiac.equinix.design.optimizer.enums.WorkloadType;
import com.eqixiac.equinix.design.optimizer.model.OptimizationResult;
import com.eqixiac.equinix.design.spi.Objective;
import com.eqixiac.equinix.fabric.model.implementation.cloud.CloudProviderType;

OptimizationResult result = fabric.optimizeMetros()
    .name("dr-pair")
    .addSite("Reston HQ").nearestMetro(MetroCode.DC).headcount(800).done()
    .addSite("Dallas office").nearestMetro(MetroCode.DA).headcount(200).done()
    .requireProvider(CloudProviderType.AWS).done()
    .addWorkload("erp").type(WorkloadType.TRANSACTIONAL).bandwidthMbps(1_000).dependsOn(CloudProviderType.AWS).done()
    .addWorkload("erp-dr").type(WorkloadType.DISASTER_RECOVERY).bandwidthMbps(500).dependsOn(CloudProviderType.AWS).done()
    .constraints().redundancy(RedundancyTier.MULTI_METRO).requireMetro("DC").maxMetros(2).done()
    .strategy(OptimizationStrategy.REDUNDANCY_FIRST)
    .optimize();

System.out.println(result.getRecommended().portfolio().metros());
System.out.println(result.getRecommended().values().get(Objective.RESILIENCE).explanation());

The explanation states the surviving share of sites, clouds and workloads after the worst metro loss, and the same for the worst region loss. RedundancyTier.MULTI_REGION forces two Equinix regions, for example AMER and EMEA. Two metros in one country are MULTI_METRO.

For services that depend on peering, the blast radius report gives the corresponding figure for exchange presence.

Step 2: redundant on-ramps in each metro#

A redundancy group holds a primary and a secondary connection. For AWS, the two hosted connections should end on different Direct Connect locations or devices, which AWS assigns from the service profile and the metro.

Java
import com.eqixiac.equinix.fabric.enums.ConnectionType;
import com.eqixiac.equinix.fabric.enums.RedundancyPriority;
import com.eqixiac.equinix.fabric.model.CloudRouter;
import com.eqixiac.equinix.fabric.model.Connection;
import com.eqixiac.equinix.fabric.model.implementation.cloud.AwsDirectConnectAdapter;

CloudRouter fcrDc = fabric.cloudRouters().getByUuid(routerUuid);

List<Connection> onRamps = new ArrayList<>();
for (RedundancyPriority priority : RedundancyPriority.values()) {
    onRamps.add(fabric.connections().define(ConnectionType.IP_VC)
        .name("fcr-dc-to-aws-" + priority.name().toLowerCase())
        .bandwidth(1000)
        .redundancy("fcr-dc-aws", priority)
        .aSideAccessPointCloudRouter(fcrDc)
        .zSideCloudProvider(AwsDirectConnectAdapter.of("123456789012", "us-east-1", awsProfileUuid, "DC"))
        .notification("noc@example.com")
        .create());
}

Repeat for Dallas with us-east-2 and metro "DA". Each of the four connections gets its own /30, a DIRECT and a BGP protocol.

Step 3: backbone between the routers#

Java
CloudRouter fcrDa = fabric.cloudRouters().getByUuid("uuid-of-fcr-da");

Connection backbone = fabric.connections().define(ConnectionType.IP_VC)
    .name("fcr-dc-to-fcr-da")
    .bandwidth(1000)
    .aSideAccessPointCloudRouter(fcrDc)
    .zSideAccessPointCloudRouter(fcrDa)
    .notification("noc@example.com")
    .dryRun()                                  // remove to create
    .create();

The backbone has to carry the traffic that crosses it during a failure, which exceeds the traffic of normal operation. If Ashburn loses its on-ramp, all traffic between Ashburn and AWS crosses the backbone to Dallas.

Step 4: BGP failover#

The recovery path is advertised at all times with less preferred BGP attributes. Failover needs no route change.

Java
import com.eqixiac.equinix.fabric.enums.RoutingProtocolType;

// Primary on-ramp: default attributes, BFD for fast detection
fabric.routingProtocols().define()
    .ofType(RoutingProtocolType.BGP)
    .withCustomerAsn(64512L)
    .withBGPIpv4("10.100.1.2", "10.100.1.1", true)
    .withBFD(true, 300)
    .create(onRamps.get(0));

// Secondary on-ramp: AS-path prepend outbound and a higher MED, so AWS and the router prefer the primary
fabric.routingProtocols().define()
    .ofType(RoutingProtocolType.BGP)
    .withCustomerAsn(64512L)
    .withBGPIpv4("10.100.2.2", "10.100.2.1", true)
    .withBGPIpv4Options(3L, 200L, 200L, null)     // outbound prepend count, inbound MED, outbound MED, max routes
    .withBFD(true, 300)
    .create(onRamps.get(1));

With BFD at a 300 ms interval and the usual multiplier of 3, a failed session is detected in about a second, and traffic moves to the next-best path. Without BFD the session is declared down when the BGP hold timer expires, which typically takes 90 seconds or more.

The preference has to be consistent in both directions. AWS chooses its outbound path from the AS-path length and the local preference communities it receives. If the design prefers the primary for inbound traffic and the secondary for outbound traffic, a stateful device on either path sees one direction of a flow and drops it.

Step 5: verify before the test#

Java
import com.eqixiac.equinix.fabric.model.RouteTableEntry;

Map<String, List<String>> nextHopsByPrefix = new TreeMap<>();
for (RouteTableEntry route : fabric.cloudRouters().searchRoutes(routerUuid).loadAll()) {
    nextHopsByPrefix.computeIfAbsent(route.getPrefix(), k -> new ArrayList<>())
        .add(route.getNextHop() + " (" + route.getState() + ")");
}
nextHopsByPrefix.forEach((prefix, hops) -> {
    if (hops.size() < 2) {
        System.out.println("NO BACKUP PATH: " + prefix + " via " + hops);
    }
});

A prefix with one next hop has no recovery path, so this check should run on a schedule and after every change.

Step 6: quarterly failover test#

  1. Announce the test window, and confirm with the check from step 5 that every production prefix has two next hops.
  2. Record the route table and the received routes on each connection as the baseline.
  3. Reset the BGP session on the primary on-ramp, which interrupts traffic on that connection.
  4. Measure the time until traffic flows over the secondary, from your application probes.
  5. Read the route table again and confirm the secondary path is active.
  6. Wait for the primary session to re-establish, and confirm traffic returns.
Java
import com.eqixiac.equinix.fabric.enums.BGPActionType;

String primaryConnection = onRamps.get(0).getUuid();
String bgpUuid = fabric.routingProtocols().list(primaryConnection).stream()
    .filter(p -> p.getType() == RoutingProtocolType.BGP)
    .findFirst().orElseThrow().getUuid();

fabric.routingProtocols().createBgpAction(primaryConnection, bgpUuid, BGPActionType.RESET_BGPIPV4);

A session reset tests the routing failover of one connection. A test of a metro loss also has to shut the cage-side sessions in Ashburn and fail the applications over, which the network API cannot do.

Record the measured failover time against the 15-minute objective each quarter. A failover time that grows from one test to the next indicates that the configuration has changed, for example by a prefix that was added on the primary path only.

Cost#

The recovery site adds a second router, two more on-ramp connections with their provider ports, and the backbone, which roughly doubles the Fabric cost. Run the TCO comparison with and without the second metro to get the monthly cost of the recovery site.