Skip to content

Instantly share code, notes, and snippets.

@mmasko
Last active September 29, 2025 15:51
Show Gist options
  • Select an option

  • Save mmasko/5bd8427f38e9bd58cbb880219c2f8b37 to your computer and use it in GitHub Desktop.

Select an option

Save mmasko/5bd8427f38e9bd58cbb880219c2f8b37 to your computer and use it in GitHub Desktop.
To prep a dev server. Includes mounting an EBS volume where you have your code base or working directories.
#!/bin/bash
# Robust user-data / standalone script to install tooling and attach+mount an EBS volume by Name tag on Nitro (NVMe) instances.
set -euo pipefail
IFS=$'\n\t'
# --- System update and package installation ---
dnf update -y
dnf upgrade -y
dnf upgrade --releasever=latest -y
dnf install -y docker git unzip cronie python3-pip
# --- System update and package installation Complete---
# --- Install VS Code's code-server to preinstall VS Code ---
# Grab the latest release tag (e.g. v4.22.0) for code-server
VERSION=$(curl -sSf https://api.github.com/repos/coder/code-server/releases/latest |
grep '"tag_name":' |
sed -E 's/.*"([^"]+)".*/\1/')
# Build the RPM URL (filename drops the leading “v”)
RPM_URL="https://github.com/coder/code-server/releases/download/${VERSION}/code-server-${VERSION#v}-amd64.rpm"
# Download the RPM to /tmp
curl -fSL "$RPM_URL" -o /tmp/code-server.rpm
#Install the RPM with dnf (no sudo needed – we are root)
dnf install -y /tmp/code-server.rpm
#Enable & start the service for ec2-user
systemctl enable --now code-server@ec2-user
# Clean up the temporary RPM file (optional)
rm -f /tmp/code-server.rpm
# --- Install VS Code Extensions ---
# Define the extension identifiers you want installed
EXTENSIONS=(
amazonwebservices.aws-toolkit-vscode
ms-azuretools.vscode-containers
ms-vscode.powershell
rooveterinaryinc.roo-cline
)
# Install each extension for ec2‑user
for EXT in "${EXTENSIONS[@]}"; do
echo "Installing extension ${EXT} for ec2-user ..."
# runuser switches to the target UID/GID without needing sudo
runuser -l ec2-user -c "code-server --install-extension ${EXT} --force"
done
# --- Install VS Code Extensions Complete ---
systemctl enable --now docker crond
usermod -aG docker ec2-user
# Install Docker Compose (standalone binary)
curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
# Ensure pip and pipx are installed for Python package management (as ec2-user)
su - ec2-user -c "python3 -m pip install --user --upgrade pip"
su - ec2-user -c "python3 -m pip install --user --upgrade pipx"
su - ec2-user -c "python3 -m pipx ensurepath"
# Install containery
# su - ec2-user -c "pipx install git+https://github.com/mmasko/containery.git"
# --- Install / refresh cron job to run remote stale_activity_checker script every 5 minutes ---
# REMOTE_PREP_URL="https://gist.githubusercontent.com/mmasko/e2ff9b4f1b55e4bcf12c74479abbfc57/raw"
# CRON_FILE="/etc/cron.d/stale_activity_checker"
# if [ ! -f "$CRON_FILE" ] || ! grep -q "$REMOTE_PREP_URL" "$CRON_FILE"; then
# echo "[INFO] Installing cron job for remote stale_activity_checker script at $CRON_FILE";
# cat > "$CRON_FILE" <<EOF
# */5 * * * * root curl -fsSL $REMOTE_PREP_URL | bash -s -- >> /var/log/stale_activity_checker.log 2>&1
# EOF
# chmod 644 "$CRON_FILE"
# else
# echo "[INFO] Cron job already present at $CRON_FILE"
# fi
# --- EBS volume attachment and mounting ---
# Usage: VOLUME_NAME=your-volume-name MOUNT_POINT=/your/mount/point ./mount_ebs.sh
# or: ./mount_ebs.sh <volume_name> <mount_point>
## Accept as env vars or positional args
VOLUME_NAME="${VOLUME_NAME:-${1:-my-volume-name}}"
MOUNT_POINT="${MOUNT_POINT:-${2:-/mnt/projects}}"
# Placeholder original device label (only used for API attach call). Nitro exposes NVMe names after attach.
REQUESTED_DEVICE_NAME="/dev/xvdf"
if [ -z "$VOLUME_NAME" ] || [ -z "$MOUNT_POINT" ]; then
echo "Usage: VOLUME_NAME=your-volume-name MOUNT_POINT=/your/mount/point $0"
echo " or: $0 <volume_name> <mount_point>"
exit 1
fi
# Query region from instance metadata
REGION=$(curl -s http://169.254.169.254/latest/dynamic/instance-identity/document | grep region | awk -F\" '{print $4}')
# Find the volume ID by Name tag
VOLUME_ID=$(aws ec2 describe-volumes \
--filters "Name=tag:Name,Values=$VOLUME_NAME" \
--query "Volumes[0].VolumeId" --output text --region "$REGION" || true)
if [ -z "${VOLUME_ID}" ] || [ "${VOLUME_ID}" = "None" ] || [ "${VOLUME_ID}" = "null" ]; then
echo "ERROR: No volume found with Name tag '$VOLUME_NAME' in region $REGION" >&2
exit 1
fi
# Get the instance ID from metadata
INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
# Attempt to attach the EBS volume (skip if already attached to this instance) with wait + logging
ATTACH_STATE=$(aws ec2 describe-volumes --volume-ids "$VOLUME_ID" --query 'Volumes[0].Attachments[0].State' --output text --region "$REGION" || true)
ATTACHED_INSTANCE=$(aws ec2 describe-volumes --volume-ids "$VOLUME_ID" --query 'Volumes[0].Attachments[0].InstanceId' --output text --region "$REGION" || true)
if [ "$ATTACH_STATE" = "attached" ] && [ "$ATTACHED_INSTANCE" = "$INSTANCE_ID" ]; then
echo "[INFO] Volume $VOLUME_ID already attached to this instance."
else
echo "[INFO] Attaching volume $VOLUME_ID to $INSTANCE_ID as $REQUESTED_DEVICE_NAME ..."
aws ec2 attach-volume --volume-id "$VOLUME_ID" --instance-id "$INSTANCE_ID" --device "$REQUESTED_DEVICE_NAME" --region "$REGION"
ATTACH_TIMEOUT=180
ATTACH_ELAPSED=0
ATTACH_SLEEP=3
while true; do
ATTACH_STATE=$(aws ec2 describe-volumes --volume-ids "$VOLUME_ID" --query 'Volumes[0].Attachments[0].State' --output text --region "$REGION" || true)
echo "[DEBUG] Waiting for attachment: state=$ATTACH_STATE elapsed=${ATTACH_ELAPSED}s"
if [ "$ATTACH_STATE" = "attached" ]; then
echo "[INFO] Volume $VOLUME_ID attached."; break
fi
if [ $ATTACH_ELAPSED -ge $ATTACH_TIMEOUT ]; then
echo "[ERROR] Timed out waiting ($ATTACH_TIMEOUT s) for volume to attach." >&2; exit 3
fi
sleep $ATTACH_SLEEP; ATTACH_ELAPSED=$((ATTACH_ELAPSED+ATTACH_SLEEP))
done
fi
# Resolve NVMe device path by volume ID symlink supporting multiple naming variants
VOLUME_ID_NOHYPHEN="${VOLUME_ID//-/}" # vol0680...
VOL_HEX_PART="${VOLUME_ID#vol-}" # 0680...
VOL_TRUNC12="${VOL_HEX_PART:0:12}" # first 12 chars
SYMLINK_CANDIDATES="/dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_${VOLUME_ID} /dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_vol${VOL_HEX_PART} /dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_vol${VOL_TRUNC12}"
echo "[INFO] Waiting for NVMe symlink (volume $VOLUME_ID). Candidates: $SYMLINK_CANDIDATES (plus dynamic scan)"
echo "[DEBUG] Volume ID hex part: $VOL_HEX_PART; truncated12: $VOL_TRUNC12"
SYMLINK_TIMEOUT=300
SYMLINK_ELAPSED=0
SYMLINK_SLEEP=3
FOUND_SYMLINK=""
normalize() { echo "$1" | tr 'A-Z' 'a-z' | tr -d '-' ; }
TARGET_NORM=$(normalize "nvme-Amazon_Elastic_Block_Store_${VOLUME_ID_NOHYPHEN}")
while [ -z "$FOUND_SYMLINK" ]; do
for c in $SYMLINK_CANDIDATES; do
if [ -e "$c" ]; then FOUND_SYMLINK="$c"; break; fi
done
# Dynamic scan
if [ -z "$FOUND_SYMLINK" ]; then
for f in /dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_*; do
[ -e "$f" ] || continue
BASE=$(basename "$f")
BNORM=$(normalize "$BASE")
if [ "$BNORM" = "$TARGET_NORM" ] || echo "$BNORM" | grep -q "$VOL_HEX_PART"; then
FOUND_SYMLINK="$f"; break
fi
done
fi
if [ -n "$FOUND_SYMLINK" ]; then
echo "[INFO] Found symlink: $FOUND_SYMLINK"; break
fi
echo "[DEBUG] Symlink not present yet. Elapsed=${SYMLINK_ELAPSED}s"
if [ $SYMLINK_ELAPSED -ge $SYMLINK_TIMEOUT ]; then
echo "[ERROR] Timed out waiting ($SYMLINK_TIMEOUT s) for NVMe symlink for volume $VOLUME_ID" >&2
echo "[DEBUG] Block devices (lsblk):"; lsblk || true
echo "[DEBUG] by-id entries (filtered):"; ls -l /dev/disk/by-id | grep -E "Amazon_Elastic_Block_Store|${VOL_HEX_PART}" || true
echo "[DEBUG] Raw by-id list:"; ls -1 /dev/disk/by-id || true
exit 4
fi
sleep $SYMLINK_SLEEP; SYMLINK_ELAPSED=$((SYMLINK_ELAPSED+SYMLINK_SLEEP))
done
DEVICE=$(readlink -f "$FOUND_SYMLINK")
echo "[INFO] Resolved block device: $DEVICE"
# Create mount point if it doesn't exist
if [ ! -d "$MOUNT_POINT" ]; then
mkdir -p "$MOUNT_POINT"
fi
# Check if device already has a filesystem
EXISTING_FS=$(blkid -o value -s TYPE "$DEVICE" || true)
if [ -z "$EXISTING_FS" ]; then
echo "No filesystem detected on $DEVICE. Creating ext4..."
mkfs.ext4 -F "$DEVICE"
elif [ "$EXISTING_FS" != "ext4" ]; then
echo "WARNING: Existing filesystem $EXISTING_FS on $DEVICE. Mounting as-is. (No reformat.)"
else
echo "ext4 filesystem already present on $DEVICE."
fi
# Mount the device if not already mounted
if ! mountpoint -q "$MOUNT_POINT"; then
mount "$DEVICE" "$MOUNT_POINT"
echo "$DEVICE mounted to $MOUNT_POINT."
else
echo "$MOUNT_POINT already mounted."
fi
# Ensure the mount point and its contents are owned by the default user (ec2-user)
chown -R ec2-user:ec2-user "$MOUNT_POINT"
# Create symlink from /home/ec2-user/projects to the mount point
USER_HOME="/home/ec2-user"
SYMLINK_TARGET="$USER_HOME/projects"
if [ ! -L "$SYMLINK_TARGET" ] && [ ! -e "$SYMLINK_TARGET" ]; then
ln -s "$MOUNT_POINT" "$SYMLINK_TARGET"
echo "Created symlink: $SYMLINK_TARGET -> $MOUNT_POINT"
elif [ -L "$SYMLINK_TARGET" ]; then
echo "Symlink $SYMLINK_TARGET already exists."
else
echo "WARNING: $SYMLINK_TARGET exists but is not a symlink. Skipping symlink creation."
fi
# Add to /etc/fstab for persistence (if not already present)
UUID=$(blkid -s UUID -o value "$DEVICE" || true)
if [ -n "$UUID" ]; then
FSTAB_LINE="UUID=$UUID $MOUNT_POINT ext4 defaults,nofail 0 2"
if ! grep -q "UUID=$UUID" /etc/fstab; then
echo "$FSTAB_LINE" >> /etc/fstab
echo "Added fstab entry for $UUID."
else
echo "fstab already contains entry for $UUID."
fi
else
echo "WARNING: Could not determine UUID for $DEVICE; skipping fstab entry." >&2
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment