Published 2020-10-24.
Last modified 2022-01-28.
Time to read: 3 minutes.
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
.
$ 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.
$ 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.
$ chmod 400 "$AWS_KEY_PAIR_FILE"
Now we can import the key pair into AWS:
$ 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
.
$ 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
.
$ 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.
$ 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
.
$ 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
.
$ AWS_SPOT_ID="$( jq -r .InstanceId <<< "$AWS_SPOT_INSTANCE" )"
$ echo "$AWS_SPOT_ID" i-012a54aefcd333de9
Wait for the instance to start.
$ 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
.
$ 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
.
$ 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:
$ sudo halt
Back in the shell that launched the spot instance, wait for the spot instance to stop before cleaning up.
$ 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:
$ 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.
$ chmod a+x createEc2Spot
Sample Usage
The script is easy to use:
$ 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:
$ sudo halt
Back in the shell on your computer, you should see:
$ 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.
$ chmod a+x createEc2Spot
Sample Usage
View the help like this:
$ 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:
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