Mike Slinn
Mike Slinn

Working With EC2 Spot Instances From AWS CLI

Published 2020-10-24. Last modified 2022-01-28.
Time to read: 3 minutes.

This site is categorized under AWS, Bash, Ubuntu.

AWS EC2 T2.medium spot instances cost less than 2 cents per hour for Linux and can be created very easily from the command line. They self-destruct once shut down. These powerful virtual machines can do an incredible amount of work in an hour for less than 2 cents!

This article shows how all this can be done via the command line. I also provide an interactive bash script for automating the process of obtaining and releasing an EC2 spot instance.

Once again this article uses AWS CLI and jq.

Create and Import a New Keypair

I want to create a new temporary ssh keypair that will just be used for this spot instance. The name of the new key pair will be of the form ~/.ssh/rsa-YYYY-MM-DD-mm-ss.

Shell
$ AWS_KEY_PAIR_NAME="rsa-$( date '+%Y-%m-%d-%H-%M-%S' )"

$ echo "$AWS_KEY_PAIR_NAME"
rsa-2020-11-04-10-43-54 

$ AWS_KEY_PAIR_FILE="~/.ssh/$AWS_KEY_PAIR_NAME"

$ echo "$AWS_KEY_PAIR_FILE"
~/.ssh/rsa-2020-11-04-10-43-54 

Now we can make the keypair. AWS EC2 does not accept keys longer than 2048 bits.

Shell
$ ssh-keygen -b 2048 -f "$AWS_KEY_PAIR_FILE" -P "" -N "" -t rsa
Generating public/private rsa key pair.
  Your identification has been saved in /home/mslinn/.ssh/rsa-2020-11-04-10-43-54
  Your public key has been saved in /home/mslinn/.ssh/rsa-2020-11-04-10-43-54.pub
  The key fingerprint is:
  SHA256:bQScX0UMn0xGDorxSvElMZzwMyyk7hgs2FNbshBNenA mslinn@mslinn.com
  The key's randomart image is:
  +---[RSA 2048]----+
  |  ooE  .*++o+**  |
  |   =.  ooXo=.B.. |
  |  o + o +.X.  =  |
  | o = * . =.o     |
  |. + = . S o      |
  |   o +   .       |
  |    . .          |
  |                 |
  |                 |
  +----[SHA256]-----+

The new public key will be stored in ~/.ssh/2020-11-04-10-43-54.pub and the new private key will be stored in ~/.ssh/2020-11-04-10-43-54.

Now set the permissions for the key.

Shell
$ chmod 400 "$AWS_KEY_PAIR_FILE"

Now we can import the key pair into AWS:

Shell
$ aws ec2 import-key-pair \
  --key-name "$AWS_KEY_PAIR_NAME" \
  --public-key-material "fileb://${AWS_KEY_PAIR_FILE}.pub"
{
  "KeyFingerprint": "c7:76:90:53:17:d0:fc:ba:45:dd:93:d2:93:03:c2:19",
  "KeyName": "2020-11-04-10-43-54",
  "KeyPairId": "key-092a2306ec3f4aff6"
}

Select an AMI

New AMIs become available every day. You probably want your EC2 spot instance to be created from the most recent AMI that matches your needs. For most of my work I want an Ubuntu 64-bit Intel/AMD server distribution. AWS documentation is helpful and gives us a head start in automating the AMI selection.

The following incantation sets an environment variable called AWS_AMI to the details in JSON syntax of the AMI for the most recent 64-bit Ubuntu server release for Intel/AMD architecture. The OwnerId of Canonical, the publisher of Ubuntu, is 099720109477.

Shell
$ AWS_AMI="$(
  aws ec2 describe-images \
    --owners 099720109477 \
    --filters "Name=name,Values=ubuntu/images/hvm-ssd/ubuntu-????????-????????-amd64-server-????????" \
              "Name=state,Values=available" \
    --query "reverse(sort_by(Images, &CreationDate))[:1]" | \
  jq -r '.[0]'
)"

$ echo "$AWS_AMI"
{
  "Architecture": "x86_64",
  "CreationDate": "2020-10-30T14:07:42.000Z",
  "ImageId": "ami-0c71ec98278087e60",
  "ImageLocation": "099720109477/ubuntu/images/hvm-ssd/ubuntu-groovy-20.10-amd64-server-20201030",
  "ImageType": "machine",
  "Public": true,
  "OwnerId": "099720109477",
  "PlatformDetails": "Linux/UNIX",
  "UsageOperation": "RunInstances",
  "State": "available",
  "BlockDeviceMappings": [
    {
      "DeviceName": "/dev/sda1",
      "Ebs": {
        "DeleteOnTermination": true,
        "SnapshotId": "snap-00bf581086dd686e5",
        "VolumeSize": 8,
        "VolumeType": "gp2",
        "Encrypted": false
      }
    },
    {
      "DeviceName": "/dev/sdb",
      "VirtualName": "ephemeral0"
    },
    {
      "DeviceName": "/dev/sdc",
      "VirtualName": "ephemeral1"
    }
  ],
  "Description": "Canonical, Ubuntu, 20.10, amd64 groovy image build on 2020-10-30",
  "EnaSupport": true,
  "Hypervisor": "xen",
  "Name": "ubuntu/images/hvm-ssd/ubuntu-groovy-20.10-amd64-server-20201030",
  "RootDeviceName": "/dev/sda1",
  "RootDeviceType": "ebs",
  "SriovNetSupport": "simple",
  "VirtualizationType": "hvm"
} 

Now let's extract the ID of the AMI image and save it as AWS_AMI_ID.

Shell
$ AWS_AMI_ID="$( jq -r '.[0].ImageId' <<< "$AWS_AMI" )"

$ echo "$AWS_AMI_ID"
ami-0c71ec98278087e60 

Create an EC2 Spot Instance

For my work I often want my spot instance to be created in the same VPC subnet as my other resources, with the same security group. That is why the following environment variables are defined for the Groups and SubnetId values within the network-interfaces option, as well as the AWS region. The script at the end of this article offers an easier way of obtaining all these values.

Shell
$ AWS_GROUP=sg-4cbc6f35

$ AWS_SUBNET=subnet-49de033f

$ AWS_ZONE=us-east-1c

$ AWS_EC2_TYPE=t2.medium

The following creates an AWS EC2 spot instance with a public IP address and runs it. Details about the newly created spot instance are stored as JSON in AWS_SPOT_INSTANCE.

Shell
$ AWS_SPOT_INSTANCE="$(
  aws ec2 run-instances \
    --image-id "$AWS_AMI_ID" \
    --instance-market-options '{ "MarketType": "spot" }' \
    --instance-type "$AWS_EC2_TYPE" \
    --key-name "$AWS_KEY_PAIR_NAME" \
    --network-interfaces "[ {
        \"DeviceIndex\": 0,
        \"Groups\": [\"$AWS_GROUP\"],
        \"SubnetId\": \"$AWS_SUBNET\",
        \"DeleteOnTermination\": true,
        \"AssociatePublicIpAddress\": true
      } ]" \
    --placement "{ \"AvailabilityZone\": \"$AWS_ZONE\" }" | \
    jq -r .Instances[0]
)"

$ echo "$AWS_SPOT_INSTANCE"
{
  "AmiLaunchIndex": 0,
  "ImageId": "ami-0dba2cb6798deb6d8",
  "InstanceId": "i-012a54aefcd333de9",
  "InstanceType": "t2.small",
  "KeyName": "rsa-2020-11-03.pub",
  "LaunchTime": "2020-11-03T23:19:50.000Z",
  "Monitoring": {
      "State": "disabled"
  },
  "Placement": {
      "AvailabilityZone": "us-east-1c",
      "GroupName": "",
      "Tenancy": "default"
  },
  "PrivateDnsName": "ip-10-0-0-210.ec2.internal",
  "PrivateIpAddress": "10.0.0.210",
  "ProductCodes": [],
  "PublicDnsName": "",
  "State": {
      "Code": 0,
      "Name": "pending"
  },
  "StateTransitionReason": "",
  "SubnetId": "subnet-49de033f",
  "VpcId": "vpc-f16a0895",
  "Architecture": "x86_64",
  "BlockDeviceMappings": [],
  "ClientToken": "026583fb-c94e-4bca-bdd2-8dcdcaa3aae9",
  "EbsOptimized": false,
  "EnaSupport": true,
  "Hypervisor": "xen",
  "InstanceLifecycle": "spot",
  "NetworkInterfaces": [
      {
          "Attachment": {
              "AttachTime": "2020-11-03T23:19:50.000Z",
              "AttachmentId": "eni-attach-04feb4d36cf5c6792",
              "DeleteOnTermination": true,
              "DeviceIndex": 0,
              "Status": "attaching"
          },
          "Description": "",
          "Groups": [
              {
                  "GroupName": "testSG",
                  "GroupId": "sg-4cbc6f35"
              }
          ],
          "Ipv6Addresses": [],
          "MacAddress": "0a:6d:ba:c5:65:4b",
          "NetworkInterfaceId": "eni-09ef90920cfb29dd9",
          "OwnerId": "031372724784",
          "PrivateIpAddress": "10.0.0.210",
          "PrivateIpAddresses": [
              {
                  "Primary": true,
                  "PrivateIpAddress": "10.0.0.210"
              }
          ],
          "SourceDestCheck": true,
          "Status": "in-use",
          "SubnetId": "subnet-49de033f",
          "VpcId": "vpc-f16a0895",
          "InterfaceType": "interface"
      }
  ],
  "RootDeviceName": "/dev/sda1",
  "RootDeviceType": "ebs",
  "SecurityGroups": [
      {
          "GroupName": "testSG",
          "GroupId": "sg-4cbc6f35"
      }
  ],
  "SourceDestCheck": true,
  "SpotInstanceRequestId": "sir-rrs9gm3j",
  "StateReason": {
      "Code": "pending",
      "Message": "pending"
  },
  "VirtualizationType": "hvm",
  "CpuOptions": {
      "CoreCount": 1,
      "ThreadsPerCore": 1
  },
  "CapacityReservationSpecification": {
      "CapacityReservationPreference": "open"
  },
  "MetadataOptions": {
      "State": "pending",
      "HttpTokens": "optional",
      "HttpPutResponseHopLimit": 1,
      "HttpEndpoint": "enabled"
  }
} 

Now extract the EC2 spot instance id and save it in AWS_SPOT_ID.

Shell
$ AWS_SPOT_ID="$(
  jq -r .InstanceId <<< "$AWS_SPOT_INSTANCE"
)"

$ echo "$AWS_SPOT_ID"
i-012a54aefcd333de9 

Wait for the instance to start.

Shell
$ aws ec2 wait instance-running --instance-ids "$AWS_SPOT_ID"

Connect to the Spot Instance

In order to ssh into the spot instance we first need to discover its IP address, which is saved in AWS_SPOT_IP.

Shell
$ AWS_SPOT_IP="$(
  aws ec2 describe-instances \
    --instance-ids $AWS_SPOT_ID | \
  jq -r .Reservations[0].Instances[0].PublicIpAddress
)"

$ echo "$AWS_SPOT_IP"
54.242.88.254 

Now we can connect to the spot instance via ssh. The default userid for Ubuntu is ubuntu.

Shell
$ ssh -i "$AWS_KEY_PAIR_FILE" "ubuntu@$AWS_SPOT_IP"
Warning: No xauth data; using fake authentication data for X11 forwarding.
Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 5.11.0-1027-aws x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Thu Jan 27 20:28:22 UTC 2022

  System load:  0.06              Processes:             113
  Usage of /:   18.3% of 7.69GB   Users logged in:       0
  Memory usage: 5%                IPv4 address for eth0: 10.0.0.29
  Swap usage:   0%

1 update can be applied immediately.
To see these additional updates run: apt list --upgradable


The list of available updates is more than a week old.
To check for new updates run: sudo apt update


The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

/usr/bin/xauth:  file /home/ubuntu/.Xauthority does not exist
To run a command as administrator (user "root"), use "sudo ".
See "man sudo_root" for details.

ubuntu@ip-10-0-0-29:~$ 

Do your work on the spot instance now. We'll disconnect and clean up next.

Disconnect from the Spot Instance and Clean Up

Using the Command Line

Once the spot instance stops it is automatically terminated. The instance will survive a reboot, but not a halt. From a prompt on the spot instance, type:

shell (Spot Instance)
$ sudo halt

Back in the shell that launched the spot instance, wait for the spot instance to stop before cleaning up.

Shell
$ aws ec2 wait instance-stopped --instance-ids $AWS_SPOT_ID

Delete the temporary ssh keypair we created. Copies exist on AWS and the local machine; we need to remove all of them, like this:

Shell
$ aws ec2 delete-key-pair --key-name $AWS_KEY_PAIR_NAME

$ rm $AWS_KEY_PAIR_FILE $AWS_KEY_PAIR_FILE.pub

Checking With Web Console

You can use the web console to verify that all the spot instances were shut down, and the key pairs were deleted.

Bash Script createEc2Spot

Source Code

This script does everything discussed above, plus it prompts the user with default values for parameters unique to each invocation. Click on the name of the script and save it this script to a directory on your PATH.

#!/bin/bash

# Author: Mike Slinn mslinn@mslinn.com
# Initial version 2020-1-25
# Last modified 2022-01-27

set -e

function readWithDefault {
  >&2 printf "\n$1: "
  read -e -i "$2" VALUE
  echo "$VALUE"
}

echo "The AWS EC2 spot instance needs to share settings with the existing EC2 instance you want to affect."
echo "The easiest way to do this is to reference an EC2 instance with a Name tag, so it can be identified."
echo "If there is no such EC2 instance, delete the default value in the next prompt, and you will be able to specify the details manually."
AWS_EC2_NAME="$( readWithDefault "AWS EC2 Name tag value" production )"
if [ "$AWS_EC2_NAME" ]; then
  AWS_EC2_PRODUCTION="$(
    aws ec2 describe-instances | \
    jq ".Reservations[].Instances[] | select((.Tags[]?.Key==\"Name\") and (.Tags[]?.Value==\"$AWS_EC2_NAME\"))"
  )"
  AWS_GROUP="$( jq -r ".NetworkInterfaces[].Groups[].GroupId" <<< "$AWS_EC2_PRODUCTION" )"
  AWS_SUBNET="$( jq -r ".SubnetId" <<< "$AWS_EC2_PRODUCTION" )"
  AWS_ZONE="$( jq -r ".Placement.AvailabilityZone" <<< "$AWS_EC2_PRODUCTION" )"
else
  echo "Please answer a few questions so the AWS EC2 spot instance can be created."
  AWS_GROUP="$( readWithDefault "AWS security group" sg-4cbc6f35 )"
  AWS_SUBNET="$( readWithDefault "EC2 subnet" subnet-49de033f )"
  AWS_ZONE="$( readWithDefault "AWS availability zone" us-east-1c )"
fi

echo "EC2 spot instances are really inexpensive, so be generous with the size of the machine type for this spot instance."
AWS_EC2_TYPE="$( readWithDefault "EC2 machine type" t2.medium )"

AWS_KEY_PAIR_NAME="rsa-$( date '+%Y-%m-%d-%H-%M-%S' )"

AWS_KEY_PAIR_FILE="$HOME/.ssh/$AWS_KEY_PAIR_NAME"

ssh-keygen -b 2048 -f "$AWS_KEY_PAIR_FILE" -P "" -N "" -t rsa # -q
chmod 400 "$AWS_KEY_PAIR_FILE"

aws ec2 import-key-pair \
  --key-name "$AWS_KEY_PAIR_NAME" \
  --public-key-material "fileb://$AWS_KEY_PAIR_FILE.pub"

echo "Searching for the latest 64-bit Intel/AMD Ubuntu AMI by Canonical."
AWS_AMI="$(
  aws ec2 describe-images \
    --owners 099720109477 \
    --filters "Name=name,Values=ubuntu/images/hvm-ssd/ubuntu-????????-????????-amd64-server-????????" \
              "Name=state,Values=available" \
    --query "reverse(sort_by(Images, &CreationDate))[:1]" | \
  jq -r '.[0]'
)"

echo "Obtaining the AMI image ID."
AWS_AMI_ID="$( jq -r '.ImageId' <<< "$AWS_AMI" )"

echo "Creating the EC2 spot instance."
AWS_SPOT_INSTANCE="$(
  aws ec2 run-instances \
    --image-id $AWS_AMI_ID \
    --instance-market-options '{ "MarketType": "spot" }' \
    --instance-type $AWS_EC2_TYPE \
    --key-name $AWS_KEY_PAIR_NAME \
    --network-interfaces "[ {
        \"DeviceIndex\": 0,
        \"Groups\": [\"$AWS_GROUP\"],
        \"SubnetId\": \"$AWS_SUBNET\",
        \"DeleteOnTermination\": true,
        \"AssociatePublicIpAddress\": true
      } ]" \
    --placement "{ \"AvailabilityZone\": \"$AWS_ZONE\" }" | \
    jq -r .Instances[0]
)"

echo "Obtaining the EC2 spot instance ID."
AWS_SPOT_ID="$(
  jq -r .InstanceId <<< "$AWS_SPOT_INSTANCE"
)"

echo "Awaiting for the EC2 spot instance $AWS_SPOT_ID to enter the running state."
aws ec2 wait instance-running --instance-ids $AWS_SPOT_ID

echo "Obtaining the IP address of the new EC2 spot instance $AWS_SPOT_ID."
AWS_SPOT_IP="$(
  aws ec2 describe-instances \
    --instance-ids $AWS_SPOT_ID | \
  jq -r .Reservations[0].Instances[0].PublicIpAddress
)"

echo "About to ssh to the EC2 spot instance as ubuntu@$AWS_SPOT_IP using $AWS_KEY_PAIR_FILE."
echo "When you are done, type: sudo halt."
echo "The spot instance will then terminate and be gone forever."
echo "Any predefined resources, such as volumes that you attach will be freed."
ssh -i "$AWS_KEY_PAIR_FILE" "ubuntu@$AWS_SPOT_IP"

echo "Waiting for the EC2 spot instance $AWS_SPOT_ID to enter the stopped state."
aws ec2 wait instance-stopped --instance-ids "$AWS_SPOT_ID"

echo "The spot instance is no longer available. Deleting its keypair."
aws ec2 delete-key-pair --key-name AWS_KEY_PAIR_NAME
rm $AWS_KEY_PAIR_FILE $AWS_KEY_PAIR_FILE.pub

Make the script executable.

Shell
$ chmod a+x createEc2Spot

Sample Usage

The script is easy to use:

Shell
$ createEc2Spot
The AWS EC2 spot instance needs to share settings with the existing EC2 instance you want to affect.
The easiest way to do this is to reference an EC2 instance with a Name tag, so it can be identified.
If there is no such EC2 instance, delete the default value in the next prompt, and you will be able to specify the details manually.

AWS EC2 Name tag value: production

EC2 spot instances are really inexpensive, so be generous with the size of the machine type for this spot instance.
EC2 machine type: t2.medium

Generating public/private rsa key pair.
  Your identification has been saved in /home/mslinn/.ssh/rsa-2020-11-04-10-43-54
  Your public key has been saved in /home/mslinn/.ssh/rsa-2020-11-04-10-43-54.pub
  The key fingerprint is:
  SHA256:bQScX0UMn0xGDorxSvElMZzwMyyk7hgs2FNbshBNenA mslinn@mslinn.com
  The key's randomart image is:
  +---[RSA 2048]----+
  |  ooE  .*++o+**  |
  |   =.  ooXo=.B.. |
  |  o + o +.X.  =  |
  | o = * . =.o     |
  |. + = . S o      |
  |   o +   .       |
  |    . .          |
  |                 |
  |                 |
  +----[SHA256]-----+
    "KeyFingerprint": "be:19:50:59:a1:83:ea:c1:91:1e:2f:d6:31:64:9a:c0",
    "KeyName": "rsa-2022-01-27-50-02",
    "KeyPairId": "key-072a3f0545864526a"
}
Searching for the latest 64-bit Intel/AMD Ubuntu AMI by Canonical.
Obtaining the AMI image ID.
Creating the EC2 spot instance.
Obtaining the EC2 spot instance ID.
Awaiting for the EC2 spot instance i-03d09e364ed15a448 to enter the running state.
Obtaining the IP address of the new EC2 spot instance i-03d09e364ed15a448.
54.242.88.254
When you are done, type: sudo halt.
The spot instance will then terminate and be gone forever.
Any predefined resources, such as volumes that you attach will be freed.

Waiting for the EC2 spot instance $AWS_SPOT_ID to enter the stopped state. 

Now do your work on the spot instance, and then run sudo halt. Once the spot instance shuts down, it is destroyed and the script cleans up. Do not try to reboot the spot instance, because that shuts it down and it goes away instead of coming back up. Now do your work on the spot instance. From a prompt on the spot instance, type:

shell (Spot Instance)
$ sudo halt

Back in the shell on your computer, you should see:

Shell
$ sudo halt
The spot instance is no longer available. Deleting its keypair. 

Bash Script aws_ec2_functions

Source Code

This is another script does the same thing as the previous script, but in steps. Click on the name of the script and save it this script to a directory on your PATH.

#!/bin/bash

# Author: Mike Slinn mslinn@mslinn.com
# Initial version 2022-01-28
# Last modified 2022-01-28


function readWithDefault {
  # prompt user for a value, with a default
  >&2 printf "\n$1: "
  read -e -i "$2" VALUE
  echo "$VALUE"
}


function requires {
  # Halts if any of the supplied arguments is not the name of a defined environment variable
  for ENV_VAR in "$@"; do
    if [ -z "${!ENV_VAR}" ]; then
    echo "Error: ${ENV_VAR} is undefined."
    return 1 2> /dev/null || exit 1
  #else
  #  echo "${ENV_VAR} has value ${!ENV_VAR}"
  fi
  done
}


function attachVolumeToSpot {
  requires AWS_SPOT_ID AWS_NEW_VOLUME_ID || return

  export AWS_ATTACH_VOLUME="$(
    aws ec2 attach-volume \
      --device /dev/xvdh \
      --instance-id $AWS_SPOT_ID \
      --volume-id $AWS_NEW_VOLUME_ID
  )"

  aws ec2 wait volume-in-use --volume-id "$AWS_NEW_VOLUME_ID"

  export AWS_ATTACH_VOLUME_DEVICE="$(
    aws ec2 describe-volumes \
      --volume-id "$AWS_NEW_VOLUME_ID" | \
    jq -r .Volumes[0].Attachments[0].Device
  )"
}


function chroot {
  requires AWS_NEW_VOLUME_ID || return

  # Use the /tmp/mounter script built by attachVolumeToSpot

  aws ec2 detach-volume --volume-id $AWS_NEW_VOLUME_ID
  aws ec2 wait volume-available --volume-id $AWS_NEW_VOLUME_ID
}


function copyScriptToSpot {
  requires AWS_ATTACH_VOLUME_DEVICE

  cat >/tmp/mounter <<EOF
sudo mount "${AWS_ATTACH_VOLUME_DEVICE}1" /mnt

sudo mount -o bind /dev /mnt/dev
sudo mount -o bind /dev/shm /mnt/dev/shm
sudo mount -o bind /sys /mnt/sys
sudo mount -o bind /run /mnt/run
sudo mount -t proc proc /mnt/proc
sudo mount -t devpts devpts /mnt/dev/pts
sudo chroot /mnt

# If the user types 'exit' then this script continues.
# Otherwise, if the user types 'sudo halt' this script is not needed, AWS does the cleanup.

sudo umount /mnt/dev
sudo umount /mnt/dev/shm
sudo umount /mnt/sys
sudo umount /mnt/run
sudo umount /mnt/proc
sudo umount /mnt/dev/pts
sudo umount /mnt
EOF
  set -xv
  chmod a+x /tmp/mounter

  scpToSpot /tmp/mounter mounter

  echo "About to ssh into the spot instance. Run ./mounter to enter chroot using the new volume"
  sshToSpot
  # Proves the drive is mounted:
  #df -h | grep '^/dev/' | grep -v '^/dev/loop'

  rm /tmp/mounter
}


function deleteEc2SpotInstance {
  requires AWS_KEY_PAIR_NAME AWS_SPOT_ID || return

  # Hope that this does not bomb out if the user types 'sudo halt'
  aws ec2 cancel-spot-instance-requests --spot-instance-request-ids "$AWS_SPOT_ID"

  echo "Waiting for the EC2 spot instance $AWS_SPOT_ID to enter the stopped state."
  aws ec2 wait instance-stopped --instance-ids "$AWS_SPOT_ID"

  echo "The spot instance is no longer available. Deleting its keypair."
  aws ec2 delete-key-pair --key-name AWS_KEY_PAIR_NAME
  rm $AWS_KEY_PAIR_FILE $AWS_KEY_PAIR_FILE.pub
}


function findEc2 {
  echo "The existing AWS EC2 instance needs a snapshot to be made, which will then be turned into a volume and then mounted on a new AWS EC2 spot instance."
  echo "This script looks for an EC2 instance with a Name tag, so it can be identified."
  export AWS_EC2_ORIGINAL_NAME="$( readWithDefault "AWS EC2 Name tag value" production )"

  export AWS_EC2_ORIGINAL="$(
    aws ec2 describe-instances | \
    jq ".Reservations[].Instances[] | select((.Tags[]?.Key==\"Name\") and (.Tags[]?.Value==\"$AWS_EC2_ORIGINAL_NAME\"))"
  )"

  export AWS_ORIGINAL_EC2_INSTANCE_ID="$(
    jq -r .InstanceId <<< "$AWS_EC2_ORIGINAL"
  )"

  export AWS_ORIGINAL_EC2_IP="$(
    jq -r .PublicIpAddress <<< "$AWS_EC2_ORIGINAL"
  )"

  AWS_ORIGINAL_VOLUME_ID="$(
    jq -r '.BlockDeviceMappings[].Ebs.VolumeId' <<< "$AWS_EC2_ORIGINAL"
  )"

  export AWS_GROUP="$(
    jq -r ".NetworkInterfaces[].Groups[].GroupId" <<< "$AWS_EC2_ORIGINAL"
  )"

  export AWS_SUBNET="$( jq -r ".SubnetId" <<< "$AWS_EC2_ORIGINAL" )"
  export AWS_ZONE="$( jq -r .Placement.AvailabilityZone <<< "$AWS_EC2_ORIGINAL" )"

  echo "Original EC2 instance $AWS_ORIGINAL_EC2_INSTANCE_ID
  is at IP address $AWS_ORIGINAL_EC2_IP,
  has EBS volume $AWS_ORIGINAL_VOLUME_ID,
  with security group $AWS_GROUP,
  in $AWS_SUBNET,
  in the $AWS_ZONE zone."
}


function findSnapshot {
  # Look for a snapshot previously created by this script
  export AWS_SNAPSHOT_ID="$(
    aws ec2 describe-snapshots \
      --owner-ids self \
      --filters Name=tag:Name,Values=TestScript | \
    jq -r .Snapshots[].SnapshotId
  )"
}


function findVolume {
  # Look for a snapshot previously created by this script

  requires AWS_ZONE AWS_SNAPSHOT_ID || return

  export AWS_ATTACH_VOLUME_DEVICE="$(
    aws ec2 describe-volumes \
      --filters Name=tag:Name,Values=TestScript | \
    jq -r .Volumes[0].Attachments[0].Device
  )"
}


function fn_help {
  echo "Bash functions to make working with AWS EC2 easier via the command line.
Source this file then call the functions:
  attachVolumeToSpot
  chroot
  copyScriptToSpot
  deleteEc2SpotInstance
  findEc2
  findSnapshot
  findVolume
  latestUbuntuAmi
  makeEc2SpotInstance
  makeSnapshot
  makeVolumeFromSnapshot
  mountVolumeOnSpot
  scpToSpot
  sshToSpot

Typical usage requires ' || true' to keep the terminal open if a problem occurs.
This is unnecessary if you invoke from another bash script.
  source aws_ec2_functions
  findEc2 || true
  makeSnapshot || true
  makeVolumeFromSnapshot || true
  latestUbuntuAmi || true
  makeEc2SpotInstance || true
  attachVolumeToSpot || true
  copyScriptToSpot || true

  ... or, to perform all of the above:
  source aws_ec2_functions
  prepare_spot || true

  ... another way to perform all of the above:
  aws_ec2_functions run

  ... to pick up from a failed attempt, which created a snapshot and a volume, but did not make a spot instance, or the spot instance has been cancelled:
  findSnapshot || true
  findVolume || true
  latestUbuntuAmi || true
  makeEc2SpotInstance || true
  attachVolumeToSpot || true
  copyScriptToSpot || true
"
}


function latestUbuntuAmi {
  export AWS_AMI="$(
    aws ec2 describe-images \
      --owners 099720109477 \
      --filters "Name=name,Values=ubuntu/images/hvm-ssd/ubuntu-????????-????????-amd64-server-????????" \
                "Name=state,Values=available" \
      --query "reverse(sort_by(Images, &CreationDate))[:1]" | \
    jq -r '.[0]'
  )"

  export AWS_AMI_ID="$( jq -r '.ImageId' <<< "$AWS_AMI" )"

  echo "The most recent Ubuntu AMI ID is $AWS_AMI_ID"
}


function makeEc2SpotInstance {
  requires AWS_AMI_ID AWS_GROUP AWS_SUBNET AWS_ZONE || return

  export AWS_KEY_PAIR_NAME="rsa-$( date '+%Y-%m-%d-%H-%M-%S' )"

  export AWS_KEY_PAIR_FILE="$HOME/.ssh/$AWS_KEY_PAIR_NAME"

  ssh-keygen -b 2048 -f "$AWS_KEY_PAIR_FILE" -P "" -N "" -t rsa # -q
  chmod 400 "$AWS_KEY_PAIR_FILE"

  aws ec2 import-key-pair \
    --key-name "$AWS_KEY_PAIR_NAME" \
    --public-key-material "fileb://$AWS_KEY_PAIR_FILE.pub"

  echo "EC2 spot instances are really inexpensive, so be generous with the size of the machine type for this spot instance."
  export AWS_EC2_TYPE="$( readWithDefault "EC2 machine type" t2.medium )"

  export AWS_SPOT_INSTANCE="$(
    aws ec2 run-instances \
      --image-id $AWS_AMI_ID \
      --instance-market-options '{ "MarketType": "spot" }' \
      --instance-type $AWS_EC2_TYPE \
      --key-name $AWS_KEY_PAIR_NAME \
      --network-interfaces "[ {
          \"DeviceIndex\": 0,
          \"Groups\": [\"$AWS_GROUP\"],
          \"SubnetId\": \"$AWS_SUBNET\",
          \"DeleteOnTermination\": true,
          \"AssociatePublicIpAddress\": true
        } ]" \
      --placement "{ \"AvailabilityZone\": \"$AWS_ZONE\" }" | \
      jq -r .Instances[0]
  )"

  export AWS_SPOT_ID="$(
    jq -r .InstanceId <<< "$AWS_SPOT_INSTANCE"
  )"

  echo "Waiting for the EC2 spot instance $AWS_SPOT_ID to enter the running state. This usually takes about 1 minute."
  aws ec2 wait instance-running --instance-ids "$AWS_SPOT_ID"

  export AWS_SPOT_IP="$(
    aws ec2 describe-instances \
      --instance-ids $AWS_SPOT_ID | \
    jq -r .Reservations[0].Instances[0].PublicIpAddress
  )"
  echo "The IP address of the new EC2 spot instance $AWS_SPOT_ID is $AWS_SPOT_IP."
}


function makeSnapshot {
  # Makes an AWS EC2 snapshot with name TestScript from AWS_ORIGINAL_VOLUME_ID

  requires AWS_ORIGINAL_VOLUME_ID || return

  COMPLETION_TIME="$(date --date="@$(($(date +%s)+120))" +"%H":"%M":"%S")"
  echo "Snapshots take about 2 minutes. This one should complete by $COMPLETION_TIME."

  export AWS_SNAPSHOT_ID="$(
    aws ec2 create-snapshot --volume-id "$AWS_ORIGINAL_VOLUME_ID" \
    --description "production $( date '+%Y-%m-%d' )" \
    --tag-specifications "ResourceType=snapshot,Tags=[{Key=Created, Value=`date '+%Y-%m-%d'`},{Key=Name, Value=\"TestScript\"}]" | \
    jq -r .SnapshotId
  )"

  aws ec2 wait snapshot-completed --snapshot-ids "$AWS_SNAPSHOT_ID"

  echo "Snapshot $AWS_SNAPSHOT_ID is complete."
}


function makeVolumeFromSnapshot {
  # Makes an AWS EC2 volume with name TestScript

  requires AWS_ZONE AWS_SNAPSHOT_ID || return

  export AWS_NEW_VOLUME_ID="$(
    aws ec2 create-volume \
    --availability-zone $AWS_ZONE \
    --snapshot-id $AWS_SNAPSHOT_ID \
    --tag-specifications 'ResourceType=volume,Tags=[{Key=Name,Value=TestScript}]' | \
    jq -r .VolumeId
  )"

  echo "Waiting for AWS volume $AWS_NEW_VOLUME_ID to become available. This usually takes about 20 seconds."

  aws ec2 wait volume-available --volume-id "$AWS_NEW_VOLUME_ID"

  echo "$AWS_NEW_VOLUME_ID"
}


function prepare_spot {
  # Run everything
  findEc2
  makeSnapshot
  makeVolumeFromSnapshot
  latestUbuntuAmi
  makeEc2SpotInstance
  attachVolumeToSpot
  copyScriptToSpot
}


function scpToSpot {
  requires AWS_KEY_PAIR_FILE AWS_SPOT_IP || return

  scp -pi "$AWS_KEY_PAIR_FILE" "$1" "ubuntu@$AWS_SPOT_IP:$2"
}


function sshToSpot {
  requires AWS_KEY_PAIR_FILE AWS_SPOT_IP || return

  echo "When you are done, type: sudo halt."
  echo "The AWS EC2 spot instance will then terminate and be gone forever."
  echo "Any predefined resources, such as volumes that you attach will be freed."

  ssh -i "$AWS_KEY_PAIR_FILE" "ubuntu@$AWS_SPOT_IP" "$*"
}


if [ "$1" == prepare_spot ]; then
  echo "Starting..."
  prepare_spot || true
elif [ "$1" ]; then
  fn_help || true
else
  echo "Type fn_help to obtain help information"
fi

Make the script executable.

Shell
$ chmod a+x createEc2Spot

Sample Usage

View the help like this:

Shell
$ aws_ec2_functions -h
Bash functions to make working with AWS EC2 easier via the command line.
Source this file then call the functions, listed alphabetically:
  attachVolumeToSpot
  chroot
  copyScriptToSpot
  deleteEc2SpotInstance
  findEc2
  findSnapshot
  findVolume
  latestUbuntuAmi
  makeEc2SpotInstance
  makeSnapshot
  makeVolumeFromSnapshot
  mountVolumeOnSpot
  scpToSpot
  sshToSpot

Typical usage requires ' || true' to keep the terminal open if a problem occurs.
This is unnecessary if you invoke from another bash script.
  source aws_ec2_functions
  findEc2 || true
  makeSnapshot || true
  makeVolumeFromSnapshot || true
  latestUbuntuAmi || true
  makeEc2SpotInstance || true
  attachVolumeToSpot || true
  copyScriptToSpot || true

  ... or, to perform all of the above:
  source aws_ec2_functions
  prepare_spot || true

  ... another way to perform all of the above:
  aws_ec2_functions run

  ... to pick up from a failed attempt, which created a snapshot and a volume, but did not make a spot instance, or the spot instance has been cancelled:
  findSnapshot || true
  findVolume || true
  latestUbuntuAmi || true
  makeEc2SpotInstance || true
  attachVolumeToSpot || true
  copyScriptToSpot || true 

Using the chroot

Using either of the preceding two ways to set up the chroot, enter it using the mounter script:

shell on Spot Instance
ubuntu@ip-10-0-0-193:~$ ls
mounter

ubuntu@ip-10-0-0-193:~$ ./mounter

ubuntu@ip-10-0-0-193:~$ echo "127.0.1.1 $(hostname)" >> /etc/hosts

root@ip-10-0-0-193:/# su ubuntu

ubuntu@ip-10-0-0-193:/$ # Do whatever you need to do

ubuntu@ip-10-0-0-193:/$ sudo halt  # Shut everything down