mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-19 19:01:42 +00:00
8409799cbf
* fix: default the OCI helper to the DEFAULT profile oci_env.py shipped our own internal Oracle compartment profile as the default for every reader, with a second internal name as a fallback. A fresh tenancy's config holds only `[DEFAULT]`, so the shipped default was a name nobody else can have, and the fallback chain could land a deployment in whichever tenancy a different profile happened to point at. The profile is now `DEFAULT`, overridable by `OCI_CLI_PROFILE` or `--profile`, and a named profile the config does not hold is an error rather than a fallback. `DEFAULT` needs its own existence test, because configparser treats it as the fallback for every other section and `has_section` denies it exists even when `oci setup config` wrote nothing else. `eip-move-test.sh` and the config table in the README carried the same name. CI reads its credential from the environment rather than from a profile, so nothing there referenced it. * docs: look the OCI compartment OCID up by name The guide asked the reader to paste `ocid1.compartment.oc1..xxx` three times, and the `oci` CLI it already requires can resolve the name instead. The lookup needs its own check. `oci iam compartment list --query ... | [0]` exits 0 and prints nothing when the name matches nothing, so an unset `$CID` would otherwise reach Terraform and fail with an OCI error naming neither the compartment nor the cause. Two compartments in one tenancy can share a name, which the listing command shows and the lookup cannot resolve, so the paste form stays as the answer for that case. Records why the tenancy root is the wrong value: the step 2.1 policy is scoped to whatever `$CID` holds. * Revert "docs: look the OCI compartment OCID up by name" This reverts commit5443149cce. * Reapply "docs: look the OCI compartment OCID up by name" This reverts commit1535d94c8a. * fix: stop validate-topology.sh failing on macOS bash `tf_principal_var` is empty on every run that does not pass `--instance-principal`, and bash before 4.4 treats `"${arr[@]}"` on an empty array as an unbound variable under `set -u`. macOS ships bash 3.2, so the documented deployment command aborted at the tf_vars assembly with `tf_principal_var[@]: unbound variable` before building anything. Assigning the array was not enough, which is why the earlier `HOSTS=()` fix did not cover this: the error is in the expansion, not the reference. Both sites now expand only behind a length test, which is valid on every version. `capture_journals` had the same shape for `HOSTS`, reachable when a run fails before the apply. Reproduced and fixed against bash 3.2.57 in a container, which emits the identical message, and the script now runs to the terraform call on 3.2 for vm-single, vm-single-principal and `--instance-principal`, including the `--keep` cleanup path with no hosts. * docs: record the bash 3.x requirement in the OCI scripts These three are the scripts the OCI guide tells an operator to run, and macOS ships bash 3.2, so a bash 4 construct in any of them breaks a deployment rather than a test. The constraint has now been rediscovered twice, through declare -A and mapfile, and then through an empty array expanded under set -u. Names the three traps at the top of each file so the next change does not have to find them by failing on someone's Mac. * fix: stream the workbook suite instead of hiding it in a file The nine workbooks ran with stdout redirected to `workbooks.log`, so a run wedged inside one printed nothing until the whole suite finished. A CI job then sat silent for hours with no way to tell which workbook it was in, or whether it was working at all. The output is now teed, so the file the per-workbook table is parsed from is unchanged while the run is visible live. Each of the driver's own RUN/PASS/FAIL lines is stamped with elapsed seconds and the workbook name, which is what identifies the slow one without reading the log. `pipefail` carries the ssh exit status through the pipe, so a failed suite still takes the branch that records the table and captures the journals. * fix: grant the OCI policy the subnet verb AssignPrivateIP requires An OCI node with a pool-mode external interface formed, reported healthy, and then refused every launch wanting a public address with a 404 NotAuthorizedOrNotFound from AssignPrivateIP. AssignPrivateIP calls CreatePrivateIp, which Oracle documents as needing PRIVATE_IP_CREATE, PRIVATE_IP_ASSIGN, VNIC_ASSIGN and SUBNET_ATTACH, and DeletePrivateIp as needing SUBNET_DETACH. The policy granted use vnics, manage private-ips and manage public-ips, and no subnet permission at all, so SUBNET_ATTACH was never held. Only the subnets use verb carries it. Measured on a live node as the instance principal: GetVnic and ListPrivateIps succeed, GetSubnet and CreatePrivateIp both 404. Authentication, the dynamic group's matching rule and the compartment were all correct, so the credential was authentic and unauthorised. The documented operator policy for the API-key route carried the same three statements, so neither credential route could allocate. A node now proves its own credential at pool start by making one read in each permission class, and logs the missing verb if one fails. The topology gate waited for "resolved the external VNIC", which comes from the metadata service and needs no IAM policy, so an unauthorised node passed the gate and failed an hour later inside a workbook as capacity. It now waits for the authorisation line and fails immediately on a refusal. Reads rather than a real assign-and-release round trip: a round trip also fails on a full subnet, and reporting that as an authorisation fault sends an operator to IAM for a capacity problem. * docs: say what a principal run costs in a tenancy with no dynamic group No principal topology can pass there, and the only previous hint was one clause about OCI_INSTANCE_PRINCIPAL needing the group to exist. * test: cover the subnet read and the authorisation verdict Diff coverage was 39.1% against a 60% gate. Three of the four gaps were real. GetSubnet on the SDK-backed client had no test, so the request shape was unverified: addressing the collection rather than one subnet by OCID would answer from a different permission and report a credential as working when it is not. Also pins that a sparse body is not an error and that the unauthorised 404 arrives as ErrNotFound, which is what stops the check retrying a verdict that will never change. The fake's GetSubnet was exercised only from the ocinet tests, and coverage is recorded per package under test, so it read as zero-covered. reportAuthorisation now has a test asserting the exact log text, because validate-topology.sh greps for it and a rename would quietly turn the gate into one that never fires. The six lines still uncovered are inside FromPoolConfig, which needs a live JetStream to reach. Diff coverage 87.0%. * fix: refuse a run whose tfvars file contradicts the compartment it was given Terraform ranks *.auto.tfvars above TF_VAR_*, so a leftover file wins silently over an operator targeting a different tenancy by environment, and the guide tells every operator to write one. Hit for real: a stale terraform.auto.tfvars naming a different tenancy's compartment and a bare-metal shape overrode TF_VAR_compartment_ocid, and the plan was for 11 resources in a compartment nobody had chosen. Only a dry run caught it. The run now stops and names both values rather than building. * fix: collect orphaned OCI public IPs and name the limit that was hit A reserved public IP attached to nothing is on no VNIC, so the reconcile that exists to find leaked addresses could not see the ones that matter. It also ran only at startup, while an address is leaked on release and on teardown. Twenty accumulated in one compartment and exhausted the tenancy's limit of 50, after which every launch needing a public address aborted. Reconcile now lists reserved public IPs in the compartment and collects those that carry our prefix, are attached to nothing, are named by no binding and are older than a grace period. All four are needed: an allocated but unassociated Elastic IP is detached too and is the customer's to keep, and a young detached address is what another node's CreatePublicIP looks like until it attaches. A pool with no binding record is not swept at all, since that is indistinguishable from a store that cannot be read. The pass now repeats on a ticker rather than running once at startup. Both OCI ceilings also resolved to InsufficientAddressCapacity, which tells an operator the region is out of addresses when the fix is to release addresses they already hold. AWS separates them, so a tenancy out of reserved public IPs is now AddressLimitExceeded and a full VNIC is PrivateIpAddressLimitExceeded. * feat: run the OCI principal e2e in a tenancy that can hold its dynamic group vm-single-principal needs the tenancy's dynamic group, and creating one is a tenancy-root write that a shared test tenancy does not grant us. The topology could never pass there however correct the code was, so it now takes its own credential from OCI_PRINCIPAL_*, all six or none. A partial set is refused before the build rather than at the apply, because it would build one tenancy's instances against another's compartment. With none set the topology is recorded SKIPPED instead of spending a provision to fail. The sweep takes the same credential, or a leftover principal deployment is destroyed against a tenancy that never held it and reported clean. Two defects found while proving it: --dry-run reported FAIL on every topology with a tree source, because the dry run skips the build and then refuses the tarball it would have produced. The flag operators are told to run first could not pass. The dry run truncated its plan to the last 30 lines, which is the outputs block. That dropped the resource count and every compartment and shape line, so the one check a dry run exists for could not be made from its output. It now writes the whole plan and prints the summary. Removing a deployment is documented as leaving public IPs behind. Spinifex allocates them on the running node, so they are in no Terraform state and destroy cannot see them; twenty accumulated and exhausted a tenancy's limit, which then refused every launch in every compartment. * fix: canonicalise the Opc-Next-Page header in the pagination test CI's linter rejects a non-canonical header name and the local preflight does not, so this passed here and failed there.
535 lines
18 KiB
Markdown
535 lines
18 KiB
Markdown
---
|
|
title: "Setting Up Your Cluster"
|
|
seoTitle: "Set Up Your First Spinifex Cluster — Spinifex Docs"
|
|
description: "Import an AMI and the RDS, ECS and EKS base images, create an SSH key pair and a VPC with a public subnet, then launch your first EC2 instance on a fresh Spinifex cluster."
|
|
category: "Admin"
|
|
tags:
|
|
- setup
|
|
- vpc
|
|
- ec2
|
|
- quickstart
|
|
resources:
|
|
- title: "Spinifex Repository"
|
|
url: "https://github.com/mulgadc/spinifex"
|
|
- title: "AWS EC2 CLI Reference"
|
|
url: "https://docs.aws.amazon.com/cli/latest/reference/ec2/"
|
|
---
|
|
|
|
# Setting Up Your Cluster
|
|
|
|
> Import an AMI and the system base images, create a VPC with a public subnet, and launch your first instance.
|
|
|
|
## Table of Contents
|
|
|
|
- [1. Import an AMI](#1-import-an-ami)
|
|
- [Option C: Import the system base images](#option-c-import-the-system-base-images)
|
|
- [2. Create an SSH Key](#2-create-an-ssh-key)
|
|
- [3. Create a VPC and Public Subnet](#3-create-a-vpc-and-public-subnet)
|
|
- [4. Launch an Instance](#4-launch-an-instance)
|
|
- [5. Connect via SSH](#5-connect-via-ssh)
|
|
- [6. Managing Instances](#6-managing-instances)
|
|
- [7. Launching the Web UI](#7-launching-the-web-ui)
|
|
- [8. Secure the Install](#8-secure-the-install)
|
|
- [Troubleshooting](#troubleshooting)
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
This guide walks through the first steps on a freshly installed cluster: importing an AMI and the system base images RDS, ECS and EKS need, creating an SSH key and a VPC with a public subnet, launching an instance, and connecting to it.
|
|
|
|
## Instructions
|
|
|
|
## Prerequisites
|
|
|
|
- Spinifex installed and running — see [Single-Node Install](/docs/install), [Multi-Node Install](/docs/install-multi-node), or [Source Install](/docs/install-source)
|
|
- AWS CLI configured with the `spinifex` profile:
|
|
|
|
```bash
|
|
export AWS_PROFILE=spinifex
|
|
```
|
|
|
|
## 1. Import an AMI
|
|
|
|
### Option A: Import a bundled image
|
|
|
|
List the bundled images and import one matching your architecture:
|
|
|
|
```bash
|
|
spx admin images list
|
|
```
|
|
|
|
```
|
|
NAME | DISTRO | VERSION | ARCH | BOOT
|
|
debian-13-arm64 | debian | 13 | arm64 | uefi
|
|
debian-13-x86_64 | debian | 13 | x86_64 | uefi
|
|
ubuntu-26.04-arm64 | ubuntu | 26.04 | arm64 | uefi
|
|
ubuntu-26.04-x86_64 | ubuntu | 26.04 | x86_64 | uefi
|
|
```
|
|
|
|
```bash
|
|
spx admin images import --name ubuntu-26.04-x86_64
|
|
```
|
|
|
|
Verify the import and note the AMI ID:
|
|
|
|
```bash
|
|
AMI_ID=$(aws ec2 describe-images --query 'Images[0].ImageId' --output text)
|
|
```
|
|
|
|
### Option B: Import a local image file
|
|
|
|
```bash
|
|
spx admin images import --file ~/images/ubuntu-26.04-server-cloudimg-amd64.img --checksum ~/images/SHA256SUMS --distro ubuntu --version 26.04 --arch x86_64 --boot-mode uefi
|
|
```
|
|
|
|
Verify the import and note the AMI ID:
|
|
|
|
```bash
|
|
AMI_ID=$(aws ec2 describe-images --query 'Images[0].ImageId' --output text)
|
|
```
|
|
|
|
### Option C: Import the system base images
|
|
|
|
**RDS, ECS and EKS each launch from an image of their own, and none of them is imported for you.** Until these are in the catalog, `CreateDBInstance`, `CreateCluster` and `CreateNodegroup` all fail at the point they go looking for a node image — so a cluster without them has those three services present in the API and unusable in practice. Import them once, now, and the install is functional from the start.
|
|
|
|
Run these one at a time rather than in parallel, so predastore is not taking four concurrent uploads:
|
|
|
|
```bash
|
|
for img in spinifex-rds-postgres spinifex-rds-mariadb spinifex-ecs-node spinifex-eks-node; do
|
|
sudo spx admin images import --name "$img" --config /etc/spinifex/spinifex.toml
|
|
done
|
|
```
|
|
|
|
| Image | Serves | Contents |
|
|
| ----------------------- | -------------------------------- | ----------------------------------------------- |
|
|
| `spinifex-rds-postgres` | RDS, `--engine postgres` | Alpine 3.24.1 + PostgreSQL 18 + `rds-init` |
|
|
| `spinifex-rds-mariadb` | RDS, `--engine mariadb` | Alpine 3.24.1 + MariaDB 11.8 + `rds-init` |
|
|
| `spinifex-ecs-node` | ECS container instances | Alpine 3.21.7 + containerd + ecs-agent |
|
|
| `spinifex-eks-node` | EKS control plane and nodegroups | Alpine 3.21.7 + K3s v1.32.5 + eks-token-webhook |
|
|
|
|
All four boot **BIOS**, unlike the distro images, which boot UEFI. That is a property of how they are built and nothing you need to pass.
|
|
|
|
If you run GPU workloads, add `spinifex-ecs-node-gpu` and `spinifex-eks-node-gpu` as well — Ubuntu 26.04 with the NVIDIA driver, selected automatically for a GPU instance type. They boot UEFI and they are large, so import them only if you have GPUs.
|
|
|
|
> [!IMPORTANT]
|
|
> **These register under generic names, and that is correct.** `describe-images` shows `spinifex-eks-node` and `spinifex-ecs-node` both as `ami-alpine-3.21.7-x86_64`, and the two RDS images both as `ami-alpine-3.24.1-x86_64`. Nothing resolves them by name: ECS and EKS find theirs by the `spinifex:managed-by=ecs|eks` tag, and RDS by `spinifex:managed-by=rds` plus the `engine` and `engine-version` tags. **Verify the tags, not the names** — four rows with duplicate-looking names is what success looks like:
|
|
>
|
|
> ```bash
|
|
> aws ec2 describe-images \
|
|
> --filters 'Name=tag-key,Values=spinifex:managed-by' \
|
|
> --query 'Images[].[ImageId,State,Tags[?Key==`spinifex:managed-by`].Value|[0],Tags[?Key==`engine`].Value|[0]]' \
|
|
> --output text
|
|
> ```
|
|
|
|
`spx admin images list --config /etc/spinifex/spinifex.toml` on a node shows the whole catalog, including the images not imported here.
|
|
|
|
## 2. Create an SSH Key
|
|
|
|
### Option A: Import an existing key
|
|
|
|
```bash
|
|
aws ec2 import-key-pair \
|
|
--key-name "spinifex-key" \
|
|
--public-key-material fileb://~/.ssh/id_rsa.pub
|
|
```
|
|
|
|
### Option B: Create a new key pair
|
|
|
|
```bash
|
|
aws ec2 create-key-pair --key-name spinifex-key \
|
|
| jq -r '.KeyMaterial | rtrimstr("\n")' > ~/.ssh/spinifex-key
|
|
|
|
chmod 600 ~/.ssh/spinifex-key
|
|
ssh-keygen -y -f ~/.ssh/spinifex-key > ~/.ssh/spinifex-key.pub
|
|
```
|
|
|
|
Verify:
|
|
|
|
```bash
|
|
aws ec2 describe-key-pairs
|
|
```
|
|
|
|
## 3. Create a VPC and Public Subnet
|
|
|
|
### Create a VPC
|
|
|
|
```bash
|
|
VPC_ID=$(aws ec2 create-vpc --cidr-block 10.200.0.0/16 \
|
|
--query 'Vpc.VpcId' --output text)
|
|
```
|
|
|
|
### Create an Internet Gateway
|
|
|
|
An Internet Gateway enables instances in public subnets to reach the internet and be reachable from the LAN/WAN.
|
|
|
|
```bash
|
|
IGW_ID=$(aws ec2 create-internet-gateway \
|
|
--query 'InternetGateway.InternetGatewayId' --output text)
|
|
|
|
aws ec2 attach-internet-gateway \
|
|
--internet-gateway-id $IGW_ID \
|
|
--vpc-id $VPC_ID
|
|
```
|
|
|
|
### Create a Subnet
|
|
|
|
Create the subnet your instances will launch into. The routing and public-IP steps below are what make it a _public_ subnet.
|
|
|
|
```bash
|
|
SUBNET_ID=$(aws ec2 create-subnet \
|
|
--vpc-id $VPC_ID \
|
|
--cidr-block 10.200.1.0/24 \
|
|
--query 'Subnet.SubnetId' --output text)
|
|
```
|
|
|
|
### Create a Route Table
|
|
|
|
A subnet is only **public** if its route table has a default route to the Internet Gateway. Spinifex does **not** add this route automatically — a new VPC's route table only routes traffic within the VPC (matching AWS). Without a `0.0.0.0/0` route to the IGW, instances in the subnet cannot reach the internet (and inbound connections cannot complete) even with a public IP assigned.
|
|
|
|
```bash
|
|
RT_ID=$(aws ec2 create-route-table \
|
|
--vpc-id $VPC_ID \
|
|
--query 'RouteTable.RouteTableId' --output text)
|
|
|
|
# Default route to the internet via the IGW
|
|
aws ec2 create-route \
|
|
--route-table-id $RT_ID \
|
|
--destination-cidr-block 0.0.0.0/0 \
|
|
--gateway-id $IGW_ID
|
|
|
|
# Associate the route table with the subnet
|
|
aws ec2 associate-route-table \
|
|
--route-table-id $RT_ID \
|
|
--subnet-id $SUBNET_ID
|
|
```
|
|
|
|
### Enable Auto-Assign Public IP
|
|
|
|
Give every instance launched into the subnet a routable public IP, making it directly reachable from your network.
|
|
|
|
```bash
|
|
aws ec2 modify-subnet-attribute \
|
|
--subnet-id $SUBNET_ID \
|
|
--map-public-ip-on-launch
|
|
```
|
|
|
|
### Allow SSH and ICMP
|
|
|
|
Every VPC gets a default security group that **blocks inbound traffic from outside the group** (matching AWS). Instances launched without an explicit security group use this default, so you must authorize ingress before you can SSH or ping the instance.
|
|
|
|
```bash
|
|
SG_ID=$(aws ec2 describe-security-groups \
|
|
--filters Name=vpc-id,Values=$VPC_ID \
|
|
--query 'SecurityGroups[0].GroupId' --output text)
|
|
|
|
# Allow SSH from anywhere
|
|
aws ec2 authorize-security-group-ingress \
|
|
--group-id $SG_ID \
|
|
--protocol tcp --port 22 --cidr 0.0.0.0/0
|
|
|
|
# Allow ICMP (ping) from anywhere
|
|
aws ec2 authorize-security-group-ingress \
|
|
--group-id $SG_ID \
|
|
--protocol icmp --port -1 --cidr 0.0.0.0/0
|
|
```
|
|
|
|
**Note:** `0.0.0.0/0` opens these ports to every source. For anything beyond a quick evaluation, scope the `--cidr` to a trusted range. Rule changes apply immediately — no instance restart needed.
|
|
|
|
### Verify
|
|
|
|
```bash
|
|
aws ec2 describe-vpcs --vpc-ids $VPC_ID
|
|
aws ec2 describe-subnets --subnet-ids $SUBNET_ID
|
|
aws ec2 describe-route-tables --route-table-ids $RT_ID
|
|
aws ec2 describe-security-groups --group-ids $SG_ID
|
|
```
|
|
|
|
## 4. Launch an Instance
|
|
|
|
Launch an instance in the public subnet on a `t3.micro` (2 vCPU / 1 GiB).
|
|
|
|
> **Note:** On an **arm64** host, use `t4g.micro` instead of `t3.micro`.
|
|
|
|
```bash
|
|
INSTANCE_ID=$(aws ec2 run-instances \
|
|
--image-id $AMI_ID \
|
|
--instance-type t3.micro \
|
|
--key-name spinifex-key \
|
|
--subnet-id $SUBNET_ID \
|
|
--count 1 \
|
|
--query 'Instances[0].InstanceId' --output text)
|
|
```
|
|
|
|
Wait for the instance to reach `running` state:
|
|
|
|
```bash
|
|
aws ec2 describe-instances --instance-ids $INSTANCE_ID \
|
|
--query 'Reservations[0].Instances[0].[State.Name, PrivateIpAddress, PublicIpAddress]' \
|
|
--output text
|
|
```
|
|
|
|
Expected output:
|
|
|
|
```
|
|
running 10.200.1.4 192.168.1.155
|
|
```
|
|
|
|
The instance has both a private IP (VPC overlay) and a public IP (from your external pool, routable on your network).
|
|
|
|
## 5. Connect via SSH
|
|
|
|
SSH directly to the instance's public IP:
|
|
|
|
```bash
|
|
PUBLIC_IP=$(aws ec2 describe-instances --instance-ids $INSTANCE_ID \
|
|
--query 'Reservations[0].Instances[0].PublicIpAddress' --output text)
|
|
|
|
ssh -i ~/.ssh/spinifex-key ubuntu@$PUBLIC_IP
|
|
```
|
|
|
|
> **Note:** cloud-init takes 30-60 seconds to configure the instance. If SSH is refused, wait and retry.
|
|
|
|
Once connected, verify the instance has internet access:
|
|
|
|
```bash
|
|
curl -s http://ifconfig.me
|
|
```
|
|
|
|
This should return the instance's public IP or the gateway's SNAT address.
|
|
|
|
## 6. Managing Instances
|
|
|
|
### Stop
|
|
|
|
```bash
|
|
aws ec2 stop-instances --instance-ids $INSTANCE_ID
|
|
```
|
|
|
|
### Start
|
|
|
|
```bash
|
|
aws ec2 start-instances --instance-ids $INSTANCE_ID
|
|
```
|
|
|
|
### Terminate
|
|
|
|
```bash
|
|
aws ec2 terminate-instances --instance-ids $INSTANCE_ID
|
|
```
|
|
|
|
### Console Output
|
|
|
|
View the instance's serial console log (useful for debugging boot issues):
|
|
|
|
```bash
|
|
aws ec2 get-console-output --instance-id $INSTANCE_ID \
|
|
--query 'Output' --output text
|
|
```
|
|
|
|
### Multi-Node: Check Instance Placement
|
|
|
|
On a multi-node cluster, instances are distributed across nodes:
|
|
|
|
```bash
|
|
spx get vms
|
|
```
|
|
|
|
## 7. Launching the Web UI
|
|
|
|
Spinifex ships with a built-in web console — an alternative to the AWS CLI, SDKs, and Terraform, analogous to the AWS Management Console. Every action in the UI is a standard AWS SigV4 API call, so the same IAM policies and audit behaviour apply.
|
|
|
|
<p align="center">
|
|
<img src="../../../.github/assets/spinifex-ui.jpg" alt="Spinifex Web UI" width="900">
|
|
</p>
|
|
|
|
### Open the Console
|
|
|
|
The UI is served by each node on port `3000` over TLS. Replace `YOUR_NODE_IP` with the address of the node you installed Spinifex on (or `localhost` if you're on the node itself):
|
|
|
|
```
|
|
https://YOUR_NODE_IP:3000
|
|
```
|
|
|
|
### Trust the Self-Signed Certificate (required)
|
|
|
|
On first load, your browser will show a TLS warning — Spinifex generates a self-signed certificate at install time. This is expected.
|
|
|
|
1. Accept the warning to reach the login page (exact wording varies by browser — e.g. Chrome: _Advanced → Proceed to ..._, Firefox: _Advanced → Accept the Risk and Continue_).
|
|
2. On the login page, click **Download Certificate** and save `spinifex-ca.pem` to your machine.
|
|
3. Install the certificate as a **trusted root** on your workstation, following the steps for your platform below.
|
|
4. Restart your browser and reload `https://YOUR_NODE_IP:3000`. The padlock should now show a valid certificate.
|
|
|
|
Step 3, by platform:
|
|
|
|
- **macOS:** open `spinifex-ca.pem` in Keychain Access → _System_ keychain → set _Trust_ to **Always Trust**.
|
|
- **Linux:** `sudo cp spinifex-ca.pem /usr/local/share/ca-certificates/spinifex-ca.crt && sudo update-ca-certificates`
|
|
- **Windows:** double-click the file → _Install Certificate_ → _Local Machine_ → _Trusted Root Certification Authorities_.
|
|
- **Browser-only (Firefox):** _Settings → Privacy & Security → Certificates → View Certificates → Authorities → Import_ and tick _Trust this CA to identify websites_.
|
|
|
|
> **Why this is required:** the UI logs in by reading your AWS credentials through a trusted TLS channel. Browsers refuse to send credentials over an untrusted connection, so the certificate must be installed as trusted — temporary "Proceed anyway" exceptions won't work for login.
|
|
|
|
### Log In with AWS Credentials
|
|
|
|
The console authenticates against the AWS credentials in `~/.aws/credentials` on the node where Spinifex is installed:
|
|
|
|
```ini
|
|
[spinifex]
|
|
aws_access_key_id = AKIA...
|
|
aws_secret_access_key = ...
|
|
```
|
|
|
|
At the Spinifex login screen, paste the **Access Key ID** and **Secret Access Key** from the `[spinifex]` profile (or whichever profile maps to the IAM user or role you want to use). Additional users and policies can be managed through the UI or via `aws iam` commands — see [IAM Users and Policies](/docs/iam-users-and-policies).
|
|
|
|
Once logged in, you have browser-based access to every Spinifex feature: launch and manage instances, attach EBS volumes, browse S3 buckets, configure VPCs and security groups, and manage IAM users and keys — all backed by the same AWS-compatible control plane the CLI uses.
|
|
|
|
## 8. Secure the Install
|
|
|
|
Your cluster works. Before it carries anything real, close the host down.
|
|
|
|
**Arm the host firewall.** A binary install leaves it off, because it lands on a machine that was already doing something. Armed, it keeps OVN, NATS and the rest of the cluster plane reachable by cluster members only, and everything outside the public group closed:
|
|
|
|
```toml
|
|
# /etc/spinifex/spinifex.toml
|
|
[network]
|
|
firewall_enabled = true
|
|
```
|
|
|
|
**Narrow SSH.** It is accepted from every source by default, so that arming the policy cannot lock you out of the node you are arming it from. Replace `0.0.0.0/0` with your management networks in `/etc/spinifex/firewall/custom.nft` — the one file an upgrade never overwrites.
|
|
|
|
[Host Firewall](/docs/host-firewall) covers both, what the policy allows, and how each node tracks cluster membership without a peer list you maintain.
|
|
|
|
## Additional Options
|
|
|
|
### Private Subnets (No Public IP)
|
|
|
|
Create a subnet without `--map-public-ip-on-launch`:
|
|
|
|
```bash
|
|
PRIVATE_SUBNET=$(aws ec2 create-subnet \
|
|
--vpc-id $VPC_ID \
|
|
--cidr-block 10.200.2.0/24 \
|
|
--query 'Subnet.SubnetId' --output text)
|
|
```
|
|
|
|
Instances in a private subnet get a private IP only and are not reachable from your network. By default they also have **no internet access**: with no `0.0.0.0/0` route in the subnet's route table, Spinifex gates egress with a drop policy — matching AWS, where a private subnet has no route off the VPC. They can still reach other instances in the same VPC.
|
|
|
|
To give a private subnet outbound-only internet access, deploy a NAT Gateway in a public subnet and point the private subnet's default route at it (the AWS pattern):
|
|
|
|
```bash
|
|
# Allocate an Elastic IP for the NAT Gateway
|
|
NAT_EIP=$(aws ec2 allocate-address --query AllocationId --output text)
|
|
|
|
# Create the NAT Gateway in the PUBLIC subnet (the one with the IGW route)
|
|
NATGW_ID=$(aws ec2 create-nat-gateway \
|
|
--subnet-id $SUBNET_ID \
|
|
--allocation-id $NAT_EIP \
|
|
--query 'NatGateway.NatGatewayId' --output text)
|
|
|
|
# Give the private subnet its own route table with a default route to the NAT Gateway
|
|
PRIVATE_RT=$(aws ec2 create-route-table \
|
|
--vpc-id $VPC_ID \
|
|
--query 'RouteTable.RouteTableId' --output text)
|
|
|
|
aws ec2 create-route \
|
|
--route-table-id $PRIVATE_RT \
|
|
--destination-cidr-block 0.0.0.0/0 \
|
|
--nat-gateway-id $NATGW_ID
|
|
|
|
aws ec2 associate-route-table \
|
|
--route-table-id $PRIVATE_RT \
|
|
--subnet-id $PRIVATE_SUBNET
|
|
```
|
|
|
|
Instances in the private subnet now reach the internet outbound through the NAT Gateway's public IP, but remain unreachable from the WAN.
|
|
|
|
### Multiple Accounts
|
|
|
|
Create isolated accounts with their own resources:
|
|
|
|
```bash
|
|
spx admin account create --name myteam
|
|
export AWS_PROFILE=spinifex-myteam
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
### Instance Stuck in Pending
|
|
|
|
```bash
|
|
journalctl -u spinifex-daemon -f
|
|
aws ec2 describe-images
|
|
```
|
|
|
|
### SSH Connection Refused or Times Out
|
|
|
|
cloud-init needs 30-60 seconds after boot. Check instance state:
|
|
|
|
```bash
|
|
aws ec2 describe-instances --instance-ids $INSTANCE_ID
|
|
```
|
|
|
|
A connection that **times out** (rather than being refused) usually means the security group is blocking port 22. Confirm the default security group allows SSH ingress:
|
|
|
|
```bash
|
|
aws ec2 describe-security-groups --group-ids $SG_ID \
|
|
--query 'SecurityGroups[0].IpPermissions'
|
|
```
|
|
|
|
If there are no rules for TCP 22, authorize it (see [Allow SSH and ICMP](#allow-ssh-and-icmp)):
|
|
|
|
```bash
|
|
aws ec2 authorize-security-group-ingress \
|
|
--group-id $SG_ID --protocol tcp --port 22 --cidr 0.0.0.0/0
|
|
```
|
|
|
|
### No Public IP Assigned
|
|
|
|
Verify the subnet has `MapPublicIpOnLaunch` enabled:
|
|
|
|
```bash
|
|
aws ec2 describe-subnets --subnet-ids $SUBNET_ID
|
|
```
|
|
|
|
If `MapPublicIpOnLaunch` is false:
|
|
|
|
```bash
|
|
aws ec2 modify-subnet-attribute --subnet-id $SUBNET_ID --map-public-ip-on-launch
|
|
```
|
|
|
|
Also verify an Internet Gateway is attached to the VPC:
|
|
|
|
```bash
|
|
aws ec2 describe-internet-gateways
|
|
```
|
|
|
|
### Instance Has No Internet Access
|
|
|
|
First confirm the subnet's route table has a default route to the IGW. Spinifex gates a subnet's egress with a drop policy when this route is missing, so an instance with a public IP still cannot reach the internet:
|
|
|
|
```bash
|
|
aws ec2 describe-route-tables \
|
|
--filters Name=association.subnet-id,Values=$SUBNET_ID \
|
|
--query 'RouteTables[0].Routes'
|
|
# Expect a 0.0.0.0/0 route with a GatewayId of igw-...
|
|
```
|
|
|
|
If the route is missing, add it (see [Create a Route Table](#create-a-route-table)):
|
|
|
|
```bash
|
|
aws ec2 create-route --route-table-id $RT_ID \
|
|
--destination-cidr-block 0.0.0.0/0 --gateway-id $IGW_ID
|
|
```
|
|
|
|
If the route is present, check the VPC router's NAT rules (from the host):
|
|
|
|
```bash
|
|
sudo ovn-nbctl lr-nat-list $(sudo ovn-nbctl lr-list | awk '{print $2}' | head -1)
|
|
```
|
|
|
|
Verify the default route exists:
|
|
|
|
```bash
|
|
sudo ovn-nbctl lr-route-list $(sudo ovn-nbctl lr-list | awk '{print $2}' | head -1)
|
|
```
|