障害発生時にスクリプトで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

これで問題なく切り替われば完了です。