diff --git a/README.md b/README.md index 655686e..e090d6b 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,14 @@ The final AMI is tagged with: - `NixSnapshotId` - `NixosSystemClosure` +The publisher also writes SSM parameters that point to the AMI ID: + +- `/quixos/amis///recommended` +- `/quixos/amis///by-builder-rev/` +- `/quixos/amis///by-input-hash/` + +Central services read the `recommended` parameter by default. + Do not deregister the AMI or delete either snapshot while runtime deployments use it. @@ -69,6 +77,13 @@ List published AMIs and cross-reference qx-deploy runtime registry records: nix run ./tofu/ami-builder#ami-artifacts -- list ``` +Publish SSM parameters for an existing AMI, useful for AMIs created before SSM +publication was added: + +```bash +nix run ./tofu/ami-builder#ami-artifacts -- publish-ssm --ami-id ami-... +``` + Delete an unused AMI and its root and `/nix` snapshots: ```bash diff --git a/manage-amis.ts b/manage-amis.ts index 8fff874..5f363b6 100644 --- a/manage-amis.ts +++ b/manage-amis.ts @@ -10,14 +10,15 @@ const repoRoot = process.env.QUIXOS_REPO_ROOT ?? path.resolve(sourceScriptDir, " const amiBuilderDir = process.env.QUIXOS_AMI_BUILDER_DIR ?? path.resolve(repoRoot, "tofu/ami-builder"); const centralDir = path.resolve(amiBuilderDir, "../central"); -const usage = `Usage: nix run ./tofu/ami-builder#ami-artifacts -- [options] +const usage = `Usage: nix run ./tofu/ami-builder#ami-artifacts -- [options] Commands: - list List published AMI artifacts and deployment references - delete --ami-id Deregister an AMI and delete its root and /nix snapshots + list List published AMI artifacts and deployment references + delete --ami-id Deregister an AMI and delete its root and /nix snapshots + publish-ssm --ami-id Publish an existing AMI into the SSM artifact registry Options: - --ami-id AMI to delete + --ami-id AMI to operate on --aws-region AWS region (default: AWS_REGION or us-west-2) --name AMI project tag (default: nixos-zfs-ec2) --architecture Filter by architecture @@ -62,7 +63,7 @@ const parseArgs = (argv) => { console.error(usage.trimEnd()); process.exit(command ? 0 : 1); } - if (command !== "list" && command !== "delete") { + if (command !== "list" && command !== "delete" && command !== "publish-ssm") { throw new Error(`Unknown command: ${command}\n\n${usage}`); } @@ -287,6 +288,72 @@ const deleteArtifact = (state) => { } }; +const getImage = (state) => { + const result = awsJson([ + "ec2", + "describe-images", + "--region", + state.awsRegion, + "--owners", + "self", + "--image-ids", + state.amiId, + ]); + const image = result?.Images?.[0]; + if (!image) { + throw new Error(`AMI not found: ${state.amiId}`); + } + return image; +}; + +const putSsmParameter = (state, name, value) => { + run("aws", [ + "ssm", + "put-parameter", + "--region", + state.awsRegion, + "--name", + name, + "--type", + "String", + "--value", + value, + "--overwrite", + ]); +}; + +const publishSsmArtifact = (state) => { + if (!state.amiId) { + throw new Error("Missing --ami-id"); + } + + const image = getImage(state); + const tags = amiTags(image); + const architecture = tags.Architecture ?? image.Architecture ?? ""; + const builderGitRev = tags.BuilderGitRev ?? ""; + const inputHash = tags.InputHash ?? ""; + if (!architecture) { + throw new Error(`AMI ${state.amiId} is missing Architecture metadata`); + } + if (!builderGitRev) { + throw new Error(`AMI ${state.amiId} is missing BuilderGitRev tag`); + } + if (!inputHash) { + throw new Error(`AMI ${state.amiId} is missing InputHash tag`); + } + + const prefix = `/quixos/amis/${state.name}/${architecture}`; + const parameters = [ + `${prefix}/recommended`, + `${prefix}/by-builder-rev/${builderGitRev}`, + `${prefix}/by-input-hash/${inputHash}`, + ]; + for (const parameter of parameters) { + console.error(`Writing ${parameter} = ${state.amiId}`); + putSsmParameter(state, parameter, state.amiId); + } +}; + const main = () => { const state = parseArgs(process.argv.slice(2)); requireCommand("aws"); @@ -296,6 +363,10 @@ const main = () => { listArtifacts(state); return; } + if (state.command === "publish-ssm") { + publishSsmArtifact(state); + return; + } deleteArtifact(state); }; diff --git a/publish-ami.sh b/publish-ami.sh index 098d0c7..61e57a7 100755 --- a/publish-ami.sh +++ b/publish-ami.sh @@ -271,6 +271,20 @@ aws ec2 create-tags \ "Key=RootSnapshotId,Value=${root_snapshot_id}" \ "Key=NixSnapshotId,Value=${nix_snapshot_id}" +ssm_ami_prefix="/quixos/amis/${name}/${architecture}" +for ssm_ami_parameter_name in \ + "${ssm_ami_prefix}/recommended" \ + "${ssm_ami_prefix}/by-builder-rev/${builder_git_rev}" \ + "${ssm_ami_prefix}/by-input-hash/${input_hash}" +do + aws ssm put-parameter \ + --region "$aws_region" \ + --name "$ssm_ami_parameter_name" \ + --type String \ + --value "$image_id" \ + --overwrite >/dev/null +done + aws ec2 deregister-image --region "$aws_region" --image-id "$capture_image_id" >/dev/null jq -n \ @@ -285,6 +299,7 @@ jq -n \ --arg rootDeviceName "$root_device_name" \ --arg nixDeviceName "$nix_device_name" \ --arg nixPool "$nix_pool" \ + --arg ssmRecommendedParameter "${ssm_ami_prefix}/recommended" \ '{ architecture: $architecture, system: $system, @@ -296,7 +311,8 @@ jq -n \ nixSnapshotId: $nixSnapshotId, rootDeviceName: $rootDeviceName, nixDeviceName: $nixDeviceName, - nixPool: $nixPool + nixPool: $nixPool, + ssmRecommendedParameter: $ssmRecommendedParameter }' > artifact.json if [ "$keep_builder" = false ]; then @@ -313,3 +329,4 @@ if [ "$keep_builder" = false ]; then fi echo "Published AMI: $image_id" +echo "Published SSM parameter: ${ssm_ami_prefix}/recommended"