Log in
scripts/aws_bench.sh 256 lines · 9.4 KB · bash Blame
1
#!/usr/bin/env bash
2
set -euo pipefail
3
4
# Provision an EC2 box, run the Oak benchmarks on it, download results/,
5
# then destroy everything it created (instance, security group, key pair).
6
#
7
# Usage:
8
#   scripts/aws_bench.sh [options] [-- <extra bench.py args>]
9
#
10
# Options:
11
#   --profile {micro,smoke,standard,large}   Benchmark profile (default: smoke)
12
#   --instance-type TYPE   EC2 instance type (default: c6id.2xlarge β€” has
13
#                          local NVMe, used as TMPDIR so fixture I/O hits
14
#                          instance-store SSD, not EBS)
15
#   --region REGION        AWS region (default: AWS CLI configured region)
16
#   --disk-gb N            Root EBS volume size in GB (default: 60)
17
#   --timeout-mins N       Max minutes to wait for the benchmark (default: 360)
18
#   --keep                 Do not destroy the instance afterwards (prints SSH cmd)
19
#   -- ...                 Everything after -- is passed through to bench.py
20
#
21
# Examples:
22
#   scripts/aws_bench.sh                                  # smoke run
23
#   scripts/aws_bench.sh --profile standard
24
#   scripts/aws_bench.sh --profile large -- --skip-diff
25
#
26
# Requires: aws CLI (authenticated β€” run `aws login` first), ssh, rsync, curl.
27
# Results land in results/aws/<run-id>/ locally.
28
29
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
30
BENCH_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
31
32
PROFILE=smoke
33
INSTANCE_TYPE=c6id.2xlarge
34
REGION="$(aws configure get region 2>/dev/null || true)"
35
DISK_GB=60
36
TIMEOUT_MINS=360
37
KEEP=0
38
BENCH_EXTRA_ARGS=()
39
40
while [ $# -gt 0 ]; do
41
  case "$1" in
42
    --profile)        PROFILE="$2"; shift 2 ;;
43
    --instance-type)  INSTANCE_TYPE="$2"; shift 2 ;;
44
    --region)         REGION="$2"; shift 2 ;;
45
    --disk-gb)        DISK_GB="$2"; shift 2 ;;
46
    --timeout-mins)   TIMEOUT_MINS="$2"; shift 2 ;;
47
    --keep)           KEEP=1; shift ;;
48
    --)               shift; BENCH_EXTRA_ARGS=("$@"); break ;;
49
    -h|--help)        sed -n '4,30p' "$0"; exit 0 ;;
50
    *) echo "unknown option: $1" >&2; exit 2 ;;
51
  esac
52
done
53
54
[ -n "$REGION" ] || { echo "no AWS region configured; pass --region" >&2; exit 2; }
55
56
RUN_ID="oak-bench-$(date +%Y%m%d-%H%M%S)"
57
STATE_DIR="$(mktemp -d "${TMPDIR:-/tmp}/${RUN_ID}.XXXX")"
58
PEM="$STATE_DIR/key.pem"
59
SSH_OPTS=(-i "$PEM"
60
  -o StrictHostKeyChecking=accept-new
61
  -o UserKnownHostsFile="$STATE_DIR/known_hosts"
62
  -o ConnectTimeout=10
63
  -o ServerAliveInterval=15
64
  -o ServerAliveCountMax=8)
65
SSH_USER=ubuntu
66
67
log() { printf '[aws-bench] %s\n' "$*" >&2; }
68
69
aws_ec2() { aws ec2 --region "$REGION" "$@"; }
70
71
INSTANCE_ID=""
72
SG_ID=""
73
KEY_CREATED=0
74
75
cleanup() {
76
  local rc=$?
77
  set +e
78
  if [ "$KEEP" = 1 ] && [ -n "$INSTANCE_ID" ]; then
79
    log "--keep set: leaving $INSTANCE_ID running"
80
    log "ssh: ssh -i $PEM $SSH_USER@${PUBLIC_IP:-<ip>}"
81
    log "destroy later with:"
82
    log "  aws ec2 --region $REGION terminate-instances --instance-ids $INSTANCE_ID"
83
    log "  aws ec2 --region $REGION delete-security-group --group-id $SG_ID"
84
    log "  aws ec2 --region $REGION delete-key-pair --key-name $RUN_ID"
85
    exit "$rc"
86
  fi
87
  if [ -n "$INSTANCE_ID" ]; then
88
    log "terminating $INSTANCE_ID"
89
    aws_ec2 terminate-instances --instance-ids "$INSTANCE_ID" >/dev/null
90
    aws_ec2 wait instance-terminated --instance-ids "$INSTANCE_ID"
91
  fi
92
  if [ -n "$SG_ID" ]; then
93
    # SG deletion can lag the instance's ENI teardown; retry briefly.
94
    for _ in 1 2 3 4 5 6; do
95
      aws_ec2 delete-security-group --group-id "$SG_ID" 2>/dev/null && break
96
      sleep 10
97
    done
98
  fi
99
  [ "$KEY_CREATED" = 1 ] && aws_ec2 delete-key-pair --key-name "$RUN_ID" >/dev/null
100
  rm -rf "$STATE_DIR"
101
  exit "$rc"
102
}
103
trap cleanup EXIT
104
105
# --- Preflight -------------------------------------------------------------
106
aws sts get-caller-identity >/dev/null 2>&1 || {
107
  echo "AWS credentials missing/expired β€” run: aws login" >&2; exit 2; }
108
109
log "run id: $RUN_ID  region: $REGION  type: $INSTANCE_TYPE  profile: $PROFILE"
110
111
AMI_ID="$(aws --region "$REGION" ssm get-parameter \
112
  --name /aws/service/canonical/ubuntu/server/24.04/stable/current/amd64/hvm/ebs-gp3/ami-id \
113
  --query Parameter.Value --output text)"
114
log "ubuntu 24.04 ami: $AMI_ID"
115
116
MY_IP="$(curl -fsS https://checkip.amazonaws.com)"
117
118
# --- Provision -------------------------------------------------------------
119
aws_ec2 create-key-pair --key-name "$RUN_ID" --key-type ed25519 \
120
  --query KeyMaterial --output text > "$PEM"
121
chmod 600 "$PEM"
122
KEY_CREATED=1
123
124
VPC_ID="$(aws_ec2 describe-vpcs --filters Name=isDefault,Values=true \
125
  --query 'Vpcs[0].VpcId' --output text)"
126
[ "$VPC_ID" != "None" ] || { echo "no default VPC in $REGION" >&2; exit 2; }
127
128
SG_ID="$(aws_ec2 create-security-group --group-name "$RUN_ID" \
129
  --description "ephemeral oak benchmark runner" --vpc-id "$VPC_ID" \
130
  --query GroupId --output text)"
131
aws_ec2 authorize-security-group-ingress --group-id "$SG_ID" \
132
  --protocol tcp --port 22 --cidr "${MY_IP}/32" >/dev/null
133
134
INSTANCE_ID="$(aws_ec2 run-instances \
135
  --image-id "$AMI_ID" \
136
  --instance-type "$INSTANCE_TYPE" \
137
  --key-name "$RUN_ID" \
138
  --security-group-ids "$SG_ID" \
139
  --block-device-mappings "[{\"DeviceName\":\"/dev/sda1\",\"Ebs\":{\"VolumeSize\":$DISK_GB,\"VolumeType\":\"gp3\",\"DeleteOnTermination\":true}}]" \
140
  --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=$RUN_ID},{Key=purpose,Value=oak-benchmarks}]" \
141
  --query 'Instances[0].InstanceId' --output text)"
142
log "instance: $INSTANCE_ID (waiting for running state)"
143
144
aws_ec2 wait instance-running --instance-ids "$INSTANCE_ID"
145
PUBLIC_IP="$(aws_ec2 describe-instances --instance-ids "$INSTANCE_ID" \
146
  --query 'Reservations[0].Instances[0].PublicIpAddress' --output text)"
147
[ "$PUBLIC_IP" != "None" ] || { echo "instance has no public IP" >&2; exit 1; }
148
log "public ip: $PUBLIC_IP (waiting for SSH)"
149
150
for i in $(seq 1 30); do
151
  ssh "${SSH_OPTS[@]}" "$SSH_USER@$PUBLIC_IP" true 2>/dev/null && break
152
  [ "$i" = 30 ] && { echo "SSH never came up" >&2; exit 1; }
153
  sleep 10
154
done
155
156
# --- Upload + setup --------------------------------------------------------
157
log "syncing benchmarks repo to instance"
158
rsync -az -e "ssh ${SSH_OPTS[*]}" \
159
  --exclude results/ --exclude workdirs/ --exclude worktrees/ --exclude runs/ \
160
  --exclude fixtures/ --exclude __pycache__/ --exclude '.git/' --exclude '.oak/' \
161
  "$BENCH_ROOT/" "$SSH_USER@$PUBLIC_IP:benchmarks/"
162
163
log "installing dependencies (git, python3, oak)"
164
ssh "${SSH_OPTS[@]}" "$SSH_USER@$PUBLIC_IP" 'bash -s' <<'SETUP'
165
set -euo pipefail
166
sudo apt-get update -qq
167
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
168
  git python3 rsync curl ca-certificates >/dev/null
169
170
# Use instance-store NVMe (if this type has one) as the benchmark temp dir,
171
# so fixture generation hits local SSD rather than the EBS root volume.
172
ROOT_DISK="$(lsblk -no PKNAME "$(findmnt -no SOURCE /)" | head -1)"
173
for dev in /dev/nvme*n1; do
174
  [ -b "$dev" ] || continue
175
  [ "$(basename "$dev")" = "$ROOT_DISK" ] && continue
176
  if [ -z "$(lsblk -no MOUNTPOINTS "$dev" | tr -d '[:space:]')" ]; then
177
    sudo mkfs.ext4 -q -F "$dev"
178
    sudo mkdir -p /mnt/bench
179
    sudo mount "$dev" /mnt/bench
180
    sudo chown "$USER" /mnt/bench
181
    echo "[setup] instance-store NVMe mounted at /mnt/bench"
182
    break
183
  fi
184
done
185
186
curl -fsSL oak.space/install | sh
187
export PATH="$HOME/.oak/bin:$HOME/.local/bin:/usr/local/bin:$PATH"
188
echo "[setup] git:    $(git --version)"
189
echo "[setup] python: $(python3 --version)"
190
echo "[setup] oak:    $(command -v oak) ($(oak --version 2>&1 | head -1))"
191
SETUP
192
193
# --- Run benchmark (detached, so an SSH drop doesn't kill a long run) -------
194
EXTRA_ARGS_STR=""
195
[ ${#BENCH_EXTRA_ARGS[@]} -gt 0 ] && EXTRA_ARGS_STR="$(printf ' %q' "${BENCH_EXTRA_ARGS[@]}")"
196
197
log "starting bench.py --profile $PROFILE${EXTRA_ARGS_STR}"
198
ssh "${SSH_OPTS[@]}" "$SSH_USER@$PUBLIC_IP" 'bash -s' <<RUN
199
set -euo pipefail
200
cat > run_bench.sh <<'INNER'
201
#!/usr/bin/env bash
202
set -uo pipefail
203
export PATH="\$HOME/.oak/bin:\$HOME/.local/bin:/usr/local/bin:\$PATH"
204
if mountpoint -q /mnt/bench; then
205
  export TMPDIR=/mnt/bench/tmp
206
  mkdir -p "\$TMPDIR"
207
fi
208
cd "\$HOME/benchmarks"
209
python3 scripts/bench.py --profile $PROFILE${EXTRA_ARGS_STR}
210
echo \$? > "\$HOME/bench.exit"
211
INNER
212
chmod +x run_bench.sh
213
rm -f bench.exit bench.log
214
setsid nohup ./run_bench.sh > bench.log 2>&1 < /dev/null &
215
RUN
216
217
# Poll until the exit file appears, streaming new log output as it arrives.
218
DEADLINE=$(( $(date +%s) + TIMEOUT_MINS * 60 ))
219
LOG_OFFSET=0
220
BENCH_EXIT=""
221
while :; do
222
  sleep 20
223
  CHUNK="$(ssh "${SSH_OPTS[@]}" "$SSH_USER@$PUBLIC_IP" \
224
    "tail -c +$((LOG_OFFSET + 1)) bench.log 2>/dev/null; \
225
     [ -f bench.exit ] && printf '\n__BENCH_EXIT__%s' \"\$(cat bench.exit)\"" \
226
    2>/dev/null)" || { log "ssh poll failed; retrying"; continue; }
227
  if [[ "$CHUNK" == *__BENCH_EXIT__* ]]; then
228
    BENCH_EXIT="${CHUNK##*__BENCH_EXIT__}"
229
    CHUNK="${CHUNK%$'\n'__BENCH_EXIT__*}"
230
  fi
231
  if [ -n "$CHUNK" ]; then
232
    printf '%s\n' "$CHUNK"
233
    LOG_OFFSET=$((LOG_OFFSET + $(printf '%s' "$CHUNK" | wc -c) + 1))
234
  fi
235
  [ -n "$BENCH_EXIT" ] && break
236
  if [ "$(date +%s)" -ge "$DEADLINE" ]; then
237
    echo "benchmark exceeded ${TIMEOUT_MINS}m timeout" >&2
238
    exit 1
239
  fi
240
done
241
log "bench.py exited with code $BENCH_EXIT"
242
243
# --- Download results ------------------------------------------------------
244
DEST="$BENCH_ROOT/results/aws/$RUN_ID"
245
mkdir -p "$DEST"
246
rsync -az -e "ssh ${SSH_OPTS[*]}" \
247
  "$SSH_USER@$PUBLIC_IP:benchmarks/results/" "$DEST/" 2>/dev/null || true
248
ssh "${SSH_OPTS[@]}" "$SSH_USER@$PUBLIC_IP" "cat bench.log" > "$DEST/bench.log" || true
249
250
log "results downloaded to results/aws/$RUN_ID/"
251
if [ -f "$DEST/latest.summary.md" ]; then
252
  echo
253
  cat "$DEST/latest.summary.md"
254
fi
255
256
exit "$BENCH_EXIT"