Skip to content

Instantly share code, notes, and snippets.

@shokoe
Last active March 7, 2023 11:32
Show Gist options
  • Save shokoe/e7b4d80b63ea5931ba23bcad1c7684d4 to your computer and use it in GitHub Desktop.
Save shokoe/e7b4d80b63ea5931ba23bcad1c7684d4 to your computer and use it in GitHub Desktop.
#!/bin/bash
# Syntax: ec2_kamino.sh <source instance 'Name' tag> [target new instance 'Name' tag].
# If target name is not given <source name>-clone is used.
# Description: Builds a temporary template from a given instance and runs the latest found ami from that template.
# New instance settings can be overriden by $overrides.
# AMI selection is filtered by $ami_select.
# New instance in also tagged with 'clone-source' tag of source instance.
# New instance EBS volumes are tagged with the same 'Name' tag of the new instance.
# Written by Shoko, Oct2019.
# Requires: jq, awscli, standard gnu utils.
# Bugs: `aws ec2 get-launch-template-data` does not return 'CreditSpecification>CpuCredits' for t3.
# If you need 'standard' add an override. Tested with awscli 1.16.138.
help_output="Syntax: ec2_kamino.sh [-h] [-s <source instance name tag>] [-t <target instance name>] [-o <instance start cmd overrides>] [-a <specific ami-id>] [-n <subnet id>]"
# opts default
##############
# template overrides for run-instances
overrides="--placement Tenancy=default --instance-type c4.xlarge"
#overrides="--placement Tenancy=default"
# getopts
#########
while [ ! -z "$*" ]; do
case "$1" in
# source instance name
-s|--source) shift; ins_name="$1"; target_name="${ins_name}-clone";;
# target instance name
-t|-target-name) shift; target_name="$1";;
# instace start overrides
-o|--overrides) shift; overrides="$1";;
# source AMI
-a|--ami-id) shift; ami="$1";;
-n|--subnet) shift; subnet="$1";;
-h|--help|*) echo "$help_output"; exit;;
esac
shift
done
if [ -z "$ami" ]; then
ami_select="Name=tag:origin,Values=$ins_name Name=tag:type,Values=EC2amibackup"
else
ami_select="Name=image-id,Values=$ami"
fi
echo "Input Vars:
Source Name $ins_name
Target Name $target_name
Overrides $overrides
AMI select $ami_select
"
[ -z "$ins_name" ] && echo "$help_output" && exit
dbg_var(){ echo "${!1}" | sed "s#^# $1: #" >&2; }
aws_cmd(){
local S="$1"; local A="$2";
#for w in "$@"; do echo " >$S/$A>>> $w"; done >&2
#echo aws "$@" | sed 's#^# >> #' >&2
local O=$(aws "$@" 2>&1); local E="$?";
# test for valid json since awscli exit codes are shit
if [[ $* =~ "--output json" ]] || ([[ ! $* =~ --output ]] && [ `aws configure get output` = "json" ]); then
echo "$O" | jq . &>/dev/null; E=$?
fi
if [ $E -eq 0 ]; then
echo "AWS $S $A - OK" >&2
echo "$O"
else
(tput setaf 1; echo "AWS $S $A - FAIL"; echo "$O" | sed 's#^# ERROR:#'; tput sgr0) >&2
fi
return $E
}
# get instance id by Name tag
#############################
ins_id=`aws ec2 describe-instances --filter "Name=tag:Name,Values=${ins_name}" "Name=instance-state-name,Values=running" --query 'Reservations[].Instances[0].InstanceId' | head -1`
echo "Cloning $ins_name ($ins_id) > $target_name"
echo " AMI filter: $ami_select"
echo
# get latest ami
################
# this strange usage of json output with jq flattening at the end (instead of text output) is just for using the json validation in aws_cmd()
# cause some developers think that exit codes are not that important and all the rest of the world can just go fuck themselves
output_ami=`aws_cmd ec2 describe-images --owners self --output json --filter $ami_select \
--query "Images[].[Name,ImageId,CreationDate,State]" | jq -r '.[] | join(" ")' | \
grep available | sort -k 3r | head -1`
[ -z "$output_ami" ] && echo "CRITICAL: AMI not found" && exit 2
read ami_name ami_id ami_date X <<< "$output_ami"
ami_age=$((`date +%s`-`date -d "$ami_date" +%s`))
echo " Found AMI '$ami_name' ($ami_id), $(($ami_age/3600)) hours old"
echo
# create template
#################
tmpl_source=`aws_cmd ec2 get-launch-template-data --instance-id $ins_id --query 'LaunchTemplateData' --output json`
# removals are required to launch template from an ami
tmpl_fix=`echo "$tmpl_source" |\
jq 'delpaths([path(.BlockDeviceMappings), path(.NetworkInterfaces[].PrivateIpAddresses), path(.CpuOptions)])'`
if [ ! -z "$subnet" ]; then
tmpl_fix=`echo "$tmpl_fix" |\
jq --arg subnet "$subnet" '.NetworkInterfaces[0].SubnetId = $subnet' |\
jq 'delpaths([path(.Placement.AvailabilityZone)])'`
fi
#echo "$tmpl_fix" | sed 's#^# fixed template: #' | cat -n
echo
#exit
output_create=$(aws_cmd ec2 create-launch-template --launch-template-name ${ins_name}-tmp-clone \
--launch-template-data "${tmpl_fix//"/\\"}" --output json --query "LaunchTemplate.CreateTime")
[ $? -ne 0 ] && echo "CRITICAL: Create template failed" && exit 2
# run instance
##############
output_run=$(aws_cmd ec2 run-instances --output json --launch-template LaunchTemplateName=${ins_name}-tmp-clone,Version=1\
--image-id $ami_id \
$overrides \
--tag-specifications \
"ResourceType=instance,Tags=[{Key=Name,Value=$target_name},{Key=clone-source,Value=$ins_name}]" \
"ResourceType=volume,Tags=[{Key=Name,Value=$target_name}]")
new_ins_id=`echo "$output_run" | jq -r '.Instances[] | "\(.InstanceId)"'`
new_ins_tenancy=`echo "$output_run" | jq -r '.Instances[] | "\(.Placement.Tenancy)"'`
echo " New instance ID - $new_ins_id"
echo " New instance tenancy - $new_ins_tenancy"
echo
# tidy up
#########
output_delete=`aws_cmd ec2 delete-launch-template --launch-template-name ${ins_name}-tmp-clone`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment