--- 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.

Spinifex Web UI

### 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) ```