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#
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 topology | Links for N metros | Trade-off |
|---|---|---|
FULL_MESH | N(N-1)/2 | Direct path between every pair; cost grows quadratically |
HUB_SPOKE | N-1 | Lowest cost; traffic between spokes transits the primary metro, which becomes a single point of failure |
RING | N | Two paths between any pair; latency depends on the position in the ring |
| Bandwidth strategy | Sizing |
|---|---|
PER_WORKLOAD | Each provider connection carries the sum of the bandwidths of the workloads that depend on it |
AGGREGATED | Every connection in a metro is sized to the metro total |
CUSTOM | Values 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.
| Layer | Check | Needs the API |
|---|---|---|
| 1 | Structure and catalogue: types, required fields, package, notifications | No |
| 2 | Each Cloud Router create as a live dryRun=true | Yes |
| 3 | Each connection as a live dry run, where both endpoints already exist | Yes |
The plan reports the findings in three separate lists.
| Group | Meaning | Effect |
|---|---|---|
getValidationErrors() | A defect, or a rejection by the API | The plan is invalid |
getDeferredValidations() | A connection dry run that must wait until its Cloud Router exists | Checked during execution |
getSkippedValidations() | A check that could not be attempted, with the reason | Does not invalidate the plan |
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#
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());
}| Policy | On a failed step |
|---|---|
ABORT_AND_ROLLBACK | Stops, 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_ERROR | Continues 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.
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.
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());
}DriftStatus | Meaning |
|---|---|
MISSING | Planned, not found live |
PRESENT_MATCHING | Found, and the compared fields match |
PRESENT_DIFFERENT | Found, with field differences listed |
UNREADABLE | The read failed; the state is not known |
NOT_EXECUTABLE | A 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.
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 OptimizationResultThe 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
variablemarkedsensitive, 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_onon its DIRECT sibling, which is the order Fabric requires. - A native multicloud link produces a comment block and one account-identifier variable, without a
resourceblock 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 reconcile | Terraform export | |
|---|---|---|
| State | Live Fabric state plus checkpoints | Terraform state file |
| Drift | plan.diff() | terraform plan |
| Deletes | Rollback of a run only | Full lifecycle |
| Fits | Programmatic pipelines, one-off builds, an agent-proposed plan | Teams 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.