This guide is the operating reference for CloudVpc: how it is built, how to configure each supported topology, and how to scale, upgrade, monitor, secure, troubleshoot and remove it. It assumes you have read the Overview; the Quick Start walks through one deployment end to end.
Architecture
Components
| Component | Where | What it does |
|---|---|---|
| Relay | EC2 instance in your VPC | Boots the Cloud Spectra Gateway image in relay mode. Holds the private IPv4 address its worker wears, terminates the worker's WireGuard tunnel, forwards between the tunnel and the VPC without translating addresses, and serves its enrolment and health endpoints on port 28300. Systemd units: cloudspectra_relay (the datapath), cloudspectra_relay_dash (enrolment, health and dashboard), cloudspectra_relay_upgrade (upgrade at boot) and cloudspectra_relaymode (sets the image up as a relay). |
| Worker | Linux VM in OCI or DigitalOcean | Runs your workload. At first boot it downloads the Cloud Spectra downloader, installs the client package with your licence, and runs cloudspectra_relay_attach bootstrap-worker, which pairs it with a relay and brings up its tunnel interface, cswg0, wearing the relay's private address. |
| Rendezvous block | IPv6 addresses in each relay subnet | Pre-allocated by Terraform. Each relay claims one at boot; workers use them to find relays. |
| Load balancer | AWS | An Application Load Balancer whose target group is the relay Auto Scaling group. A request to a relay's private address is forwarded down the tunnel and answered by its worker. |
| Peer map | DynamoDB table (on-demand) | Pool topologies. Each relay writes the row for its own worker, including the worker's host address, so workers can reach each other directly. Enabled by default. |
| Controller | EKS topology | Scans relays and workers by tag, and launches, pairs and retires workers. See EKS topology. |
Data path
VPC client or load balancer worker (OCI or DigitalOcean)
| ^
v to 10.43.0.21 (the relay's private IPv4) | cswg0 wears 10.43.0.21
relay --- eBPF redirect into the WireGuard tunnel -------------+
(no address translation)
worker --- 169.254.169.254 (metadata) --- tunnel --- relay --- VPC metadata service
worker --- VPC range -------------------- tunnel --- relay --- VPC
worker --- other destinations ----------- the worker's own cloud (split tunnel)
worker --- another worker --------------- direct, GENEVE over UDP 6081 (fallback: via the relays)
- North-south. Traffic addressed to the relay's private IPv4 is redirected into the tunnel by eBPF programs on the relay and delivered to the worker, which holds that address on its tunnel interface and answers as it.
- Metadata. The worker routes
169.254.169.254/32through the tunnel. The relay forwards without consuming a hop and caps the metadata service's replies to one hop, so a process on the worker host gets an answer and a container behind it does not. - Split tunnel. The tunnel carries the VPC's range and the metadata address. The ranges are issued by the relay when the worker enrols and refreshed afterwards. The relay never issues a default route; a full tunnel is a per-client choice that the CloudVpc configurations do not make.
- East-west. Workers that know each other's host address exchange traffic directly, in GENEVE over UDP port 6081, spread across four source ports (49152-49155). Until a peer's address is known, traffic goes through the relays.
What happens at boot
A relay boots the Cloud Spectra Gateway image with its configuration in user data, and first upgrades itself in place if the image is older than package_version. When it has a rendezvous block, it claims an address from its own Availability Zone's block before it serves anything; if it cannot hold one, it refuses to start, its health check fails, and the Auto Scaling group replaces it. A running relay re-asserts its claim every 3 minutes. The relay fetches its datapath from delivery_url and proves its licence; a relay without a valid licence keeps running but is parked and forwards nothing.
A worker runs a short cloud-init document: it writes /etc/cloudspectra/licence and /etc/cloudspectra/worker.yaml (both mode 0600), downloads the downloader, installs the client, and runs cloudspectra_relay_attach bootstrap-worker. That command executes an ordered plan and stops at the first step that fails: wait for the distribution's package jobs, install base packages, install the operator SSH key, write the client environment, pair with a relay and bring the tunnel up, wait until the VPC metadata service answers through the tunnel, and then -- depending on the configuration -- join the Kubernetes cluster or start the demo application on port 8080. It narrates each step to the serial console and to /var/log/cloudspectra-worker-bootstrap.log. bootstrap-worker -dry-run prints every step, file and command without changing anything.
Addressing
Each relay announces which of its addresses does what:
| Field | Meaning |
|---|---|
| Endpoint and port | The public transport address the worker dials -- a public IPv4 or an IPv6 global address -- and the WireGuard port. Never transferred. |
| Public key | The relay's WireGuard public key. |
| Identity | The private IPv4 address the relay holds and transfers to its worker. |
| IPv6 identity | An IPv6 global address transferred alongside the private IPv4, only when the endpoint is a public IPv4 address. |
| Host address | The worker's own address on its own network, reported by the worker. It lets worker-to-worker traffic bypass the relays; when it is missing, that traffic still flows through the relays. |
In the autoscaling topologies the identity is disposable: AWS assigns each relay's private IPv4 at launch, so a replacement relay has a new one. What stays stable is the load balancer and the rendezvous block.
The rendezvous block
Each relay subnet has its own IPv6 /64, and Terraform pre-allocates addresses in it: slots_per_az per Availability Zone in the pool configurations (default 20, at most 200), and 8 in the EKS configuration. A relay claims an address with the EC2 AssignIpv6Addresses call. AWS assigns an IPv6 address to exactly one network interface, so a second relay asking for the same address is refused and tries the next. Nothing else records who holds what; run the watch_claims output to see the current claims. AWS releases an address when the relay's interface is deleted.
Enrolment and pairing
- The fleet enrolment token. Terraform generates one token per fleet and seeds a copy into every relay at its first boot. In the pool configurations each relay accepts it once, within
enrol_token_ttl(default168h) of its own boot: the first worker to reach a relay pairs with it, and the next is refused and moves on. That is how N workers spread over N relays. A replacement relay seeds a fresh copy. The token authorises enrolment and a read-only download, nothing else. - Keys stay on the machine that uses them. A worker generates its own WireGuard key pair and sends only the public half when it enrols. The relays in these configurations also generate their own keys, so no WireGuard private key is written to Terraform state.
- Workers spread their attempts. Each worker walks the rendezvous list from a starting point derived from its host name, so workers booting together do not all try the same relay first.
How the number of workers is kept equal to the number of relays depends on the topology:
| Topology | Who keeps the counts equal |
|---|---|
| OCI pool | An agent on every worker (cloudspectra-client-agent.service) reads the relay group's size every 15 seconds. Any worker may grow the OCI instance pool to the relay count. Only a worker whose own relay has gone removes itself -- with a detach that also decrements the pool -- because only it knows which instance is redundant. A worker concludes that its relay is gone only when another relay answers, so a network partition never shrinks the pool, and the pool never goes below worker_floor. |
| DigitalOcean pool | Nobody: the number of Droplets is fixed by worker_count. Keep it equal to the relay count yourself. |
| EKS | The controller. Every reconcile_interval_seconds (default 60) it assigns existing unpaired workers first, launches a worker for each relay still unpaired, and retires workers whose relays AWS reports terminated -- within per-pass limits. |
On scale-in, the relay group's lifecycle hook (notify-worker-on-terminate) holds a terminating relay so that its worker can be withdrawn first -- for up to terminate_hook_seconds (default 120) in the pool configurations. The hook is best effort and lets the termination continue on timeout; the worker's own check, or the controller's next pass, is what guarantees that no worker is left orphaned.
Topologies
These docs cover three Terraform configurations. Each creates its own dual-stack AWS VPC for the relays.
| Configuration | Relays | Workers | Pairing | Kubernetes |
|---|---|---|---|---|
aws-asg-oci-pool | EC2 Auto Scaling group | OCI instance pool that follows the relay group | Enrolment; no controller | No |
aws-asg-do-pool | EC2 Auto Scaling group | DigitalOcean Droplets, fixed count | Enrolment; no controller | No |
eks-oci-asg | Self-managed EKS node group (an Auto Scaling group) | OCI or DigitalOcean, launched by the controller | Reconciled by the controller | Yes -- workers are EKS nodes |
OCI pool -- aws-asg-oci-pool
The topology of the Quick Start. Terraform creates the dual-stack VPC (one public subnet and one IPv6 /64 per Availability Zone, in at least two zones), the relay Auto Scaling group behind an Application Load Balancer, the peer map table, an IAM user for the workers, and an OCI VCN with IPv6 holding an instance pool of workers. Workers use the latest Canonical Ubuntu 24.04 image for the chosen shape. After creation Terraform ignores the pool's size; the workers own it.
- The workers resize their pool with OCI instance-principal credentials, which needs a tenancy-level dynamic group and policy. The configuration creates them unless
manage_oci_iam = false(see below). - Each worker holds the access key of an IAM user named
<name>-client-reader, which can callautoscaling:DescribeAutoScalingGroupsand nothing else, to read the relay count. - There is no fleet-wide safety rail: each worker acts on what it sees. The rules above -- act only on positive readings, never go below the floor, size from the group rather than from probes -- are what keep that safe.
DigitalOcean pool -- aws-asg-do-pool
The same AWS half as the OCI pool; the workers are a fixed number of DigitalOcean Droplets in a DigitalOcean VPC. It takes the same inputs as the OCI pool, without the oci_*, worker_floor and worker_initial_size settings, and adds:
| Variable | Default | Notes |
|---|---|---|
do_token | empty | DigitalOcean API token. Leave it empty to use DIGITALOCEAN_TOKEN from the environment. terraform destroy needs it too. |
do_region | nyc2 | Region for the Droplets. Size availability changes over time; choose a region that currently offers do_size. The configuration checks this at plan time. |
do_size | s-2vcpu-4gb | Droplet size. Droplets are x86_64; the client package is chosen to match. |
do_image | debian-13-x64 | Stock Debian 13. |
do_vpc_cidr | derived from name | DigitalOcean refuses overlapping VPC ranges across an account, so the range is derived from the name. Give each deployment its own name. |
do_ssh_key_ids | [] | Account SSH keys to add to the Droplets. |
worker_count | 2 | Number of Droplets. It cannot exceed the number of rendezvous addresses allocated, and it does not follow the relay group. |
ssh_public_key_override | empty | Your SSH public key; empty reads ~/.ssh/id_rsa.pub. |
EKS -- eks-oci-asg
One Amazon EKS cluster. Its relays are an Auto Scaling group that is a self-managed node group, and its workers run in OCI or DigitalOcean (worker_cloud = "oci" or "do") and join the cluster as nodes, each named after the relay whose identity it wears and labelled cloudspectra.ai/compute-location. The configuration also creates a small native node group (native_count, default 1) and installs Calico in VXLAN mode as the CNI (calico_version, default v3.29.1; pod_cidr, default 192.168.128.0/17).
Nothing is paired at plan time. With in_cluster_controller = true, the controller runs as the cloudspectra-master Deployment in the cloudspectra-system namespace; the relays reach it on a NodePort inside the VPC, and it has no public endpoint. Exactly one of in_cluster_controller or master_endpoint (an external controller) must be set; the plan is refused otherwise.
| Variable | Default | Notes |
|---|---|---|
package_version | -- (required) | The exact Cloud Spectra version. A release version also selects the release download and API hosts, so delivery_url and the download hosts need not be set. |
license_key | empty | Set it explicitly. When it is empty the relays fall back to ~/.cloudspectra/credentials, but the workers and the controller do not. |
in_cluster_controller | false | Set true to run the controller in the cluster. |
worker_cloud | oci | oci or do. With do, set the do_* variables and DIGITALOCEAN_TOKEN. |
fleet_name | default | Becomes the cs_fleet tag that relays and workers carry. Give each fleet in the same accounts its own name. |
relay_min / relay_desired / relay_max | 0 / 2 / 6 | The relay group's size, and so the number of worker nodes. Relays are placed in one subnet with 8 rendezvous addresses, so keep relay_max at 8 or below. |
relay_instance_type / relay_arch | t4g.small / arm64 | Keep both consistent with the relay image. Use 4 vCPUs or more on the Enterprise lane. |
relay_ami_id | empty | The relay image; set it explicitly. |
oci_shape / oci_ocpus / oci_memory_gbs | VM.Standard.A1.Flex / 2 / 12 | OCI worker size. The OCI tenancy and region are read from ~/.oci/config; the compartment defaults to the tenancy. |
reconcile_max_terminations_per_pass | 2 | The most workers one controller pass may terminate. |
reconcile_refuse_terminate_fraction | 0.34 | A pass that would remove more than this share of the fleet is refused. Only workers whose relays merely disappeared from a reading count; a worker whose relay AWS reports terminated is retired without spending this budget. |
reconcile_max_pairs | 0 | Ceiling on pairs: beyond it new pairings are refused and nothing running is removed. 0 means no ceiling. |
wireguard_mtu | 1380 | Tunnel MTU; Calico's pod MTU is 50 less. |
admin_cidrs | [] | Ranges for recovery SSH and the EKS public endpoint. Empty leaves the EKS endpoint open and SSH closed. |
fleet_manifest_binary | cloudspectra_fleet_manifest | The Cloud Spectra tool that renders the controller's manifest on the machine running Terraform -- at plan, apply and destroy. It must be on PATH, or given by path. |
On the machine that runs Terraform you also need Python 3, the AWS CLI and kubectl, which the configuration uses to bootstrap the cluster, and the Python boto3 package -- plus the oci Python SDK for OCI workers -- which it uses to drain the fleet before a destroy. Useful outputs: kubeconfig_command, verify_nodes, scale_command, relay_asg, fleet_id, app_url.
eval "$(terraform output -raw kubeconfig_command)"
eval "$(terraform output -raw verify_nodes)"
NAME STATUS VERSION COMPUTE-LOCATION INSTANCE-TYPE
ip-10-42-0-31.ec2.internal Ready v1.32.x oci VM.Standard.A1.Flex
ip-10-42-0-87.ec2.internal Ready v1.32.x oci VM.Standard.A1.Flex
Configuration reference
Each configuration ships a generated terraform.tfvars.example listing every variable with its default, required ones first. The tables below cover the pool configurations; aws-asg-do-pool differs as listed above, and the EKS configuration's main inputs are listed above.
Required
| Variable | Description |
|---|---|
license_key | Your Cloud Spectra licence key (CSTW1-...). Sensitive. Relays park without a valid one; workers use it to download the client. |
delivery_url | The Cloud Spectra API: relays fetch their datapath from it, and the workers' downloader exchanges the licence there for a short-lived download token. For a release: https://api.gateway.cloudspectra.ai. Must be https://. |
package_version | The exact Cloud Spectra version to run, for example 1.0.0. Never latest. |
client_apt_base_url | Declared and required, but not used by the install path, which downloads the client through the downloader. Any value is accepted. |
oci_tenancy_ocid, oci_compartment_ocid | OCI pool only. The tenancy OCID itself (availability domains are listed against the tenancy) and the compartment for the workers. |
Optional
| Variable | Default | Description |
|---|---|---|
name | cs-asgpool | Prefix for every resource, including IAM names; 24 characters or fewer. Use a different name for each deployment in the same accounts. |
aws_region | us-east-1 | Region for the relays. |
vpc_cidr | 10.43.0.0/16 | The new VPC's IPv4 range; an IPv6 block is added automatically. |
az_count | 2 | Availability Zones for the relays; at least 2, because the load balancer needs two subnets. |
slots_per_az | 20 | Rendezvous addresses per zone, 1 to 200. |
relay_min / relay_desired / relay_max | 2 / 2 / 6 | Relay Auto Scaling group size. |
relay_instance_type | t4g.small | Relay size. The relays in these configurations are Graviton (arm64) instances. On the Enterprise lane, use at least 4 vCPUs, for example t4g.xlarge. |
relay_ami_id | empty | The relay image (arm64). When it is empty the configuration searches relay_ami_owner (default self) for images named like relay_ami_name_filter (default cloudspectra-gateway-*.*-*); set it explicitly. |
relay_guard_vendor | stripe | The billing channel the relay proves its licence against. Leave it as stripe for a licence bought from Cloud Spectra; an empty value makes the relay exit. |
public_artifact_base | https://downloads-dev.cloudspectra.ai | Public host the workers download the downloader from. Set it to https://downloads.cloudspectra.ai for a release. Must be https://. |
admin_cidrs | [] | Ranges allowed to reach the load balancer and the relays' recovery SSH ports. |
lb_internal | true | Internal load balancer. Set false to publish it; it still admits only admin_cidrs. |
listener_port / app_port | 80 / 8080 | The load balancer's listener, and the port the workload listens on behind each relay. |
wireguard_port / dash_port | 28302 / 28300 | The relays' WireGuard port (UDP) and enrolment and health port (TCP). |
imds_hop_limit | 1 | Metadata hop limit on the relays. 1 lets the worker host reach the metadata service and keeps containers on it from doing so. |
enrol_token_ttl | 168h | How long after its boot a relay accepts the fleet's enrolment token. |
health_check_grace_seconds | 600 | Grace before the group acts on a failing load balancer health check. |
terminate_hook_seconds | 120 | How long a terminating relay has to tell its worker. |
relay_key_name | empty | EC2 key pair for recovery SSH to the relays. Not needed if you supply an SSH public key. |
relay_extra_env, tags | {} | Extra relay environment variables, and tags for every resource. |
pair_scaling_enabled / pair_cpu_target | false / 70 | Scale the relay group on pair load. See Scaling on pair load. |
oci_region | us-ashburn-1 | OCI region for the workers. With manage_oci_iam = true it must be your tenancy's home region. |
oci_shape / oci_ocpus / oci_memory_gbs | VM.Standard.A1.Flex / 1 / 6 | Worker size. A1 shapes are arm64 and E-series shapes are x86_64; the client package is chosen to match. |
oci_vcn_cidr / oci_subnet_cidr | 10.97.0.0/16 / 10.97.1.0/24 | The workers' own network. |
oci_ssh_public_key | empty | SSH key for the workers and the relays; empty reads ~/.ssh/id_rsa.pub. |
worker_initial_size | 2 | Starting size of the OCI pool only; the workers manage it afterwards. |
worker_floor | 1 | The pool never shrinks below this. It must be at least 1: a pool with no workers has nobody left to grow it again. |
manage_oci_iam | true | Create the OCI dynamic group and policy. See OCI IAM without tenancy rights. |
Outputs
| Output | What it gives you |
|---|---|
app_url | The demo application through the load balancer. |
verify | A command that sends ten requests and prints, for each, the cloud that served it and the AWS instance ID it presents as. |
watch_targets | A command showing every relay in the target group and its health. |
watch_claims | A command listing each interface's IPv6 rendezvous address and private IPv4 address. |
asg_name, scale | The relay Auto Scaling group, and the command that resizes it. |
rendezvous_addresses | The pre-allocated IPv6 addresses, by zone. |
worker_pool_name | The worker pool's name. |
peermap_table, peermap_fleet | The peer map's DynamoDB table and fleet key. |
pair_metrics_namespace, pair_metrics_dimension | Where the relays publish pair load in CloudWatch. |
pair_scaling_policy, relay_throttled_alarm | The scaling policy and the alarm, or null when pair scaling is off. |
OCI IAM without tenancy rights
The OCI pool's workers resize their own pool with instance-principal credentials, which needs a dynamic group and a policy at tenancy level. OCI accepts these only from the tenancy's home region, and only from a user who may change tenancy IAM; otherwise the apply fails (for example with 403-NotAllowed and a request to use the home region). In that case set manage_oci_iam = false and have a tenancy administrator create the equivalents:
dynamic group ALL {instance.compartment.id = '<compartment OCID>'}
policy Allow dynamic-group <name> to use instance-pools in compartment id <compartment OCID>
Allow dynamic-group <name> to inspect instance-pools in compartment id <compartment OCID>
Without them the fleet pairs correctly, but the pool never follows the relay group.
The cloudspectra/gateway Terraform provider
The public provider's cloudspectra_ecmp_pipe resource accepts mode = "cloud_vpc" and reports at plan time what that mode derives to -- for example, that the client end runs the Cloud Spectra datapath and how the pipe is billed. It is a planning check only: it creates no relays, workers or networks. CloudVpc itself is deployed with the configurations above.
Scaling
The relay Auto Scaling group is the only place the fleet size is decided. Resize it with the scale output's command, with terraform apply -var relay_desired=N, or with a scaling policy.
- OCI pool: the workers follow within a poll interval.
- DigitalOcean pool: set
worker_countto match and apply. - EKS: the controller follows within a reconcile interval.
Scaling on pair load
A relay's EC2 metrics describe the relay, not the worker running the workload. So, in the pool configurations, each relay publishes its pair's load to CloudWatch every 30 seconds, and pair_scaling_enabled = true creates:
- a target-tracking policy on
PairCPUPercent-- the higher of the relay's and the worker's CPU, never their average -- that holds it atpair_cpu_target(default 70%); - an alarm on the relays' packets-per-second and bandwidth-out allowance-exceeded rates that adds one relay when either has been above zero for two consecutive minutes.
A pair publishes only while its worker is reporting: a relay without a fresh worker reading withholds the pair metrics instead of publishing half of them. Leave pair scaling off wherever something else sets the group's size; the EKS configuration does not offer it.
Licence limits that affect scaling
- Developer lane: an account may run at most 16 vCPUs. Relays beyond the limit park and carry no traffic until room frees -- for example, eight 2-vCPU relays fit.
- Enterprise lane: no account limit, but a relay with fewer than 4 vCPUs is refused.
Upgrades
Every configuration pins an exact package_version; nothing follows a "latest" pointer.
- Relays read the version they were given at first boot and, at every boot, upgrade themselves in place to it from the release host when the image is older. The download is checked against its SHA-256 and the release signature. Upgrades across a major version are refused in place; use a new relay image (
relay_ami_id) for those. - Workers install the client version given by
package_versiononce, at first boot. There is no in-place client upgrade.
To move a fleet to a new release, change package_version (and relay_ami_id if the image changes) and apply, then replace instances so they boot with the new configuration. Running relays keep what they booted with: replace them one at a time -- for example by terminating a relay and letting the group launch its successor. In the OCI pool and the EKS topology, a replaced relay's worker is retired and its successor is launched with the new client. In the DigitalOcean pool the apply itself replaces the Droplets, because their user data changes.
Monitoring
| What to watch | How |
|---|---|
| Relay health | The load balancer target group (watch_targets). The health check is the relay's own /healthz on port 28300 over HTTPS; /readyz on the same port says whether the relay is ready and why not. |
| Rendezvous claims | watch_claims. |
| End-to-end service | The verify output: every line should name the worker cloud and an AWS instance ID. |
| Pair state on a worker | cloudspectra_relay_client status shows the relay the worker enrolled with and the address it wears. In the pool configurations, /run/cloudspectra-client-state holds the agent's current view. |
| Pair load (pool configurations) | CloudWatch namespace CloudSpectra/CloudVpc/<name>, dimension AutoScalingGroupName = <name>-relay: PairCPUPercent, PairMemoryPercent and PairDiskPercent (the busier half of each pair); RelayCPUPercent and WorkerCPUPercent (which half is busy); relay and worker packet and drop rates; and the relays' allowance-exceeded rates (for example RelayBandwidthOutExceededPerMinute and RelayPacketsExceededPerMinute). |
| East-west peers | The peer map table (peermap_table): one row per worker, written by its relay while the worker's tunnel is up and removed when it stops handshaking. |
| Kubernetes | verify_nodes, and the controller's log: kubectl -n cloudspectra-system logs deploy/cloudspectra-master, where reconcile passes are logged with the prefix cloudvpcrecon:. |
Logs
| Where | What to read |
|---|---|
| Relay | sudo journalctl -u cloudspectra_relay -u cloudspectra_relay_dash -u cloudspectra_relaymode, and /var/log/cloud-init-output.log for first boot. Note the underscores: the hyphenated unit names print no entries on these relays. |
| Worker, at boot | /var/log/cloudspectra-worker-bootstrap.log and the serial console, where bootstrap-worker narrates every step. |
| Worker, running | journalctl -u cloudspectra-client-agent -u cloudspectra-client-attach in the pool configurations; journalctl -u cloudspectra-relay-enrolled in the EKS configuration. |
On the relay, wg show shows nothing: the relay runs WireGuard inside its eBPF datapath rather than as a kernel interface. On a worker, the tunnel interface is cswg0.
Relay dashboard
Each relay serves a small HTTPS dashboard on port 28300 with a self-signed certificate. In the pool configurations that port is open to the internet over IPv6 only, so reach it at the relay's rendezvous address. The login password is generated at first boot into /var/lib/cloudspectra/credentials on the relay; read it over SSH with sudo cat. The Monitoring tab shows the relay's CPU, memory, storage, network throughput and egress allowance.
Security model
- Accounts are the boundary. Relays, their IAM role and the peer map live in your AWS account; workers live in your OCI or DigitalOcean account. Tags select resources; they do not grant access.
- The worker holds the relay's IAM role. Any process on the worker host that can reach the metadata service obtains temporary credentials for the relay's instance profile -- the role is
<name>-relayin the pool configurations -- including the permissions CloudVpc itself grants the relay. Add your workload's permissions to that role, and nothing more. - Containers do not. With the default
imds_hop_limit = 1, and with the relay capping metadata replies to one hop, a container one forwarding hop away from the worker host cannot reach the metadata service. The relays require IMDSv2. - Encrypted transport. All relay-worker traffic is inside WireGuard. Workers and relays generate their own keys, and no WireGuard private key is stored in Terraform state. Direct worker-to-worker GENEVE traffic is not encrypted by CloudVpc; it stays between the workers on their own cloud network, and the OCI configuration allows it only from the worker subnet.
- Narrow enrolment. The fleet's enrolment token authorises only enrolment and a read-only download; changes in the dashboard need a signed-in session. In the pool configurations each relay accepts the token once, within
enrol_token_ttlof its boot. - Exposed ports. Each relay accepts the WireGuard port (UDP
28302) and the enrolment and health port (TCP28300) from anywhere, because workers in another cloud cannot be known by address in advance. Recovery SSH (22 and 28301) is limited toadmin_cidrs, empty by default. There is no IPv6 NAT: a relay is reachable at its rendezvous address as soon as it claims it, and the security group is what limits that. - Worker firewalls. In the OCI pool the workers' security list admits only SSH from
admin_cidrsand GENEVE (UDP 6081) from the worker subnet; application traffic arrives through the tunnel the worker opened. The DigitalOcean pool configuration creates no DigitalOcean firewall; add one. - Secrets on the worker. The licence key (
/etc/cloudspectra/licence) andworker.yaml-- which holds the enrolment token and, in the OCI pool, the read-only AWS key -- are written with mode 0600. They travel in the worker's launch metadata and are present in Terraform state; protect the state accordingly.
Access for debugging
The relays and workers run no SSM agent, so SSH is the way in. It is closed unless you open it.
- Relays. Supply your SSH public key (
oci_ssh_public_key, orssh_public_key_overrideon DigitalOcean;~/.ssh/id_rsa.pubby default) or an EC2 key pair (relay_key_name), and setadmin_cidrs. Log in asadmin: on port 22 while the relay boots, and on port 28301 once it is running, because the relay moves SSH there. - Workers. The same key is installed for the workers' login users. Log in as
ubuntuon OCI and asrooton DigitalOcean.
Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
terraform plan fails with "No relay AMI was resolved" | relay_ami_id is empty and nothing in your account matches the name filter. | Set relay_ami_id. |
OCI apply fails with 403-NotAllowed asking for the home region | The dynamic group and policy can only be created from the tenancy's home region. | Set oci_region to the home region, or see OCI IAM without tenancy rights. |
| Relays launch and are replaced over and over | The relay never becomes healthy: the image is not arm64, the relay cannot hold a rendezvous address, or the relay exited. | SSH to a relay during boot and read its journal. "refusing to start: this relay cannot hold its rendezvous address" means the zone's block is exhausted (slots_per_az) or the claim failed; "no vendor configured" means relay_guard_vendor is empty. |
| Relays are healthy but carry no traffic | The relay is parked: no licence or an invalid one, a Developer account over its 16-vCPU limit, or -- on the Enterprise lane -- a relay with fewer than 4 vCPUs. | Read the relay's journal: a licence problem is logged as "no licence key was found -- the relay is PARKED" or "NOT ENTITLED -- serving nobody". Fix license_key, reduce the vCPUs the account runs, or on the Enterprise lane use a relay with at least 4 vCPUs, such as t4g.xlarge. |
| Workers never pair | The worker cannot download the client, cannot reach a relay over IPv6, or no relay will accept its token. | Read /var/log/cloudspectra-worker-bootstrap.log or the serial console. A failure at the first download usually means a wrong public_artifact_base, delivery_url, package_version or licence. Check that the worker reaches an address from rendezvous_addresses. "that enrolment token is not valid" from every relay means each relay's single use of the token is spent or its enrol_token_ttl has passed; a replaced relay accepts the token again. |
| Fewer workers than relays (OCI pool) | The workers cannot resize their pool: the OCI dynamic group or policy is missing. | See OCI IAM without tenancy rights. |
| Fewer workers than relays (DigitalOcean pool) | This pool does not follow the relay group. | Set worker_count to the relay count. |
| No workers launched (EKS) | The controller refuses to launch, for example because a relay has not announced itself or the pair ceiling is reached. | Read the controller's log; each refusal is logged with a reason such as relay_not_announced or ceiling. Check that license_key is set. |
| Containers on a worker cannot get AWS credentials | The metadata hop limit is 1, by design. | Run code that needs the relay's role on the worker host, or raise imds_hop_limit deliberately, knowing that every container then holds the role. |
| Worker-to-worker traffic is slow | The workers do not know each other's host address yet, so traffic goes through the relays. | Check that the peer map table has a row for each worker, and that UDP 6081 is allowed between the workers. |
| Large transfers stall while small requests work (EKS) | An MTU mismatch between the tunnel and the pod network. | Keep wireguard_mtu at its default of 1380; Calico's pod MTU is derived from it. |
terraform destroy stops with DependencyViolation ("mapped public address") | The relays are still terminating, so the internet gateway cannot be detached yet. | Wait a minute and run terraform destroy again. |
terraform destroy stops with "Can not delete VPC with members" (DigitalOcean, EKS) | Droplets the controller launched are still in the DigitalOcean VPC; they are not in Terraform state. | Set DIGITALOCEAN_TOKEN for the destroy so the drain can remove them, or delete the fleet's Droplets, wait until they are gone, and destroy again. |
When you contact support@cloudspectra.ai, include the configuration name, its terraform output (without secrets), the relay journal and the worker's bootstrap log.
Teardown
Run terraform destroy in the configuration you applied, with the same variables and credentials.
- Pool configurations. Everything is in Terraform state, including the OCI pool and the DigitalOcean Droplets. If the destroy stops on a
DependencyViolationabout mapped public addresses, run it again after a minute. - EKS. The workers are launched by the controller, not by Terraform, so the configuration drains the fleet first: a destroy-time step scales the relay group to zero, releases the relays' lifecycle hooks, waits for the controller to retire the workers (
drain_timeout_seconds, default 300), terminates any that remain, and only then lets the cluster, the relays and the worker network go. It needs Python 3 withboto3, theociSDK for OCI workers orDIGITALOCEAN_TOKENfor DigitalOcean workers, andcloudspectra_fleet_manifest. If the drain cannot finish, the destroy still runs and reports what it could not delete. - Afterwards, confirm in each cloud that no instance tagged with your fleet remains. Destroying the deployment does not change your Cloud Spectra subscription; manage it on your account page.