Add NixOS ZFS EC2 AMI builder
This commit is contained in:
@@ -0,0 +1,45 @@
|
|||||||
|
# NixOS ZFS EC2 AMI Builder
|
||||||
|
|
||||||
|
This project publishes a matched EC2 AMI artifact:
|
||||||
|
|
||||||
|
- root volume: bootable NixOS EC2 image
|
||||||
|
- secondary volume: preseeded ZFS pool mounted at `/nix`
|
||||||
|
|
||||||
|
The root snapshot and `/nix` snapshot are registered together in one AMI block
|
||||||
|
device mapping. Runtime deployments should consume the AMI ID directly and
|
||||||
|
should not independently choose a `/nix` snapshot.
|
||||||
|
|
||||||
|
The image is intentionally project-agnostic. It provides NixOS, ZFS, SSM,
|
||||||
|
flakes, and systemd ordering so the Nix daemon waits for `/nix`.
|
||||||
|
|
||||||
|
## Publish
|
||||||
|
|
||||||
|
The smallest supported flow is:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd tofu/ami-builder
|
||||||
|
nix build .#packages.aarch64-linux.rootImage
|
||||||
|
./publish-ami.sh --bucket s3://my-vm-import-bucket
|
||||||
|
```
|
||||||
|
|
||||||
|
`publish-ami.sh` builds/imports the declarative NixOS root image, boots a
|
||||||
|
temporary seeder instance once to create/export the ZFS `/nix` volume, creates
|
||||||
|
the final AMI from the stopped instance, and writes `artifact.json`.
|
||||||
|
|
||||||
|
The OpenTofu project in this directory owns only the final AMI registration
|
||||||
|
metadata. Its state is separate from any runtime Quixos deployment state.
|
||||||
|
|
||||||
|
## Artifact Metadata
|
||||||
|
|
||||||
|
The final AMI is tagged with:
|
||||||
|
|
||||||
|
- `Architecture`
|
||||||
|
- `InputHash`
|
||||||
|
- `BuilderGitRev`
|
||||||
|
- `FlakeLockRev`
|
||||||
|
- `RootSnapshotId`
|
||||||
|
- `NixSnapshotId`
|
||||||
|
- `NixosSystemClosure`
|
||||||
|
|
||||||
|
Do not deregister the AMI or delete either snapshot while runtime deployments
|
||||||
|
use it.
|
||||||
Generated
+27
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1781577229,
|
||||||
|
"narHash": "sha256-lrp67w8AulE9Ks53n27I45ADSzbOCn4H+CNW1Ck8B+8=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "567a49d1913ce81ac6e9582e3553dd90a955875f",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
description = "Project-agnostic NixOS EC2 AMI with ZFS /nix support";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, ... }:
|
||||||
|
let
|
||||||
|
supportedSystems = [ "aarch64-linux" "x86_64-linux" ];
|
||||||
|
forAllSystems = f:
|
||||||
|
nixpkgs.lib.genAttrs supportedSystems (system: f system);
|
||||||
|
mkConfig = system:
|
||||||
|
nixpkgs.lib.nixosSystem {
|
||||||
|
inherit system;
|
||||||
|
modules = [
|
||||||
|
./nixos/baseline.nix
|
||||||
|
];
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
nixosConfigurations = {
|
||||||
|
baseline-aarch64-linux = mkConfig "aarch64-linux";
|
||||||
|
baseline-x86_64-linux = mkConfig "x86_64-linux";
|
||||||
|
};
|
||||||
|
|
||||||
|
packages = forAllSystems (system:
|
||||||
|
let
|
||||||
|
config = self.nixosConfigurations."baseline-${system}".config;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
rootImage = config.system.build.images.amazon;
|
||||||
|
systemClosure = config.system.build.toplevel;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
locals {
|
||||||
|
resource_name = "${var.name}-${var.architecture}-${substr(var.input_hash, 0, 12)}"
|
||||||
|
tags = {
|
||||||
|
Name = local.resource_name
|
||||||
|
Project = var.name
|
||||||
|
Architecture = var.architecture
|
||||||
|
InputHash = var.input_hash
|
||||||
|
BuilderGitRev = var.builder_git_rev
|
||||||
|
FlakeLockRev = var.flake_lock_rev
|
||||||
|
RootSnapshotId = var.root_snapshot_id
|
||||||
|
NixSnapshotId = var.nix_snapshot_id
|
||||||
|
NixosSystemClosure = var.nixos_system_closure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_ami" "matched" {
|
||||||
|
name = local.resource_name
|
||||||
|
description = "NixOS EC2 AMI with matched root and ZFS /nix snapshots"
|
||||||
|
architecture = var.architecture
|
||||||
|
virtualization_type = "hvm"
|
||||||
|
root_device_name = var.root_device_name
|
||||||
|
ena_support = true
|
||||||
|
|
||||||
|
ebs_block_device {
|
||||||
|
device_name = var.root_device_name
|
||||||
|
snapshot_id = var.root_snapshot_id
|
||||||
|
volume_size = var.root_volume_size_gb
|
||||||
|
volume_type = "gp3"
|
||||||
|
delete_on_termination = true
|
||||||
|
}
|
||||||
|
|
||||||
|
ebs_block_device {
|
||||||
|
device_name = var.nix_device_name
|
||||||
|
snapshot_id = var.nix_snapshot_id
|
||||||
|
volume_size = var.nix_volume_size_gb
|
||||||
|
volume_type = "gp3"
|
||||||
|
delete_on_termination = true
|
||||||
|
}
|
||||||
|
|
||||||
|
tags = local.tags
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
prevent_destroy = true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{ config, lib, modulesPath, pkgs, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
nixPool = "nixos-zfs-nix";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
"${modulesPath}/virtualisation/amazon-image.nix"
|
||||||
|
];
|
||||||
|
|
||||||
|
networking.hostName = "nixos-zfs-ec2";
|
||||||
|
networking.hostId = "6e69787a";
|
||||||
|
|
||||||
|
nix.settings.experimental-features = [ "nix-command" "flakes" ];
|
||||||
|
|
||||||
|
environment.systemPackages = [
|
||||||
|
pkgs.amazon-ssm-agent
|
||||||
|
pkgs.git
|
||||||
|
pkgs.zfs
|
||||||
|
];
|
||||||
|
|
||||||
|
services.amazon-ssm-agent.enable = true;
|
||||||
|
|
||||||
|
boot.supportedFilesystems = [ "zfs" ];
|
||||||
|
boot.zfs.extraPools = [ nixPool ];
|
||||||
|
boot.zfs.forceImportRoot = false;
|
||||||
|
|
||||||
|
fileSystems."/nix" = {
|
||||||
|
device = "${nixPool}/nix";
|
||||||
|
fsType = "zfs";
|
||||||
|
neededForBoot = true;
|
||||||
|
options = [
|
||||||
|
"nofail"
|
||||||
|
"x-systemd.device-timeout=30s"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.sockets.nix-daemon = {
|
||||||
|
after = [ "nix.mount" ];
|
||||||
|
requires = [ "nix.mount" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.nix-daemon = {
|
||||||
|
after = [ "nix.mount" ];
|
||||||
|
requires = [ "nix.mount" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
system.stateVersion = "25.05";
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
output "ami_id" {
|
||||||
|
value = aws_ami.matched.id
|
||||||
|
}
|
||||||
|
|
||||||
|
output "architecture" {
|
||||||
|
value = var.architecture
|
||||||
|
}
|
||||||
|
|
||||||
|
output "input_hash" {
|
||||||
|
value = var.input_hash
|
||||||
|
}
|
||||||
|
|
||||||
|
output "builder_git_rev" {
|
||||||
|
value = var.builder_git_rev
|
||||||
|
}
|
||||||
|
|
||||||
|
output "flake_lock_rev" {
|
||||||
|
value = var.flake_lock_rev
|
||||||
|
}
|
||||||
|
|
||||||
|
output "root_snapshot_id" {
|
||||||
|
value = var.root_snapshot_id
|
||||||
|
}
|
||||||
|
|
||||||
|
output "nix_snapshot_id" {
|
||||||
|
value = var.nix_snapshot_id
|
||||||
|
}
|
||||||
|
|
||||||
|
output "nixos_system_closure" {
|
||||||
|
value = var.nixos_system_closure
|
||||||
|
}
|
||||||
Executable
+363
@@ -0,0 +1,363 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
aws_region="${AWS_REGION:-us-west-2}"
|
||||||
|
architecture="arm64"
|
||||||
|
bucket=""
|
||||||
|
subnet_id=""
|
||||||
|
security_group_id=""
|
||||||
|
instance_profile_name=""
|
||||||
|
instance_type="t4g.small"
|
||||||
|
nix_volume_size_gb="50"
|
||||||
|
name="nixos-zfs-ec2"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat >&2 <<'EOF'
|
||||||
|
Usage: publish-ami.sh --bucket s3://bucket[/prefix] [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--architecture <arm64|x86_64> AMI architecture (default: arm64)
|
||||||
|
--aws-region <region> AWS region (default: AWS_REGION or us-west-2)
|
||||||
|
--bucket <s3-uri> S3 URI used by EC2 VM import
|
||||||
|
--subnet-id <subnet> Subnet for the temporary seeder instance
|
||||||
|
--security-group-id <sg> Security group for the temporary seeder instance
|
||||||
|
--instance-profile-name <profile> Optional IAM instance profile for the seeder
|
||||||
|
--instance-type <type> Seeder instance type (default: t4g.small)
|
||||||
|
--nix-volume-size-gb <gb> /nix volume size (default: 50)
|
||||||
|
--name <name> AMI name prefix
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--architecture)
|
||||||
|
architecture="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--aws-region)
|
||||||
|
aws_region="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--bucket)
|
||||||
|
bucket="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--subnet-id)
|
||||||
|
subnet_id="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--security-group-id)
|
||||||
|
security_group_id="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--instance-profile-name)
|
||||||
|
instance_profile_name="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--instance-type)
|
||||||
|
instance_type="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--nix-volume-size-gb)
|
||||||
|
nix_volume_size_gb="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--name)
|
||||||
|
name="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown argument: $1" >&2
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$bucket" ]; then
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$instance_profile_name" ]; then
|
||||||
|
echo "Missing required --instance-profile-name for SSM seeding" >&2
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$architecture" in
|
||||||
|
arm64)
|
||||||
|
nix_system="aarch64-linux"
|
||||||
|
;;
|
||||||
|
x86_64)
|
||||||
|
nix_system="x86_64-linux"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported architecture: $architecture" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
require() {
|
||||||
|
if ! command -v "$1" >/dev/null 2>&1; then
|
||||||
|
echo "Missing required command: $1" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
require aws
|
||||||
|
require git
|
||||||
|
require jq
|
||||||
|
require nix
|
||||||
|
require sha256sum
|
||||||
|
require tofu
|
||||||
|
|
||||||
|
repo_root="$(git rev-parse --show-toplevel)"
|
||||||
|
if ! git diff --quiet || ! git diff --cached --quiet; then
|
||||||
|
echo "The AMI builder git tree must be clean before publishing." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
builder_git_rev="$(git rev-parse HEAD)"
|
||||||
|
flake_lock_rev="$(jq -r '.nodes.nixpkgs.locked.rev // .nodes.nixpkgs.locked.narHash // "unknown"' "$repo_root/flake.lock")"
|
||||||
|
input_hash="$(printf '%s\n%s\n%s\n' "$builder_git_rev" "$flake_lock_rev" "$architecture" | sha256sum | awk '{ print $1 }')"
|
||||||
|
|
||||||
|
root_image="$(nix build --print-out-paths ".#packages.${nix_system}.rootImage")"
|
||||||
|
nixos_system_closure="$(nix build --print-out-paths ".#packages.${nix_system}.systemClosure")"
|
||||||
|
|
||||||
|
tmpdir="$(mktemp -d)"
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$tmpdir"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
bucket_no_scheme="${bucket#s3://}"
|
||||||
|
bucket_name="${bucket_no_scheme%%/*}"
|
||||||
|
bucket_prefix="${bucket_no_scheme#"$bucket_name"}"
|
||||||
|
bucket_prefix="${bucket_prefix#/}"
|
||||||
|
s3_key_prefix="${bucket_prefix:+$bucket_prefix/}${name}/${architecture}/${input_hash}"
|
||||||
|
|
||||||
|
image_file="$(find "$root_image" -type f \( -name '*.vhd' -o -name '*.raw' \) | head -n 1)"
|
||||||
|
if [ -z "$image_file" ]; then
|
||||||
|
echo "Could not locate built root image under $root_image" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$image_file" in
|
||||||
|
*.vhd)
|
||||||
|
import_format="vhd"
|
||||||
|
;;
|
||||||
|
*.raw)
|
||||||
|
import_format="raw"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported root image format: $image_file" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
aws s3 cp "$image_file" "s3://${bucket_name}/${s3_key_prefix}/root-image"
|
||||||
|
|
||||||
|
container_json="$tmpdir/container.json"
|
||||||
|
jq -n \
|
||||||
|
--arg bucket "$bucket_name" \
|
||||||
|
--arg key "${s3_key_prefix}/root-image" \
|
||||||
|
--arg format "$import_format" \
|
||||||
|
'[{Description:"nixos-zfs-ec2-root",Format:$format,UserBucket:{S3Bucket:$bucket,S3Key:$key}}]' \
|
||||||
|
> "$container_json"
|
||||||
|
|
||||||
|
import_task_id="$(
|
||||||
|
aws ec2 import-image \
|
||||||
|
--region "$aws_region" \
|
||||||
|
--architecture "$architecture" \
|
||||||
|
--platform Linux \
|
||||||
|
--disk-containers "file://$container_json" \
|
||||||
|
--query ImportTaskId \
|
||||||
|
--output text
|
||||||
|
)"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
import_json="$(aws ec2 describe-import-image-tasks --region "$aws_region" --import-task-ids "$import_task_id")"
|
||||||
|
status="$(jq -r '.ImportImageTasks[0].Status' <<<"$import_json")"
|
||||||
|
case "$status" in
|
||||||
|
completed)
|
||||||
|
root_ami_id="$(jq -r '.ImportImageTasks[0].ImageId' <<<"$import_json")"
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
deleted|deleting)
|
||||||
|
echo "$import_json" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
status_message="$(jq -r '.ImportImageTasks[0].StatusMessage // ""' <<<"$import_json")"
|
||||||
|
echo "import-image: $status $status_message" >&2
|
||||||
|
sleep 20
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
root_snapshot_id="$(
|
||||||
|
aws ec2 describe-images \
|
||||||
|
--region "$aws_region" \
|
||||||
|
--image-ids "$root_ami_id" \
|
||||||
|
--query 'Images[0].BlockDeviceMappings[?Ebs != `null`][0].Ebs.SnapshotId' \
|
||||||
|
--output text
|
||||||
|
)"
|
||||||
|
|
||||||
|
run_args=(
|
||||||
|
--region "$aws_region"
|
||||||
|
--image-id "$root_ami_id"
|
||||||
|
--instance-type "$instance_type"
|
||||||
|
--block-device-mappings "DeviceName=/dev/sdf,Ebs={VolumeSize=${nix_volume_size_gb},VolumeType=gp3,DeleteOnTermination=true}"
|
||||||
|
--tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=${name}-seed-${input_hash:0:12}}]"
|
||||||
|
--query 'Instances[0].InstanceId'
|
||||||
|
--output text
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ -n "$subnet_id" ]; then
|
||||||
|
run_args+=(--subnet-id "$subnet_id")
|
||||||
|
fi
|
||||||
|
if [ -n "$security_group_id" ]; then
|
||||||
|
run_args+=(--security-group-ids "$security_group_id")
|
||||||
|
fi
|
||||||
|
if [ -n "$instance_profile_name" ]; then
|
||||||
|
run_args+=(--iam-instance-profile "Name=$instance_profile_name")
|
||||||
|
fi
|
||||||
|
|
||||||
|
instance_id="$(aws ec2 run-instances "${run_args[@]}")"
|
||||||
|
aws ec2 wait instance-running --region "$aws_region" --instance-ids "$instance_id"
|
||||||
|
aws ec2 wait instance-status-ok --region "$aws_region" --instance-ids "$instance_id"
|
||||||
|
|
||||||
|
cat > "$tmpdir/seed-nix.sh" <<'EOS'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
pool="nixos-zfs-nix"
|
||||||
|
device=""
|
||||||
|
for candidate in /dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_*; do
|
||||||
|
if [ -e "$candidate" ] && ! findmnt -n -S "$candidate" >/dev/null 2>&1; then
|
||||||
|
device="$candidate"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$device" ]; then
|
||||||
|
echo "Unable to find unattached EBS device for /nix seed" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
zpool create \
|
||||||
|
-f \
|
||||||
|
-o ashift=12 \
|
||||||
|
-O acltype=posixacl \
|
||||||
|
-O atime=off \
|
||||||
|
-O compression=zstd \
|
||||||
|
-O mountpoint=none \
|
||||||
|
-O xattr=sa \
|
||||||
|
"$pool" "$device"
|
||||||
|
|
||||||
|
zfs create -o mountpoint=legacy "$pool/nix"
|
||||||
|
mkdir -p /mnt/nixos-zfs-nix
|
||||||
|
mount -t zfs "$pool/nix" /mnt/nixos-zfs-nix
|
||||||
|
rsync -aHAX --numeric-ids \
|
||||||
|
--exclude='/var/nix/daemon-socket/*' \
|
||||||
|
/nix/ /mnt/nixos-zfs-nix/
|
||||||
|
rm -f /mnt/nixos-zfs-nix/var/nix/daemon-socket/socket
|
||||||
|
umount /mnt/nixos-zfs-nix
|
||||||
|
zpool export "$pool"
|
||||||
|
EOS
|
||||||
|
|
||||||
|
jq -n --rawfile commands "$tmpdir/seed-nix.sh" '{commands: [$commands]}' > "$tmpdir/ssm-parameters.json"
|
||||||
|
|
||||||
|
command_id="$(
|
||||||
|
aws ssm send-command \
|
||||||
|
--region "$aws_region" \
|
||||||
|
--instance-ids "$instance_id" \
|
||||||
|
--document-name AWS-RunShellScript \
|
||||||
|
--parameters "file://$tmpdir/ssm-parameters.json" \
|
||||||
|
--query Command.CommandId \
|
||||||
|
--output text
|
||||||
|
)"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
invocation_json="$(aws ssm get-command-invocation --region "$aws_region" --command-id "$command_id" --instance-id "$instance_id" 2>/dev/null || true)"
|
||||||
|
status="$(jq -r '.Status // "Pending"' <<<"$invocation_json")"
|
||||||
|
case "$status" in
|
||||||
|
Success)
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
Failed|Cancelled|TimedOut)
|
||||||
|
echo "$invocation_json" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "seed /nix: $status" >&2
|
||||||
|
sleep 10
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
aws ec2 stop-instances --region "$aws_region" --instance-ids "$instance_id" >/dev/null
|
||||||
|
aws ec2 wait instance-stopped --region "$aws_region" --instance-ids "$instance_id"
|
||||||
|
|
||||||
|
nix_volume_id="$(
|
||||||
|
aws ec2 describe-instances \
|
||||||
|
--region "$aws_region" \
|
||||||
|
--instance-ids "$instance_id" \
|
||||||
|
--query 'Reservations[0].Instances[0].BlockDeviceMappings[?DeviceName==`/dev/sdf`].Ebs.VolumeId | [0]' \
|
||||||
|
--output text
|
||||||
|
)"
|
||||||
|
|
||||||
|
nix_snapshot_id="$(
|
||||||
|
aws ec2 create-snapshot \
|
||||||
|
--region "$aws_region" \
|
||||||
|
--volume-id "$nix_volume_id" \
|
||||||
|
--description "${name} ${architecture} ZFS /nix ${input_hash}" \
|
||||||
|
--tag-specifications "ResourceType=snapshot,Tags=[{Key=Name,Value=${name}-nix-${architecture}-${input_hash:0:12}},{Key=InputHash,Value=${input_hash}},{Key=BuilderGitRev,Value=${builder_git_rev}}]" \
|
||||||
|
--query SnapshotId \
|
||||||
|
--output text
|
||||||
|
)"
|
||||||
|
aws ec2 wait snapshot-completed --region "$aws_region" --snapshot-ids "$nix_snapshot_id"
|
||||||
|
|
||||||
|
tofu init
|
||||||
|
tofu apply \
|
||||||
|
-var "aws_region=$aws_region" \
|
||||||
|
-var "name=$name" \
|
||||||
|
-var "architecture=$architecture" \
|
||||||
|
-var "root_snapshot_id=$root_snapshot_id" \
|
||||||
|
-var "nix_snapshot_id=$nix_snapshot_id" \
|
||||||
|
-var "input_hash=$input_hash" \
|
||||||
|
-var "builder_git_rev=$builder_git_rev" \
|
||||||
|
-var "flake_lock_rev=$flake_lock_rev" \
|
||||||
|
-var "nixos_system_closure=$nixos_system_closure" \
|
||||||
|
-var "nix_volume_size_gb=$nix_volume_size_gb"
|
||||||
|
|
||||||
|
ami_id="$(tofu output -raw ami_id)"
|
||||||
|
|
||||||
|
aws ec2 terminate-instances --region "$aws_region" --instance-ids "$instance_id" >/dev/null
|
||||||
|
aws ec2 deregister-image --region "$aws_region" --image-id "$root_ami_id" >/dev/null || true
|
||||||
|
|
||||||
|
jq -n \
|
||||||
|
--arg architecture "$architecture" \
|
||||||
|
--arg inputHash "$input_hash" \
|
||||||
|
--arg builderGitRev "$builder_git_rev" \
|
||||||
|
--arg flakeLockRev "$flake_lock_rev" \
|
||||||
|
--arg amiId "$ami_id" \
|
||||||
|
--arg rootSnapshotId "$root_snapshot_id" \
|
||||||
|
--arg nixSnapshotId "$nix_snapshot_id" \
|
||||||
|
--arg nixosSystemClosure "$nixos_system_closure" \
|
||||||
|
'{
|
||||||
|
architecture: $architecture,
|
||||||
|
inputHash: $inputHash,
|
||||||
|
builderGitRev: $builderGitRev,
|
||||||
|
flakeLockRev: $flakeLockRev,
|
||||||
|
amiId: $amiId,
|
||||||
|
rootSnapshotId: $rootSnapshotId,
|
||||||
|
nixSnapshotId: $nixSnapshotId,
|
||||||
|
nixosSystemClosure: $nixosSystemClosure
|
||||||
|
}' > artifact.json
|
||||||
|
|
||||||
|
echo "Published AMI: $ami_id"
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
variable "aws_region" {
|
||||||
|
type = string
|
||||||
|
default = "us-west-2"
|
||||||
|
description = "AWS region."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "name" {
|
||||||
|
type = string
|
||||||
|
default = "nixos-zfs-ec2"
|
||||||
|
description = "Name prefix for the published AMI."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "architecture" {
|
||||||
|
type = string
|
||||||
|
description = "AMI architecture, such as arm64 or x86_64."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "root_snapshot_id" {
|
||||||
|
type = string
|
||||||
|
description = "Snapshot ID for the bootable NixOS root volume."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nix_snapshot_id" {
|
||||||
|
type = string
|
||||||
|
description = "Snapshot ID for the preseeded ZFS /nix volume."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "input_hash" {
|
||||||
|
type = string
|
||||||
|
description = "Stable hash of the AMI builder inputs."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "builder_git_rev" {
|
||||||
|
type = string
|
||||||
|
description = "Git revision of this AMI builder."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "flake_lock_rev" {
|
||||||
|
type = string
|
||||||
|
description = "Revision or hash representing flake.lock inputs."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nixos_system_closure" {
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
description = "NixOS system closure path used to build the root image."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "root_device_name" {
|
||||||
|
type = string
|
||||||
|
default = "/dev/xvda"
|
||||||
|
description = "Root block device name used for AMI registration."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nix_device_name" {
|
||||||
|
type = string
|
||||||
|
default = "/dev/sdf"
|
||||||
|
description = "AMI block device name for the /nix EBS snapshot. Runtime imports by ZFS pool identity, not this device name."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "root_volume_size_gb" {
|
||||||
|
type = number
|
||||||
|
default = 30
|
||||||
|
description = "Root EBS volume size."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nix_volume_size_gb" {
|
||||||
|
type = number
|
||||||
|
default = 50
|
||||||
|
description = "/nix EBS volume size."
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.8.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
aws = {
|
||||||
|
source = "hashicorp/aws"
|
||||||
|
version = "~> 5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "aws" {
|
||||||
|
region = var.aws_region
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user