Last active
September 29, 2025 15:51
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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