Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Garbage collection #201

Merged
merged 8 commits into from
Dec 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ jobs:
- uses: DeterminateSystems/nix-installer-action@7993355175c2765e5733dae74f3e0786fe0e5c4f # v12
- uses: DeterminateSystems/magic-nix-cache-action@87b14cf437d03d37989d87f0fa5ce4f5dc1a330b # v8
- run: nix build .#amazonImage -L --system ${{ matrix.runs-on.system }}
- run: nix flake check -L --system ${{ matrix.runs-on.system }}
# - run: nix flake check -L --system ${{ matrix.runs-on.system }}
5 changes: 5 additions & 0 deletions .github/workflows/upload-legacy-ami.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ jobs:
--copy-to-regions \
--public

- name: Delete deprecated AMIs
if: github.ref == 'refs/heads/main'
run: |
nix run .#delete-deprecated-images

deploy-pages:
name: Deploy images page
if: github.ref == 'refs/heads/main'
Expand Down
7 changes: 4 additions & 3 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 26 additions & 26 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
{
description = "A very basic flake";

inputs = {
nixpkgs.url = "github:NixOS/nixpkgs";
};
inputs = { nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-24.11"; };

outputs = { self, nixpkgs, ... }:
let inherit (nixpkgs) lib; in
let inherit (nixpkgs) lib;

{
in {
nixosModules = {
ec2-instance-connect = ./modules/ec2-instance-connect.nix;

legacyAmazonProfile = nixpkgs + "nixos/modules/virtualisation/amazon-image.nix";
legacyAmazonImage = nixpkgs + "/nixos/maintainers/scripts/ec2/amazon-image.nix";
legacyAmazonProfile = nixpkgs
+ "nixos/modules/virtualisation/amazon-image.nix";
legacyAmazonImage = nixpkgs
+ "/nixos/maintainers/scripts/ec2/amazon-image.nix";

amazonProfile = ./modules/amazon-profile.nix;
amazonImage = ./modules/amazon-image.nix;
Expand All @@ -27,11 +27,14 @@
};
};

lib.supportedSystems = [ "aarch64-linux" "x86_64-linux" "aarch64-darwin" ];
lib.supportedSystems =
[ "aarch64-linux" "x86_64-linux" "aarch64-darwin" ];

packages = lib.genAttrs self.lib.supportedSystems (system:
let pkgs = nixpkgs.legacyPackages.${system}; in {
ec2-instance-connect = pkgs.callPackage ./packages/ec2-instance-connect.nix { };
let pkgs = nixpkgs.legacyPackages.${system};
in {
ec2-instance-connect =
pkgs.callPackage ./packages/ec2-instance-connect.nix { };
amazon-ec2-metadata-mock = pkgs.buildGoModule rec {
pname = "amazon-ec2-metadata-mock";
version = "1.11.2";
Expand Down Expand Up @@ -64,7 +67,10 @@
boot.loader.grub.enable = false;
boot.loader.systemd-boot.enable = true;
}
{ ec2.efi = true; amazonImage.sizeMB = "auto"; }
{
ec2.efi = true;
amazonImage.sizeMB = "auto";
}
self.nixosModules.version
];
}).config.system.build.amazonImage;
Expand All @@ -74,11 +80,12 @@
apps = lib.genAttrs self.lib.supportedSystems (system:
let
upload-ami = self.packages.${system}.upload-ami;
mkApp = name: _: { type = "app"; program = "${upload-ami}/bin/${name}"; };
in
lib.mapAttrs mkApp self.packages.${system}.upload-ami.passthru.pyproject.project.scripts
);

mkApp = name: _: {
type = "app";
program = "${upload-ami}/bin/${name}";
};
in lib.mapAttrs mkApp
self.packages.${system}.upload-ami.passthru.pyproject.project.scripts);

# TODO: unfortunately I don't have access to a aarch64-linux hardware with virtualisation support
checks = lib.genAttrs [ "x86_64-linux" ] (system:
Expand All @@ -98,8 +105,7 @@

};
};
in
{
in {
resize-partition = lib.nixos.runTest {
hostPkgs = pkgs;
imports = [ config ./tests/resize-partition.nix ];
Expand All @@ -110,13 +116,7 @@
};
});

devShells = lib.genAttrs [ "x86_64-linux" "aarch64-darwin" ] (system: {
default = let pkgs = nixpkgs.legacyPackages.${system}; in pkgs.mkShell {
nativeBuildInputs = [
pkgs.awscli2
pkgs.opentofu
];
};
});
devShells = lib.genAttrs [ "x86_64-linux" "aarch64-darwin" ]
(system: { default = self.packages.${system}.upload-ami; });
};
}
111 changes: 52 additions & 59 deletions site/index.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
<!DOCTYPE html>
<html lang="en">

<head>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NixOS Amazon Images / AMIs</title>
<style>
table {
table {
border-collapse: collapse;
}

Expand Down Expand Up @@ -38,7 +37,7 @@
}
</style>
<script type="module">
(async function () {
(async function () {
try {
/**
* @typedef {Object} AWSImages
Expand Down Expand Up @@ -204,21 +203,18 @@
}
})()
</script>
</head>

<body>
</head>
<body>
<h1>Amazon Images / AMIs</h1>
<p>
NixOS can be deployed to Amazon EC2 using our official AMI. We publish
NixOS can be deployed to Amazon EC2 using our official AMI. We publish
AMIs to all AWS regions for both `x86_64` and `arm64` on a weekly basis.
</p>
<p>We will start deprecating and garbage collecting images older than 90 days
in the future.
<p>We deprecate and garbage collecting images older than 90 days.
This is why we suggest using a terraform data source or the AWS API to query
for the latest AMI.
</p>
<p>NixOS images are published under AWS Account ID <span id="owner-id"></span></p>

for the latest AMI.</p>
<p>NixOS images are published under AWS Account ID
<span id="owner-id"></span></p>
<h2>Terraform / OpenTofu</h2>
<p>You can use terraform to query for the latest image</p>
<pre id="terraform">
Expand Down Expand Up @@ -249,52 +245,49 @@ <h2>AWS CLI</h2>
<pre id="awscli2">
aws ec2 describe-images --owners _OWNER_ID_ --filter 'Name=name,Values=nixos/24.11*' 'Name=architecture,Values=arm64' --query 'sort_by(Images, &CreationDate)'
</pre>

<h2>AMI table</h2>
<p>Here are the latest NixOS images available in the Amazon cloud.</p>
<table id="images-table">
<thead>
<tr>
<th class="region">
<label for="search-regions">Region</label>
<input type="search" class="region" list="regions-datalist" placeholder="Region">
<datalist class="region" id="regions-datalist">
</datalist>
</th>
<th class="architecture">
<label for="search-architectures">Architecture</label>
<input type="search" class="architecture" list="architectures-datalist" placeholder="Architecture">
<datalist class="architecture" id="architectures-datalist">
</datalist>
</th>
<th class="name">
<label for="search-name">Name</label>
<input type="search" class="name" placeholder="Name">
<datalist class="name" id="names-datalist">
</datalist>
</th>
<th class="creation-date" aria-sort="descending">
Creation date
<button class="sort">
<span>▼</span>
</button>
</th>
<th>Image ID</th>

</tr>
</thead>
<tbody>
<template id="row-template">
<tr>
<td class="region"></td>
<td class="architecture"></td>
<td class="name"></td>
<td class="creation-date"></td>
<td class="image-id"></td>
</tr>
</template>
</tbody>
<thead>
<tr>
<th class="region">
<label for="search-regions">Region</label>
<input type="search" class="region" list="regions-datalist" placeholder="Region">
<datalist class="region" id="regions-datalist">
</datalist>
</th>
<th class="architecture">
<label for="search-architectures">Architecture</label>
<input type="search" class="architecture" list="architectures-datalist" placeholder="Architecture">
<datalist class="architecture" id="architectures-datalist">
</datalist>
</th>
<th class="name">
<label for="search-name">Name</label>
<input type="search" class="name" placeholder="Name">
<datalist class="name" id="names-datalist">
</datalist>
</th>
<th class="creation-date" aria-sort="descending">
Creation date
<button class="sort">
<span>▼</span>
</button>
</th>
<th>Image ID</th>
</tr>
</thead>
<tbody>
<template id="row-template">
<tr>
<td class="region"></td>
<td class="architecture"></td>
<td class="name"></td>
<td class="creation-date"></td>
<td class="image-id"></td>
</tr>
</template>
</tbody>
</table>
</body>

</html>
</body>
</html>
4 changes: 4 additions & 0 deletions upload-ami/default.nix
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{ buildPythonApplication
, python3Packages
, awscli2
, opentofu
, lib
}:

Expand Down Expand Up @@ -33,6 +35,8 @@ buildPythonApplication {
pyproject = true;
nativeBuildInputs =
map (name: python3Packages.${name}) pyproject.build-system.requires ++ [
opentofu
awscli2
python3Packages.mypy
python3Packages.black
];
Expand Down
4 changes: 3 additions & 1 deletion upload-ami/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ disable-image-block-public-access = "upload_ami.disable_image_block_public_acces
enable-regions = "upload_ami.enable_regions:main"
request-public-ami-quota-increase = "upload_ami.request_public_ami_quota_increase:main"
describe-images = "upload_ami.describe_images:main"
delete-images = "upload_ami.delete_images:main"
delete-images-by-name = "upload_ami.delete_images_by_name:main"
delete-deprecated-images = "upload_ami.delete_deprecated_images:main"
delete-orphaned-snapshots = "upload_ami.delete_orphaned_snapshots:main"
[tool.mypy]
strict=true
78 changes: 78 additions & 0 deletions upload-ami/src/upload_ami/delete_deprecated_images.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import logging
import boto3
from mypy_boto3_ec2 import EC2Client
import argparse
import botocore.exceptions
import datetime

logger = logging.getLogger(__name__)


def delete_deprecated_images(ec2: EC2Client, dry_run: bool) -> None:
"""
Delete an image by its name.

Name can be a filter

Idempotent, unlike nuke
"""

images_paginator = ec2.get_paginator("describe_images")
images_iterator = images_paginator.paginate(Owners=["self"])
for pages in images_iterator:
for image in pages["Images"]:
if "DeprecationTime" in image:
# HACK: As python can not parse ISO8601 strings with
# milliseconds, but it **can** produce them, instead of parsing
# the datetime from the API, we format the current time as an
# ISO8601 string and compare the strings. This works because
# ISO8601 strings are lexicographically comparable.
current_time = datetime.datetime.isoformat(
datetime.datetime.now(), timespec="milliseconds"
)
if current_time >= image["DeprecationTime"]:
assert "ImageId" in image
assert "Name" in image
logger.info(f"Deleting image {image['Name']} : {image['ImageId']}. DeprecationTime: {image['DeprecationTime']}")
try:
ec2.deregister_image(ImageId=image["ImageId"], DryRun=dry_run)
except botocore.exceptions.ClientError as e:
if "DryRunOperation" in str(e):
logger.info(f"Would have deleted image {image['ImageId']}")
else:
raise
assert "BlockDeviceMappings" in image
assert "Ebs" in image["BlockDeviceMappings"][0]
assert "SnapshotId" in image["BlockDeviceMappings"][0]["Ebs"]
snapshot_id = image["BlockDeviceMappings"][0]["Ebs"]["SnapshotId"]
logger.info(f"Deleting snapshot {snapshot_id}")
try:
ec2.delete_snapshot(SnapshotId=snapshot_id, DryRun=dry_run)
except botocore.exceptions.ClientError as e:
if "DryRunOperation" in str(e):
logger.info(f"Would have deleted snapshot {snapshot_id}")
else:
raise


def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"--dry-run",
action="store_true",
help="Do not actually delete anything, just log what would be deleted",
)
logging.basicConfig(level=logging.INFO)
ec2: EC2Client = boto3.client("ec2")

args = parser.parse_args()
regions = ec2.describe_regions()["Regions"]
for region in regions:
assert "RegionName" in region
ec2r = boto3.client("ec2", region_name=region["RegionName"])
logging.info(f"Checking region {region['RegionName']}")
delete_deprecated_images(ec2r, args.dry_run)


if __name__ == "__main__":
main()
Loading
Loading