Last active
June 17, 2025 14:24
-
-
Save Trozz/451a0d91e793c8b6683bc0bdb6136b21 to your computer and use it in GitHub Desktop.
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 | |
| # 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