この記事は公開されてから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つです。
- 対話形式で対象のEC2インスタンスを選択できる
- リストア時のオペレーションを簡素化できる
- 複数の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インスタンスをリストアしようとした場合は、元の情報を揃える必要があるという手間が発生してしまうため、そこをうめるような仕組みを実装してみました。
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のバックアップ・リストアスクリプトもありますよー!