CloudWatch アラームを起点に SSM Run Command を実行する

AWS認定試験サンプル問題で見た CloudWatch Alarm → SNS → Lambda → Run Command 実行の仕組みを実装してみました。
2020.07.28

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

哈喽大家好、コンサルティング部の西野です。

2020年7月28日現在公式ウェブサイトにアップロードされている AWS Certified DevOps Engineer Professional のサンプル問題 (8) は、あるレガシーアプリケーションの障害を回避するために取るべき手段を選択する問題です。

この問題では下記の選択肢が正解になっています。(突然のネタバレ)

  • メモリ使用率メトリクスに対する CloudWatch アラームを作成し、メモリ使用率が 80% を上回ったときに Amazon SNS トピックに発行する。

  • AWS Lambda 関数を Amazon SNS トピックにサブスクライブする。この関数は、AWS Systems Manager の Run コマンドを使用してアプリケーションを再起動するものである。

面白そうだったので、この仕組みを構築してみました。

構成図

① EC2 の CPUUtilization が80%を超えるとアラーム状態になる CloudWatch アラームを作成する。通知先として SNS トピックを指定しておく。
② SNS はサブスクリプションとして登録された Lambda 関数を実行する。
③ Lambda 関数が SSM Run Command を実行する。
④ SSM Run Command により httpd を再起動する( httpd をアプリケーションの代替とします。)
⑤ SSM Run Command により Slack に通知を行なう。(再起動するだけでは地味、という理由でつけたおまけです。)

Slack の Incoming Webhook 作成

再起動時の通知先である Incoming Webhook を用意し URL を控えておきます。

VPC・EC2

下記のようにVPC・EC2を用意します。

  • default VPC を使用
  • パブリック IP を持つ EC2 を起動
  • IAM Policy AmazonSSMManagedInstanceCore(AWS管理ポリシー) の権限を付与した IAM ロールをアタッチ
  • SSM Session Managerから対象インスタンスに接続後下記のコマンドを入力し Apache とテスト用 stress コマンドをインストール
$ sudo yum update -y
$ sudo yum install httpd -y
$ sudo systemctl enable httpd.service
$ sudo systemctl start httpd.service
$ sudo yum install stress -y

SNS トピックの作成

アラームの通知先であるトピック( DOP-Sample-Topic)を作成します。 サブスクリプションの設定は Lambda 関数作成後に行なうので、ここではトピックのみを作成します。

CloudWatch アラームの作成

対象インスタンスのCPUUtilizationメトリクスに対してしきい値 80% を超えた場合にアラーム状態となる CloudWatch アラーム (DOP-Sample-Alarm)を作成します。
アラーム状態への遷移時には先ほど作成した SNS トピック DOP-Sample-Topic へ通知するよう設定します。

Lambda

Lambda関数用IAMロールの作成

下記の IAM ポリシーをアタッチした Lambda 関数用 IAM ロール(DOP-Sample-Lambda-Role)を作成します。

  • AmazonSSMFullAccess(AWS管理ポリシー)
  • AWSLambdaBasicExecutionRole(AWS管理ポリシー)

Lambda関数

Lambda 関数(DOP-Sample-Lambda-Function)を作成します。
ランタイムは Python 3.8を、 IAM ロールは先の手順で作成したものを使用します。
コードは下記のとおりです。

import json
import boto3

#  あらかじめ用意した Incoming Webhook の URL を指定
WEBHOOK_URL = "https://hooks.slack.com/services/XXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXXXXX"

ssm = boto3.client('ssm')
    
def lambda_handler(event, context):
    # CloudWatch アラームから SNS 経由で受け取った event より instance_id を取得
    message = event["Records"][0]["Sns"]["Message"]
    message_obj = json.loads(message)
    instance_id = message_obj['Trigger']['Dimensions'][0]["value"]

    # 対象インスタンスで実行するシェルスクリプトを記述
    commands = '''\
    WEBHOOK_URL={url}
    sudo systemctl restart httpd.service
    if [ $? = "0" ]; then
      curl -X POST --data-urlencode "payload={{\\"text\\": \\"Apache を再起動しました\\"}}" ${{WEBHOOK_URL}}
    else
      curl -X POST --data-urlencode "payload={{\\"text\\": \\"Apache の再起動に失敗しました。\\"}}" ${{WEBHOOK_URL}}
    fi'''.format(url=WEBHOOK_URL)
    
    response = ssm.send_command(
            InstanceIds = [instance_id],
            DocumentName = "AWS-RunShellScript",
            Parameters = {
                "commands": [commands]
            },
        )
    print(response)

SNS サブスクリプションの設定

DOP-Sample-Topicのサブスクリプションとして Lambda 関数 DOP-Sample-Lambda-Function を指定します。

動作確認

SSM Session Manager で対象インスタンスに接続し、下記のコマンドで CPU に負荷をかけます。

$ stress -c 8

しばらく待つとDOP-Sample-Alarmがアラーム状態に遷移します。

Slack にも通知が飛んできました。

念のため、再起動がきちんと行われているかチェックしてみます。

[ec2-user@ip-172-31-29-158 ~]$ sudo journalctl -f -u httpd
-- Logs begin at 火 2020-07-28 06:13:32 UTC. --
 7月 28 09:11:04 ip-172-31-29-158.ap-northeast-1.compute.internal systemd[1]: Started The Apache HTTP Server.
 7月 28 09:21:03 ip-172-31-29-158.ap-northeast-1.compute.internal systemd[1]: Stopping The Apache HTTP Server...
 7月 28 09:21:04 ip-172-31-29-158.ap-northeast-1.compute.internal systemd[1]: Starting The Apache HTTP Server...
 7月 28 09:21:05 ip-172-31-29-158.ap-northeast-1.compute.internal systemd[1]: Started The Apache HTTP Server.
 7月 28 09:32:04 ip-172-31-29-158.ap-northeast-1.compute.internal systemd[1]: Stopping The Apache HTTP Server...
 7月 28 09:32:05 ip-172-31-29-158.ap-northeast-1.compute.internal systemd[1]: Starting The Apache HTTP Server...
 7月 28 09:32:05 ip-172-31-29-158.ap-northeast-1.compute.internal systemd[1]: Started The Apache HTTP Server.
 7月 28 10:11:03 ip-172-31-29-158.ap-northeast-1.compute.internal systemd[1]: Stopping The Apache HTTP Server...
 7月 28 10:11:04 ip-172-31-29-158.ap-northeast-1.compute.internal systemd[1]: Starting The Apache HTTP Server...
 7月 28 10:11:05 ip-172-31-29-158.ap-northeast-1.compute.internal systemd[1]: Started The Apache HTTP Server.

通知と同一時刻に再起動のログがあることが見て取れます。

以上の作業で CloudWatch アラームを起点にインスタンス内でコマンドを実行する仕組みが構築できました。

終わりに

AWS Certified DevOps Engineer Professional のサンプル問題に取り上げられているだけあり、工夫次第で様々な応用が効く仕組みだと思います。

このブログがほんの少しでも世界を良くできれば嬉しいです。
コンサルティング部の西野 (@xiyegen) がお送りしました。