Skip to content

Commit 6f36de0

Browse files
committed
feat(nix): add stage 1 AMI build caching based on input hash
Implement content-based caching for stage 1 AMI builds by computing a hash from all input sources. Then the build-ami script checks for existing AMIs with matching input hash before building.
1 parent a7327e0 commit 6f36de0

File tree

4 files changed

+119
-2
lines changed

4 files changed

+119
-2
lines changed

.github/workflows/testinfra-ami-build.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,15 +107,23 @@ jobs:
107107
echo 'postgres-version = "'$PG_VERSION'"' > common-nix.vars.pkr.hcl
108108
# Ensure there's a newline at the end of the file
109109
echo "" >> common-nix.vars.pkr.hcl
110+
git add -f common-nix.vars.pkr.hcl
110111
111112
- name: Build AMI stage 1
112113
env:
113114
AWS_MAX_ATTEMPTS: 10
114115
AWS_RETRY_MODE: adaptive
115116
run: |
116117
GIT_SHA=${{github.sha}}
117-
nix run github:supabase/postgres/${GIT_SHA}#packer -- init amazon-arm64-nix.pkr.hcl
118-
nix run github:supabase/postgres/${GIT_SHA}#packer -- build -var "git-head-version=${GIT_SHA}" -var "packer-execution-id=${EXECUTION_ID}" -var-file="development-arm.vars.pkr.hcl" -var-file="common-nix.vars.pkr.hcl" -var "ansible_arguments=" -var "postgres-version=${{ steps.random.outputs.random_string }}" -var "region=ap-southeast-1" -var 'ami_regions=["ap-southeast-1"]' -var "force-deregister=true" -var "ansible_arguments=-e postgresql_major=${POSTGRES_MAJOR_VERSION}" amazon-arm64-nix.pkr.hcl
118+
nix run .#build-ami -- \
119+
-var "git-head-version=${GIT_SHA}" \
120+
-var "packer-execution-id=${EXECUTION_ID}" \
121+
-var "ansible_arguments=-e postgresql_major=${POSTGRES_MAJOR_VERSION}" \
122+
-var "region=ap-southeast-1" \
123+
-var 'ami_regions=["ap-southeast-1"]' \
124+
-var-file="development-arm.vars.pkr.hcl" \
125+
-var-file="common-nix.vars.pkr.hcl" \
126+
amazon-arm64-nix.pkr.hcl
119127
120128
- name: Build AMI stage 2
121129
env:

amazon-arm64-nix.pkr.hcl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ variable "force-deregister" {
9292
default = false
9393
}
9494

95+
variable "input-hash" {
96+
type = string
97+
default = ""
98+
description = "Content hash of all input sources"
99+
}
100+
95101
packer {
96102
required_plugins {
97103
amazon = {
@@ -172,6 +178,7 @@ source "amazon-ebssurrogate" "source" {
172178
appType = "postgres"
173179
postgresVersion = "${var.postgres-version}-stage1"
174180
sourceSha = "${var.git-head-version}"
181+
inputHash = "${var.input-hash}"
175182
}
176183

177184
communicator = "ssh"

nix/packages/build-ami.nix

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
{
2+
lib,
3+
stdenv,
4+
writeShellApplication,
5+
packer,
6+
awscli2,
7+
jq,
8+
...
9+
}:
10+
11+
let
12+
root = ../..;
13+
packerSources = stdenv.mkDerivation {
14+
name = "packer-sources";
15+
src = lib.fileset.toSource {
16+
inherit root;
17+
fileset = lib.fileset.unions [
18+
(root + "/ebssurrogate")
19+
(root + "/ansible")
20+
(root + "/migrations")
21+
(root + "/scripts")
22+
(root + "/amazon-arm64-nix.pkr.hcl")
23+
(root + "/development-arm.vars.pkr.hcl")
24+
(lib.fileset.maybeMissing (root + "/common-nix.vars.pkr.hcl"))
25+
];
26+
};
27+
28+
phases = [
29+
"unpackPhase"
30+
"installPhase"
31+
];
32+
installPhase = ''
33+
mkdir -p $out
34+
cp -r . $out/
35+
'';
36+
};
37+
in
38+
writeShellApplication {
39+
name = "build-ami";
40+
41+
runtimeInputs = [
42+
packer
43+
awscli2
44+
jq
45+
];
46+
47+
text = ''
48+
set -euo pipefail
49+
50+
set -x
51+
REGION="''${AWS_REGION:-ap-southeast-1}"
52+
POSTGRES_VERSION="''${POSTGRES_MAJOR_VERSION:-15}"
53+
PACKER_SOURCES="${packerSources}"
54+
INPUT_HASH=$(basename "$PACKER_SOURCES" | cut -d- -f1)
55+
56+
echo "Checking for existing AMI..."
57+
set +e
58+
AMI_OUTPUT=$(aws ec2 describe-images \
59+
--region "$REGION" \
60+
--owners self \
61+
--filters \
62+
"Name=tag:inputHash,Values=$INPUT_HASH" \
63+
"Name=tag:postgresVersion,Values=$POSTGRES_VERSION-stage1" \
64+
"Name=state,Values=available" \
65+
--query 'Images[0].ImageId' \
66+
--output text 2>&1)
67+
AWS_EXIT_CODE=$?
68+
set -e
69+
70+
if [ $AWS_EXIT_CODE -ne 0 ] && [ $AWS_EXIT_CODE -ne 255 ]; then
71+
echo "Error querying AWS: $AMI_OUTPUT"
72+
exit 1
73+
fi
74+
75+
AMI_ID="$AMI_OUTPUT"
76+
if [ "$AMI_ID" != "None" ] && [ -n "$AMI_ID" ]; then
77+
echo "Found existing AMI: $AMI_ID"
78+
exit 0
79+
fi
80+
81+
echo "No cached AMI found"
82+
83+
cd "$PACKER_SOURCES"
84+
packer init amazon-arm64-nix.pkr.hcl
85+
packer build \
86+
-var "input-hash=$INPUT_HASH" \
87+
-var "postgres-version=$POSTGRES_VERSION" \
88+
-var "region=$REGION" \
89+
"$@"
90+
'';
91+
92+
meta = {
93+
description = "Build AMI if not cached based on input hash";
94+
longDescription = ''
95+
The input hash is computed from all source files that affect the build.
96+
Before building, we verify the existence of an AMI with the same hash.
97+
If found, the build is skipped. Otherwise, a new AMI is created and
98+
tagged with the input hash for future cache hits.
99+
'';
100+
};
101+
}

nix/packages/default.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
{
3030
packages = (
3131
{
32+
build-ami = pkgs.callPackage ./build-ami.nix { packer = self'.packages.packer; };
3233
build-test-ami = pkgs.callPackage ./build-test-ami.nix { };
3334
cleanup-ami = pkgs.callPackage ./cleanup-ami.nix { };
3435
dbmate-tool = pkgs.callPackage ./dbmate-tool.nix { inherit (self.supabase) defaults; };

0 commit comments

Comments
 (0)