11#! /bin/bash
22
3- # Preliminary setup
4- # - Install AWS CLI v2 (perhaps it works on v1, untested):
5- # - on Arch Linux: paru -S aws-cli-v2
6- # - Configure AWS Credentials: aws configure
3+ # This script launches EC2 instances to benchmark your project.
4+ #
5+ # Requirements:
6+ # - The programs `git`, `ssh`, `rsync`, and `aws` must be installed.
7+ # - AWS CLI v2 installed and configured (`aws configure`)
8+ # - An EC2-compatible SSH key must exist in AWS, or the script will generate one (and save locally).
9+ #
10+ # Required AWS IAM permissions:
11+ # - ec2:RunInstances
12+ # - ec2:TerminateInstances
13+ # - ec2:DescribeInstances
14+ # - ec2:DescribeVpcs
15+ # - ec2:CreateSecurityGroup
16+ # - ec2:DeleteSecurityGroup
17+ # - ec2:AuthorizeSecurityGroupIngress
18+ #
19+ # Optional environment variables:
20+ # AWS_KEY_NAME use an existing key pair instead of creating one
21+ # AWS_SECURITY_GROUP use an existing SG instead of creating one
22+
23+ set -euo pipefail
24+
25+ # --------------------
26+ # User-configurable variables
27+ # --------------------
28+
29+ # Ubuntu 24.04 AMI IDs for x86_64 and aarch64 architectures
30+ declare -A AMI_MAP=(
31+ [" x86_64" ]=" ami-020cba7c55df1f615"
32+ [" aarch64" ]=" ami-07041441b708acbd6"
33+ )
34+
35+ # We need biggest (metal) instances to access perf events on x86
36+ INSTANCES_x86_64=(
37+ " c5n.metal" # Skylake
38+ " c6i.metal" # Ice Lake
39+ " c7i.metal-24xl" # Sapphire Rapids
40+ " c5a.24xlarge" # EPYC Zen 2
41+ " c6a.metal" # EPYC Zen 3
42+ " c7a.metal-48xl" # EPYC Zen 4
43+ )
44+ INSTANCES_aarch64=(
45+ " c6g.medium" # Graviton 2 - Neoverse N1
46+ " c7g.medium" # Graviton 3 - Neoverse V1
47+ " c8g.medium" # Graviton 4 - Neoverse V2
48+ )
749
8- set -e # Exit script on first error
50+ VOLUME_SIZE=10 # in GB
951
10- AMI_ID_x86_64= " ami-020cba7c55df1f615 "
11- AMI_ID_aarch64= " ami-07041441b708acbd6 "
52+ KEY_NAME= " ${AWS_KEY_NAME :- aws_auto} " # Key path is assumed to be ~/.ssh/${KEY_NAME}.pem
53+ SECURITY_GROUP= " ${AWS_SECURITY_GROUP :- } "
1254
13- INSTANCES_x86_64=()
14- INSTANCES_aarch64=(" c6g.medium" " c7g.medium" " c8g.medium" )
55+ # --------------------
56+ # Internal variables (do not modify)
57+ # --------------------
1558
16- KEY_NAME=" openssh"
17- KEY_PATH=" ~/.ssh/openssh.pem"
59+ KEY_PATH=" $HOME /.ssh/${KEY_NAME} .pem"
1860SSH_COMMAND=" ssh -i ${KEY_PATH} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
19- SECURITY_GROUP=" sg-0315466a0d7fc99e1"
20- VOLUME_SIZE=10 # in GB
2161
22- PROJECT_DIR=$( basename $( pwd) )
62+ PROJECT_DIR=$( basename " $( git rev-parse --show-toplevel) " )
63+ CREATED_SECURITY_GROUP=" "
64+
65+ # Cleanup function to delete created security group on exit
66+ cleanup () {
67+ if [ -n " ${CREATED_SECURITY_GROUP} " ]; then
68+ echo " Cleaning up security group: ${CREATED_SECURITY_GROUP} "
69+ aws ec2 delete-security-group --group-id " ${CREATED_SECURITY_GROUP} " || true
70+ fi
71+ }
72+
73+ check_prerequisites () {
74+ if (( BASH_VERSINFO[0 ] < 4 )) ; then
75+ echo " Error: This script requires Bash version 4 or higher." >&2
76+ exit 1
77+ fi
78+
79+ for cmd in git ssh rsync aws; do
80+ if ! command -v " $cmd " > /dev/null 2>&1 ; then
81+ echo " Error: Required command '$cmd ' is not installed." >&2
82+ exit 1
83+ fi
84+ done
85+
86+ if ! git rev-parse --show-toplevel > /dev/null 2>&1 ; then
87+ echo " Error: This script must be run from within a Git repository." >&2
88+ exit 1
89+ fi
90+
91+ if ! aws sts get-caller-identity > /dev/null 2>&1 ; then
92+ echo " Error: AWS credentials not configured. Run 'aws configure'." >&2
93+ exit 1
94+ fi
95+ }
96+
97+ create_key_pair () {
98+ if aws ec2 describe-key-pairs --key-names " ${KEY_NAME} " > /dev/null 2>&1 ; then
99+ echo " Using existing AWS key pair: ${KEY_NAME} "
100+ return
101+ fi
102+
103+ echo " Creating new AWS key pair named ${KEY_NAME} "
104+ mkdir -p ~ /.ssh
105+ aws ec2 create-key-pair --key-name " $KEY_NAME " \
106+ --query ' KeyMaterial' --output text > " $KEY_PATH "
107+ chmod 400 " $KEY_PATH "
108+ echo " Created and saved key pair private key to $KEY_PATH "
109+ }
110+
111+ create_security_group () {
112+ if [ -n " ${SECURITY_GROUP} " ]; then
113+ echo " Using existing security group: ${SECURITY_GROUP} "
114+ return
115+ fi
116+
117+ echo " Creating a new security group for SSH access..."
118+ VPC_ID=$( aws ec2 describe-vpcs \
119+ --filters Name=isDefault,Values=true \
120+ --query " Vpcs[0].VpcId" \
121+ --output text)
122+
123+ CREATED_SECURITY_GROUP=$( aws ec2 create-security-group \
124+ --group-name ssh-public-access \
125+ --description " Allow SSH access from anywhere (0.0.0.0/0)" \
126+ --vpc-id " ${VPC_ID} " \
127+ --query " GroupId" \
128+ --output text)
129+
130+ aws ec2 authorize-security-group-ingress \
131+ --group-id " ${CREATED_SECURITY_GROUP} " \
132+ --protocol tcp \
133+ --port 22 \
134+ --cidr 0.0.0.0/0
135+
136+ SECURITY_GROUP=" ${CREATED_SECURITY_GROUP} "
137+ echo " Created security group: ${SECURITY_GROUP} "
138+ }
139+
140+ get_arch () {
141+ local instance_name=" $1 "
142+ if printf ' %s\n' " ${INSTANCES_aarch64[@]} " | grep -qx " $instance_name " ; then
143+ echo " aarch64"
144+ else
145+ echo " x86_64"
146+ fi
147+ }
23148
24149process_instance () {
25150 INSTANCE_NAME=$1
@@ -41,8 +166,7 @@ process_instance() {
41166
42167 PUBLIC_IP=$( aws ec2 describe-instances \
43168 --instance-ids ${INSTANCE_ID} \
44- --query " Reservations[0].Instances[0].PublicIpAddress" \
45- --output text)
169+ --query " Reservations[0].Instances[0].PublicIpAddress" --output text)
46170 echo " Instance ${INSTANCE_ID} public IP: ${PUBLIC_IP} "
47171
48172 git ls-files -z | rsync -avz --partial --progress --from0 --files-from=- -e " ${SSH_COMMAND} " \
@@ -53,10 +177,12 @@ process_instance() {
53177
54178 echo "Updating and installing dependencies on ${INSTANCE_NAME} ..."
55179 sudo apt update
56- sudo DEBIAN_FRONTEND=noninteractive \
57- apt install -y linux-tools-common linux-tools-generic \
58- g++ clang cmake python3
59- echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid
180+ sudo DEBIAN_FRONTEND=noninteractive apt install -y \
181+ linux-tools-common linux-tools-generic g++ clang cmake python3
182+
183+ # Enable access to perf events for benchmarking
184+ # Must use ` sudo tee` since shell redirection (` > ` ) is not affected by sudo
185+ echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid > /dev/null
60186
61187 echo "Saving some info about the environment..."
62188 mkdir -p outputs
84210 echo " Terminated instance: ${INSTANCE_ID} "
85211}
86212
87- echo " Launching ${# INSTANCES_aarch64[@]} aarch64 instances and ${# INSTANCES_x86_64[@]} x86_64 instances in parallel..."
88- for INSTANCE_NAME in " ${INSTANCES_x86_64[@]} " " ${INSTANCES_aarch64[@]} " ; do
89- if printf ' %s\n' " ${INSTANCES_aarch64[@]} " | grep -qx " ${INSTANCE_NAME} " ; then
90- AMI_ID=${AMI_ID_aarch64}
91- else
92- AMI_ID=${AMI_ID_x86_64}
93- fi
213+ main () {
214+ trap cleanup EXIT
215+ check_prerequisites
216+ create_key_pair
217+ create_security_group
94218
95- process_instance " ${INSTANCE_NAME} " " ${AMI_ID} " 2>&1 | tee " ${INSTANCE_NAME} .log" &
96- done
219+ echo " Launching ${# INSTANCES_aarch64[@]} aarch64 instances and ${# INSTANCES_x86_64[@]} x86_64 instances in parallel..."
220+ for INSTANCE_NAME in " ${INSTANCES_x86_64[@]} " " ${INSTANCES_aarch64[@]} " ; do
221+ ARCH=$( get_arch " $INSTANCE_NAME " )
222+ AMI_ID=" ${AMI_MAP[$ARCH]} "
223+
224+ process_instance " ${INSTANCE_NAME} " " ${AMI_ID} " 2>&1 | tee " ${INSTANCE_NAME} .log" &
225+ done
226+
227+ # Wait for all background jobs to finish
228+ wait
229+ echo " All instances completed."
230+ }
97231
98- # Wait for all background jobs to finish
99- wait
100- echo " All instances completed. "
232+ if [ " $0 " = " $BASH_SOURCE " ] ; then
233+ main
234+ fi
0 commit comments