AMIのバックアップをawscliで自動でとる

こんにちは。OPSのほうの小宮です。

どうも社内で需要があるようだったのと、
javaよりpythonのツールが早いということで検索したけどawscliのは見つからなかったのでつくりました。
タグでとるとらない自動判定などはしておりません。bkup_numに世代数を指定すると複数世代とれるように直しました。
引数1にインスタンスID、引数2にホスト名を指定して実行する仕様です。
–no-rebootを付けてあるので稼働中のインスタンスが再起動されることはありません。
データ領域のEBSがあってもamiコマンドが勝手に複数とってくれるようだったので、
osデバイスかdataデバイスか判定してわかりやすいタグつけるようにしてみました。

※※要注意※※
–no-rebootつけてAMIを取得する対象がMyISAMストレージエンジンのmysqlのDBサーバだった場合に、データが壊れて止まるという現象が発生しました。
InnoDBじゃない場合にどうしてもAMI取得したい時はメンテ推奨で止めてやるのがよいかと思われます。
※AWSでは無停止でのAMI取得に関するデータの整合性は保証しておりません。
 参考URL:http://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/creating-an-ami-ebs.html
ということはストレージ系のサーバは気をつけたほうがよい(メンテ推奨)ようです。

・awscliが入ってなければインストールする
入れ方参考:(入ってない場合はpip install awscliとする。pipはeasy_installで入れる)
AWS Command Line Tool Python版 | Developers.IO
pythonでeasy_install – ハネ@日記
AWS Command Line Interface(awscli)を使ってみた – 元RX-7乗りの適当な日々

スクリプトから呼ぶAWSのアクセスキーとリージョンの設定ファイルを作っておきます。
[shell]# cat /root/.ec2/aws.config
————
[default]
aws_access_key_id=<アクセスキーID>
aws_secret_access_key=<シークレットアクセスキーID>
region=ap-northeast-1
————[/shell]

・コマンドの調査(いちおう載せておきます)
[shell]# aws ec2 copy-image help
————
copy-image
DESCRIPTION
Initiates the copy of an AMI from the specified source region to the
region in which the request was made.
リージョン間コピーなのでいま要らない。
————
create-image
Creates an Amazon EBS-backed AMI from an Amazon EBS-backed instance
that is either running or stopped.
create-image
[–dry-run | –no-dry-run]
–instance-id <value>
–name <value>
[–description <value>]
[–no-reboot | –reboot]
[–block-device-mappings <value>]
Command:

aws ec2 create-image –instance-id i-10a64379 –name "My server" –description "An AMI for my server"

Output:

{
"ImageId": "ami-5731123e"
}
————
describe-images
世代管理するなら–filterオプションでタグ検索して出てきたimageidやホスト名使うとかするとよさそう。
————
describe-instances
これはRootDeviceNameを得るのにつかう感じです。
————
deregister-image
Deregisters the specified AMI. After you deregister an AMI, it can’t be
used to launch new instances.
SYNOPSIS
deregister-image
[–dry-run | –no-dry-run]
–image-id <value>
Command:

aws ec2 deregister-image –image-id ami-4fa54026

Output:

{
"return": "true"
}
————
NAME
create-tags –
SYNOPSIS
create-tags
[–dry-run | –no-dry-run]
–resources <value>
–tags <value>
EXAMPLES
Command:

aws ec2 create-tags –resources ami-78a54011 –tags Key=Stack,Value=production

Output:

{
"return": "true"
}
————
# aws ec2 delete-snapshot help
NAME
delete-snapshot –
DESCRIPTION
Deletes the specified snapshot.
SYNOPSIS
delete-snapshot
[–dry-run | –no-dry-run]
–snapshot-id <value>
————
# aws ec2 describe-snapshots help
NAME
describe-snapshots –
SYNOPSIS
describe-snapshots
[–dry-run | –no-dry-run]
[–snapshot-ids <value>]
[–owner-ids <value>]
[–restorable-by-user-ids <value>]
[–filters <value>]
Syntax:
–filters (list)
o tag :key =*value* – The key/value combination of a tag assigned
to the resource.
descriptionでamiidをキーにfilter検索し消したいsnapshotidを得るためにつかう
————
メタデータのとりかた

# /usr/bin/curl -s http://169.254.169.254/latest/meta-data/instance-id
i-292a442c
————[/shell]

・スクリプトを作る

引数があればそのインスタンス、なければ自分のAMIをcreateし
戻り値のimageidをファイルにだしてそれを用いてタグをつける、
このときRootDeviceNameのsnapshotかどうかでタグがosと判断しデバイス名もタグ名に含める。
戻り値のimageidでホスト名のフィルタで検索してホストにかかわる全てのimageidを得て日時のフィールドで降順にソートして
残したい世代数の行分を削除して消す対象の古いimageidを得て、deregistとsnapshot消去する、という流れです。

世代数はbkup_numという変数に定義します。

[shell]# vi /opt/bin/aws_image_bkup.sh
————
#!/bin/bash
#
# aws_image_bkup.sh : awscliでAMIバックアップする
# 依存関係:awsコマンド
# 更新履歴:20140314 – create komiyay
#
export PATH=$PATH:/usr/local/bin
export AWS_CONFIG_FILE=/root/.ec2/aws.config

if [ $# -eq 0 ];then
echo "instance-idの指定がない場合local instanceのAMIバックアップを行います"
echo "usage: $0 [instance-id] [hostname]"
myInstanceID=/usr/bin/curl -s http://169.254.169.254/latest/meta-data/instance-id
host_name=uname -n
else
myInstanceID=$1
host_name=$2
fi

## default variables
base=dirname "$0"
LOG=/opt/log/bkup.log
CurrentImage_LOG=${base}/cimageid_${myInstanceID}.log
#OLDimageID=cat ${CurrentImage_LOG}|grep ImageId|awk '{FS=":";print $2}'|sed -e 's/[ "]//g'
current_result=/opt/log/current_result.log
today=/bin/date +%Y%m%d
amitime=/bin/date +%Y%m%d_%H%M
datetime=/bin/date +%Y/%m/%d_%H:%M:%S
mail_to=<宛先メールアドレス>
#mail_to=<宛先メールアドレス>
bkup_num=1

## check_param.
if [ -z ${myInstanceID} ];then
echo "error: no instance-id, end script ${datetime}."|tee -a ${LOG}
exit 1
fi

## function
### mail_send
mail_send(){
echo ${mail_body}|tee -a ${LOG}|mail -s ${mail_title} ${mail_to}
}

### create_ami
create_ami(){
aws ec2 create-image –instance-id ${myInstanceID} –name "${host_name}_${amitime}" \
–description "${host_name}_${myInstanceID}_${datetime}" –no-reboot |tee ${CurrentImage_LOG}
grep ImageId ${CurrentImage_LOG}
res_create=$?
}

### deregister_ami
deregister_ami(){
if [ -z ${OLDimageID} ];then
res_deregister=0
else
aws ec2 deregister-image –image-id ${OLDimageID}|tee ${current_result}
grep true ${current_result}
res_deregister=$?
if [ ${res_deregister} -ne 0 ];then
mail_body="NG deregister ami for ${myInstanceID}(${host_name}) at ${datetime}"
mail_titile="NG_deregister_ami_${today}"
mail_send
fi
fi
}

### delete_snapshot
delete_snapshot(){
if [ -z ${OLDimageID} ];then
res_delsnap=0
else
snapshot_ids=aws ec2 describe-snapshots --filters Name=description,Values=*${OLDimageID}* \
|grep SnapshotId|awk -F : '{print $2}'|sed -e 's/[ ",]//g'

for i in ${snapshot_ids}
do
aws ec2 delete-snapshot –snapshot-id ${i}|tee ${current_result}
grep true ${current_result}
res_delsnap=$?
if [ ${res_delsnap} -ne 0 ];then
mail_body="NG delete ami snapshot(${i}) for ${myInstanceID}(${host_name}) at ${datetime}"
mail_titile="NG_delete_ami-snapshot_${today}"
mail_send
fi
done
fi
}

### create_tag
create_tag(){
### create tag for ami
CurImageID=cat ${CurrentImage_LOG}|grep ImageId|awk '{FS=":";print $2}'|sed -e 's/[ "]//g'
aws ec2 create-tags –resources ${CurImageID} –tags "Key=Name,Value=${host_name}_${today}"
### create tags for snapshots
rootDeviceName=aws ec2 describe-instances --instance-ids ${myInstanceID} \
|grep RootDeviceName|awk '{print $2}'|sed -e 's/[",]//g'

snapshot_ids=aws ec2 describe-snapshots --filters Name=description,Values=*${CurImageID}* \
|grep SnapshotId|awk -F : '{print $2}'|sed -e 's/[ ",]//g'

for i in ${snapshot_ids}
do
volume_id=aws ec2 describe-snapshots --snapshot-ids ${i}|grep VolumeId \
|awk '{print $2}'|sed -e 's/[",]//g'

deviceName=aws ec2 describe-volumes --volume-ids ${volume_id}|grep Device \
|awk '{print $2}'|sed -e 's/"//g'

if [ "${rootDeviceName}" == "${deviceName}" ];then
aws ec2 create-tags –resources ${i} \
–tags "Key=Name,Value=AMI_${host_name}_${today}-os_${deviceName}"
else
aws ec2 create-tags –resources ${i} \
–tags "Key=Name,Value=AMI_${host_name}_${today}-data_${deviceName}"
fi
done
}

## main
exec >> ${LOG}
exec 2>&1

echo "start ami backup. ${datetime}"
create_ami

## rotate_ami
delete_images=aws ec2 describe-images --filters Name=name,Values=*${host_name}* \
|egrep -a 'ImageId|ImageLocation'|sed -e "N;s/\n//" -e 's/[",]//g'|awk '{print $2" "$4}' \
|sort -r -t _ -k 2,3|sed -e "1,${bkup_num}d"|awk '{print $1}'

for i in ${delete_images}
do
OLDimageID=${i}
deregister_ami
delete_snapshot
done

## if get error, then send mail. the other case, creating tags.
if [ ${res_create} -ne 0 ];then
mail_body="NG create ami for ${myInstanceID}(${host_name}) at ${datetime}"
mail_titile="NG_create_ami_${today}"
mail_send
elif [ ${res_create} -eq 0 ];then
create_tag
fi
echo "end ami backup. ${datetime}"

## logrotate
DAY=date +%m%d
if [ $DAY = "0101" ]
then
OY=date -d '1 year ago' +%Y
mv $LOG $LOG.$OY
find /opt/log -name "$LOG.*" -type f -atime +730 -exec rm -f {} \;
fi

exit 0
————
# chmod +x /opt/bin/aws_image_bkup.sh[/shell]

ためしにとってみる
[shell]# bash -x /opt/bin/aws_image_bkup.sh i-292a442c komiya-test-mysql01
# cat /opt/bin/cimageid_i-292a442c.log
# cat /opt/log/bkup.log [/shell]

実行テストして特に問題なく動くようにはなりました。
注意点としては、
AMI_NAMEが被ると両方消えてなくなるようで、そんなことはしないとは思いますが同じ分内に2回実行しないほうがいいです。
分をまたげば普通にAMIがとれますが、
たしか実行回数が増えすぎると利用料が上がる気がしますのでデイリー等の使い方を推奨します。

古いAMIを残したい場合は、bkup_numという変数に世代数を定義してください。

複数インスタンスのAMIをとりたい場合は、以下のようにリストを作っておいてループ処理する感じです。
[shell]vi bkup-hosts.txt
—-
i-xxxxx,hoge-web01
i-yyyyy,hoge-web02

—-
for i in cat bkup-hosts.txt
do
instance-id=echo $i|awk -F, '{print $1}'
host-name=echo $i|awk -F, '{print $2}'
/opt/bin/aws_image_bkup.sh ${instance-id} ${host-name}
done[/shell]

データだけとっとけば復旧はできるしバックアップ量も節約できるしAMIは変えたときだけとればいい、というパターンもあるでしょう。
オートスケーリング等のために最新のAMIとっときたいという場合には、確かに自動AMIバックアップ要りそうな気がします。
バックアップは復旧計画次第というところでしょうか。

今回スクリプト作ってて個人的に勉強になったなと思ったのは、
sed -e “N;s/\n//” 奇数行の改行を削除
のところでしょうか。他はまあ使ったことあったけど、これはキーワード検索してみたらできることを知った感じでした。
まだまだ修行がたらないようで。

・おまけ
awscliのchefレシピを載せます。

クックブックを作る
[shell]knife cookbook create aws -o site-cookbooks[/shell]

awscli.rbというレシピを作る
[shell]# vi site-cookbooks/aws/recipes/awscli.rb
—————————————————–
if node["platform_version"] >= 6
package "python-setuptools" do
action :install
end
bash "pip-awscli-install" do
not_if "which aws"
code <<-EOC
easy_install pip
pip install awscli
EOC
end
elsif node["platform_version"] >= 5 && node["platform_version"] < 6
bash "python26-pip-awscli-install" do
not_if "which aws"
code <<-EOC
yum -y install python26 python26-devel python26-distribute –enablerepo=epel
easy_install-2.6 pip
pip2.6 install awscli
EOC
end
end

directory "/root/.ec2" do
action :create
end

awstest1 = Chef::EncryptedDataBagItem.load("awskeys","awstest1")
awstest1key_id = awstest1["aws_access_key_id"]
awstest1secret_key = awstest1["aws_secret_access_key"]
awstest1region = awstest1["region"]

bash "set-aws-config" do
not_if "grep access_key /root/.ec2/aws.config"
code <<-EOC
echo ‘[default]’ >> /root/.ec2/aws.config
echo "aws_access_key_id=#{awstest1key_id}" >> /root/.ec2/aws.config
echo "aws_secret_access_key=#{awstest1secret_key}" >> /root/.ec2/aws.config
echo "region=#{awstest1region}" >> /root/.ec2/aws.config
EOC
end
—————————————————–[/shell]
アクセスキーIDとかはdata_bagsで管理すべきと思われる。

[shell]awstest1 = Chef::EncryptedDataBagItem.load("awskeys","awstest1")
awstest1key_id = awstest1["aws_access_key_id"]
awstest1secret_key = awstest1["aws_secret_access_key"]
awstest1region = awstest1["region"][/shell]

data_bagsにawsアクセスキー等を登録する

[shell]knife solo data bag create awskeys awstest1 –secret-file ~/.chef/encrypted_data_bag_secret
{
"id": "awstest1",
"aws_access_key_id": "AKIAID4CBY76********",
"aws_secret_access_key": "4+saCCqOQL8+CKnzdHOpc2CKg2oWmKRO********",
"region": "ap-northeast-1"
}

# knife solo data bag show awskeys awstest1 –secret-file data_bag_key
WARNING: The encrypted_data_bag_secret option defined in knife.rb was overriden by the command line.
aws_access_key_id: AKIAID4CBY76********
aws_secret_access_key: 4+saCCqOQL8+CKnzdHOpc2CKg2oWmKR********
id: awstest1
region: ap-northeast-1[/shell]

シンタックステスト
[shell]# knife cookbook test aws[/shell]

以上、読んでいただいてありがとうございました。

おすすめ記事