Skip to content

3.1 Provision Compute

Compute provisioning on AWS has two stages: building the Talos AMI, then deploying EC2 instances via Formae.

Prerequisites: Build the Talos AMI

There is no official Talos AMI in af-south-1. You must build and register a custom AMI using the bootstrap pipeline.

Step 1: Deploy Bootstrap Resources

The bootstrap.pkl forma creates the S3 bucket and IAM vmimport role needed for AMI registration:

cd formae/cluster/aws
formae apply --mode reconcile bootstrap.pkl

This creates:

  • S3 bucket: rciis-talos-images-af-south-1 with a 30-day lifecycle expiry
  • IAM role: vmimport with permissions for EC2 VM Import/Export

Step 2: Generate the Talos Disk Image

Option A — Vanilla image (no extensions):

Download the official Talos disk image for AWS:

curl -LO https://factory.talos.dev/image/376567988ad370138ad8b2698212367b8edcb69b5fd68c80be1f2ec7d603b4ba/v1.12.2/aws-amd64.raw.xz
xz -d aws-amd64.raw.xz
Invoke-WebRequest -Uri "https://factory.talos.dev/image/376567988ad370138ad8b2698212367b8edcb69b5fd68c80be1f2ec7d603b4ba/v1.12.2/aws-amd64.raw.xz" -OutFile "aws-amd64.raw.xz"
7z x aws-amd64.raw.xz

Option B — Custom image with extensions:

Edit schematic.yaml to enable the extensions you need, then submit to Image Factory:

# Submit schematic and get the schematic ID
SCHEMATIC_ID=$(curl -X POST --data-binary @schematic.yaml \
  https://factory.talos.dev/schematics)

# Download the custom disk image
curl -LO "https://factory.talos.dev/image/${SCHEMATIC_ID}/v1.12.2/aws-amd64.raw.xz"
xz -d aws-amd64.raw.xz
# Submit schematic and get the schematic ID
$SCHEMATIC_ID = Invoke-RestMethod -Method Post -InFile schematic.yaml -Uri "https://factory.talos.dev/schematics"

# Download the custom disk image
Invoke-WebRequest -Uri "https://factory.talos.dev/image/$SCHEMATIC_ID/v1.12.2/aws-amd64.raw.xz" -OutFile "aws-amd64.raw.xz"
7z x aws-amd64.raw.xz

Available extensions are listed at github.com/siderolabs/extensions.

Step 3: Upload to S3

  aws s3 cp aws-amd64.raw \
  s3://rciis-talos-images-af-south-1/v1.12.2/aws-amd64.raw \
  --region af-south-1
  aws s3 cp aws-amd64.raw `
  s3://rciis-talos-images-af-south-1/v1.12.2/aws-amd64.raw `
  --region af-south-1

Step 4: Import as EBS Snapshot

IMPORT_TASK=$(aws ec2 import-snapshot \
  --region af-south-1 \
  --description "Talos v1.12.2" \
  --disk-container "Format=raw,UserBucket={S3Bucket=rciis-talos-images-af-south-1,S3Key=v1.12.2/aws-amd64.raw}" \
  --query 'ImportTaskId' --output text)

echo "Import task: $IMPORT_TASK"
$IMPORT_TASK = aws ec2 import-snapshot `
  --region af-south-1 `
  --description "Talos v1.12.2" `
  --disk-container "Format=raw,UserBucket={S3Bucket=rciis-talos-images-af-south-1,S3Key=v1.12.2/aws-amd64.raw}" `
  --query 'ImportTaskId' --output text

Write-Output "Import task: $IMPORT_TASK"

Check import progress (optional)

If you want to monitor progress while the import is running (e.g. in a separate terminal), use describe-import-snapshot-tasks. Unlike wait, this returns immediately with the current status and percentage:

aws ec2 describe-import-snapshot-tasks \
  --import-task-ids $IMPORT_TASK \
  --region af-south-1 \
  --query 'ImportSnapshotTasks[0].SnapshotTaskDetail.{Status:Status,Progress:Progress,SnapshotId:SnapshotId}' \
  --output table
aws ec2 describe-import-snapshot-tasks `
  --import-task-ids $IMPORT_TASK `
  --region af-south-1 `
  --query 'ImportSnapshotTasks[0].SnapshotTaskDetail.{Status:Status,Progress:Progress,SnapshotId:SnapshotId}' `
  --output table

Step 5: Wait for Import and Get Snapshot ID

The wait snapshot-imported command blocks until the import finishes — it polls automatically and returns when the snapshot is ready:

aws ec2 wait snapshot-imported \
  --region af-south-1 \
  --import-task-ids $IMPORT_TASK

SNAPSHOT_ID=$(aws ec2 describe-import-snapshot-tasks \
  --region af-south-1 \
  --import-task-ids $IMPORT_TASK \
  --query 'ImportSnapshotTasks[0].SnapshotTaskDetail.SnapshotId' \
  --output text)

echo "Snapshot: $SNAPSHOT_ID"
aws ec2 wait snapshot-imported `
  --region af-south-1 `
  --import-task-ids $IMPORT_TASK

$SNAPSHOT_ID = aws ec2 describe-import-snapshot-tasks `
  --region af-south-1 `
  --import-task-ids $IMPORT_TASK `
  --query 'ImportSnapshotTasks[0].SnapshotTaskDetail.SnapshotId' `
  --output text

Write-Output "Snapshot: $SNAPSHOT_ID"

Step 6: Register the AMI

VolumeSize must match the snapshot size

The VolumeSize in --block-device-mappings must be equal to or larger than the imported snapshot. The Talos v1.12.2 snapshot is 11 GiB. Using a smaller value (e.g. 4) will fail with InvalidParameterValue.

AMI=$(aws ec2 register-image \
  --region af-south-1 \
  --name "talos-v1.12.2-rciis" \
  --root-device-name /dev/xvda \
  --block-device-mappings "DeviceName=/dev/xvda,Ebs={SnapshotId=${SNAPSHOT_ID},VolumeSize=11,VolumeType=gp3,DeleteOnTermination=true}" \
  --virtualization-type hvm \
  --architecture x86_64 \
  --ena-support \
  --query 'ImageId' --output text)

echo "AMI: $AMI"
$AMI = aws ec2 register-image `
  --region af-south-1 `
  --name "talos-v1.12.2-rciis" `
  --root-device-name /dev/xvda `
  --block-device-mappings "DeviceName=/dev/xvda,Ebs={SnapshotId=$SNAPSHOT_ID,VolumeSize=11,VolumeType=gp3,DeleteOnTermination=true}" `
  --virtualization-type hvm `
  --architecture x86_64 `
  --ena-support `
  --query 'ImageId' --output text

Write-Output "AMI: $AMI"

Deploy EC2 Instances

With the AMI ready, deploy the full cluster infrastructure:

formae apply --mode reconcile --ami $AMI main.pkl

Formae will display a deployment summary and prompt for confirmation before creating resources.

What Gets Created

The modules/compute.pkl module provisions:

Control Plane Instances (default: 3):

Property Value
Instance type t3.large (configurable)
Subnet Private (distributed across AZs round-robin)
Security group Control plane SG
IAM profile talos-node-role-profile
Root volume gp3, encrypted, deleteOnTermination=true
EBS optimised Yes
Source/dest check Disabled (required for Cilium)
Data volume Attached at /dev/xvdf (configurable size, 0 = none)

Worker Instances (default: 5):

Property Value
Instance type t3.xlarge (configurable)
Subnet Private (distributed across AZs round-robin)
Security group Worker SG
IAM profile talos-node-role-profile
Root volume gp3, encrypted, deleteOnTermination=true
EBS optimised Yes
Source/dest check Disabled (required for Cilium)
Data volume Attached at /dev/xvdf (default 200 GB)

Resource Tags

All instances are tagged with:

Tag Value
ManagedBy formae
Environment Environment name
Project rciis
Cluster Cluster name
NodeType control-plane or worker
TalosVersion Talos release version
Schedule always-on (CP) or weekday-business-hours (workers)
kubernetes.io/cluster/<name> owned

Override at Deploy Time

formae apply --mode reconcile \
  --ami $AMI \
  --control-plane-count 1 \
  --worker-count 2 \
  --cp-instance-type t3a.small \
  --worker-instance-type t3a.medium \
  main.pkl

Teardown

formae destroy main.pkl