eqixiacEquinix Infrastructure as Code

Deployment Wizard

From an optimization result to a priced, validated plan, then to execution, drift detection, reconcile and rollback, or to Terraform.

Pipeline#

The wizard turns an OptimizationResult into a DeploymentPlan: one Cloud Router per metro, the connections from each router to the clouds, the backbone links between metros, and a DIRECT and a BGP routing protocol per connection. Planning, validation, repricing and export change nothing in the account. execute and reconcile create resources.

plan()immutable plandryRun()three layerssave / loadJSON, plan idexecute()all at oncereconcile()missing onlydiff()read-onlyrollback()orphan reportTerraformMermaid
Plan lifecycle. The plan is a JSON document with a content-derived plan id. diff reads live state. reconcile creates only what is missing and checkpoints every step.

Plan#

Java
import com.eqixiac.equinix.design.optimizer.wizard.enums.BackboneTopology;
import com.eqixiac.equinix.design.optimizer.wizard.enums.BandwidthStrategy;
import com.eqixiac.equinix.design.optimizer.wizard.model.DeploymentPlan;
import com.eqixiac.equinix.fabric.enums.GatewayPackageCode;

DeploymentPlan plan = fabric.deploymentWizard(result)             // result: an OptimizationResult
    .deploymentName("payments-platform")
    .routerPackage(GatewayPackageCode.STANDARD)
    .routerNamePrefix("fcr")
    .backboneTopology(BackboneTopology.FULL_MESH)
    .backboneBandwidthMbps(10_000)
    .bandwidthStrategy(BandwidthStrategy.PER_WORKLOAD)
    .customerAsn(65100L)
    .subnetBase("10.200.0.0")                                     // base for the /30 link subnets
    .withBFD(true, 300)                                           // interval in ms
    .accountNumber(123456L)
    .projectId(projectId)
    .notifications("noc@example.com", "netops@example.com")       // required: Fabric needs a recipient per Cloud Router
    .plan();

System.out.println(plan.toSummary());
plan.getCloudRouters().forEach(r -> System.out.println(r.getName() + " in " + r.getMetroId()));
plan.getProviderConnections().forEach(c ->
    System.out.println(c.getName() + " " + c.getBandwidthMbps() + " Mbps to " + c.getZSideProviderLabel()));
plan.getBackboneLinks().forEach(l ->
    System.out.println(l.getMetroA() + " to " + l.getMetroZ() + " " + l.getBandwidthMbps() + " Mbps"));
Files.writeString(Path.of("deployment-plan.md"), plan.toMarkdown());

Every planned connection starts on a Cloud Router and therefore has the type IP_VC. Validation reports an error for any other providerConnectionType(...).

Backbone topologyLinks for N metrosTrade-off
FULL_MESHN(N-1)/2Direct path between every pair; cost grows quadratically
HUB_SPOKEN-1Lowest cost; traffic between spokes transits the primary metro, which becomes a single point of failure
RINGNTwo paths between any pair; latency depends on the position in the ring
Bandwidth strategySizing
PER_WORKLOADEach provider connection carries the sum of the bandwidths of the workloads that depend on it
AGGREGATEDEvery connection in a metro is sized to the metro total
CUSTOMValues from customBandwidthMap(...)

A requested bandwidth is rounded up to a size the service profile offers. Plans executed into one project need different subnetBase(...) values, so that their /30 subnets do not overlap.

Pricing#

plan.getPricing() holds the monthly and one-time totals with the price source of each item, through the rate-card chain or a card set with rateCard(...). fabric.deploymentWizard(result)...reprice(plan) prices an existing plan again with current rates. plan.valueRealization() nets the plan's cost against the egress saving it enables.

Validation and execution#

dryRun() validates in three layers and returns a new plan that holds the outcome. The plan it was called on is unchanged.

LayerCheckNeeds the API
1Structure and catalogue: types, required fields, package, notificationsNo
2Each Cloud Router create as a live dryRun=trueYes
3Each connection as a live dry run, where both endpoints already existYes

The plan reports the findings in three separate lists.

GroupMeaningEffect
getValidationErrors()A defect, or a rejection by the APIThe plan is invalid
getDeferredValidations()A connection dry run that must wait until its Cloud Router existsChecked during execution
getSkippedValidations()A check that could not be attempted, with the reasonDoes not invalidate the plan
Java
import com.eqixiac.equinix.design.optimizer.wizard.model.DeploymentOutcome;
import com.eqixiac.equinix.design.optimizer.wizard.model.ExecutionInputs;

DeploymentPlan checked = plan.dryRun();
if (!checked.isValid()) {
    checked.getValidationErrors().forEach(System.err::println);
    return;
}

// Inputs only the customer can supply: the cloud authentication key and VLAN per connection.
checked.getRequiredInputs().forEach(input -> System.out.println(input.describe()));

DeploymentOutcome outcome = checked.execute(ExecutionInputs.builder()
    .authenticationKey("fcr-DC-to-aws", "123456789012")
    .vlanTag("fcr-DC-to-aws", 1001)
    .build());

System.out.println(outcome.toMarkdown());

The connection names passed to ExecutionInputs are the names the plan reports in getRequiredInputs().

Execution creates the Cloud Routers first and waits for each to reach PROVISIONED. It then creates the provider connections, after a dry run of each against its router, then the backbone links, and last the routing protocols, DIRECT before BGP. execute(...) throws IllegalStateException for an invalid plan and fails before the first create when an authentication key is missing.

Failure policy#

Java
import com.eqixiac.equinix.design.plan.ExecutionOptions;
import com.eqixiac.equinix.design.plan.FailurePolicy;

ExecutionOptions options = ExecutionOptions.builder()
    .failurePolicy(FailurePolicy.ABORT_AND_ROLLBACK)      // the default
    .provisionTimeout(Duration.ofMinutes(20))
    .build();

DeploymentOutcome guarded = plan.execute(ExecutionInputs.none(), options);
if (!guarded.isFullySuccessful()) {
    guarded.getErrors().forEach(e -> System.err.println(e));
    System.err.println(guarded.getRollback().toMarkdown());
}
PolicyOn a failed step
ABORT_AND_ROLLBACKStops, and deletes what the run created in reverse order. Connection deletes are asynchronous, so the rollback waits for each before it deletes the Cloud Router. Anything it could not remove is reported as an orphan with its UUID.
CONTINUE_ON_ERRORContinues with the steps that do not depend on the failed one and reports the partial state

An orphan is a resource that still exists and is billed, so RollbackReport.orphans() should be checked after every failed run.

Plan as data New in 4.0#

A plan can be written to JSON and read back. Its plan id is a hash of the desired state, and the document records the fingerprint of the DesignSpec it came from.

Java
plan.save(Path.of("plans/payments-platform.json"));

DeploymentPlan loaded = DeploymentPlan.load(Path.of("plans/payments-platform.json"))
    .withFabric(fabric);                       // a loaded plan needs a gateway before it can validate or execute
System.out.println(loaded.getPlanId() + " from spec " + loaded.getSpecFingerprint());

When the plan file is stored with the design document, a reviewer approves the list of resources in the file, and the run that creates them loads the same file.

Drift detection and reconcile New in 4.0#

diff() reads the live Fabric state, compares it with the plan and changes nothing. It finds the resources of a plan by name within the plan's project, and connections also by a marker that the plan writes into the connection's additionalInfo.

Java
import com.eqixiac.equinix.design.plan.DriftStatus;
import com.eqixiac.equinix.design.plan.FileCheckpointStore;
import com.eqixiac.equinix.design.plan.PlanDiff;

PlanDiff drift = plan.diff();
System.out.println(drift.toMarkdown());

drift.withStatus(DriftStatus.MISSING).forEach(r -> System.out.println("missing: " + r.kind() + " " + r.name()));
drift.withStatus(DriftStatus.PRESENT_DIFFERENT).forEach(r ->
    System.out.println("differs: " + r.name() + " " + r.differences() + " patchable=" + r.fullyPatchable()));
drift.unmanaged().forEach(u -> System.out.println("carries the plan marker but is not in the plan: " + u));

if (!drift.isInSync()) {
    DeploymentOutcome reconciled = plan.reconcile(
        ExecutionInputs.none(), ExecutionOptions.defaults(), new FileCheckpointStore(Path.of("plans/checkpoints")));
    System.out.println(reconciled.toSummary());
}
DriftStatusMeaning
MISSINGPlanned, not found live
PRESENT_MATCHINGFound, and the compared fields match
PRESENT_DIFFERENTFound, with field differences listed
UNREADABLEThe read failed; the state is not known
NOT_EXECUTABLEA plan element the SDK never creates

reconcile(...) reads live state first, adopts what exists, creates only what is missing in dependency order, and saves a checkpoint after every step. A run that crashed resumes from its checkpoint without creating duplicates, and a re-run of a completed plan does nothing. Updates to existing resources are off unless ExecutionOptions.builder().applyUpdates(true) is set, and they are limited to fields the API can patch. Reconcile does not delete a live resource that is absent from the plan. The diff lists such a resource as unmanaged.

plan.rollback(outcome) removes what a given outcome created.

Native multicloud links, and Fabric Interconnects added with interconnect(...), have the status NOT_EXECUTABLE. The SDK includes them in the plan and its reports and does not create them.

Terraform and diagram export#

A team that provisions through Terraform can export the plan as HCL and apply it there.

Java
import com.eqixiac.equinix.design.export.TerraformExporter;
import com.eqixiac.equinix.design.export.TopologyDiagram;

String hcl = new TerraformExporter().export(plan);
Files.writeString(Path.of("deployment.tf"), hcl);

String mermaid = new TopologyDiagram().toMermaid(plan);       // also accepts an OptimizationResult

The HCL uses the Equinix Terraform provider's equinix_fabric_cloud_router, equinix_fabric_connection and equinix_fabric_routing_protocol resources, with references between them.

  • Each connection's cloud authentication key is a variable marked sensitive, so the file contains no secret.
  • A VLAN tag that the plan has not fixed is also a variable.
  • Each BGP routing protocol has depends_on on its DIRECT sibling, which is the order Fabric requires.
  • A native multicloud link produces a comment block and one account-identifier variable, without a resource block or a cloud provider block.

A plan that is executed by the SDK and also applied through Terraform creates every resource twice, so one of the two has to be chosen for each plan.

SDK execute or reconcileTerraform export
StateLive Fabric state plus checkpointsTerraform state file
Driftplan.diff()terraform plan
DeletesRollback of a run onlyFull lifecycle
FitsProgrammatic pipelines, one-off builds, an agent-proposed planTeams with Terraform as the system of record

Cloud-to-cloud flows Beta#

A workload that depends on two clouds implies a flow between them. With the default CloudToCloudStrategy.COMPARE the wizard plans the Equinix connections and adds the native provider-to-provider link as a priced alternative (Cloud-to-cloud).

Limits#

  • The wizard plans Cloud Routers, their connections and routing protocols. It does not plan ports, cross connects, Network Edge devices or Internet Access.
  • The wizard creates Equinix resources. The AWS virtual interface, the Azure circuit peering or the Google VLAN attachment is configured with the provider's tooling, and BGP comes up only after that.
  • A dry run reserves no resource, so a VLAN or an account limit can change between validation and execution.
  • Drift detection compares the fields that the plan models and does not report a change to any other field.