See the repo for this script here.
In the last post, I created an Ansible role to configure an EC2 instance to shutdown when no active SSH connection was detected.
I still had a couple of problems, though:
Problems
First, when an EC2 instance reboots, its public IP address will change (if configured to have one). This meant that my local ~/.ssh/config would be out of date. I’m connecting to an IP in the Hostname
setting, so I won’t connect if the IP is wrong!
Second, the instance is shutdown, which means I can’t SSH to it without bringing it back up.
Possible Solutions
I had four thoughts on fixing things:
Elastic IP
I could use an Elastic IP Address.
These can be associated with an instance or an ENI, which would make it easy to set up. However, I believe your account is limited to 5 Elastic IPs, and you’re charged for your Elastic IP if your instance is not running. This is the opposite of what I want!
This also does not address the problem of bringing the instance back up.
ENI
An ENI (Elastic Network Interface) can also have a public IP address and attach to an instance. It will not bring the instance up.
Bash Script with AWS CLI
This is the solution I settled on for now. See the script below. The script allows me to start and stop an instance by name, and also updates the IP address of its associated ~/.ssh/config entry.
I like this solution because I could use it with another cloud provider.
One drawback is that it won’t automatically start an instance that is shutdown. I have to manually run my script to bring the instance up.
I believe I have to either go through a bastion server or set up a Lambda with ProxyCommand
set in my ~/.ssh/config. I haven’t decided how or if I’ll set this up.
Use AWS SSM
AWS SSM (Systems Manager Sessions Manager) would solve my problems when working with AWS. I would set ProxyCommand
in ~/.ssh/config to run some sort of SSM script that would check the instance’s status, boot if necessary, then start a session.
This solution won’t work with other cloud providers, but I might implement it in the future to try it out.
The Solution
I wrote a bash script to start and stop an AWS instance by name. It will also update the ~/.ssh/config so you can seamlessly SSH into your instance.
Make it executable and use it like this:
# Start an instance
./ec2 start <some_server_name>
# Stop an instance
./ec2 stop <some_server_name>
Here are notes on the script:
- The name variable matches the
Host
value in my ~/.ssh/config and the instance name in the AWS instance console. In other words, I have a Host that looks likeHost some_server_name
. --no-cli-pager
is handy. This turns off the AWS CLI pager, which makes the script interactive. You can also do this by setting the env variableAWS_PAGER=""
.- The replacement of the IP address in line 61 is brittle. I’m grepping a hardcoded 4 lines after finding the name. If the SSH config changes in my Ansible role, this might fail.
- Does a global replacement on the IP address in ~/.ssh/config.
- There is no error handling (ex. you can call it without an argument).
1#!/bin/bash
2# Start and stop and EC2 instance.
3# If started, replace the old IP address of the instance with the new one in ~/.ssh/config.
4
5# Parse command-line arguments
6if [ "$1" == "start" ]; then
7 action="start"
8elif [ "$1" == "stop" ]; then
9 action="stop"
10else
11 echo "Usage: $0 [start|stop] <instance-name>"
12 exit 1
13fi
14
15sleep_seconds=5
16
17# Get the instance name from the command line
18name="$2"
19
20instance_id=$(aws ec2 describe-instances --filters "Name=tag:Name,Values=$name" --query "Reservations[].Instances[].InstanceId" --output=text)
21
22# Check if the instance is already running or stopped
23status=$(aws ec2 describe-instances --filters "Name=tag:Name,Values=$name" --query "Reservations[*].Instances[*].State.Name" --output text)
24if [ "$status" == "running" ] && [ "$action" == "start" ]; then
25 echo "Instance $name ($instance_id) is already running"
26 exit 0
27elif [ "$status" == "stopped" ] && [ "$action" == "stop" ]; then
28 echo "Instance $name ($instance_id) is already stopped"
29 exit 0
30fi
31
32# Start or stop the instance
33if [ "$action" == "start" ]; then
34 aws ec2 start-instances --no-cli-pager --instance-ids $(aws ec2 describe-instances --filters "Name=tag:Name,Values=$name" --query "Reservations[*].Instances[*].InstanceId" --output text)
35elif [ "$action" == "stop" ]; then
36 aws ec2 stop-instances --no-cli-pager --instance-ids $(aws ec2 describe-instances --filters "Name=tag:Name,Values=$name" --query "Reservations[*].Instances[*].InstanceId" --output text)
37fi
38
39# Wait for the instance to start or stop
40# Polls every $sleep_seconds seconds
41while true; do
42 status=$(aws ec2 describe-instances --filters "Name=tag:Name,Values=$name" --query "Reservations[*].Instances[*].State.Name" --output text)
43 if [ "$action" == "start" ] && [ "$status" == "running" ]; then
44 echo "Instance $name ($instance_id) started"
45 break
46 elif [ "$action" == "stop" ] && [ "$status" == "stopped" ]; then
47 echo "Instance $name ($instance_id) stopped"
48 break
49 fi
50 echo Waiting...
51 sleep $sleep_seconds
52done
53
54# Update SSH config file with new IP address
55if [ "$action" == "start" ]; then
56 # Get instance IP address
57 instance_ip=$(aws ec2 describe-instances --instance-ids "$instance_id" --query "Reservations[].Instances[].PublicIpAddress" --output=text)
58
59 # Find the SSH config entry for the instance by name
60 # This is brittle! Change if Ansible ec2 role SSH changes.
61 config=$(grep -A4 -w "Host $name\$" ~/.ssh/config)
62
63 # Get the current hostname for the SSH config entry
64 old_hostname=$(echo "$config" | grep Hostname | awk '{print $2}')
65
66 # Replace the old hostname with the new IP address in the SSH config file
67 # Change every matching IP in the file
68 sed -i "s/$old_hostname/$instance_ip/g" ~/.ssh/config
69
70 echo "Instance started with ID $instance_id and IP address $instance_ip"
71fi
72
73exit 0