ラベル Lambda の投稿を表示しています。 すべての投稿を表示
ラベル Lambda の投稿を表示しています。 すべての投稿を表示

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分まで延ばします。

2022年12月27日火曜日

Lambda(S3にあるファイルを呼び出してみる)

以下、Pythonを使ってS3にあるファイルを呼び出してみる

-------------ソースコード--------

import json
import boto3
# バケット名,オブジェクト名
BUCKET_NAME = 'バケット名'
OBJECT_KEY_NAME = 'バケットにあるファイル名'
s3_client = boto3.client('s3')
def lambda_handler(event, context):
    response = s3_client.get_object(Bucket=BUCKET_NAME, Key=OBJECT_KEY_NAME)
    body = response['Body'].read().decode('utf-8')
    print(body)
    return ''
-------------------------------------------------





2022年4月10日日曜日

Lambda内部で生成したファイルをS3に圧縮して保管してみる。

課題:
Lambda内部で生成したテキストファイルをS3側で圧縮して保管できるか試してみる。



■IAM


1)ポリシーを作成する。

















########### サンプル ##############

{

    "Version": "2012-10-17",

    "Statement": [

        {

            "Effect": "Allow",

            "Action": [

                "s3:PutObject",

                "s3:PutObjectAcl"

            ],

            "Resource": "arn:aws:s3:::バケット名/*”

        }

    ]

}


################################



2)IAMロールの作成を行う。

上記で作成したIAMポリシーを選択













■S3


1)以下、バケットポリシーの編集を行う。























########## サンプルコード ################

{

    "Version": "2012-10-17",

    "Statement": [

        {

            "Effect": "Allow",

            "Principal": {

                "AWS": "arn:aws:iam::アカウントID:role/”iam role名”

            },

            "Action": [

                "s3:PutObject",

                "s3:PutObjectAcl",

                "s3:GetObject",

                "s3:GetObjectAcl",

                "s3:AbortMultipartUpload"

            ],

            "Resource": "arn:aws:s3:::バケット名/*”

        }

    ]

}

#####################################




■Lambda


1)ソースコード入力




















###########サンプルコード########

import boto3

from datetime import datetime

import gzip

import shutil

 

print('Loading function')                                                                                      # ②Functionのロードをログに出力

 

s3 = boto3.resource('s3')                                                                                    # ③S3オブジェクトを取得

 

# ④Lambdaのメイン関数

def lambda_handler(event, context):

    bucket = ‘バケット名’                                                                                     # ⑤バケット名を指定

    prefix = 'test_' + datetime.now().strftime('%Y-%m-%d-%H-%M-%S')         # ⑥保存するファイルのprefixを指定

    file_name = prefix + '.txt'                                                                                 # ⑦保存するテキストファイル名

    key = prefix + '.txt.gz'                                                                                      # ⑧保存するgzファイル名

    file_contents = 'Lambda test'                                                                          # ⑨ファイルに保存するテキストの内容

    

# ⑩/tmpにtxtファイルを出力

    with open('/tmp/' + file_name, 'w') as f:

        f.write(file_contents)

    

# ⑪/tmpにgzファイルを作成

    with open('/tmp/' + file_name, 'rb') as f_in:

        with gzip.open('/tmp/' + key , 'wb') as f_out:

            shutil.copyfileobj(f_in, f_out)

    

    obj = s3.Object(bucket,key)                                                                           # ⑫バケット名とパスを指定

    obj.upload_file( '/tmp/' + key )                                                                       # ⑬gzファイルをS3に出力

    return

###############################



2)Lambda側で上記で作成したIAM Roleを選択する。














































3)Lambda側で実行後、S3のバケットで確認する


結果:

Lambda内部で生成したテキストファイルをS3側で圧縮して保管していることが把握できる。







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

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