This guide covers running CloudConnector: how a deployment is shaped, every input and output of the Terraform roots, how traffic is routed into the pipe, how it is sized and scaled, how it fails and recovers, how to watch it, and how to troubleshoot it. For a first deployment, start with the Quick Start; for what CloudConnector is, see the Overview.
Topology and deployment shapes
A pipe always has the same parts: an AWS relay fleet, a Google Cloud relay fleet, and one controller that both fleets register with under a single fleet id. Two Terraform shapes build them:
| Shape | Roots | Use it for |
|---|---|---|
| Evaluation | examples/cloudconnector, which calls terraform/cloudspectra-relay-deploy | Trying CloudConnector. One apply creates new networks in both clouds, the controller and both fleets, and generates every credential; they are kept in Terraform state. |
| Production | terraform/cloudspectra-relay-controller, then terraform/cloudspectra-relay-pipe | A pipe between networks you already own. You supply the networks, subnets, route tables and credentials; the controller is deployed once and can serve several fleets. |
In the production shape the two roots are joined by one value: the pipe root's sensitive controller_side_tokens output is the controller root's pipe_side_tokens input. It carries each side's announce credential, so the side a relay is placed on and the side the controller recognises come from one definition.
Zones
Each end starts in one zone: <region>a on AWS and <region>-a on Google Cloud. A one-zone end has nowhere to place a replacement during a zone outage, so production deployments should use two or more:
- AWS. Every zone gets its own Auto Scaling group, its own GWLB endpoint and its own routes, and each zone runs at least one relay -- a zone without one would drop the traffic entering through its endpoint. Cross-zone load balancing is off, so the load balancer adds no inter-zone hop. The AWS end therefore rounds its relay count up across zones: a target of 5 relays over 3 zones runs 6. Set
aws_network_azs(created networks) oraws_zone_subnets(your networks). - Google Cloud. The managed instance group is regional and the internal load balancer is regional, so the group spreads exactly
fleet_sizerelays over the zones you list ingcp_zones.
Relay addressing
Relays dial each other directly over the internet; the load balancers only steer traffic into the local relays. When it registers, a relay announces the address the far side should dial: its IPv6 global address if it has one, otherwise its public IPv4 address, otherwise its private address. A private address is not reachable from the other cloud, so every relay needs one of the first two:
- Google Cloud relays get an external address of the family set by
pipe_external_addressing:ipv6(the default, because both clouds bill public IPv4 per address per hour),ipv4ordual. Created networks are dual-stack and have Cloud NAT, so relays without an external IPv4 can still reach IPv4 destinations. - AWS relays sit in the zone subnets you give them and must be reachable by the Google Cloud relays and able to reach the controller, the Cloud Spectra licence service and the far side.
The two ends must share an address family the far side can reach.
Sides and the fleet id
Both ends register under one fleet id (fleet_id, which defaults to name). The ends are named aws and gcp (aws_side_id, gcp_side_id); these names must match the side ids in the pipe configuration, which the pipe root writes from the same variables.
Configuration reference
Only the inputs and outputs listed here are part of the interface. Every default shown is the code's.
The cloudconnector example
| Input | Default | Meaning |
|---|---|---|
name | "cloudconnector" | Names the deployment and becomes the fleet id both ends register under. |
release | "1.0.0" | The release whose relay images are deployed; the AWS AMI and the Google Cloud image are looked up from it. |
aws_region | "us-east-1" | Region of the AWS end and of the controller. |
gcp_region | "us-central1" | Region of the Google Cloud end. |
target_gbps | 10 | Capacity to size each end for (see Sizing). |
Its outputs are controller_url, aws_endpoint_service, gcp_ilb_ip, wg_port_range, relay_target_per_end, relay_count_aws, relay_count_gcp and relay_count, each described under the deployment root below. Any other input of the deployment root can be set by adding it to the example's module "cloudconnector" block.
Deployment root: cloudspectra-relay-deploy
Every input is optional.
| Input | Default | Meaning |
|---|---|---|
name | "cc-demo" | Prefix for every resource on both clouds; also the fleet id. |
aws_region, gcp_region | "us-east-1", "us-central1" | Where each end is built. |
gcp_project | "" | The Google Cloud project. Empty takes the project of the service-account key in gcp_credentials_file, else the Google provider's own default (for example GOOGLE_PROJECT). |
gcp_zones | [] | Zones for the Google Cloud group. Empty takes one zone, <gcp_region>-a. |
release | "1.0.0" | Release whose images are deployed. |
arch | "amd64" | amd64 or arm64. Selects the images and the relay instance types. |
aws_ami_id, gcp_source_image | "" | Override the images looked up from release; setting one skips that lookup. |
controller_ami_id | "" | Image for the controller. Empty uses the relay AMI, which also carries the controller. |
image_manifest_bucket, image_manifest_region | Cloud Spectra's releases bucket, "us-east-1" | Where the per-release image manifest is read from, with your AWS credentials. Point it at a mirror to deploy images published elsewhere. |
aws_advertise_cidrs | ["10.60.0.0/16"] | What is reachable through the AWS end. Defaults to the AWS network this root creates. |
gcp_advertise_cidrs | ["10.70.0.0/16"] | What is reachable through the Google Cloud end. Defaults to the network this root creates. |
target_gbps | 1 | Sizes both fleets. |
tunnel_members | 2 | Width of each relay's WireGuard port band, 2-32 (see The bond ceiling). |
gcp_service_account_email | "" | Service account the Google Cloud relays run as. Empty uses the project's default Compute Engine service account. |
delivery_url | "" | Where relays fetch their signed datapath. Empty is the ordinary case; set it only to serve the object from your own mirror. |
aws_credentials_file, aws_profile | "~/.secrets/.aws.credentials", "" | An AWS shared-credentials file and profile for the root's own provider, used only if the file exists. |
gcp_credentials_file | "~/.secrets/.gcp.credentials" | A service-account key for the root's own Google provider, used only if the file exists. |
license_key | "" (sensitive) | The licence key. Empty reads the first file in license_key_files that exists. |
license_key_files | ["~/.secrets/.cloudspectra.relay.credentials", "~/.secrets/.cloudspectra.relay.test.credentials"] | Where the licence is looked for, in order. |
| Output | Meaning |
|---|---|
controller_url | The controller both fleets register with: http://<elastic-ip>:8080. |
fleet_id | The one fleet id both ends register under. |
aws_endpoint_service | The GWLB endpoint service of the AWS end; its per-zone endpoints are your AWS route targets. |
gcp_ilb_ip | The internal load balancer of the Google Cloud end. |
wg_port_range | The UDP band both ends listen on, 28302 to 28302 + tunnel_members - 1. |
relay_target_per_end | Relays per end that target_gbps derives. |
relay_count_aws, relay_count_gcp, relay_count | Relays each end runs, and both ends together -- the number that is metered. |
credentials | Sensitive: read_token, write_token, config_token, aws_announce_token, gcp_announce_token. |
Production pipe root: cloudspectra-relay-pipe
Builds both fleets, sizes them with cloudspectra_ecmp_pipe, creates the steering routes and writes the pipe configuration to the controller. Inputs without a default are required.
| Input | Default | Meaning |
|---|---|---|
| Networks | ||
create_networks | false | true creates a VPC and a network instead of joining yours. Set it, or give your own networks below -- not both and not neither. |
aws_network_cidr, gcp_network_cidr, aws_network_azs | "10.60.0.0/16", "10.70.0.0/16", [] | Only with create_networks. The two ranges must not overlap; empty zones take one, <aws_region>a. |
aws_vpc_id, aws_vpc_cidr | "" | Your VPC and its range. |
aws_zone_subnets | {} | Availability zone to the one subnet the relays use in it. |
aws_zone_route_tables | {} | Availability zone to a route table to steer. For each listed zone, every Google Cloud range gets a route to that zone's GWLB endpoint. A zone not listed gets no route. |
gcp_network, gcp_subnetwork | "" | Your network and subnetwork. |
gcp_zones | [] | Zones of the regional group; empty takes <gcp_region>-a. |
gcp_client_tags | required | Network tags of the Google Cloud VMs whose traffic uses the pipe. Must not include the relays' own tag. |
pipe_external_addressing | "ipv6" | External address family of the Google Cloud relays: ipv6, ipv4 or dual. |
| What each side carries | ||
aws_advertise_cidrs | required | Ranges reachable through the AWS end -- its own ranges. They become what the Google Cloud end steers into the pipe. |
gcp_advertise_cidrs | required | Ranges reachable through the Google Cloud end, steered into the pipe by the AWS end. |
| Sizing and the bond | ||
target_gbps | 3 | Sizes both fleets, rounding up. |
tunnel_members | 2 | Width of the WireGuard port band on both ends' firewalls, 2-32. |
aws_per_zone_max | 4 | Maximum size of each AWS zone group. |
wg_port | 28302 | First UDP port of the band. |
compression | false | Have both fleets negotiate tunnel compression. |
| Instances | ||
aws_region, gcp_region, gcp_project | required | Where each end runs. |
arch | "amd64" | Image architecture; the instance types derive from it. |
aws_ami_id, gcp_source_image | required | The relay images. |
aws_instance_type, gcp_machine_type | "" | Override the derived shapes (c5n.large / c6gn.medium, n2-standard-4 / t2a-standard-4). The plan warns if a shape does not match arch. |
aws_instance_profile_name | required | Instance profile for the AWS relays. It must let a relay turn off source/destination checking on itself at boot; without that, the AWS end forwards nothing. |
aws_ssh_key_name | null | Optional EC2 key pair. |
gcp_service_account_email | required | Service account the Google Cloud relays run as. |
package_version | "" | Exact package version each relay installs at boot (see Upgrades). Empty keeps the image's own. |
| Controller and credentials | ||
name, fleet_id | "relay-pipe", "" | Resource prefix; the fleet id both ends register under (empty means name). |
fleet_controller_url | "" | The controller both fleets register with. Empty leaves the relays standalone. |
fleet_roster_token, fleet_write_token | "" (sensitive) | The fleet read and write credentials; they must differ. |
fleet_config_token | "" (sensitive) | The credential that writes the pipe configuration. Empty falls back to the write token, which is on every relay. |
aws_announce_token, gcp_announce_token | "" (sensitive) | The credentials that place each fleet on its side. They must differ: one value would place both fleets on one side. |
aws_side_id, gcp_side_id | "aws", "gcp" | Side ids; must match the pipe configuration. |
write_pipe_config | true | Write the pipe configuration to the controller. Set false only when something else owns it. |
license_key | "" (sensitive) | The licence both fleets run under. |
delivery_url | required | Pass "" to fetch the datapath from Cloud Spectra; set a URL only for your own mirror. |
relay_privkey, client_pubkey | "" | Leave empty: a pipe end generates its own key pair. |
tags, labels | {} | Extra AWS tags and Google Cloud labels. |
| Output | Meaning |
|---|---|
plan_summary | What the intent derives to; read it before applying. |
fleet_size | Relays per end that target_gbps derives -- a target. |
aws_instance_count, gcp_instance_count, instance_count | Relays each end runs, and both together. |
billed_as | cloud_connector. |
aws_endpoint_service, aws_endpoints_by_zone | The GWLB endpoint service, and each zone's endpoint id (your route targets). |
gcp_ilb_ip, wg_port_range | As in the deployment root. |
controller_side_tokens | Sensitive. Feed it to the controller root's pipe_side_tokens. |
Production controller root: cloudspectra-relay-controller
The inputs that matter for a pipe:
| Input | Default | Meaning |
|---|---|---|
name, region, ami_id | "cloudspectra-relay-controller", required, required | The controller runs from the relay image, which carries the controller software. |
instance_type | "t4g.small" | An arm64 shape: it must match the architecture of ami_id. |
create_network, network_cidr | false, "10.240.0.0/16" | Create a VPC with public subnets in two zones, or use vpc_id and subnet_ids. |
assign_public_ip | false | The controller must reach DynamoDB and the EC2 API before it takes its Elastic IP: give it a public address or a subnet with a NAT gateway. |
listen_port | 8080 | The port relays and operators call. |
relay_cidrs | [] | Sources allowed to reach listen_port. Empty means no ingress rule at all, so list the addresses your relays (and any monitoring) reach the controller from. |
admin_cidrs | [] | Sources allowed to SSH (TCP 22). |
fleet_id, read_token, write_token | "" | The fleet and its read and write credentials: at least 24 characters each, and different from each other. |
announce_token, config_token | "" | Optional narrower credentials for registering relays and for writing the pipe configuration; both fall back to the write token. |
pipe_side_tokens | [] | Each side's side_id, announce_token and read_token. Without them the controller places no relay on any side; a side without a read_token has its relays placed but served no far side, so give that side's relays the same read token. |
pipe_alert_webhook | "" | URL that pipe health changes are POSTed to (see Monitoring). |
scaling, scalable_groups, scaler_cloud, autoscale_min, autoscale_max, autoscale_high, autoscale_low | off | Lets the controller resize relay groups (see Scaling by the controller). |
protect_durable_state | true | Protects the Elastic IP and the DynamoDB table against deletion. Decide it when you create the controller: it cannot be switched off on a live deployment. Set false for a controller you intend to destroy. |
Outputs include controller_url (http://<elastic-ip>:<listen_port>), elastic_ip, eip_allocation_id, table_name and security_group_id.
The cloudspectra provider
Both pipe resources live in the Cloud Spectra Gateway provider, cloudspectra/gateway. Only cloudspectra_pipe_config talks to anything; it needs two provider settings:
provider "cloudspectra" {
controller_url = "http://203.0.113.10:8080" # or CS_FLEET_CONTROLLER
config_token = var.fleet_config_token # or CS_FLEET_CONFIG_TOKEN
}
Resource cloudspectra_ecmp_pipe
The pipe's intent, and the product's own derivation of it. It runs the same sizing and validation rules the relays are built from, at plan time, and creates no infrastructure: its delete is empty and it calls no cloud API. The deployment roots use it to size both fleets; on its own it is a sizing calculator (see Size a pipe before you build it).
| Argument | Required | Values and meaning |
|---|---|---|
mode | yes | cloud_connector for this product. Never defaulted: the four modes build different infrastructure and bill differently, so an unset mode is refused at plan. |
endpoint_a, endpoint_b | yes | One block per end: cloud and region (both required; the region is the cloud's own spelling), network_scope_id (the VPC or network), subnet_id, advertise_cidrs (the ranges reachable through that end -- its own ranges, not the far side's). |
addressing | yes | server = direct or nlb; client = direct or nat. There is deliberately no nat for the server: a NAT gateway does not preserve the source. |
ingress | yes | managed_lb (the cloud's passthrough load balancer in front of an autoscaled fleet -- what the deployment roots build) or single_instance. With single_instance the fleet is pinned to one relay whatever target_gbps says, and nlb server addressing is refused. |
target_gbps | no | Throughput to size each end for. Must not be negative. |
compression | no | Request tunnel compression. Adds the compression licence capability. |
ipv6, fleet_manage, licence_key | no | Accepted, but they do not change the derived plan in this release. The licence the relays run under is supplied to the deployment, not to this resource (see Quick Start, Step 2). |
| Attribute (computed) | Meaning |
|---|---|
fleet_size | Relays per end that target_gbps derives -- a target, not a count (see Sizing). |
billing_variant | cloud_connector. |
capabilities_requested | tunnel-bond, plus compression when requested. |
plan_summary | What the intent derives to: relays, addressing, whether the source survives, the licence capabilities, and any notes. |
id | Derived from the mode and the two ends (cloud_connector.aws-us-east-1.gcp-us-central1), so re-applying the same intent addresses the same pipe. |
Resource cloudspectra_pipe_config
The pipe configuration stored on the controller. The controller builds each relay's roster from it -- which sides exist and what is reachable through each -- and refuses to serve any roster without it, so the deployment roots write it for you (write_pipe_config, default true). The resource talks to the controller named by the provider's controller_url with its config_token.
side blocks -- side ids, advertised ranges and kind -- from the stored configuration. The bond, MTU, weight-policy, compression and timing fields are validated and stored, but relays do not apply them: bond width follows the far side's relay count, weights come from instance types, the tunnel MTU is 1380, and compression is set on the fleets (the pipe root's compression input).| Argument | Required | Values and meaning |
|---|---|---|
fleet_id | yes | The fleet both ends register under. Changing it replaces the resource. |
side (block) | yes | Exactly two. side_id must equal the side the relays are placed on, or they are reported as stranded. advertise_cidrs (required): the IPv4 ranges reachable through this side -- at least one, no /0, no overlap with the other side; the controller accepts up to 64, but relays steer only the ranges their fleet was deployed with (at most 8 per side), so keep the two lists identical -- the pipe root writes both from the same variables. network_ids: up to 16, informational. kind: relay (the default) for a relay fleet; the deployment roots in this guide use relays on both sides. |
bond_members | yes | 2-32. The controller refuses fewer than two. |
bond_slots | no | Width of the datapath's member table; 0 takes the default (32). Must be at least bond_members. |
bond_base_port | no | First UDP port of the band. Member m listens on base + m. |
bond_weight_policy | no | equal (the default) or capacity. |
compression | no | Ask both ends to negotiate tunnel compression. A request: the two relays agree an algorithm at runtime. |
mtu | no | Tunnel MTU, 576-9000. 0 takes the relay default. |
roster_interval_seconds, stale_bound_seconds, park_budget_seconds | no | How often each end re-reads the roster, how long a held roster stays trustworthy, and how long an end may run without reaching the controller. 0 takes the default. |
key_max_age_seconds, force_rotate_after | no | Rotate relay keys older than this (must exceed the 300-second rotation overlap; 0 disables age-based rotation), and a Unix time before which every relay key is due for rotation. |
set_by | no | Recorded beside the config so a conflict can name the other writer. Defaults to terraform. |
Computed: revision (the compare-and-swap token), set_at, mode (always cloud_connector). Every write reads the stored revision first and names it; if someone changed the config in between, the controller answers HTTP 409 naming the other writer instead of overwriting. Destroying this resource only removes it from state: the controller has no delete verb, and removing a config under running relays would take the roster down with it.
Routing traffic into the pipe
Each side advertises its own ranges, and each fleet steers the other side's ranges into the pipe. The pipe roots wire that crossover once, from aws_advertise_cidrs and gcp_advertise_cidrs; everything else in your networks keeps its normal routing. A relay checks the ranges it was deployed with at start-up and refuses to start, with the reason in its log, unless they are IPv4, at most 8 per side, not 0.0.0.0/0, non-overlapping between the sides, and clear of 169.254.0.0/16 and 168.63.129.16.
AWS
- Target the zone's GWLB endpoint. In the route table of each workload subnet, route every Google Cloud range to the GWLB endpoint in the subnet's own zone (
aws_endpoints_by_zone). A route cannot target the load balancer or the endpoint service. The pipe root can create these routes for you: list the route tables inaws_zone_route_tables. - Never steer the relays' own subnets. The relay subnets' route table must not send the far side's ranges back to the endpoint.
- Admit the far side. Packets arrive with their original Google Cloud sources, so workload security groups and network ACLs must allow those ranges. Allow ICMP from the relays too: path-MTU messages come from them.
- Manage a table's routes one way. If you manage route tables with Terraform, do not mix inline
routeblocks with separateaws_routeresources on the same table: Terraform then deletes the separate routes on a later apply, and the pipe silently stops carrying.
Google Cloud
- Tag the workloads. The pipe root creates one route per AWS range, next hop the internal load balancer, priority 900, applying only to VMs with a tag in
gcp_client_tags(the evaluation root uses<name>-client). Routes in Google Cloud apply network-wide unless tagged, which is why the relays' tag (<name>-gcp-relay) must never be a client tag: a relay would become the next hop for traffic it is forwarding. - Admit both directions. The relays' firewall rules admit the WireGuard band, Google's health checkers, ICMP and the AWS ranges. Add a rule letting your workload ranges reach the relays (target tag
<name>-gcp-relay) and a rule letting the AWS ranges reach your workloads.
Which side opens connections
Connections opened from AWS workloads to Google Cloud workloads, and their replies, are the direction Cloud Spectra's live tests carry. On the AWS end the Gateway Load Balancer is used for ingress only: packets arriving from Google Cloud are delivered straight to the AWS host, so the load balancer never sees the first half of a connection opened from Google Cloud -- and a Gateway Load Balancer does not forward the reply half of a flow whose first half it never saw. Validate connections opened from the Google Cloud side before you depend on them.
Sizing and scaling
Sizing
Each relay is sized as 3 Gbps of pipe. target_gbps becomes a per-end relay count: ceil(target_gbps / 3), at least 1. That count is a target:
- the AWS end runs
ceil(count / zones)relays in every zone, so it can run more than the target; - the Google Cloud end runs exactly the target, spread over its zones.
The outputs report both: the target (relay_target_per_end or fleet_size) and what each end runs.
Capacity scales across many conversations, not within one. Every flow between one pair of addresses enters the pipe through a single relay, so one bulk transfer between two hosts gets one relay's worth of throughput however large the fleet. Many hosts talking across the pipe spread over all relays. And because each relay bonds with every relay on the far side, grow both ends together: growing one end alone adds little.
Size a pipe before you build it
The plan-only/ directory beside the cloudconnector example applies only cloudspectra_ecmp_pipe. It needs the cloudspectra/gateway provider and nothing else -- no cloud credentials -- and it creates nothing, so you can try shapes freely:
cd examples/cloudconnector/plan-only
terraform init
terraform apply \
-var aws_vpc_id=vpc-0123456789abcdef0 \
-var aws_subnet_id=subnet-0123456789abcdef0 \
-var gcp_network=projects/my-project/global/networks/my-net \
-var gcp_subnetwork=projects/my-project/regions/us-central1/subnetworks/my-subnet \
-var target_gbps=10
terraform output plan_summary
With the defaults (ingress = "managed_lb", direct addressing on both sides) and target_gbps = 10, the summary reads:
mode cloud_connector: ECMP L3 forwarding across the fleet, source preserved
servers: 4 (managed_lb ingress, direct addressing)
client: direct addressing, runs the CloudSpectra datapath
source: preserved end to end (no NAT)
licence needs: tunnel-bond
controller: a peer-map container is stood up in your account
billed as: cloud_connector
note: this mode stands up the peer-map controller container in your own account (roughly $15-50/mo); your client and peer data never leaves it
The same notes are printed as About this pipe warnings during terraform plan of the full deployment. A pipe sized to a single relay per end adds a note that it has no redundancy: losing that relay loses the pipe. The cost in the controller note is the derivation's rough estimate; what you pay for the controller is its AWS resources (see Cost considerations).
The bond ceiling
A relay's bond has one member per far-side relay, and member m listens on UDP wg_port + m. The firewalls on both ends open tunnel_members ports from wg_port (the wg_port_range output), so tunnel_members must cover the most relays either end will ever run. The plan warns when it is below the derived relay count or below what the AWS groups may reach (aws_per_zone_max times the number of zones). It can be 2 to 32; a relay bonds with at most 32 far relays in any case, and beyond that the highest-numbered far relays carry no traffic.
Changing the size
After the first apply the group sizes belong to the running system: Terraform ignores later changes to the AWS groups' desired capacity and to the Google Cloud group's target size, so that an apply never reverts a fleet that has scaled. Raising target_gbps raises the AWS groups' minimum size, which pulls them up; the Google Cloud group keeps its size until you resize it directly. Otherwise resize the groups directly or let the controller do it.
When a side changes size, the other side follows on its own. A new relay registers and the far side widens its bonds; a removed relay leaves the roster once its 90-second registration lapses, and the far side narrows. Each far relay rebuilds its bond with a brief restart of its datapath. In Cloud Spectra's measurements the far side widened about 2.5 minutes after a relay was added and narrowed about 7 minutes after one was removed; once the bond is rebuilt, traffic flows again within seconds.
Scaling by the controller
The controller can resize relay groups in one cloud (scaler_cloud), either on request (scaling = "manual") or from the relays' reported load (scaling = "auto"). It is off by default and acts only on the groups listed in scalable_groups. In automatic mode it grows a group above 70% utilisation and shrinks it below 30% (autoscale_high, autoscale_low), waiting 2 minutes before growing and 10 before shrinking, with a 5-minute cooldown, within autoscale_min and autoscale_max. Manual and automatic are exclusive. Keep tunnel_members above the largest size you allow.
High availability and failure behaviour
| Failure | What happens |
|---|---|
| A relay dies | On AWS, the target group's health check takes it out of the load balancer and existing flows are rebalanced to healthy relays; the Auto Scaling group replaces the instance when the instance itself fails (the groups use EC2 health checks, so a running instance whose datapath is down is taken out of the load balancer but not replaced). On Google Cloud, the managed group's autohealing replaces a relay that fails the load balancer's health check. On the far side, the bond member to the dead relay stays until its 90-second registration lapses and the bond is rebuilt, and the flows hashed onto that member are lost until then. |
| An end with one relay loses it | The pipe stops until the replacement registers. Use at least two relays per end for redundancy -- the plan notes when a pipe has none. |
| A zone fails | A one-zone end has nowhere to place a replacement. Use two or more zones (see Zones). |
| The controller is unreachable or replaced | The controller is not on the data path. Relays keep forwarding with the roster they hold; after 300 seconds without a fresh one they log that it is stale, and they never tear the pipe down on their own. Scale events are not followed until the controller is back. The controller runs as an Auto Scaling group of exactly one: a replacement instance takes over the Elastic IP, and all state lives in DynamoDB. |
| The Cloud Spectra licence service is unreachable | Relays check in every two minutes and keep forwarding through a 48-hour grace window, logging warnings as it runs out; after that they park and stop forwarding. |
Monitoring and alerts
Pipe status
The controller derives the pipe's health each time it is asked:
curl -s -H "Authorization: Bearer <read-token>" \
"http://<controller>:8080/fleet/v1/pipe/status?fleet=<fleet-id>"
| Field | Meaning |
|---|---|
health | up: every side has live relays and none is idle. degraded: it carries, but not as asked -- a side short of relays, an idle relay, or an unplaced relay. down: a side has no live relay, so the pipe cannot carry in at least one direction. unknown: the controller could not take a reading (no configuration, or no relays registered) -- a statement about the controller, not the pipe. |
summary | One sentence describing the state. |
configured, compression | Whether a pipe configuration is stored, and whether it asks for compression. |
sides[] | Per side: side_id, advertise_cidrs, live_relays, idle_relays, capacity_hundredths_gbps, and each relay's relay_id, endpoint, live, instance_type, weight, idle, carried_bytes and last_carried_at. |
unplaced | Relays registered with a credential that matches no side. They dial nobody and are dialled by nobody. |
stranded | Relays placed on a side the pipe configuration does not describe -- usually a renamed side. |
problems | Everything wrong, in words. |
A relay is idle when it is live but has carried nothing for 270 seconds, so a pipe with no traffic reads degraded. Two more read-only endpoints use the same token: GET /fleet/v1/pipe?fleet=<fleet-id> returns the stored configuration and its revision, and GET /fleet/v1/relays?fleet=<fleet-id> returns the relay registry.
Alerts
The controller turns health changes into alerts: pipe_down (critical), pipe_degraded, pipe_idle_relay, pipe_unplaced_relay, pipe_not_visible (warnings) and pipe_recovered. Every alert is logged by the controller, and POSTed as JSON (kind, title, message, severity, fleet_id, at) to pipe_alert_webhook when it is set. Alerts are edge-triggered with a six-hour cooldown per kind, and they are evaluated when the status endpoint is read -- so poll /fleet/v1/pipe/status from your monitoring, every minute or so, to get them.
On a relay
- Logs:
journalctl -u cloudspectra-relayfor the datapath agent;cloudspectra-relay-dashandcloudspectra-relay-upgradeare the dashboard and boot-time upgrade units. Look forrelay: cloud connector pipe configured(it also names any far relays beyond the 32-member limit),relay: bond shape resolvedand the line reporting the prefixes armed. - Datapath facts:
/run/cloudspectra/relay-facts.jsonrecords what the datapath was loaded with, includingcs_pipe_nlocal,cs_pipe_nremote,cs_pipe_mtu,cs_wg_nmembersandcs_wg_nslots. - Cloud health: the GWLB target group's health and the managed instance group's health show whether each relay's datapath is answering.
The relay's own dashboard (HTTPS on port 28300) has no pipe view; pipe health lives on the controller.
Upgrades
A relay runs the software in the image it booted from, and can move to a newer package at boot:
- Images follow
release. The deployment root looks up the AWS AMI and the Google Cloud image for thereleaseyou name. Changing it updates the AWS launch template and the Google Cloud instance template, but running relays are not replaced: the Auto Scaling groups launch from the template's$Latestversion with no instance refresh, and the managed instance group's update policy isOPPORTUNISTIC. New relays -- scale-out or replacement -- boot the new image; existing ones keep the old one until you replace them. - In-place package upgrade at boot. Before it brings up its datapath, every relay runs a one-shot upgrade unit (
cloudspectra-relay-upgrade.service). When a target version is set (package_versionon the production pipe root), the relay downloads that exact package, verifies its checksum and signature, installs it and restarts into it. A failed upgrade is not fatal: the relay comes up on the software it already has. In-place upgrades never cross a major version.
To roll a fleet onto a new release without taking the pipe down, replace relays a few at a time on each side, keeping at least one relay in service per side and waiting for each replacement to register before the next. The far side picks up a new relay within a few minutes and drops a removed one after the registry's 90-second time-to-live (see Scaling):
# AWS: one Auto Scaling group per zone, named <name>-aws-<zone>
aws autoscaling start-instance-refresh --auto-scaling-group-name <name>-aws-us-east-1a
# Google Cloud: the regional managed instance group, named <name>-gcp-mig
gcloud compute instance-groups managed rolling-action replace <name>-gcp-mig \
--region us-central1 --max-surge 1 --max-unavailable 0
Security model
Data in transit
Between the clouds, traffic travels only inside WireGuard tunnels between your own relays (kernel WireGuard; no pre-shared key is configured). Cloud Spectra is not on the data path. Inside each cloud, packets move between your hosts, the load balancers and the relays unencapsulated, as in any routed network. The datapath caps the TTL of forwarded packets at 16, so a routing loop dies quickly, and refuses compressed-protocol frames (IP protocol 253) arriving from outside the pipe.
Keys
Each relay generates its WireGuard key pair on first boot and stores the private key on the instance (/var/lib/cloudspectra/relay-self.key, mode 0600). Only the public key is sent to the controller and served to the far side, so no relay private key exists in Terraform state. A replacement instance gets a new key. Relay keys are not rotated automatically in this release.
Credentials
| Credential | Held by | Allows |
|---|---|---|
| Read token (fleet and per side) | Every relay | Reading the roster and the pipe status. A side's read token decides which far side a relay is served. |
| Write token | The relays in the shipped roots | Fleet writes; the fallback for the two credentials below when they are unset. |
| Announce token, one per side | That side's relays | Registering a relay. The controller places the relay on the side whose token it presented; the two sides' tokens must differ. |
| Config token | Whoever writes the pipe configuration (Terraform) | Changing the pipe configuration, which redirects the pipe's traffic. Set it so that a relay's credentials cannot. |
| Licence key | Every relay (instance user data) | Running the relays. It is in Terraform state. |
The evaluation root generates all of these in Terraform state and exposes them as a sensitive output. In production, generate them yourself, keep them in your secret store, and keep the state file encrypted and access-controlled.
The controller
The controller serves plain HTTP on listen_port (8080) at its Elastic IP; the deployment roots configure no TLS certificate for it, so the bearer tokens above cross the network unencrypted. Limit relay_cidrs to the addresses your relays and monitoring reach it from, and treat the read and write tokens accordingly. The controller instance requires IMDSv2.
The relays
- The WireGuard band (UDP from 28302) is open to any IPv4 source on both ends; packets that do not authenticate as a known far relay are dropped by WireGuard.
- AWS relays accept GENEVE (UDP 6081) and the health check only from the VPC's own range, require IMDSv2, and have encrypted root volumes.
- The relay datapath runs under systemd with only the capabilities it needs (
CAP_NET_ADMIN,CAP_BPF,CAP_SYS_ADMIN,CAP_NET_RAW) and a read-only system. Its eBPF datapath is fetched signed at boot.
What source preservation means for your rules
Nothing on the pipe hides the sender. The far side's firewalls, security groups and logs see every host's own private address -- which is the point, and also means each side must explicitly admit the other side's ranges, at whatever granularity you choose.
Troubleshooting
Terraform's check warnings appear during plan and apply but do not stop them, so read them first. On a relay, start with journalctl -u cloudspectra-relay; on the controller, start with the pipe status.
| Symptom | Likely cause | What to do |
|---|---|---|
Plan warns No licence. Looked in: ... | No licence file or license_key | Provide the licence (Quick Start Step 2). Without it relays come up and forward nothing. |
Plan warns No AMI for release ..., No GCP image for release ... or that an image is not an AMI id | The image manifest for that release and arch is empty or wrong | Check release and arch, or set aws_ami_id and gcp_source_image. |
Plan warns tunnel_members (...) is below ... | The port band is narrower than the fleet may grow | Raise tunnel_members (see The bond ceiling). |
| Plan warns that the announce tokens are the same value | One credential for both sides | Give each side its own announce token. |
| Plan warns that the created networks start at the same address | aws_network_cidr and gcp_network_cidr collide | Choose distinct ranges. The check only catches identical starts; a partial overlap must be avoided by you. |
Plan warns Set create_networks = true OR give aws_vpc_id ... | Both or neither network sources given | Choose one. |
| Plan warns that an availability zone is not offered | Zone names are mapped per account | Set aws_network_azs explicitly. |
Plan warns that an instance shape does not match arch | An arm64 shape with an amd64 image, or the reverse | Fix aws_instance_type / gcp_machine_type; an image does not boot on the other architecture. |
Apply fails writing the pipe configuration: the controller answered HTTP 409 | Someone changed the stored configuration since Terraform read it | Run the apply again; it reads the current revision first. The message names the other writer. |
Apply fails writing the pipe configuration: HTTP 400 | The configuration is invalid, or it names a side the controller holds no announce credential for | The message lists every problem at once (two sides, IPv4 ranges, no overlap, bond 2-32, and so on). For an unknown side, check the controller's pipe_side_tokens. |
Apply fails: no controller URL is configured or reaching the controller at ... | The provider cannot reach the controller | Set controller_url (or CS_FLEET_CONTROLLER) and make sure the machine running Terraform is admitted by relay_cidrs. |
Status unknown | No configuration stored, or no relay has registered | Check that relays can reach the controller (its security group admits only relay_cidrs) and that they have finished booting. |
Status down | One side has no live relay | Look at that side's relays: licence, controller reachability, boot logs. |
Relays listed under unplaced | Their announce token matches no configured side | The fleet's announce token and the controller's pipe_side_tokens must agree -- feed the pipe root's controller_side_tokens output to the controller. |
Relays listed under stranded | The side they were placed on is not in the pipe configuration | Make aws_side_id / gcp_side_id match the configuration's side ids. |
| Relays live and idle, nothing crosses | Nothing is steered in, or replies are dropped | Check the AWS route targets the zone's GWLB endpoint, the Google Cloud VMs carry a client tag, the firewall rules admit both directions, and the AWS relays' instance profile lets them turn off source/destination checking. |
| Ping crosses but TCP transfers stall | Packets larger than the tunnel MTU (1380) | Allow ICMP from the relays to your hosts so "fragmentation needed" messages arrive; TCP connections through the pipe have their MSS clamped automatically. |
| A relay exits at start-up | Its pipe ranges are invalid | The journal names the rule broken (IPv4 only, at most 8 per side, no overlap, no /0). |
| Relays park | No licence check-in for 48 hours, or the Developer lane's 16-vCPU account limit | Restore access to the licence service, or reduce the relay vCPUs, or move to the Enterprise lane. |
| Relays refused at registration on the Enterprise lane | An instance below the lane's 4-vCPU minimum | Use AWS relay shapes with at least 4 vCPUs (aws_instance_type). |
terraform destroy refuses the controller's Elastic IP or DynamoDB table | protect_durable_state | See Teardown. |
Teardown
- Remove what you added. Delete your own routes to the GWLB endpoints, firewall rules and test instances first; Terraform cannot delete networks that still hold resources it does not manage.
- Production order. Destroy the pipe root before the controller root. Removing
cloudspectra_pipe_configonly removes it from Terraform state -- the controller has no delete for it, so the stored configuration stays until the controller itself is destroyed, and a later apply adopts it. - The controller's durable state. With
protect_durable_state = true(the default) the Elastic IP cannot be destroyed and the DynamoDB table has deletion protection, and the setting cannot be switched off on a live deployment. To destroy a protected controller, remove the address from Terraform state, disable the table's deletion protection, destroy, and then release the address yourself -- the commands are in the Quick Start's teardown step. - Billing. Destroying infrastructure does not cancel your Cloud Spectra subscription.
Cost considerations
- Licence. Per relay vCPU, both ends, hourly; the second vCPU of each instance is free (see Pricing). The controller is not licensed.
- Inter-cloud data. Traffic between the clouds crosses the internet, so each cloud bills the bytes leaving it as internet egress, in both directions. This is usually the largest cost of a busy pipe; where a private interconnect with cheaper transfer already exists, compare.
- AWS. The relay instances, the Gateway Load Balancer and its endpoints, the controller instance, its Elastic IP and its on-demand DynamoDB table. Cross-zone load balancing is off and each zone's subnets should route to their own zone's endpoint, so no inter-zone transfer is added.
- Google Cloud. The relay VMs, the internal load balancer, and Cloud NAT where the root creates it.
- Public addresses. Both clouds bill public IPv4 addresses per hour; that is why Google Cloud relays default to IPv6.
- Right-sizing. Relays are sized at 3 Gbps each; size by
target_gbpsrather than by instance size alone, and remember one conversation never uses more than one relay.