diff --git a/docs/spec/disk-images.md b/docs/spec/disk-images.md index 989e7e9..05ef8d9 100644 --- a/docs/spec/disk-images.md +++ b/docs/spec/disk-images.md @@ -492,3 +492,15 @@ SHA256 checksums of all output files must be written to a `SHA256SUMS` file. > `Os`, `OsVersion`, `Variant`, `Architecture`, `Version`, `Features`, and > `Builder`. `OsVersion` must hold the numeric Ubuntu release > (`` above). + +> r[image.output.aws-ami-public+4] +> Each released AMI must be made publicly launchable, and a public copy must be +> registered in every mirror region listed in the build matrix, so consumers +> can launch directly in their own region without first copying the AMI across +> regions. +> +> AWS caps the number of public images allowed per region. To stay within that +> cap, publishing a release's AMIs in a region must first make any public AMIs +> from earlier releases in that region private. After a region has been +> published, the only AMIs left public in it must be those belonging to the +> release being published. diff --git a/scripts/copy-ami-to-region.sh b/scripts/copy-ami-to-region.sh index 66e3c1c..950e6d0 100755 --- a/scripts/copy-ami-to-region.sh +++ b/scripts/copy-ami-to-region.sh @@ -25,6 +25,8 @@ set -euo pipefail # source-region AWS region where the AMI was originally registered # target-region AWS region to copy the AMI into +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + ARCH="${1:-}" SUITE="${2:-}" VERSION="${3:-}" @@ -165,6 +167,12 @@ aws ec2 create-tags \ echo "Tagged backing snapshot $SNAPSHOT_ID" echo "" +# --- Demote prior releases' public AMIs to stay under the per-region cap --- + +# r[impl image.output.aws-ami-public+4] +"$SCRIPT_DIR/demote-public-amis.sh" "$TARGET_REGION" "$VERSION" +echo "" + # --- Make AMI and snapshot public --- echo "Publishing AMI and snapshot ..." diff --git a/scripts/demote-public-amis.sh b/scripts/demote-public-amis.sh new file mode 100755 index 0000000..b137c19 --- /dev/null +++ b/scripts/demote-public-amis.sh @@ -0,0 +1,93 @@ +#!/bin/bash +set -euo pipefail + +# r[impl image.output.aws-ami-public+4] +# Make every self-owned public AMI in a region private, except those belonging +# to the release we are about to publish. AWS caps public images per region +# (default 5); without this, each release's new public AMIs stack on top of the +# previous release's and eventually exceed the quota at publish time. +# +# Keeping the version we're publishing public makes this safe to run +# concurrently across the per-(arch, suite) matrix legs that all share one +# version: no leg ever demotes an AMI another leg is about to publish, and +# revoking an already-revoked permission is a no-op. +# +# Usage: ./demote-public-amis.sh +# +# Arguments: +# region AWS region to sweep +# keep-version Release version (Version tag value) to leave public + +REGION="${1:-}" +KEEP_VERSION="${2:-}" + +if [ -z "$REGION" ] || [ -z "$KEEP_VERSION" ]; then + echo "Usage: $0 " + exit 1 +fi + +echo "Demoting stale public AMIs in $REGION (keeping version $KEEP_VERSION) ..." + +# Self-owned public AMIs whose Version tag is missing or differs from the +# version being published, paired with their backing snapshot. A missing tag +# still counts as stale: this is a dedicated publishing account, so anything +# public that isn't the current release is fair game. +list_stale() { + aws ec2 describe-images \ + --region "$REGION" \ + --owners self \ + --filters "Name=is-public,Values=true" \ + --output json \ + | jq -r --arg keep "$KEEP_VERSION" ' + .Images[] + | { id: .ImageId, + snap: (.BlockDeviceMappings[0].Ebs.SnapshotId // ""), + ver: ((.Tags // []) | map(select(.Key == "Version")) | .[0].Value // "") } + | select(.ver != $keep) + | "\(.id)\t\(.snap)"' +} + +# Revoke public permissions, then re-check. describe-images is eventually +# consistent, so a single pass can still report a just-revoked AMI as public; +# loop until the region is clean (or give up). The revoke itself is idempotent, +# so re-revoking across rounds is harmless. +MAX_ROUNDS=6 +round=0 +while true; do + STALE=$(list_stale) + if [ -z "$STALE" ]; then + echo "No stale public AMIs remain in $REGION." + break + fi + + round=$((round + 1)) + if [ "$round" -gt "$MAX_ROUNDS" ]; then + echo "ERROR: public AMIs from other releases still present in $REGION after $MAX_ROUNDS rounds:" >&2 + echo "$STALE" >&2 + exit 1 + fi + + while IFS=$'\t' read -r AMI_ID SNAPSHOT_ID; do + [ -z "$AMI_ID" ] && continue + echo " Making $AMI_ID private (snapshot ${SNAPSHOT_ID:-none}) ..." + aws ec2 modify-image-attribute \ + --region "$REGION" \ + --image-id "$AMI_ID" \ + --launch-permission 'Remove=[{Group=all}]' + if [ -n "$SNAPSHOT_ID" ] && [ "$SNAPSHOT_ID" != "None" ]; then + aws ec2 modify-snapshot-attribute \ + --region "$REGION" \ + --snapshot-id "$SNAPSHOT_ID" \ + --create-volume-permission 'Remove=[{Group=all}]' + fi + done <<< "$STALE" + + # Give the revoke a moment to propagate before re-checking. + sleep 5 +done + +# r[verify image.output.aws-ami-public+4] +# The loop only exits once list_stale returns empty, so reaching here means no +# AMI from another release is left public — the publish that follows starts +# well under the per-region cap. +echo "Demotion complete." diff --git a/scripts/register-ami-for-release.sh b/scripts/register-ami-for-release.sh index 6eff93a..6d75857 100755 --- a/scripts/register-ami-for-release.sh +++ b/scripts/register-ami-for-release.sh @@ -224,6 +224,12 @@ aws ec2 create-tags \ echo "Tagged AMI and snapshot" echo "" +# --- Demote prior releases' public AMIs to stay under the per-region cap --- + +# r[impl image.output.aws-ami-public+4] +"$SCRIPT_DIR/demote-public-amis.sh" "$REGION" "$VERSION" +echo "" + # --- Make AMI and snapshot public --- # Public launch on the AMI lets any AWS account run instances from it; public