Skip to content

Instantly share code, notes, and snippets.

@Trozz
Last active June 17, 2025 14:24
Show Gist options
  • Save Trozz/451a0d91e793c8b6683bc0bdb6136b21 to your computer and use it in GitHub Desktop.
Save Trozz/451a0d91e793c8b6683bc0bdb6136b21 to your computer and use it in GitHub Desktop.
#!/bin/bash
# AWS SSM Connect Script
# Connects to EC2 instances via SSM using the instance Name tag
set -euo pipefail
# Default values
DEFAULT_PROFILE=""
DEFAULT_REGION=""
# Script variables
PROFILE="$DEFAULT_PROFILE"
REGION="$DEFAULT_REGION"
INSTANCE_NAME=""
SCRIPT_NAME=$(basename "$0")
PORT_FORWARD_MODE=false
LOCAL_PORT=""
REMOTE_PORT=""
REMOTE_HOST="localhost"
# Colours for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Colour
# Function to display usage
usage() {
cat << EOF
Usage: $SCRIPT_NAME [OPTIONS] INSTANCE_NAME
Connect to AWS EC2 instance via SSM using the instance Name tag.
OPTIONS:
-p, --profile PROFILE AWS profile to use (default: $DEFAULT_PROFILE)
-r, --region REGION AWS region to use (default: $DEFAULT_REGION)
-f, --port-forward Enable port forwarding mode
-L, --local-port PORT Local port for port forwarding (required with -f)
-R, --remote-port PORT Remote port for port forwarding (required with -f)
-H, --remote-host HOST Remote host on target instance (default: localhost)
-h, --help Show this help message
EXAMPLES:
# Standard SSM shell session
$SCRIPT_NAME web-server-01
$SCRIPT_NAME --profile prod --region us-east-1 database-server
# Port forwarding examples
$SCRIPT_NAME -f -L 8080 -R 80 web-server-01
$SCRIPT_NAME --port-forward --local-port 3306 --remote-port 3306 database-server
$SCRIPT_NAME -f -L 5432 -R 5432 -H 192.168.1.100 app-server
PORT FORWARDING:
Port forwarding allows you to access services running on the remote instance
through your local machine. The connection is established through SSM, so no
direct network connectivity to the instance is required.
While the port forwarding session is active, you can access the remote service
at: http://localhost:<local-port>
EOF
}
# Function to log messages with colours
log_info() {
echo -e "${BLUE}[INFO]${NC} $1" >&2
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1" >&2
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1" >&2
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1" >&2
}
# Function to check if required tools are installed
check_dependencies() {
local missing_deps=()
if ! command -v aws &> /dev/null; then
missing_deps+=("aws-cli")
fi
if ! command -v jq &> /dev/null; then
missing_deps+=("jq")
fi
if [ ${#missing_deps[@]} -ne 0 ]; then
log_error "Missing required dependencies: ${missing_deps[*]}"
log_error "Please install the missing tools and try again."
exit 1
fi
}
# Function to validate AWS credentials and region
validate_aws_config() {
log_info "Validating AWS configuration..."
if ! aws sts get-caller-identity --profile "$PROFILE" --region "$REGION" &> /dev/null; then
log_error "Failed to authenticate with AWS using profile '$PROFILE' in region '$REGION'"
log_error "Please check your AWS credentials and configuration."
exit 1
fi
log_success "AWS authentication successful"
}
# Function to query instances by Name tag
query_instances() {
local name_filter="$1"
log_info "Searching for instances with Name tag: '$name_filter'"
# First, get basic instance information
local aws_output
aws_output=$(aws ec2 describe-instances \
--profile "$PROFILE" \
--region "$REGION" \
--filters "Name=tag:Name,Values=$name_filter" "Name=instance-state-name,Values=running" \
--output json 2>&1)
local exit_code=$?
if [ $exit_code -ne 0 ]; then
log_error "AWS CLI command failed:"
log_error "$aws_output"
exit 1
fi
# Validate JSON output
if ! echo "$aws_output" | jq empty 2>/dev/null; then
log_error "Invalid JSON output from AWS CLI:"
log_error "$aws_output"
exit 1
fi
# Extract the instances with a simpler query
echo "$aws_output" | jq -r '.Reservations[].Instances[] | {
InstanceId: .InstanceId,
Name: (.Tags[] | select(.Key == "Name") | .Value),
InstanceType: .InstanceType,
State: .State.Name,
PrivateIpAddress: .PrivateIpAddress,
PublicIpAddress: .PublicIpAddress,
Tags: .Tags
}'
}
# Function to display instance information nicely
display_instance_info() {
local instance_data="$1"
local index="$2"
local instance_id=$(echo "$instance_data" | jq -r '.InstanceId')
local name=$(echo "$instance_data" | jq -r '.Name')
local instance_type=$(echo "$instance_data" | jq -r '.InstanceType')
local state=$(echo "$instance_data" | jq -r '.State')
local private_ip=$(echo "$instance_data" | jq -r '.PrivateIpAddress // "N/A"')
local public_ip=$(echo "$instance_data" | jq -r '.PublicIpAddress // "N/A"')
local tags=$(echo "$instance_data" | jq -r '.Tags')
echo -e "${YELLOW}[$index]${NC} Instance: ${GREEN}$instance_id${NC}" >&2
echo " Name: $name" >&2
echo " Type: $instance_type" >&2
echo " State: $state" >&2
echo " Private IP: $private_ip" >&2
echo " Public IP: $public_ip" >&2
# Display other relevant tags
local other_tags=$(echo "$tags" | jq -r '.[] | select(.Key != "Name") | " \(.Key): \(.Value)"' 2>/dev/null || true)
if [ -n "$other_tags" ]; then
echo " Other Tags:" >&2
echo "$other_tags" >&2
fi
echo >&2
}
# Function to check if SSM agent is running on the instance
check_ssm_availability() {
local instance_id="$1"
log_info "Checking SSM availability for instance: $instance_id"
# Check if instance is managed by SSM
local ssm_check=$(aws ssm describe-instance-information \
--profile "$PROFILE" \
--region "$REGION" \
--filters "Key=InstanceIds,Values=$instance_id" \
--query 'InstanceInformationList[0].InstanceId' \
--output text 2>/dev/null || echo "None")
if [ "$ssm_check" = "None" ] || [ -z "$ssm_check" ]; then
log_error "Instance $instance_id is not available for SSM connection."
log_error "This could be because:"
log_error " - SSM agent is not installed or not running"
log_error " - Instance doesn't have the required IAM role"
log_error " - Instance is not reachable by SSM service"
return 1
fi
log_success "Instance is available for SSM connection"
return 0
}
# Function to connect to instance via SSM
connect_to_instance() {
local instance_id="$1"
log_info "Instance ID: ${GREEN}$instance_id${NC}"
if ! check_ssm_availability "$instance_id"; then
exit 1
fi
# Get instance details for the summary
local instance_info=$(aws ec2 describe-instances \
--profile "$PROFILE" \
--region "$REGION" \
--instance-ids "$instance_id" \
--query 'Reservations[0].Instances[0].[Tags[?Key==`Name`].Value|[0],InstanceType,PrivateIpAddress,State.Name]' \
--output text 2>/dev/null)
local instance_name=$(echo "$instance_info" | cut -f1)
local instance_type=$(echo "$instance_info" | cut -f2)
local private_ip=$(echo "$instance_info" | cut -f3)
local state=$(echo "$instance_info" | cut -f4)
# Record start time
local start_time=$(date +%s)
local start_time_human=$(date '+%Y-%m-%d %H:%M:%S')
if [ "$PORT_FORWARD_MODE" = true ]; then
# Port forwarding mode
log_info "Setting up port forwarding: localhost:${LOCAL_PORT} -> ${REMOTE_HOST}:${REMOTE_PORT}"
log_info "Access the remote service at: ${GREEN}http://localhost:${LOCAL_PORT}${NC}"
log_info "Press Ctrl+C to terminate the port forwarding session"
echo >&2
# Choose the appropriate SSM document based on remote host
if [ "$REMOTE_HOST" = "localhost" ] || [ "$REMOTE_HOST" = "127.0.0.1" ]; then
# Standard port forwarding to localhost on the target instance
aws ssm start-session \
--profile "$PROFILE" \
--region "$REGION" \
--target "$instance_id" \
--document-name AWS-StartPortForwardingSession \
--parameters "localPortNumber=${LOCAL_PORT},portNumber=${REMOTE_PORT}"
else
# Port forwarding to a remote host accessible from the target instance
aws ssm start-session \
--profile "$PROFILE" \
--region "$REGION" \
--target "$instance_id" \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters "localPortNumber=${LOCAL_PORT},portNumber=${REMOTE_PORT},host=${REMOTE_HOST}"
fi
else
# Standard shell session
log_info "Initiating SSM session to $instance_id..."
log_info "To terminate the session, type 'exit' or press Ctrl+C"
echo >&2
# Start SSM session
aws ssm start-session \
--profile "$PROFILE" \
--region "$REGION" \
--target "$instance_id"
fi
# Calculate session duration
local end_time=$(date +%s)
local end_time_human=$(date '+%Y-%m-%d %H:%M:%S')
local duration=$((end_time - start_time))
# Format duration nicely
local hours=$((duration / 3600))
local minutes=$(((duration % 3600) / 60))
local seconds=$((duration % 60))
local duration_formatted=""
if [ $hours -gt 0 ]; then
duration_formatted="${hours}h ${minutes}m ${seconds}s"
elif [ $minutes -gt 0 ]; then
duration_formatted="${minutes}m ${seconds}s"
else
duration_formatted="${seconds}s"
fi
# Display session summary
echo >&2
if [ "$PORT_FORWARD_MODE" = true ]; then
log_success "Port forwarding session completed"
else
log_success "SSM session completed"
fi
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" >&2
echo -e "${GREEN}SESSION SUMMARY${NC}" >&2
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" >&2
echo -e "${YELLOW}Instance Details:${NC}" >&2
echo -e " • Instance ID: ${GREEN}$instance_id${NC}" >&2
echo -e " • Name: $instance_name" >&2
echo -e " • Type: $instance_type" >&2
echo -e " • Private IP: $private_ip" >&2
echo -e " • State: $state" >&2
echo >&2
echo -e "${YELLOW}Connection Details:${NC}" >&2
if [ "$PORT_FORWARD_MODE" = true ]; then
echo -e " • Session Type: ${GREEN}Port Forwarding${NC}" >&2
echo -e " • Local Port: ${GREEN}$LOCAL_PORT${NC}" >&2
echo -e " • Remote Port: ${GREEN}$REMOTE_PORT${NC}" >&2
echo -e " • Remote Host: $REMOTE_HOST" >&2
else
echo -e " • Session Type: ${GREEN}Interactive Shell${NC}" >&2
fi
echo -e " • Started: $start_time_human" >&2
echo -e " • Ended: $end_time_human" >&2
echo -e " • Duration: ${GREEN}$duration_formatted${NC}" >&2
echo -e " • Profile: $PROFILE" >&2
echo -e " • Region: $REGION" >&2
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" >&2
}
# Function to handle multiple instances
handle_multiple_instances() {
local instances="$1"
local count=$(echo "$instances" | jq length)
log_warning "Found $count instances with the specified name:"
echo >&2
# Display all instances with indices
for ((i=0; i<count; i++)); do
local instance_data=$(echo "$instances" | jq ".[$i]")
display_instance_info "$instance_data" "$((i+1))"
done
# Prompt user to select an instance
while true; do
echo -n "Please select an instance (1-$count) or 'q' to quit: " >&2
read -r selection
if [ "$selection" = "q" ] || [ "$selection" = "Q" ]; then
log_info "Operation cancelled by user"
exit 0
fi
if [[ "$selection" =~ ^[0-9]+$ ]] && [ "$selection" -ge 1 ] && [ "$selection" -le "$count" ]; then
local selected_instance=$(echo "$instances" | jq ".[$((selection-1))]")
local instance_id=$(echo "$selected_instance" | jq -r '.InstanceId')
connect_to_instance "$instance_id"
break
else
log_error "Invalid selection. Please enter a number between 1 and $count, or 'q' to quit."
fi
done
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-p|--profile)
PROFILE="$2"
shift 2
;;
-r|--region)
REGION="$2"
shift 2
;;
-f|--port-forward)
PORT_FORWARD_MODE=true
shift
;;
-L|--local-port)
LOCAL_PORT="$2"
shift 2
;;
-R|--remote-port)
REMOTE_PORT="$2"
shift 2
;;
-H|--remote-host)
REMOTE_HOST="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
-*)
log_error "Unknown option: $1"
usage
exit 1
;;
*)
if [ -z "$INSTANCE_NAME" ]; then
INSTANCE_NAME="$1"
else
log_error "Multiple instance names provided. Please specify only one."
usage
exit 1
fi
shift
;;
esac
done
# Validate required arguments
if [ -z "$INSTANCE_NAME" ]; then
log_error "Instance name is required."
usage
exit 1
fi
# Validate port forwarding arguments
if [ "$PORT_FORWARD_MODE" = true ]; then
if [ -z "$LOCAL_PORT" ] || [ -z "$REMOTE_PORT" ]; then
log_error "Port forwarding mode requires both --local-port and --remote-port."
usage
exit 1
fi
# Validate port numbers
if ! [[ "$LOCAL_PORT" =~ ^[0-9]+$ ]] || [ "$LOCAL_PORT" -lt 1 ] || [ "$LOCAL_PORT" -gt 65535 ]; then
log_error "Local port must be a valid port number (1-65535)."
exit 1
fi
if ! [[ "$REMOTE_PORT" =~ ^[0-9]+$ ]] || [ "$REMOTE_PORT" -lt 1 ] || [ "$REMOTE_PORT" -gt 65535 ]; then
log_error "Remote port must be a valid port number (1-65535)."
exit 1
fi
fi
# Main execution
main() {
log_info "AWS SSM Connect Script"
log_info "Profile: $PROFILE"
log_info "Region: $REGION"
log_info "Instance Name: $INSTANCE_NAME"
if [ "$PORT_FORWARD_MODE" = true ]; then
log_info "Mode: ${GREEN}Port Forwarding${NC}"
log_info "Port Mapping: localhost:${LOCAL_PORT} -> ${REMOTE_HOST}:${REMOTE_PORT}"
else
log_info "Mode: ${GREEN}Interactive Shell${NC}"
fi
echo >&2
# Check dependencies
check_dependencies
# Validate AWS configuration
validate_aws_config
# Query instances
local instances_raw=$(query_instances "$INSTANCE_NAME")
# Convert the JSON objects to an array
local instances
if [ -z "$instances_raw" ]; then
instances="[]"
else
instances=$(echo "$instances_raw" | jq -s '.')
fi
# Safely get instance count
local instance_count
instance_count=$(echo "$instances" | jq length 2>/dev/null)
# Handle case where jq fails or returns empty
if [ -z "$instance_count" ] || ! [[ "$instance_count" =~ ^[0-9]+$ ]]; then
log_error "Failed to parse instance data"
log_error "Raw output: $instances_raw"
exit 1
fi
if [ "$instance_count" -eq 0 ]; then
log_error "No running instances found with Name tag: '$INSTANCE_NAME'"
log_error "Please verify the instance name and ensure the instance is running."
exit 1
elif [ "$instance_count" -eq 1 ]; then
log_success "Found 1 instance with name: '$INSTANCE_NAME'"
local instance_id=$(echo "$instances" | jq -r '.[0].InstanceId')
connect_to_instance "$instance_id"
else
handle_multiple_instances "$instances"
fi
}
# Run main function
main
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment