AWS Systems Manager オートメーション を利用して作業を自動化しよう

Systems Manager オートメーションは、AWS上で実行したい処理を
YAML/JSON形式のドキュメントとして作成することでタスクを自動化することが出来ます。

今回はEC2インスタンスのメンテナンス回避対応
(事前にインスタンスの停止・起動を実施してメンテナンス期間中の再起動を回避する)
を自動化します。

対象インスタンスは Application Load Balancer のターゲットグループに登録されている為、

  1. ターゲットグループから対象インスタンスの登録を解除
  2. 対象インスタンスの停止・起動
  3. ターゲットグループへ対象インスタンスを登録

という流れでメンテナンス回避を行う必要がある、という仮定で進めます。

一連の流れを記述したドキュメントを CloudWatch Events のターゲットとして設定し、
任意の時間でメンテナンス回避対応を行えるようにする…というのがゴールです。

設定方法

SSM オートメーション ドキュメントの作成

ドキュメントの作成

以下の通りドキュメントの作成を行います。

ドキュメントの名前は適当なものを入力します。

コンテンツの内容は、以下のYAMLを貼り付けます。こちらの内容については後述します。

---
description: Sample Automation Document Using AWS API Actions
schemaVersion: '0.3'
assumeRole: "{{ AutomationAssumeRole }}"
parameters:
  AutomationAssumeRole:
    type: String
    description: "(Optional) The ARN of the role that allows Automation to perform the actions on your behalf."
    default: ''
  TargetGroupArn:
    type: String
    description: "(Required) The ARN of the Target Group."
    default: ''
  TargetId:
    type: StringList
    description: "(Required) The ID of the Target: Instance ID."
    maxItems: 1
    minItems: 1
  SleepSeconds:
    type: String
    description: "(Required) Wait for specified seconds after deregistering instance from target group. You'd better specify draining time of the target group."
    default: '300'
mainSteps:
- name: describeInstances
  action: aws:executeAwsApi
  inputs:
    Service: ec2
    Api: DescribeInstances
    InstanceIds: "{{ TargetId }}"
  outputs:
  - Name: InstanceIdString
    Selector: "$.Reservations[0].Instances[0].InstanceId"
    Type: String
- name: deregisterTargets
  action: aws:executeAwsApi
  inputs:
    Service: elbv2
    Api: DeregisterTargets
    TargetGroupArn: "{{ TargetGroupArn }}"
    Targets: 
      - Id: "{{ describeInstances.InstanceIdString }}"
- name: sleep
  action: aws:sleep
  inputs:
    Duration: "PT{{ SleepSeconds }}S"
- name: stopInstance
  action: aws:changeInstanceState
  inputs:
    InstanceIds: "{{ TargetId }}"
    DesiredState: "stopped"
- name: startInstance
  action: aws:changeInstanceState
  inputs:
    InstanceIds: "{{ TargetId }}"
    DesiredState: "running"
- name: registerTargets
  action: aws:executeAwsApi
  inputs:
    Service: elbv2
    Api: RegisterTargets
    TargetGroupArn: "{{ TargetGroupArn }}"
    Targets: 
      - Id: "{{ describeInstances.InstanceIdString }}"

入力を終えたら[ドキュメントの作成]を選択します。

ドキュメントを試しに実行してみる

ドキュメントを作成したら、実行してみましょう。
以下のようにドキュメントを選択します。

[オートメーションの実行]を選択します。

実行にあたり、いくつかオプションを指定します。

対象のインスタンスID、ターゲットグループのARNを選択・入力します。
SleepSeconds にはターゲットグループのドレイン秒数(※)を入力します。
※AWSマネジメントコンソール上では「登録解除の遅延」と表示されています。

ドキュメントを実行すると、以下のように実行ステータスが表示されます。

EC2のコンソールでも、対象インスタンスの停止・起動やターゲットグループからの切り離しが実行されているか確認してみましょう。

IAMロールの作成

先ほど作成したドキュメントの内容を実行できる権限を付与したIAMロールを作成します。

IAMポリシーの作成

以下内容でIAMポリシーを作成します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ssm:StartAutomationExecution",
                "ec2:StopInstances",
                "ec2:StartInstances",
                "ec2:DescribeInstanceStatus",
                "ec2:DescribeInstances",
                "elasticloadbalancing:DeregisterTargets",
                "elasticloadbalancing:RegisterTargets"
            ],
            "Resource": "*"
        }
    ]
}

IAMロールの作成

先ほど作成したポリシーを適用したIAMロールを作成します。
[信頼されたエンティティ]を events.amazonaws.com に設定しておきます。

CloudWatch Events ルールの作成

以下の通りルールを作成します。

適当なルール名を設定し[ルールの作成]を選択します。

設定した時刻にタスク(インスタンスの停止・起動とターゲットグループの登録・解除)が実行されるはずです。
実行結果は以下の通り確認可能です。

SSM オートメーション ドキュメントの内容解説

パラメータなど

description: Sample Automation Document Using AWS API Actions
schemaVersion: '0.3'
assumeRole: "{{ AutomationAssumeRole }}"
parameters:
  AutomationAssumeRole:
    type: String
    description: "(Optional) The ARN of the role that allows Automation to perform the actions on your behalf."
    default: ''
  TargetGroupArn:
    type: String
    description: "(Required) The ARN of the Target Group."
    default: ''
  TargetId:
    type: StringList
    description: "(Required) The ID of the Target: Instance ID."
    maxItems: 1
    minItems: 1
  SleepSeconds:
    type: String
    description: "(Required) Wait for specified seconds after deregistering instance from target group. You'd better specify draining time of the target group."
    default: '300'

parameters: は、その名の通りオートメーションを実行する際のパラメータを定めています。

ステップ1: describeInstances

mainSteps:
- name: describeInstances
  action: aws:executeAwsApi
  inputs:
    Service: ec2
    Api: DescribeInstances
    InstanceIds: "{{ TargetId }}"
  outputs:
  - Name: InstanceIdString
    Selector: "$.Reservations[0].Instances[0].InstanceId"
    Type: String

mainSteps: 以下が実際の実行内容です。
action として設定されている aws:executeAwsApi は、
AWSのほぼ全てのAPIを呼び出せるというすぐれモノです。

このステップではEC2の DescribeInstances APIを呼び出しています。
本APIについてはこちらを参照ください。

本ステップは本来不要なのですが、以下のような意味があります。

ステップ deregisterTargets と stopInstance では
インスタンスIDを引数として受け取りますが、
前者の引数はString型であるのに対し、後者はStringListとなっています。

前述の parameters: で規定されているパラメータ TargetId は
StringList型としてインスタンスIDを格納します。
但しそのままでは deregisterTargets ステップの引数としては利用できないので、
本ステップで DescribeInstances API を呼び出し、
そのレスポンス(インスタンスID)をString型として受け取り、
変数 InstanceIdString に格納しています。

outputs: セクションは、そのようにオートメーションステップの実行結果を
変数として格納したい場合に利用します。

本ステップをAWS CLIで表すと、概ね以下のようになります。

# aws ec2 describe-instances --instance-ids i-xxxxxxxxxxxxxxxxx --query "Reservations[0].Instances[0].InstanceId"

AWS CLIはAPIの形式と似ているので aws:executeAwsApi を利用する際の参考になります。
また、CLIの query オプションが、オートメーションドキュメントの
outputs セクションと対応しているのが分かります。

ステップ2: deregisterTargets

- name: deregisterTargets
  action: aws:executeAwsApi
  inputs:
    Service: elbv2
    Api: DeregisterTargets
    TargetGroupArn: "{{ TargetGroupArn }}"
    Targets: 
      - Id: "{{ describeInstances.InstanceIdString }}"

次は、ターゲットグループから対象インスタンスの登録を解除する処理です。

前ステップと同様 aws:executeAwsApi で、
利用しているのはELBの DeregisterTargets APIです。

前ステップで設定した変数(InstanceIdString)を取り出します。
{{ describeInstances.InstanceIdString }} のように、
{{ [ステップ名].[変数名] }} という形で指定しています。

本ステップをAWS CLIで表すと、概ね以下のようになります。

# aws elbv2 deregister-targets --target-group-arn arn:aws:elasticloadbalancing:[REGION]:XXXXXXXXXXXX:targetgroup/name/xxxxxxxxxxxxxxxx --targets i-xxxxxxxxxxxxxxxxx

ステップ3: sleep

- name: sleep
  action: aws:sleep
  inputs:
    Duration: "PT{{ SleepSeconds }}S"

ターゲットグループから登録解除した後も、
既存の接続はインスタンスに残り続けます。(Connection Draining)

ターゲットグループ側で Draining のタイムアウト秒数が設定されているので、
その秒数ぶんだけ次のステップ(インスタンス停止)の実行を待ちます。

この場合 action は aws:sleep を設定します。
待機秒数を PT300S (300秒) のような単位で表します。
今回はオートメーション実行時に秒数を指定できるよう、
SleepSeconds というパラメータを利用しています。

ステップ4 & 5: stopInstance, startInstance

- name: stopInstance
  action: aws:changeInstanceState
  inputs:
    InstanceIds: "{{ TargetId }}"
    DesiredState: "stopped"
- name: startInstance
  action: aws:changeInstanceState
  inputs:
    InstanceIds: "{{ TargetId }}"
    DesiredState: "running"

こちらはインスタンス停止・起動のステップになります。

aws:changeInstanceState という action を利用していますが、
その名の通り「インスタンスを指定した状態に変える」というものなので、
指定するのは start, stop のような「動作」ではなく
stopped, running のような「状態」です。

インスタンスを「停止」→「起動」の状態にすることで、
インスタンスのメンテナンスを事前回避しています。

ステップ6: registerTargets

- name: registerTargets
  action: aws:executeAwsApi
  inputs:
    Service: elbv2
    Api: RegisterTargets
    TargetGroupArn: "{{ TargetGroupArn }}"
    Targets: 
      - Id: "{{ describeInstances.InstanceIdString }}"

インスタンスが起動状態になったら、ターゲットグループへの再登録を行います。
こちらの内容は deregisterTargets ステップとほぼ同様です。
該当するAPIリファレンスはこちら

何に使うの?

システム保守には夜間作業がつきものですが、
オートメーションを利用すればプログラム言語が扱えずとも
それらの作業を夜間に自動で実施することが可能です。

もちろん他にも用途はあると思いますので、
引き続きユースケースを探して検証していきたいと思います。