Amazon EC2のバックアップスクリプトを書いてみました。

2016.06.17

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、城内です。 今クールのドラマが続々と最終回を向かえ、意外に面白かったセカムズも終わってしまいました。ムズムズが止まらないドラマでしたが、最終回はいい感じの展開でよかったです!

というわけで、今回は、最近運用スクリプトを書くことがあったので、一部をご紹介したいと思います。 (オリジナルは共通関数とかいろいろあるので、ちょっと簡素化しています)

はじめに

今回ご紹介するのは、EC2のバックアップスクリプトです。 中身はEC2インスタンスからAMIを作成するだけのもので、おそらくもう多くの方々が書かれたかと思いますが、意外に自分の要望にビシッとはまるものがなく、改めて書いてしまいました。

ちなみに、過去の記事で今回のようなスクリプトをまとめたものがありますので、ぜひこちらもご覧ください。

スクリプト紹介

では、さっそく、、、ドン! (※88行目のIFSにはタブを代入しています)

ec2_backup_instance.sh

#!/bin/bash

## 変数定義
usage_message="[--backup-group <value>] [--no-reboot] [--region <value>]"
return_code=0
_IFS=$IFS
script_name="$(basename $0)"
tmp_file="/tmp/$(basename $0 .sh).$$"

backup_group_tag_key="BackupGroup"	# バックアップグループタグのキー名
backup_session_id_tag_key="BackupSessionId"	# バックアップセッションIDのキー名
backup_ami_name_prefix="Backup of "	# バックアップで作成するAMI名の接頭辞

## 関数定義
function usage {
	echo "Usage: $script_name $usage_message"
	exit $return_code
}

## メイン処理

# 一時ファイルの削除
trap 'test -f $tmp_file && rm -f $tmp_file ; echo ""; exit 255' 1 2 3 15

# 引数の処理
backup_group=
reboot_option=
while [ $# -gt 0 ]
do
	case "$1" in
		"--backup-group")
			if [ -n "$backup_group" ]
			then
				usage
			else
				if [ -z "$2" ]
				then
					usage
				else
					backup_group="$2"
					shift 2
				fi
			fi
		;;
		"--no-reboot")
			reboot_option="$1"
			shift
		;;
		"--region")
			if [ -z "$2" ]
			then 
				usage
			else
				export AWS_DEFAULT_REGION="$2"
				shift 2
			fi
		;;
		*)
			usage
		;;
	esac
done

# スクリプト実行サーバのEC2インスタンスIDを取得
my_instance_id=$(curl -s http://169.254.169.254/latest/meta-data/instance-id 2> /dev/null)

# EC2インスタンスの情報を一時ファイルに出力
aws ec2 describe-instances | jq '.Reservations[].Instances[] | select(.State.Name != "terminated")' > $tmp_file 2> /dev/null

# EC2インスタンスが存在しない場合は終了
if [ ! -s $tmp_file ]
then
	return_code=2
	echo "Warning: EC2 instance does not exist."
	rm -f $tmp_file
	exit $return_code
fi

# 引数でバックアップグループの指定がない場合は対話形式
target_instances=
if [ -z "$backup_group" ]
then

	# EC2インスタンスの一覧表示
	printf "\n %-5s %-30s %-20s %-15s %-15s %-15s %-20s\n" "No" "Instance Name" "Instance ID" "Public IP" "Private IP" "Instance State" "Backup Group"
	echo "----------------------------------------------------------------------------------------------------------------------------------"

	IFS="	"
	number=1
	min_number=$number
	max_number=$number
	while read instance_name instance_id public_ip private_ip instance_state backup_group
	do
		if [ "$instance_name" = " " ]
		then
			array_instance_info[$number]="${instance_id},no name"
		else
			array_instance_info[$number]="${instance_id},${instance_name}"
		fi

		printf " %-5d %-30s %-20s %-15s %-15s %-15s %-20s\n" "$number" "$instance_name" "$instance_id" "$public_ip" "$private_ip" "$instance_state" "$backup_group"

		number=$((number+1))
	done <<< "$(cat $tmp_file | jq -r '[if .Tags != null then (if (.Tags[] | select(.Key == "Name").Value) == "" then " " else (.Tags[] | select(.Key == "Name").Value) end) else " " end, .InstanceId, if .PublicIpAddress != null then .PublicIpAddress else " " end, .PrivateIpAddress, .State.Name, if .Tags != null then (.Tags[] | select(.Key == "'$backup_group_tag_key'").Value) else " " end] | join("\t")' | sort)"
	IFS=$_IFS
	max_number=$((number-1))

	# EC2インスタンスの選択(ユーザ入力)
	echo ""
	number=0
	read -p "Please select a instance number [${min_number}-${max_number}]> " number

	# 対象EC2インスタンスの設定
	if [ $((number+1)) > /dev/null 2>&1 -a $number -ge $min_number -a $number -le $max_number ]
	then
		target_instances="${array_instance_info[$number]}"
		backup_group="manual"
	else
		return_code=1
		echo "${script_name}: [ERROR] An invalid value was entered."
		exit $return_code
	fi

	# 再起動オプションの選択(デフォルトは再起動あり)
	# ただし、対象EC2インスタンスがスクリプトの実行インスタンスだった場合、再起動のオプションは強制で無効に設定
	if [ "${target_instances%%,*}" = "$my_instance_id" ]
	then
		echo "Info: The '--no-reboot' option is on because my place is a target instance."
		reboot_option="--no-reboot"
	elif [ -z "$reboot_option" ]
	then
		read -p "Do you want to reboot when you backup the server? [Y|n]> " user_input

		case "$user_input" in
			[yY] | "")
				reboot_option="--reboot"
			;;
			[nN])
				reboot_option="--no-reboot"
			;;
			*)
				return_code=1
				echo "${script_name}: [ERROR] An invalid value was entered."
				exit $return_code
			;;
		esac
	fi

# 引数でバックアップグループが指定されている場合、バックアップ対象を抽出
else
	target_instances="$(cat $tmp_file | jq -r 'if .Tags != null then select(.Tags[].Value == "'$backup_group'") else empty end | .InstanceId + "," + if .Tags != null then (if (.Tags[] | select(.Key == "Name").Value) == "" then "no name" else (.Tags[] | select(.Key == "Name").Value) end) else "no name" end' 2> /dev/null)"

	# 対象のEC2インスタンスが存在しない場合は終了
	if [ -z "$target_instances" ]
	then
		return_code=2
		echo "Warning: Instance with a tag of \"Key:${backup_group_tag_key} Value:${backup_group}\" does not exist."
		rm -f $tmp_file
		exit $return_code
	fi
fi

backup_session_id="${backup_group}-$(date '+%Y%m%d%H%M%S')"
echo -e "\nBackup Session ID: $backup_session_id"

IFS="
"
for target_instance in $target_instances
do
	instance_id="${target_instance%%,*}"
	instance_name="${target_instance#*,}"

	# 対象EC2インスタンスがスクリプトの実行インスタンスだった場合、再起動のオプションは強制で無効に設定
	no_reboot_flag=false
	if [ "$instance_id" = "$my_instance_id" ]
	then
		if [ -z "$reboot_option" ]
		then
			reboot_option="--no-reboot"
			no_reboot_flag=true
		fi
	fi

	# バックアップ処理(AMI作成とタグ付け)の実行
	tags=
	echo -n "Start backing up $instance_id (${instance_name}) ... "
	ami_id=$(aws ec2 create-image --instance-id $instance_id --name ${instance_id}_$(date '+%Y%m%d%H%M%S') $reboot_option 2> $tmp_file | jq -r '.ImageId')
	if [ "${ami_id%%-*}" = "ami" ]
	then

		# バックアップ対象のEC2インスタンスからAMIに引き継ぐ情報(タグ付け)
		# ・キーペア
		# ・セキュリティグループ
		# ・インスタンスタイプ
		# ・サブネット
		# ・ロール
		tags="Key=\"Name\",Value=\"${backup_ami_name_prefix}${instance_id} (${instance_name})\" Key=\"${backup_session_id_tag_key}\",Value=\"${backup_session_id}\""
		buff=$(aws ec2 describe-instances --instance-ids $instance_id | jq -c '.[][].Instances[] | {KeyName}, (.NetworkInterfaces[].Groups[] | {GroupId}), {InstanceType}, {SubnetId}, if .IamInstanceProfile.Arn != null then {"IamInstanceProfile": .IamInstanceProfile.Arn | sub(".*/"; "")} else empty end' | sed -e 's/^{//' -e 's/}$//' -e 's/,/\t/g')

		prev_tag_key=
		for tag in $buff
		do
			tag_key="${tag%:*}"
			tag_value="${tag#*:}"

			if [ "$prev_tag_key" = "$tag_key" ]
			then
				tags="${tags%\"} ${tag_value#\"}"
			else
				tags="$tags Key=$tag_key,Value=$tag_value"
			fi

			prev_tag_key="$tag_key"
		done

		# 作成したAMIにタグを付ける
		command="aws ec2 create-tags --resources $ami_id --tags $tags"
		eval $command 2> $tmp_file 1> /dev/null
		if [ $? -eq 0 ]
		then
			echo "done"
		else
			echo "failed"
			cat $tmp_file
			return_code=1
		fi
	else
		echo "failed"
		cat $tmp_file
		return_code=1
	fi

	if $no_reboot_flag 
	then
		echo "Warning: Reboot did not work because my place is a target instance."
		reboot_option=
	fi
done
IFS=$_IFS

test -f $tmp_file && rm -f $tmp_file

exit $return_code

解説

まず前提として、実行する際は、EC2とIAMの一部(ロール周り)の権限を持っている必要があります。

処理の流れとしては、バックアップ対象のEC2インスタンスを決めて、そこからAMIを作成します。そして、バックアップ元のEC2インスタンスからリストアに必要な一部の情報を取得し、AMIにタグとして情報を付与しています。

今回のスクリプトは、運用で使用することを念頭において作成しています。ポイントは以下の3つです。

  1. 対話形式で対象のEC2インスタンスを選択できる
  2. リストア時のオペレーションを簡素化できる
  3. 複数のEC2インスタンスをグルーピングしてバックアップすることができる

1つ目は、以下のような挙動になります。

$ ./ec2_backup_instance.sh

 No    Instance Name                  Instance ID          Public IP       Private IP      Instance State  Backup Group
----------------------------------------------------------------------------------------------------------------------------------
 1     Web Server                     i-xxxxxxxx           xx.xx.xx.xx     xx.xx.xx.xx     running         xx-system-01
 2     App Server #01                 i-yyyyyyyy                           yy.yy.yy.yy     running         xx-system-02
 3     App Server #02                 i-zzzzzzzz                           zz.zz.zz.zz     running         xx-system-02
...

Please select a instance number [1-n]>

オペレータが作業しやすいように考えてみました。

2つ目は、バックアップしたAMIからのリストアするためには、新たなEC2インスタンスを作成することになるため、最低限必要ないくつかのパラメータを指定する必要がありました。 そこに柔軟性があることはメリットではありますが、オペレータが単純に同じ設定のEC2インスタンスをリストアしようとした場合は、元の情報を揃える必要があるという手間が発生してしまうため、そこをうめるような仕組みを実装してみました。

ec2_backup_script_01

3つ目は、オプションの--backup-groupを利用することで、BackupGroupタグを付けた複数のEC2インスタンスをまとめてバックアップすることができます。

$ ./ec2_backup_instance.sh --backup-group "xx-system-02"

Backup Session ID: xx-system-02-20160601000000
Start backing up i-yyyyyyyy (App Server #01) ... done
Start backing up i-zzzzzzzz (App Server #02) ... done

すでにバックアップ対象に専用タグを付けるという方式はあったのですが、その方式の場合、EC2インスタンス毎のバックアップタイミングをコントロールしづらいという悩みがあったので、タグの値を使ってグループ分けができるようにしてみました。

こちらは、ジョブで実行するケースを想定しています。

さいごに

今回は、実際の運用で使えるようなスクリプトをご紹介しました。

こちらのスクリプトはそのまま使えるはずですが、もしかしたら細かいところでおやっとなるかもしれません。 その辺り、オリジナルは前処理やprintfの拡張関数を組み込んであり、もう少し手間をかけてあります。 また、今回のスクリプトを実行した後に、AMIの一覧を表示したくなったり、リストアのスクリプトも必要じゃん、となるかと思います。

もし今回ご紹介したようなAWSの運用スクリプトをご要望でしたら、ぜひ弊社メンバーズサービスをご利用くださいませ!(強引な宣伝w RDSやRedshiftのバックアップ・リストアスクリプトもありますよー!