at 21.11-pre 8.6 kB view raw
1#!/usr/bin/env nix-shell 2#!nix-shell -p awscli -p jq -p qemu -i bash 3# shellcheck shell=bash 4 5# Uploads and registers NixOS images built from the 6# <nixos/release.nix> amazonImage attribute. Images are uploaded and 7# registered via a home region, and then copied to other regions. 8 9# The home region requires an s3 bucket, and an IAM role named "vmimport" 10# (by default) with access to the S3 bucket. The name can be 11# configured with the "service_role_name" variable. Configuration of the 12# vmimport role is documented in 13# https://docs.aws.amazon.com/vm-import/latest/userguide/vmimport-image-import.html 14 15# set -x 16set -euo pipefail 17 18# configuration 19state_dir=$HOME/amis/ec2-images 20home_region=eu-west-1 21bucket=nixos-amis 22service_role_name=vmimport 23 24regions=(eu-west-1 eu-west-2 eu-west-3 eu-central-1 eu-north-1 25 us-east-1 us-east-2 us-west-1 us-west-2 26 ca-central-1 27 ap-southeast-1 ap-southeast-2 ap-northeast-1 ap-northeast-2 28 ap-south-1 ap-east-1 29 sa-east-1) 30 31log() { 32 echo "$@" >&2 33} 34 35if [ "$#" -ne 1 ]; then 36 log "Usage: ./upload-amazon-image.sh IMAGE_OUTPUT" 37 exit 1 38fi 39 40# result of the amazon-image from nixos/release.nix 41store_path=$1 42 43if [ ! -e "$store_path" ]; then 44 log "Store path: $store_path does not exist, fetching..." 45 nix-store --realise "$store_path" 46fi 47 48if [ ! -d "$store_path" ]; then 49 log "store_path: $store_path is not a directory. aborting" 50 exit 1 51fi 52 53read_image_info() { 54 if [ ! -e "$store_path/nix-support/image-info.json" ]; then 55 log "Image missing metadata" 56 exit 1 57 fi 58 jq -r "$1" "$store_path/nix-support/image-info.json" 59} 60 61# We handle a single image per invocation, store all attributes in 62# globals for convenience. 63image_label=$(read_image_info .label) 64image_system=$(read_image_info .system) 65image_file=$(read_image_info .file) 66image_logical_bytes=$(read_image_info .logical_bytes) 67 68# Derived attributes 69 70image_logical_gigabytes=$(((image_logical_bytes-1)/1024/1024/1024+1)) # Round to the next GB 71 72case "$image_system" in 73 aarch64-linux) 74 amazon_arch=arm64 75 ;; 76 x86_64-linux) 77 amazon_arch=x86_64 78 ;; 79 *) 80 log "Unknown system: $image_system" 81 exit 1 82esac 83 84image_name="NixOS-${image_label}-${image_system}" 85image_description="NixOS ${image_label} ${image_system}" 86 87log "Image Details:" 88log " Name: $image_name" 89log " Description: $image_description" 90log " Size (gigabytes): $image_logical_gigabytes" 91log " System: $image_system" 92log " Amazon Arch: $amazon_arch" 93 94read_state() { 95 local state_key=$1 96 local type=$2 97 98 cat "$state_dir/$state_key.$type" 2>/dev/null || true 99} 100 101write_state() { 102 local state_key=$1 103 local type=$2 104 local val=$3 105 106 mkdir -p "$state_dir" 107 echo "$val" > "$state_dir/$state_key.$type" 108} 109 110wait_for_import() { 111 local region=$1 112 local task_id=$2 113 local state snapshot_id 114 log "Waiting for import task $task_id to be completed" 115 while true; do 116 read -r state progress snapshot_id < <( 117 aws ec2 describe-import-snapshot-tasks --region "$region" --import-task-ids "$task_id" | \ 118 jq -r '.ImportSnapshotTasks[].SnapshotTaskDetail | "\(.Status) \(.Progress) \(.SnapshotId)"' 119 ) 120 log " ... state=$state progress=$progress snapshot_id=$snapshot_id" 121 case "$state" in 122 active) 123 sleep 10 124 ;; 125 completed) 126 echo "$snapshot_id" 127 return 128 ;; 129 *) 130 log "Unexpected snapshot import state: '${state}'" 131 log "Full response: " 132 aws ec2 describe-import-snapshot-tasks --region "$region" --import-task-ids "$task_id" >&2 133 exit 1 134 ;; 135 esac 136 done 137} 138 139wait_for_image() { 140 local region=$1 141 local ami_id=$2 142 local state 143 log "Waiting for image $ami_id to be available" 144 145 while true; do 146 read -r state < <( 147 aws ec2 describe-images --image-ids "$ami_id" --region "$region" | \ 148 jq -r ".Images[].State" 149 ) 150 log " ... state=$state" 151 case "$state" in 152 pending) 153 sleep 10 154 ;; 155 available) 156 return 157 ;; 158 *) 159 log "Unexpected AMI state: '${state}'" 160 exit 1 161 ;; 162 esac 163 done 164} 165 166 167make_image_public() { 168 local region=$1 169 local ami_id=$2 170 171 wait_for_image "$region" "$ami_id" 172 173 log "Making image $ami_id public" 174 175 aws ec2 modify-image-attribute \ 176 --image-id "$ami_id" --region "$region" --launch-permission 'Add={Group=all}' >&2 177} 178 179upload_image() { 180 local region=$1 181 182 local aws_path=${image_file#/} 183 184 local state_key="$region.$image_label.$image_system" 185 local task_id 186 task_id=$(read_state "$state_key" task_id) 187 local snapshot_id 188 snapshot_id=$(read_state "$state_key" snapshot_id) 189 local ami_id 190 ami_id=$(read_state "$state_key" ami_id) 191 192 if [ -z "$task_id" ]; then 193 log "Checking for image on S3" 194 if ! aws s3 ls --region "$region" "s3://${bucket}/${aws_path}" >&2; then 195 log "Image missing from aws, uploading" 196 aws s3 cp --region "$region" "$image_file" "s3://${bucket}/${aws_path}" >&2 197 fi 198 199 log "Importing image from S3 path s3://$bucket/$aws_path" 200 201 task_id=$(aws ec2 import-snapshot --role-name "$service_role_name" --disk-container "{ 202 \"Description\": \"nixos-image-${image_label}-${image_system}\", 203 \"Format\": \"vhd\", 204 \"UserBucket\": { 205 \"S3Bucket\": \"$bucket\", 206 \"S3Key\": \"$aws_path\" 207 } 208 }" --region "$region" | jq -r '.ImportTaskId') 209 210 write_state "$state_key" task_id "$task_id" 211 fi 212 213 if [ -z "$snapshot_id" ]; then 214 snapshot_id=$(wait_for_import "$region" "$task_id") 215 write_state "$state_key" snapshot_id "$snapshot_id" 216 fi 217 218 if [ -z "$ami_id" ]; then 219 log "Registering snapshot $snapshot_id as AMI" 220 221 local block_device_mappings=( 222 "DeviceName=/dev/xvda,Ebs={SnapshotId=$snapshot_id,VolumeSize=$image_logical_gigabytes,DeleteOnTermination=true,VolumeType=gp3}" 223 ) 224 225 local extra_flags=( 226 --root-device-name /dev/xvda 227 --sriov-net-support simple 228 --ena-support 229 --virtualization-type hvm 230 ) 231 232 block_device_mappings+=("DeviceName=/dev/sdb,VirtualName=ephemeral0") 233 block_device_mappings+=("DeviceName=/dev/sdc,VirtualName=ephemeral1") 234 block_device_mappings+=("DeviceName=/dev/sdd,VirtualName=ephemeral2") 235 block_device_mappings+=("DeviceName=/dev/sde,VirtualName=ephemeral3") 236 237 ami_id=$( 238 aws ec2 register-image \ 239 --name "$image_name" \ 240 --description "$image_description" \ 241 --region "$region" \ 242 --architecture $amazon_arch \ 243 --block-device-mappings "${block_device_mappings[@]}" \ 244 "${extra_flags[@]}" \ 245 | jq -r '.ImageId' 246 ) 247 248 write_state "$state_key" ami_id "$ami_id" 249 fi 250 251 make_image_public "$region" "$ami_id" 252 253 echo "$ami_id" 254} 255 256copy_to_region() { 257 local region=$1 258 local from_region=$2 259 local from_ami_id=$3 260 261 state_key="$region.$image_label.$image_system" 262 ami_id=$(read_state "$state_key" ami_id) 263 264 if [ -z "$ami_id" ]; then 265 log "Copying $from_ami_id to $region" 266 ami_id=$( 267 aws ec2 copy-image \ 268 --region "$region" \ 269 --source-region "$from_region" \ 270 --source-image-id "$from_ami_id" \ 271 --name "$image_name" \ 272 --description "$image_description" \ 273 | jq -r '.ImageId' 274 ) 275 276 write_state "$state_key" ami_id "$ami_id" 277 fi 278 279 make_image_public "$region" "$ami_id" 280 281 echo "$ami_id" 282} 283 284upload_all() { 285 home_image_id=$(upload_image "$home_region") 286 jq -n \ 287 --arg key "$home_region.$image_system" \ 288 --arg value "$home_image_id" \ 289 '$ARGS.named' 290 291 for region in "${regions[@]}"; do 292 if [ "$region" = "$home_region" ]; then 293 continue 294 fi 295 copied_image_id=$(copy_to_region "$region" "$home_region" "$home_image_id") 296 297 jq -n \ 298 --arg key "$region.$image_system" \ 299 --arg value "$copied_image_id" \ 300 '$ARGS.named' 301 done 302} 303 304upload_all | jq --slurp from_entries