Created
January 14, 2026 04:35
-
-
Save axot/8170be5ad17a79d34bc7f4d2e7dc390e to your computer and use it in GitHub Desktop.
ec2-ssh-proxy
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 | |
| # | |
| # ec2-ssh-proxy - Auto-start/stop EC2 instances for SSH connections | |
| # | |
| # DESCRIPTION: | |
| # SSH ProxyCommand wrapper that automatically starts stopped EC2 instances | |
| # when connecting and provides manual stop functionality. Works transparently | |
| # with standard ssh/scp commands via SSH ProxyCommand configuration. | |
| # | |
| # USAGE: | |
| # As ProxyCommand (automatic): | |
| # Add to ~/.ssh/config: | |
| # Host myhost | |
| # HostName xxx | |
| # ProxyCommand ~/bin/ec2-ssh-proxy --proxy %h | |
| # | |
| # Then use standard commands: | |
| # ssh myhost | |
| # scp file.txt myhost:/tmp/ | |
| # | |
| # Manual stop: | |
| # ec2-ssh-proxy --stop <elastic-ip> [profile] [region] | |
| # | |
| # BEHAVIOR: | |
| # - Fast path: If SSH port (22) is accessible, connects immediately via nc | |
| # (no AWS API calls, ~0.4s connection time) | |
| # - Slow path: If port closed, queries AWS for instance state: | |
| # - stopped: starts instance and waits for SSH | |
| # - stopping/shutting-down: waits for stopped, then starts | |
| # - running: waits for SSH to be ready | |
| # - Uses Elastic IP (EIP) as instance identifier (easier than instance IDs) | |
| # - Respects AWS_PROFILE and AWS_REGION environment variables | |
| # - Only sets AWS_REGION if explicitly provided (avoids empty string bug) | |
| # | |
| # REQUIREMENTS: | |
| # - aws CLI configured with credentials | |
| # - nc (netcat) for TCP port checking and bidirectional I/O | |
| # - timeout command (GNU coreutils) | |
| # - Instance must have Elastic IP attached | |
| # | |
| # EXAMPLES: | |
| # # SSH (auto-starts if stopped) | |
| # ssh hostname | |
| # | |
| # # SCP (works transparently) | |
| # scp file.txt sandbox:/tmp/ | |
| # | |
| # # Manual stop | |
| # ~/bin/ec2-ssh-proxy --stop host_ip | |
| # | |
| # # With custom AWS profile/region | |
| # ~/bin/ec2-ssh-proxy --stop host_ip production us-west-2 | |
| # | |
| # DESIGN DECISIONS: | |
| # - Single script approach (consolidated from multiple helper scripts) | |
| # - EIP as identifier (easier reference than instance ID/name) | |
| # - ProxyCommand integration (transparent to ssh/scp) | |
| # - Manual stop only (ProxyCommand cannot monitor SSH session lifecycle) | |
| # - Optimization first (check TCP port before AWS APIs) | |
| # - Use nc for all network operations (consistent, standard tool) | |
| # | |
| # EXIT CODES: | |
| # 0 - Success | |
| # 1 - Error (missing EIP, instance not found, start failed, unexpected state) | |
| # | |
| # AUTHOR: | |
| # Generated by AI pair programming session (2026-01-14) | |
| # | |
| set -euo pipefail | |
| check_tcp_port() { | |
| local host="$1" | |
| local port="$2" | |
| local timeout="${3:-1}" | |
| timeout "$timeout" nc -z "$host" "$port" >/dev/null 2>&1 | |
| } | |
| get_instance_id_by_eip() { | |
| local eip="$1" | |
| aws ec2 describe-instances \ | |
| --profile "$AWS_PROFILE" \ | |
| ${AWS_REGION:+--region "$AWS_REGION"} \ | |
| --filters "Name=ip-address,Values=$eip" \ | |
| --query 'Reservations[0].Instances[0].InstanceId' \ | |
| --output text 2>/dev/null | |
| } | |
| get_instance_state() { | |
| local id="$1" | |
| aws ec2 describe-instances \ | |
| --profile "$AWS_PROFILE" \ | |
| ${AWS_REGION:+--region "$AWS_REGION"} \ | |
| --instance-ids "$id" \ | |
| --query 'Reservations[0].Instances[0].State.Name' \ | |
| --output text 2>/dev/null | |
| } | |
| start_and_wait() { | |
| local instance_id="$1" | |
| local eip="$2" | |
| echo "[INFO] Starting instance $instance_id..." >&2 | |
| if ! aws ec2 start-instances \ | |
| --profile "$AWS_PROFILE" \ | |
| ${AWS_REGION:+--region "$AWS_REGION"} \ | |
| --instance-ids "$instance_id" \ | |
| --output text >/dev/null 2>&1; then | |
| echo "[ERROR] Failed to start instance" >&2 | |
| return 1 | |
| fi | |
| echo "[INFO] Waiting for instance to be running..." >&2 | |
| aws ec2 wait instance-running \ | |
| --profile "$AWS_PROFILE" \ | |
| ${AWS_REGION:+--region "$AWS_REGION"} \ | |
| --instance-ids "$instance_id" 2>/dev/null || true | |
| echo "[INFO] Waiting for SSH to be ready..." >&2 | |
| local max_attempts=60 | |
| local attempts=0 | |
| while [ $attempts -lt $max_attempts ]; do | |
| if check_tcp_port "$eip" 22 1; then | |
| echo "[INFO] SSH port is ready after $attempts seconds" >&2 | |
| return 0 | |
| fi | |
| sleep 1 | |
| ((attempts++)) | |
| done | |
| echo "[WARNING] SSH port check timed out, proceeding anyway..." >&2 | |
| } | |
| show_usage() { | |
| echo "[ERROR] Usage:" >&2 | |
| echo " ec2-ssh-proxy --proxy <eip> [profile] [region]" >&2 | |
| echo " ec2-ssh-proxy --stop <eip> [profile] [region]" >&2 | |
| exit 1 | |
| } | |
| if [ "${1:-}" = "--proxy" ]; then | |
| EIP="${2:-}" | |
| [ -z "$EIP" ] && show_usage | |
| AWS_PROFILE="${3:-${AWS_PROFILE:-default}}" | |
| [ -n "${4:-}" ] && AWS_REGION="$4" | |
| if check_tcp_port "$EIP" 22 1; then | |
| echo "[INFO] SSH port already accessible, connecting directly..." >&2 | |
| exec nc "$EIP" 22 | |
| fi | |
| echo "[INFO] SSH port closed, checking instance state..." >&2 | |
| INSTANCE_ID=$(get_instance_id_by_eip "$EIP") | |
| if [ -z "$INSTANCE_ID" ] || [ "$INSTANCE_ID" = "None" ] || [ "$INSTANCE_ID" = "null" ]; then | |
| echo "[ERROR] No instance found with EIP: $EIP" >&2 | |
| exit 1 | |
| fi | |
| STATE=$(get_instance_state "$INSTANCE_ID") | |
| case "$STATE" in | |
| running) | |
| echo "[INFO] Instance running but SSH not ready yet, waiting..." >&2 | |
| start_and_wait "$INSTANCE_ID" "$EIP" | |
| ;; | |
| stopped) | |
| start_and_wait "$INSTANCE_ID" "$EIP" | |
| ;; | |
| stopping|shutting-down) | |
| echo "[INFO] Instance is $STATE, waiting for stopped state..." >&2 | |
| aws ec2 wait instance-stopped \ | |
| --profile "$AWS_PROFILE" \ | |
| ${AWS_REGION:+--region "$AWS_REGION"} \ | |
| --instance-ids "$INSTANCE_ID" 2>/dev/null || true | |
| start_and_wait "$INSTANCE_ID" "$EIP" | |
| ;; | |
| *) | |
| echo "[ERROR] Instance in unexpected state: $STATE" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| exec nc "$EIP" 22 | |
| fi | |
| if [ "${1:-}" = "--stop" ]; then | |
| EIP="${2:-}" | |
| [ -z "$EIP" ] && show_usage | |
| AWS_PROFILE="${3:-${AWS_PROFILE:-default}}" | |
| [ -n "${4:-}" ] && AWS_REGION="$4" | |
| INSTANCE_ID=$(get_instance_id_by_eip "$EIP") | |
| if [ -z "$INSTANCE_ID" ] || [ "$INSTANCE_ID" = "None" ] || [ "$INSTANCE_ID" = "null" ]; then | |
| echo "[ERROR] No instance found with EIP: $EIP" >&2 | |
| exit 1 | |
| fi | |
| echo "[INFO] Stopping instance $INSTANCE_ID (EIP: $EIP)..." >&2 | |
| aws ec2 stop-instances \ | |
| --profile "$AWS_PROFILE" \ | |
| ${AWS_REGION:+--region "$AWS_REGION"} \ | |
| --instance-ids "$INSTANCE_ID" \ | |
| --output text >/dev/null | |
| echo "[INFO] Instance stop initiated" >&2 | |
| exit 0 | |
| fi | |
| show_usage |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment