Skip to content

Commit 0c66368

Browse files
ryan-williamsclaude
andcommitted
Refactor template with functions and fail on name collision
Changes: - Add helper functions log() and get_metadata() to reduce duplication - get_metadata() handles both IMDSv2 (token-based) and IMDSv1 fallback - Replace repeated date/logging patterns with log() function - Error out if runner name already exists (instead of --replace) - Use --unattended flag to prevent interactive prompts - Condense metadata logging to single line Benefits: - Template size reduced from ~16KB to under 15KB - More maintainable with less duplication - Fails fast on name collisions (indicates metadata fetch failure) - Cleaner, more consistent logging 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 31ec185 commit 0c66368

1 file changed

Lines changed: 68 additions & 83 deletions

File tree

src/ec2_gha/templates/user-script.sh.templ

Lines changed: 68 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,29 @@
11
#!/bin/bash
2-
# Debug: Log to syslog to see what's happening
3-
logger "EC2-GHA: Starting userdata script"
4-
echo "EC2-GHA: Starting userdata script" >&2
2+
set -e
3+
# Helper functions
4+
log() { echo "[$$(date '+%Y-%m-%d %H:%M:%S')] $$1" | tee -a /var/log/runner-setup.log; }
5+
log_error() { log "ERROR: $$1" >&2; }
6+
7+
# Get metadata (IMDSv2 compatible)
8+
get_metadata() {
9+
local path="$$1"
10+
local token=$$(curl -X PUT -H "X-aws-ec2-metadata-token-ttl-seconds: 300" http://169.254.169.254/latest/api/token 2>/dev/null || true)
11+
if [ -n "$$token" ]; then
12+
curl -s -H "X-aws-ec2-metadata-token: $$token" "http://169.254.169.254/latest/meta-data/$$path" 2>/dev/null || echo "unknown"
13+
else
14+
curl -s "http://169.254.169.254/latest/meta-data/$$path" 2>/dev/null || echo "unknown"
15+
fi
16+
}
517

6-
# Log any errors to syslog before exiting
18+
logger "EC2-GHA: Starting userdata script"
719
trap 'logger "EC2-GHA: Script failed at line $$LINENO with exit code $$?"' ERR
820

9-
set -e
10-
11-
# Set up error handling and failsafe termination
12-
# Get instance ID early for error messages (IMDSv2 compatible)
13-
IMDS_TOKEN_EARLY=$$(curl -X PUT -H "X-aws-ec2-metadata-token-ttl-seconds: 300" http://169.254.169.254/latest/api/token 2>/dev/null || true)
14-
if [ -n "$$IMDS_TOKEN_EARLY" ]; then
15-
INSTANCE_ID=$$(curl -s -H "X-aws-ec2-metadata-token: $$IMDS_TOKEN_EARLY" http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo "unknown")
16-
else
17-
INSTANCE_ID=$$(curl -s http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo "unknown")
18-
fi
21+
INSTANCE_ID=$$(get_metadata "instance-id")
1922

2023
terminate_instance() {
2124
local reason="$$1"
22-
echo "[$$(date '+%Y-%m-%d %H:%M:%S')] FATAL: $$reason" | tee -a /var/log/runner-setup.log
23-
echo "[$$(date '+%Y-%m-%d %H:%M:%S')] Terminating instance $$INSTANCE_ID due to setup failure" | tee -a /var/log/runner-setup.log
25+
log "FATAL: $$reason"
26+
log "Terminating instance $$INSTANCE_ID due to setup failure"
2427

2528
# Try to remove runner if it was partially configured
2629
if [ -f "$$homedir/config.sh" ] && [ -n "$${RUNNER_TOKEN:-}" ]; then
@@ -44,17 +47,17 @@ if ! [[ "$$REGISTRATION_TIMEOUT" =~ ^[0-9]+$$ ]]; then
4447
fi
4548
logger "EC2-GHA: Registration timeout set to $$REGISTRATION_TIMEOUT seconds"
4649
(
47-
echo "[$$( date '+%Y-%m-%d %H:%M:%S')] Watchdog: Starting $$REGISTRATION_TIMEOUT second timeout" >> /var/log/runner-setup.log
50+
log "Watchdog: Starting $$REGISTRATION_TIMEOUT second timeout"
4851
sleep $$REGISTRATION_TIMEOUT
4952
if [ ! -f /var/run/github-runner-registered ]; then
50-
echo "[$$( date '+%Y-%m-%d %H:%M:%S')] Watchdog: Registration marker not found after timeout" >> /var/log/runner-setup.log
53+
log "Watchdog: Registration marker not found after timeout"
5154
terminate_instance "Runner failed to register within $$REGISTRATION_TIMEOUT seconds"
5255
else
53-
echo "[$$( date '+%Y-%m-%d %H:%M:%S')] Watchdog: Registration marker found, exiting normally" >> /var/log/runner-setup.log
56+
log "Watchdog: Registration marker found, exiting normally"
5457
fi
5558
) &
5659
REGISTRATION_WATCHDOG_PID=$$!
57-
echo "[$$( date '+%Y-%m-%d %H:%M:%S')] Watchdog PID: $$REGISTRATION_WATCHDOG_PID"
60+
log "Watchdog PID: $$REGISTRATION_WATCHDOG_PID"
5861
# Save watchdog PID to file so it survives exec redirect
5962
echo $$REGISTRATION_WATCHDOG_PID > /var/run/github-runner-watchdog.pid
6063

@@ -70,7 +73,7 @@ if [ -z "$$homedir" ] || [ "$$homedir" = "AUTO" ]; then
7073
for user in ubuntu ec2-user centos admin debian fedora alpine arch; do
7174
if id "$$user" &>/dev/null; then
7275
homedir="/home/$$user"
73-
echo "[$$( date '+%Y-%m-%d %H:%M:%S')] Auto-detected: $$homedir"
76+
log "Auto-detected: $$homedir"
7477
break
7578
fi
7679
done
@@ -87,37 +90,37 @@ if [ -z "$$homedir" ] || [ "$$homedir" = "AUTO" ]; then
8790

8891
if [ -z "$$homedir" ]; then
8992
homedir="/home/ec2-user" # Ultimate fallback
90-
echo "[$$( date '+%Y-%m-%d %H:%M:%S')] Using fallback: $$homedir"
93+
log "Using fallback: $$homedir"
9194
else
9295
# Use stat to get the actual owner of the directory
9396
owner=$$(stat -c "%U" "$$homedir" 2>/dev/null || stat -f "%Su" "$$homedir" 2>/dev/null)
94-
echo "[$$( date '+%Y-%m-%d %H:%M:%S')] Detected: $$homedir ($$owner)"
97+
log "Detected: $$homedir ($$owner)"
9598
fi
9699
fi
97100
else
98-
echo "[$$( date '+%Y-%m-%d %H:%M:%S')] Using: $$homedir"
101+
log "Using: $$homedir"
99102
fi
100103

101104
# Set up maximum lifetime timeout - do this early to ensure cleanup
102105
MAX_LIFETIME_MINUTES=$max_instance_lifetime
103-
echo "[$$( date '+%Y-%m-%d %H:%M:%S')] Max lifetime: $${MAX_LIFETIME_MINUTES}m"
106+
log "Max lifetime: $${MAX_LIFETIME_MINUTES}m"
104107
nohup bash -c "sleep $${MAX_LIFETIME_MINUTES}m && echo '[$$(date)] Maximum lifetime reached' && shutdown -h now" > /var/log/max-lifetime.log 2>&1 &
105108

106109
# Configure CloudWatch Logs if enabled
107110
if [ "$cloudwatch_logs_group" != "" ]; then
108-
echo "[$$(date '+%Y-%m-%d %H:%M:%S')] Installing CloudWatch agent"
111+
log "Installing CloudWatch agent"
109112
# Use a subshell to prevent CloudWatch failures from stopping the entire script
110113
(
111114

112115
# Wait for dpkg lock to be released (up to 2 minutes)
113-
echo "[$$(date '+%Y-%m-%d %H:%M:%S')] Waiting for dpkg lock to be released..."
116+
log "Waiting for dpkg lock to be released..."
114117
timeout=120
115118
while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 || fuser /var/lib/dpkg/lock >/dev/null 2>&1; do
116119
if [ $$timeout -le 0 ]; then
117-
echo "[$$(date '+%Y-%m-%d %H:%M:%S')] WARNING: dpkg lock timeout, proceeding anyway"
120+
log "WARNING: dpkg lock timeout, proceeding anyway"
118121
break
119122
fi
120-
echo "[$$(date '+%Y-%m-%d %H:%M:%S')] dpkg is locked, waiting... ($$timeout seconds remaining)"
123+
log "dpkg is locked, waiting... ($$timeout seconds remaining)"
121124
sleep 5
122125
timeout=$$((timeout - 5))
123126
done
@@ -187,13 +190,13 @@ EOF
187190
-c file:/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json \
188191
-s
189192

190-
echo "[$$(date '+%Y-%m-%d %H:%M:%S')] CloudWatch agent started"
191-
) || echo "[$$(date '+%Y-%m-%d %H:%M:%S')] WARNING: CloudWatch agent installation failed, continuing without it"
193+
log "CloudWatch agent started"
194+
) || log "WARNING: CloudWatch agent installation failed, continuing without it"
192195
fi
193196

194197
# Configure SSH access if public key provided
195198
if [ -n "$ssh_pubkey" ]; then
196-
echo "[$$(date '+%Y-%m-%d %H:%M:%S')] Configuring SSH access"
199+
log "Configuring SSH access"
197200

198201
# Determine the default user based on the home directory owner
199202
DEFAULT_USER=$$(stat -c "%U" "$$homedir" 2>/dev/null || echo "root")
@@ -211,54 +214,39 @@ if [ -n "$ssh_pubkey" ]; then
211214
chown -R "$$DEFAULT_USER:$$DEFAULT_USER" "$$homedir/.ssh"
212215
fi
213216

214-
echo "[$$(date '+%Y-%m-%d %H:%M:%S')] SSH key added for user $$DEFAULT_USER"
217+
log "SSH key added for user $$DEFAULT_USER"
215218
fi
216219

217220
# Fetch instance metadata before redirecting output
218-
# Support both IMDSv1 and IMDSv2
219-
IMDS_TOKEN=$$(curl -X PUT -H "X-aws-ec2-metadata-token-ttl-seconds: 300" http://169.254.169.254/latest/api/token 2>/dev/null || true)
220-
if [ -n "$$IMDS_TOKEN" ]; then
221-
# IMDSv2
222-
INSTANCE_TYPE=$$(curl -s -H "X-aws-ec2-metadata-token: $$IMDS_TOKEN" http://169.254.169.254/latest/meta-data/instance-type || echo "unknown")
223-
INSTANCE_ID=$$(curl -s -H "X-aws-ec2-metadata-token: $$IMDS_TOKEN" http://169.254.169.254/latest/meta-data/instance-id || echo "unknown")
224-
REGION=$$(curl -s -H "X-aws-ec2-metadata-token: $$IMDS_TOKEN" http://169.254.169.254/latest/meta-data/placement/region || echo "unknown")
225-
AZ=$$(curl -s -H "X-aws-ec2-metadata-token: $$IMDS_TOKEN" http://169.254.169.254/latest/meta-data/placement/availability-zone || echo "unknown")
226-
else
227-
# IMDSv1 fallback
228-
INSTANCE_TYPE=$$(curl -s http://169.254.169.254/latest/meta-data/instance-type || echo "unknown")
229-
INSTANCE_ID=$$(curl -s http://169.254.169.254/latest/meta-data/instance-id || echo "unknown")
230-
REGION=$$(curl -s http://169.254.169.254/latest/meta-data/placement/region || echo "unknown")
231-
AZ=$$(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone || echo "unknown")
232-
fi
221+
INSTANCE_TYPE=$$(get_metadata "instance-type")
222+
INSTANCE_ID=$$(get_metadata "instance-id")
223+
REGION=$$(get_metadata "placement/region")
224+
AZ=$$(get_metadata "placement/availability-zone")
233225

234226
# Redirect runner setup logs to a file for CloudWatch
235227
exec >> /var/log/runner-setup.log 2>&1
236-
echo "[$$( date '+%Y-%m-%d %H:%M:%S')] Starting runner setup"
228+
log "Starting runner setup"
237229

238-
echo "[$$(date '+%Y-%m-%d %H:%M:%S')] Instance metadata:"
239-
echo " Instance type: $${INSTANCE_TYPE}"
240-
echo " Instance ID: $${INSTANCE_ID}"
241-
echo " Region: $${REGION}"
242-
echo " Availability zone: $${AZ}"
230+
log "Instance metadata: Type=$${INSTANCE_TYPE} ID=$${INSTANCE_ID} Region=$${REGION} AZ=$${AZ}"
243231

244-
echo "[$$(date '+%Y-%m-%d %H:%M:%S')] Working directory: $$homedir"
232+
log "Working directory: $$homedir"
245233
cd "$$homedir"
246234
echo "$script" > pre-runner-script.sh
247-
echo "[$$(date '+%Y-%m-%d %H:%M:%S')] Running pre-runner script"
235+
log "Running pre-runner script"
248236
source pre-runner-script.sh
249237
export RUNNER_ALLOW_RUNASROOT=1
250238
# Detect architecture and download appropriate runner
251239
ARCH=$$(uname -m)
252240
if [ "$$ARCH" = "aarch64" ] || [ "$$ARCH" = "arm64" ]; then
253241
# For ARM, replace x64 with arm64 in the URL
254242
RUNNER_URL=$$(echo "$runner_release" | sed 's/x64/arm64/g')
255-
echo "[$$(date '+%Y-%m-%d %H:%M:%S')] ARM detected, using: $$RUNNER_URL"
243+
log "ARM detected, using: $$RUNNER_URL"
256244
else
257245
RUNNER_URL="$runner_release"
258-
echo "[$$(date '+%Y-%m-%d %H:%M:%S')] x64 detected, using: $$RUNNER_URL"
246+
log "x64 detected, using: $$RUNNER_URL"
259247
fi
260248
curl -L $$RUNNER_URL -o runner.tar.gz
261-
echo "[$$(date '+%Y-%m-%d %H:%M:%S')] Extracting runner"
249+
log "Extracting runner"
262250
# `--no-overwrite-dir` is important, otherwise `$$homedir` ends up `chown`'d to `1001:docker`, and `sshd` will refuse connection attempts to `$$homedir`
263251
tar --no-overwrite-dir -xzf runner.tar.gz
264252
# Create job tracking scripts
@@ -343,18 +331,9 @@ systemctl daemon-reload
343331
systemctl enable runner-termination-check.timer
344332
systemctl start runner-termination-check.timer
345333

346-
# Get instance metadata for descriptive runner name (IMDSv2 compatible)
347-
# Reuse token if available, otherwise get a new one
348-
if [ -z "$$IMDS_TOKEN" ]; then
349-
IMDS_TOKEN=$$(curl -X PUT -H "X-aws-ec2-metadata-token-ttl-seconds: 300" http://169.254.169.254/latest/api/token 2>/dev/null || true)
350-
fi
351-
if [ -n "$$IMDS_TOKEN" ]; then
352-
INSTANCE_ID=$$(curl -s -H "X-aws-ec2-metadata-token: $$IMDS_TOKEN" http://169.254.169.254/latest/meta-data/instance-id || echo "unknown")
353-
INSTANCE_TYPE=$$(curl -s -H "X-aws-ec2-metadata-token: $$IMDS_TOKEN" http://169.254.169.254/latest/meta-data/instance-type || echo "unknown")
354-
else
355-
INSTANCE_ID=$$(curl -s http://169.254.169.254/latest/meta-data/instance-id || echo "unknown")
356-
INSTANCE_TYPE=$$(curl -s http://169.254.169.254/latest/meta-data/instance-type || echo "unknown")
357-
fi
334+
# Get instance metadata for descriptive runner name
335+
INSTANCE_ID=$$(get_metadata "instance-id")
336+
INSTANCE_TYPE=$$(get_metadata "instance-type")
358337

359338
# Create runner name with just the instance ID for uniqueness
360339
RUNNER_NAME="ec2-$${INSTANCE_ID}"
@@ -382,40 +361,46 @@ fi
382361
# The $labels variable already contains user labels and the critical runner-xxx label from Python
383362
ALL_LABELS="$labels$${METADATA_LABELS}"
384363

385-
echo "[$$(date '+%Y-%m-%d %H:%M:%S')] Configuring runner for repo: $repo"
386-
echo "[$$(date '+%Y-%m-%d %H:%M:%S')] Runner name: $${RUNNER_NAME}"
387-
echo "[$$(date '+%Y-%m-%d %H:%M:%S')] Labels: $${ALL_LABELS}"
364+
log "Configuring runner for repo: $repo"
365+
log "Runner name: $${RUNNER_NAME}"
366+
log "Labels: $${ALL_LABELS}"
388367
# Export token for error handler
389368
export RUNNER_TOKEN="$token"
390-
# Use --replace to force replacement if a runner with the same name exists
391-
# This prevents interactive prompts that would lose our labels
392-
./config.sh --url https://github.com/$repo --token $token --labels "$${ALL_LABELS}" --name "$${RUNNER_NAME}" --disableupdate --replace
393-
echo "[$$(date '+%Y-%m-%d %H:%M:%S')] Runner registered successfully"
369+
370+
# Attempt to register. If a runner with the same name exists, config.sh will fail
371+
# We use --unattended to prevent interactive prompts
372+
if ! ./config.sh --url https://github.com/$repo --token $token --labels "$${ALL_LABELS}" --name "$${RUNNER_NAME}" --disableupdate --unattended; then
373+
log_error "Failed to register runner '$${RUNNER_NAME}'"
374+
log_error "This usually means a runner with this name already exists"
375+
log_error "If instance ID is 'unknown', metadata fetching likely failed"
376+
terminate_instance "Runner registration failed"
377+
fi
378+
log "Runner registered successfully"
394379

395380
# Mark registration as complete and kill the watchdog
396-
echo "[$$(date '+%Y-%m-%d %H:%M:%S')] Creating registration marker file at /var/run/github-runner-registered"
381+
log "Creating registration marker"
397382
touch /var/run/github-runner-registered
398383
ls -la /var/run/github-runner-registered
399384
# Read watchdog PID from file (survives exec redirect)
400385
if [ -f /var/run/github-runner-watchdog.pid ]; then
401386
WATCHDOG_PID=$$(cat /var/run/github-runner-watchdog.pid)
402387
if kill -0 $$WATCHDOG_PID 2>/dev/null; then
403388
kill $$WATCHDOG_PID 2>/dev/null || true
404-
echo "[$$(date '+%Y-%m-%d %H:%M:%S')] Killed registration watchdog (PID: $$WATCHDOG_PID)"
389+
log "Killed registration watchdog (PID: $$WATCHDOG_PID)"
405390
else
406-
echo "[$$(date '+%Y-%m-%d %H:%M:%S')] Watchdog process $$WATCHDOG_PID already terminated"
391+
log "Watchdog process $$WATCHDOG_PID already terminated"
407392
fi
408393
rm -f /var/run/github-runner-watchdog.pid
409394
fi
410395

411-
echo "[$$(date '+%Y-%m-%d %H:%M:%S')] Starting runner"
396+
log "Starting runner"
412397
# Create marker file for cleanup service
413398
touch /var/run/github-runner-started
414399
# Ensure CloudWatch agent can read diagnostic logs
415400
# The cwagent user needs to traverse into $homedir to reach _diag
416401
# Make $homedir world-executable (but not readable) so cwagent can traverse it
417402
chmod o+x $$homedir
418-
echo "[$$(date '+%Y-%m-%d %H:%M:%S')] Made $$homedir traversable for CloudWatch agent"
403+
log "Made $$homedir traversable for CloudWatch agent"
419404
# Create _diag directory if it doesn't exist
420405
mkdir -p $$homedir/_diag
421406
# The _diag files are already world-readable by default, just ensure the directory is too

0 commit comments

Comments
 (0)