2026年10月18日日曜日

Lambda + SSM Run Commandで、数十台のEC2のuserdata結果を一括確認する

コードを1枚貼って、テストイベントを実行するだけで、
全台の確認結果が返ってくる手順です。

[ゴール]

Nameタグが web-* / api-* / batch-* のEC2すべてに確認コマンドを流し、NGの台だけを結果に表示します。確認対象のuserdata(Ubuntu + nginx)は次のとおりです。

---bash----------------
#!/bin/bash
set -e
apt-get update
apt-get install -y nginx
systemctl enable nginx
systemctl start nginx
--------------------------



前提

  • 対象EC2でSSM Agentが動いている
  • 対象EC2に AmazonSSMManagedInstanceCore 相当のIAMロールが付いている

手順

1. Lambda関数を作る

  1. Lambdaコンソール → 関数の作成 → 「一から作成」
  2. 関数名:userdata-check(任意)
  3. ランタイム:Python 3.12
  4. 作成

2. コードを貼る

lambda_function.py を全部消して、次を丸ごと貼り付けます。


-----python---------------
import re
import time
import boto3

ssm = boto3.client("ssm")
ec2 = boto3.client("ec2")

# ===== 設定(ここだけ書き換えればOK)=====
DEFAULT_PATTERNS = ["web-*", "api-*", "batch-*"]   # Nameタグのパターン
BATCH_SIZE = 50        # send_commandのInstanceIdsは1回50台まで
MAX_WAIT_SEC = 240     # 結果待ちの上限(秒)

# 確認用スクリプト(上のuserdataに対応)
SCRIPT = r"""
h=$(hostname)
echo "$h,cloud_init,$(cloud-init status 2>&1 | head -1)"
echo "$h,nginx_pkg,$(dpkg -s nginx 2>&1 | grep -E '^(Status|Version)' | tr '\n' ' ')"
echo "$h,nginx_active,$(systemctl is-active nginx 2>&1)"
echo "$h,nginx_enabled,$(systemctl is-enabled nginx 2>&1)"
echo "$h,http_local,$(curl -s -o /dev/null -w '%{http_code}' http://localhost/ 2>&1)"
"""

# NG判定に使う文字列
NG_KEYWORDS = (
    "status: error", "status: running",
    ",inactive", ",failed", ",disabled",
    "is not installed", "not-found",
    ",000", ",500", ",502", ",503", ",504",
)


def find_instances(name_patterns):
    """Nameタグ(ワイルドカード可・複数可)でrunning中のEC2を {id: name} で返す"""
    found = {}
    paginator = ec2.get_paginator("describe_instances")
    for page in paginator.paginate(Filters=[
        {"Name": "tag:Name", "Values": name_patterns},
        {"Name": "instance-state-name", "Values": ["running"]},
    ]):
        for r in page["Reservations"]:
            for i in r["Instances"]:
                name = next((t["Value"] for t in i.get("Tags", [])
                             if t["Key"] == "Name"), "")
                found[i["InstanceId"]] = name
    return found


def send_batch(instance_ids):
    res = ssm.send_command(
        InstanceIds=instance_ids,
        DocumentName="AWS-RunShellScript",
        Parameters={"commands": [SCRIPT]},
        MaxConcurrency="20",
        MaxErrors="10",
    )
    return res["Command"]["CommandId"]


def collect(cmd_id, expected):
    """全台が終わるまで待って、結果を返す"""
    deadline = time.time() + MAX_WAIT_SEC
    invs = []
    while time.time() < deadline:
        time.sleep(5)
        invs = ssm.list_command_invocations(
            CommandId=cmd_id, Details=True
        )["CommandInvocations"]
        done = [i for i in invs
                if i["Status"] not in ("Pending", "InProgress", "Delayed")]
        if len(invs) >= expected and len(done) == len(invs):
            break
    return invs


def lambda_handler(event, context):
    patterns = event.get("name_patterns", DEFAULT_PATTERNS)
    dry_run = event.get("dry_run", False)             # trueなら対象一覧だけ返す
    numeric_only = event.get("numeric_only", False)   # 「名前-数字」だけに絞る

    instances = find_instances(patterns)

    if numeric_only:
        prefixes = [p.rstrip("*") for p in patterns]
        instances = {
            k: v for k, v in instances.items()
            if any(re.fullmatch(re.escape(px) + r"\d+", v) for px in prefixes)
        }

    if not instances:
        return {"error": f"対象が0台です: {patterns}"}

    if dry_run:
        return {
            "count": len(instances),
            "targets": dict(sorted(instances.items(), key=lambda x: x[1])),
        }

    ids = list(instances.keys())
    batches = [ids[i:i + BATCH_SIZE] for i in range(0, len(ids), BATCH_SIZE)]

    # 先に全バッチを送信してから、まとめて待つ
    sent = [(send_batch(b), len(b)) for b in batches]

    lines, failed = [], []
    for cmd_id, expected in sent:
        for inv in collect(cmd_id, expected):
            iid = inv["InstanceId"]
            name = instances.get(iid, "")
            if inv["Status"] != "Success":
                failed.append(f"{name},{iid},{inv['Status']}")
                continue
            for plugin in inv["CommandPlugins"]:
                for line in plugin["Output"].strip().splitlines():
                    rest = line.split(",", 1)[1] if "," in line else line
                    lines.append(f"{name},{iid},{rest}")

    ng_lines = sorted(l for l in lines if any(k in l for k in NG_KEYWORDS))

    return {
        "target_count": len(ids),
        "ng_count": len(ng_lines),
        "failed_count": len(failed),
        "ng_lines": ng_lines,
        "failed_instances": failed,
        "all_lines": sorted(lines),
    }

-------------------------------------

貼ったら Deploy を押します。

3. タイムアウトを延ばす

設定 → 一般設定 → 編集 で、タイムアウトを 5分0秒 にします。デフォルトの3秒だと、SSMの完了を待てずに終わります。

4. 権限を付ける

設定 → アクセス権限 から実行ロールを開き、「許可を追加 → インラインポリシーを作成」でJSONを選び、次を貼ります。

json
--------------------
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeInstances",
        "ssm:SendCommand",
        "ssm:ListCommandInvocations",
        "ssm:GetCommandInvocation"
      ],
      "Resource": "*"
    }
  ]
}
--------------------------

その場限りの確認用なので Resource: "*" にしています。使い回すなら、対象を絞ってください。

5. テストイベントで実行する

テスト タブでイベントを作り、まず対象の確認から実行します。

json
{ "dry_run": true }

対象のEC2一覧が返ります。想定外の名前(web-test など)が混ざっていないか確認してください。混ざっている場合は、次の指定で「名前-数字」だけに絞れます。

json
{ "numeric_only": true }

対象が想定どおりなら、本番を実行します。

json
{}

結果の見方

json
------------------------------
{
  "target_count": 30,
  "ng_count": 2,
  "failed_count": 0,
  "ng_lines": [
    "api-3,i-0123abcd,http_local,000",
    "api-3,i-0123abcd,nginx_active,inactive"
  ],
  "failed_instances": [],
  "all_lines": ["..."]
}
------------------------------
  • ng_lines が空なら、全台OKです。
  • failed_instances は、SSMでコマンドを実行できなかった台です(Agent停止、ロール未付与など)。
  • 行の形式は Nameタグ,インスタンスID,確認項目,結果 です。
  • all_lines に全台の全結果が入っています。NG判定は文字列一致なので、迷ったときはここも確認します。

ハマりどころ

  • タイムアウト:3秒のままだと、結果が返る前に終わります。
  • ワイルドカードが広すぎる:web-* は web-test にも一致します。最初に dry_run で確認します。
  • 出力が2,500文字で切れる:API経由で取れる出力は1台あたり2,500文字までです。確認スクリプトは短く保ちます。
  • 「成功」と「問題なし」は別:SSMのステータスが成功でも、出力の中にNGが入っていることがあります。
  • 作成直後の台:SSM Agentの起動前は、対象に出ないか failed_instances に入ります。数分待って再実行します。

補足

  • 確認コマンドは読み取りだけなので、実行しても対象EC2の状態は変わりません。
  • 別のuserdataを確認したいときは、SCRIPT の中身と NG_KEYWORDS を書き換えるだけで流用できます。
  • 台数が多く5分で終わらない場合は、タイムアウトを最大15分まで延ばします。

EC2のUser Data実行完了とNginxの稼働状態をSSM Run Commandで一括チェックする方法

インフラの構築において、TerraformやCloudFormationの User Data を使ってインスタンスの初期セットアップ(ミドルウェアのインストールなど)を自動化することはよくあると思います。

しかし、こんなお悩みはありませんか?

  • 「本当にUser Dataがエラーなく最後まで走り切ったのか?」

  • 「Nginxなどのサービスがちゃんと起動して、HTTPの応答を返しているか?」

  • 1台ずつSSHでログインして確認するのは面倒だし、複数台あるとやってられない!


今回は、TerraformのUser Data(.tpl)による構築から、AWS SSM Run Command を使ってその実行結果やサービス稼働状況を安全かつ一括でチェックし、さらに S3経由でログをローカルへ一括ダウンロードする 一連の流れをご紹介します。


1. User Data(.tplファイル)の準備

まずは、インスタンス初回起動時に実行されるスクリプトの例です。今回はシンプルに Ubuntu 環境へ Nginx をインストールして起動する構成を想定します。

user_data.sh.tpl

Bash
#!/bin/bash
set -e

# Ubuntu 向け nginx のインストール
apt-get update
apt-get install -y nginx

systemctl enable nginx
systemctl start nginx

echo "Nginx installed successfully."

このスクリプトが適用されたEC2インスタンスが起動すると、内部で cloud-init を通じてセットアップが実行されます。


2. SSM Run Command で実行するチェック用スクリプト

インスタンスが立ち上がった後、AWS Systems Manager (SSM) Run Command を使って、SSH接続なしでリモートから状態を確認します。

SSMコンソールの AWS-RunShellScript の「コマンドのパラメータ」に、以下のシェルスクリプトを記述して実行します。


Bash
h=$(hostname)

echo "$h,cloud_init,$(cloud-init status 2>&1 | head -1)"
echo "$h,nginx_pkg,$(dpkg -s nginx 2>&1 | grep -E '^(Status|Version)' | tr '\n' ' ')"
echo "$h,nginx_active,$(systemctl is-active nginx 2>&1)"
echo "$h,nginx_enabled,$(systemctl is-enabled nginx 2>&1)"
echo "$h,http_local,$(curl -s -o /dev/null -w '%{http_code}' http://localhost/ 2>&1)"
echo "$h,userdata_error,$(grep -ciE 'error|fail' /var/log/cloud-init-output.log 2>&1)"


🛠️ スクリプトのポイント解説

  • h=$(hostname): 出力の先頭にホスト名を付与し、複数台実行時にもCSV風に識別しやすくしています。

  • cloud-init status: ユーザーデータの処理が正常に完了したか(status: done)を確認します。

  • dpkg -s nginx: パッケージが正しくインストールされているかをチェックします。

  • systemctl is-active / is-enabled: サービスが稼働中かつ自動起動設定が有効かを確認します。

  • curl -w '%{http_code}': ローカルから HTTP リクエストを投げ、200 OK が返るかを検証します。

  • grep -ciE 'error|fail': /var/log/cloud-init-output.log 内のエラー件数をカウントします(0 ならエラーなし)。


3. SSMの出力オプションで Amazon S3 への書き込みを有効化

Run Command 実行時、「出力オプション」 でログを Amazon S3 へ保存するように設定します。

  1. 「S3 バケットへの書き込みを有効化する」 にチェック。

  2. 保存先の S3 URI(例: s3://my-app-bucket-ap-se2)を指定。

これで、コマンドの実行結果(stdout)が自動的にS3バケットへアップロードされます。

⚠️ S3にログが出力されない(オブジェクトが0件)場合の注意点

S3バケットが空のままになる場合、EC2インスタンスにアタッチされている IAM ロール(インスタンスプロファイル)に対象S3バケットへの s3:PutObject 権限が不足しているケースがほとんどです。あらかじめIAMポリシーを確認しておきましょう。


4. 実行結果の確認

実行後、コンソールで確認できる出力結果は以下の通りです:


ip-10-0-1-29,cloud_init,status: done
ip-10-0-1-29,nginx_pkg,Status: install ok installed Version: 1.24.0-2ubuntu7.18 
ip-10-0-1-29,nginx_active,active
ip-10-0-1-29,nginx_enabled,enabled
ip-10-0-1-29,http_local,200
ip-10-0-1-29,userdata_error,0

一目で cloud_init の完了、Nginxのバージョンとアクティブ状態、HTTP 200応答、エラー0件が確認できます。


5. 複数台のログを AWS CLI で一括ダウンロードする

SSM Run Command から S3 へ出力すると、S3上には以下のような階層構造(ディレクトリ)でログが自動生成されます:


Bash
s3://my-app-bucket-ap-se2/(コマンドID)/(インスタンスID)/awsrunShellScript/0.awsrunShellScript/stdout

複数台のEC2に対して一括実行した場合、インスタンスIDごとにフォルダが生成されるため、コンソールから1つずつ手動ダウンロードするのは手間がかかります。

そこで、手元のターミナルから aws s3 sync を使うことで、一括でローカルに取得できます。

Bash
aws s3 sync s3://my-app-bucket-ap-se2/ ./ssm-results/


【実行例】

Bash
$ aws s3 sync s3://my-app-bucket-ap-se2/ ./ssm-results/
download: s3://my-app-bucket-ap-se2/8d2e3b24-b0ce-4075-b876-3b2968117da1/i-031f8ce1ca1c2c95c/awsrunShellScript/0.awsrunShellScript/stdout to ssm-results/8d2e3b24-b0ce-4075-b876-3b2968117da1/i-031f8ce1ca1c2c95c/awsrunShellScript/0.awsrunShellScript/stdout

これでローカルの ./ssm-results/ ディレクトリ配下に全台分の実行ログが一瞬で保存されるため、ローカルでの集計やログ調査が非常に楽になります。


まとめ

  • User Data (.tpl) での環境構築後、SSH不要の SSM Run Command で状態を安全・確実にチェック。

  • パースしやすいフォーマットで出力することで、稼働状況やエラー件数をひと目で把握。

  • S3出力 と aws s3 sync を組み合わせることで、複数台のサーバーログを一括で回収・管理可能。

インフラの自動構築後の検証フローとして非常に有用ですので、ぜひ試してみてください!

Lambda + SSM Run Commandで、数十台のEC2のuserdata結果を一括確認する

コードを1枚貼って、テストイベントを実行するだけで、 全台の確認結果が返ってくる手順です。 [ゴール] Nameタグが web-* / api-* / batch-* のEC2すべてに確認コマンドを流し、 NGの台だけ を結果に表示します。確認対象のuserdata(Ubu...