EC2のOS内で停止したサービスを自動で起動させる

EC2で動作しているサービスはどんなに安定しているように見えても、急に落ちることがあります。
ログを見ても何故落ちたのかよくわからず、手動で起動すると問題なく動き始めたり・・・。
そんなことで夜間、急に呼び出されるのも大変なので、一度は自動的に起動を試してみてほしいと思うようになりました。

そこでCloudWatchAlarmとSSMのRunCommandを使えばできることがわかり実装を試してみましたが、ちょっとかゆいところに手が届かない。
サービス起動を試行して失敗した場合は急いて対応しなくてはいけないので、その旨をメール通知してほしいということで、Lambdaでコードを書くことにしました。
やはり柔軟に動作を定義したいならLambdaは便利ですね。

使用例

このページのLambdaとCloudWatchAlarm、AWS SNSを使用することで以下のようなことが実現できます。
  • EC2のOS内のサービス停止時にLambda実行
  • LambdaからEC2のOSにコマンド実行
  • OS内のサービス起動試行
  • サービス起動に失敗した場合のメール通知
EC2にCloudWatchAgentをインストールして、CloudWatchAlarmでサービスの起動状態監視が実装されていることが前提になります。

構成図

本ページにて紹介するLambdaは、EC2のOS内で実行されているサービスを起動します。
boto3関数は、SSMの「send_command」と「list_command_invocations」を使用します。

EC2のOS内にCloudWatchAgentをインストールして、CloudWatchAlarmでサービスの状態監視ができるよう実装しておいて下さい。
CloudWatchAlarmからはAWS SNSを経由して、今回のLambdaを実行するように設定してください。

この実装で何らかの原因でサービスが停止するとCloudWatchAlarmが検知して、AWS SNSからLambdaが実行されます。
Lambdaは、OS内にサービス起動のコマンドを実行するように指示を出します。

うまく起動すれば処理終了ですが、起動しなかった場合はAWS SNS経由でメール送信を行います。

Lambdaソースコード

import boto3
import json
import time

ssm_client = boto3.client('ssm')
sns_client = boto3.client('sns')
def lambda_handler(event, context):
    instance_id = ''
    service_name = ''
    os_type = ''
    
    if os_type == "Windows":
        document_name = 'AWS-RunPowerShellScript'
        command_list_start = ['Start-Service -Name ' + service_name]
        command_list_check = ['$service_info = get-service ' + service_name , '$service_info.status']
        runnnig_status = 'Running\r\n'
    elif os_type == "Linux":
        document_name = 'AWS-RunShellScript'
        command_list_start = ['systemctl start ' + service_name]
        command_list_check = ['systemctl status ' + service_name + ' | grep "running" > /dev/null' , 'echo $?']
        runnnig_status = '0\n'
    
    #サービス起動コマンド実行
    response = ssm_client.send_command(
        InstanceIds = [instance_id],
        DocumentName = document_name,
        Parameters = {
            'commands': command_list_start
        }
    )
    command_id = response["Command"]["CommandId"]
    while True:
        response = ssm_client.list_command_invocations(
            CommandId = command_id,
            InstanceId = instance_id,
        )
        try:
            command_status = response["CommandInvocations"][0]["Status"]
        except Exception as e:
            command_status = "Pending"
        if command_status != 'Pending' and command_status != 'InProgress' and command_status != 'Delayed':
            break
        time.sleep(2)
    
    #サービスステータス確認コマンド実行
    response = ssm_client.send_command(
        InstanceIds = [instance_id],
        DocumentName = document_name,
        Parameters={
            'commands': command_list_check
        }
    )
    command_id = response["Command"]["CommandId"]
    while True:
        response = ssm_client.list_command_invocations(
            CommandId = command_id,
            InstanceId = instance_id,
            Details = True
        )
        try:
            command_status = response["CommandInvocations"][0]["Status"]
        except Exception as e:
            command_status = 'Pending'
        if command_status != 'Pending' and command_status != 'InProgress' and command_status != 'Delayed':
            break
        time.sleep(2)
    
    #起動失敗時メール送信
    if response["CommandInvocations"][0]["CommandPlugins"][0]["Output"] != runnnig_status:
        message = 'サービスの起動に失敗しました。\r\n'
        message += '詳細はサーバにログインし、確認してください。\r\n\r\n'
        message += ' 対象EC2:' +  instance_id + '\r\n'
        message += ' 対象サービス名:' +  service_name + '\r\n'
        
        sns_client.publish(
            TopicArn = 'arn:aws:sns:ap-northeast-1:952450169247:maeda-scskmail',
            Message = message
        )
        
    return 0   

▼基本設定

EC2インスタンスID*
サービス名*
OSタイプ*

権限(IAMロール設定)

LambdaにアタッチするIAMロールに「ssm:SendCommand」、「ssm:ListCommandInvocations」、「SNS:Publish」の権限を付与してください。

解説

紹介したLambdaのフローは以下の通りです。
  1. Windows or Linuxのサービス起動コマンド実行
  2. コマンド実行完了待機
  3. サービス状態確認コマンド実行
  4. コマンド実行完了待機
  5. サービス起動試行結果確認
  6. サービス起動失敗の場合、メール通知
boto3のsend_command関数を使用することで、EC2内のOSに対してコマンド実行を指示できます。
WindowsとLinuxで使用するコマンドが異なるので、最初にどちらのOS用のコマンドを実行するかを選択しています。

実行指示は、SSMのRunCommandサービスを経由して行われます。
コマンドが実際に実行完了するまでに時間がかかるので、while内でRunCommandの実行状態を「list_command_invocations」関数で取得して完了したらbreakします。

最初のサービス起動コマンドが完了したら、同様にサービスの起動状態を確認するコマンドを実行させます。
こちらも完了までに時間がかかるのでwhileとbreakで実行完了まで実行状態の確認を続けさせます。

最終的にサービスが起動できていたら、そのまま終了します。
サービス起動に失敗してしまった場合は、AWS SNSを経由してメール通知を行います。

使い方

対象のEC2のOS内にCloudWatchAgentをインストールし、プロセスの状態をメトリクスでCloudWatchへ連携してください。
連携したメトリクスでサービスが停止したら、AWS SNSを実行するCloudWatchAlarmを作成してください。
実行するAWS SNSでは、今回紹介したLambdaを実行させるようにしてください。
また、メール通知を行うので、別のAWS SNSを作成して通知先のメールアドレスをサブスクリプション登録しておいて下さい。