障害発生時にスクリプトでEC2を切り替えよう
現在、業務で使っているものですが、用途としてはシングルポイントのEC2(シングルAZのEC2)がAZ障害発生時に停止時間を極力抑え、簡単な処理で復旧できるように作ったスクリプトです。
スクリプトを導入すべき環境の前提は以下
【 前提 】 ・シングルポイントのEC2がap-northeast-1aに存在している ・一定の間隔でAMIを自動取得している(amirotate等を使用) ・EC2にEIPがアタッチされている ・スクリプトを導入するEC2が存在する ・EC2はCLBに登録されている(※ALBの場合は、コマンドが変わるためスクリプトの修正が必要)
障害発生時、スクリプト実行までの流れは以下
【 障害発生時の対処の流れ 】 1. AZ障害発生 2. SSM Runcommandにてスクリプトを実行し切り替え 3. 切り替わったEC2で運用継続し落ち着いたら切り戻す
スクリプトをRun Commandで実行することにより以下を実現します
1.旧サーバ(EC2)の直近のAMI IDを取得 2.1で取得したAMI IDから新サーバ(EC2)を別名で起動 3.旧サーバにアタッチされているEIPをデタッチし、新サーバにアタッチする 4.CLBに登録されている旧サーバを登録解除し、新サーバを登録する
それでは、スクリプトを作成・導入し、切り替えを実装するまでの手順を以下に記載します。
作業内容
・自動起動スクリプトの作成
・スクリプト実行サーバ(EC2)へのスクリプトの配置
・SystemManagerドキュメントの作成
作業手順
事前作業
スクリプト実行サーバ(EC2)にセットするIAMロール設定
スクリプト実行サーバ(EC2)のIAMロールに以下のポリシーを付与
▼ ポリシー名
・AmazonEC2FullAccess(AWS 管理ポリシー)
・IAMFullAccess(AWS 管理ポリシー)
・ReadOnlyAccess(AWS 管理ポリシー)
・AmazonEC2RoleforSSM(AWS 管理ポリシー)
・RecoveryPolicy(管理ポリシー)
↓RecoveryPolicy
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ec2:RunInstances", "ec2:DisassociateAddress", "ec2:AssociateAddress", "elasticloadbalancing:DeregisterInstancesFromLoadBalancer", "elasticloadbalancing:RegisterInstancesWithLoadBalancer" ], "Resource": [ "*" ] } ] }
スクリプト実行サーバへのjqインストール
以下コマンドで、jqを事前にインストールしておく
# yum -y install jq
手順
1.スクリプトを配置
1-1.作成した以下のスクリプトをスクリプト実行サーバの/home/ec2-user/bin/配下に、replace_instance.shという名前で作成する。
▼ replace_instance.sh
#!/bin/sh function alert () { echo "$(date '+%Y-%m-%d %H:%M:%S') [$1] $2" } # 1. OLD_INSTANCE=$(aws\ ec2 describe-instances \ --query \ 'Reservations[].Instances[?Tags[?Key==`Name`&&Value==`'${TAG_NAME}'`]][]' \ --filters Name=instance-state-code,Values=16,0,80 \ ) OLD_INSTANCE_ID=$(echo $OLD_INSTANCE | jq '.[].InstanceId' --raw-output) if [ "x${OLD_INSTANCE_ID}" == "x" ]; then alert "ALERT" "No instances exits." exit -1 fi alert "INFO" "Instance ID:${OLD_INSTANCE_ID}" # OLD_SUBNET_ID=$(echo $OLD_INSTANCE| jq .[].SubnetId --raw-output) OLD_INSTANCE_TYPE=$(echo $OLD_INSTANCE | jq '.[].InstanceType' --raw-output) OLD_INSTANCE_PROFILE=$(echo $OLD_INSTANCE | jq '.[].IamInstanceProfile.Arn' --raw-output) OLD_EIP=$(aws ec2 describe-addresses --filters Name=instance-id,Values=${OLD_INSTANCE_ID}) ASSOCIATION_ID=$(echo $OLD_EIP | jq '.Addresses[].AssociationId' --raw-output) ALLOCATION_ID=$(echo $OLD_EIP | jq '.Addresses[].AllocationId' --raw-output) # 2. IMAGE_ID=$(aws \ ec2 describe-images \ --owner self \ --filters Name=tag-key,Values=Name Name=tag-value,Values=${TAG_NAME} \ --query \ 'reverse(sort_by(Images[].{C:CreationDate,I:ImageId},&C))|[0].I'\ --output text \ ) if [ $? != 0 -o "x${IMAGE_ID}" == "x" ]; then alert "ALERT" "No images found." exit -1 fi alert "INFO" "image-id ${IMAGE_ID}" # 3. NEW_TAG_NAME=${TAG_NAME}-$(date +%Y%m%d%H%M%S) TAG_JSON=$(echo $OLD_INSTANCE|jq '.[]|(.Tags[]|select(.Key=="Name")|.Value) |= "'${NEW_TAG_NAME}'"|.Tags' -c|sed 's: ::g') alert "INFO" "new-tag-name ${NEW_TAG_NAME}" alert "INFO" "tag-json ${TAG_JSON}" alert "INFO" "new-subnet-id ${NEW_SUBNET_ID}" NEW_INSTANCE_ID=$(aws \ ec2 run-instances \ --image-id ${IMAGE_ID} \ --key-name $(echo $OLD_INSTANCE| jq '.[].KeyName' --raw-output) \ --instance-type ${OLD_INSTANCE_TYPE} \ --iam-instance-profile Arn=${OLD_INSTANCE_PROFILE} \ --subnet-id ${NEW_SUBNET_ID} \ --query 'Instances[].InstanceId' \ --security-group-ids $(for a in $(echo $OLD_INSTANCE| jq '.[].SecurityGroups[].GroupId' --raw-output);do echo -n $a" " ;done)\ --output text \ --tag-specifications '[{"ResourceType":"instance","Tags":'${TAG_JSON}'}]' ) if [ $? != 0 ]; then alert "ALERT" "AWS CLI cannot execute" exit -1 fi if [ "x${NEW_INSTANCE_ID}" == "x" ]; then alert "ALERT" "Cannot woke up instance." exit -1 fi alert "INFO" "New instance-id ${NEW_INSTANCE_ID}" # 4. aws \ ec2 wait instance-running \ --instance-ids ${NEW_INSTANCE_ID} if [ $? != 0 ]; then alert "ALERT" "Instance with problem." exit -1 fi alert "INFO" "New instance availavle" # 5. aws ec2 disassociate-address --association-id ${ASSOCIATION_ID} if [ $? != 0 ]; then alert "ALERT" "Cannot disassociate EIP." exit -1 fi alert "INFO" "Detach EIP" aws \ ec2 associate-address \ --instance-id ${NEW_INSTANCE_ID} \ --allocation-id ${ALLOCATION_ID} if [ $? != 0 ]; then alert "ALERT" "Cannot associate EIP." exit -1 fi alert "INFO" "Attach EIP" # 6. if [ "x${LOAD_BALANCER_NAME}" != "x" ]; then aws \ elb deregister-instances-from-load-balancer \ --load-balancer-name ${LOAD_BALANCER_NAME} \ --instances ${OLD_INSTANCE_ID} if [ $? != 0 ]; then alert "ALERT" "Cannot deregister instances from load balancer." exit -1 fi alert "INFO" "Deregister Instances from Load Balancer." aws \ elb register-instances-with-load-balancer \ --load-balancer-name ${LOAD_BALANCER_NAME} \ --instances ${NEW_INSTANCE_ID} if [ $? != 0 ]; then alert "ALERT" "Cannot register instances from load balancer." exit -1 fi alert "INFO" "Register Instances from Load Balancer." fi
1-2.スクリプトに対して権限を設定する
$ chmod 775 /home/ec2-user/bin/replace_instance.sh
スクリプトのユーザとグループが共に、ec2-userとなっていることを確認する
2.SSMドキュメントの作成
マネジメントコンソールより、AWS Systems Manager > ドキュメント >自己所有タブ > Create Command or Session ボタンを押下し、 以下のドキュメント名、コンテンツ内容(json形式)でドキュメントを作成する。
※ターゲットタイプ:指定無し、ドキュメントタイプ:コマンドドキュメント とする
※「XXXXX」の部分は自身の環境に合わせること
ドキュメント名:replace_XXXXX
▼ コンテンツ内容
{ "schemaVersion": "1.2", "description": "Run a shell script or specify the commands to run.", "parameters": {}, "runtimeConfig": { "aws:runShellScript": { "properties": [ { "id": "0.aws:runShellScript", "runCommand": [ "export LOAD_BALANCER_NAME=XXXXX", "export TAG_NAME=XXXXX", "export AWS_DEFAULT_REGION=ap-northeast-1", "export NEW_SUBNET_ID=subnet-XXXXX", "sh /home/ec2-user/bin/replace_instance.sh" ], "workingDirectory": "/home/ec2-user/bin", "timeoutSeconds": "3600" } ] } } }
3.SSM RunCommandの実行
3-1. マネジメントコンソールより「Run a command」を押下
以下の内容を設定して実行する
・コマンドのドキュメント:replace_XXXXX ・ターゲットの選択:インスタンスの手動選択 ・インスタンスの選択:スクリプト実行サーバを選択 ・タイムアウト(秒):600
これで問題なく切り替われば完了です。