作業前後のEC2バックアップを楽にしたい!Parameter Store + Lambdaで複数インスタンスのAMIを一括作成してみた!

作業前後のEC2バックアップを楽にしたい!Parameter Store + Lambdaで複数インスタンスのAMIを一括作成してみた!

2026.05.27

はじめに

AWS環境を運用していると、脆弱性対応、OSパッチ適用、ミドルウェア更新、アプリケーションリリースなど、作業前後にEC2インスタンスのバックアップを取得したい場面があります。

TerraformやCloudFormationなどのIaCでインフラを管理している環境も増えていますが、すべての会社が最初からIaC化されているわけではありません。

特に、既存環境がManagement Console中心で構築されている場合、いきなりすべてをIaC化するのは難しいことがあります。
そのような環境では、作業前にEC2のAMIを手動で取得しているケースもあると思います。
しかし、対象インスタンスが1台や2台ならまだいいですが、20台以上ある場合はどうでしょうか。

  • EC2を1台ずつ選択する
  • AMI名を入力する
  • 再起動しない設定を確認する
  • タグを付ける
  • 作業対象を間違えないように確認する

そこで今回は、Systems Manager Parameter Storeにバックアップ対象のEC2インスタンスID一覧を保存し、
LambdaからEC2 CreateImage APIを実行して、複数インスタンスのAMIを一括作成する仕組みを作ってみます。

今回作成するAMI名は以下の形式にします。
YYYYMMDD_EC2のNameタグ
例:
20260520_basecamp-step1-ec2-1
同じ名前のAMIがすでに存在する場合は、時刻も付与します。
20260520_basecamp-step1-ec2-1_153012
また、EC2を再起動しないように、NoReboot=TrueでAMIを作成します。

注意点

今回の方式ではNoReboot=Trueを使います。
これはEC2を再起動せずにAMIを作成するため、サービス停止を避けたい作業前バックアップでは便利です。
ただし、AWS CLIのcreate-imageドキュメントでも、NoRebootを使うとファイルシステムの整合性が保証されない可能性があると説明されています。
https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CreateImage.html


参考: AWS Backupで定期バックアップする方法

EC2の定期バックアップには、AWS Backupを使う方法があります。

DevelopersIOにも、EC2インスタンスにタグを付け、AWS BackupのBackup Planで定期バックアップを設定する記事があります。この記事では、EC2にタグを付与し、Backup Plan側でそのタグを条件にバックアップ対象を選択しています。バックアップ頻度として「毎日」などを指定し、バックアップボールトと保持期間を設定する流れです。
https://dev.classmethod.jp/articles/how-to-set-scheduled-snapshot-ec2-jp/

この方式は以下のような用途に向いています。

  • 定期的に(毎日、毎週、毎月)バックアップしたい
  • バックアップ対象が固定されている
  • 保持期間をAWS Backup側で管理したい

Parameter Store + LambdaでAMI一括作成

一方で、今回扱いたいのは以下のようなケースです。

  • 脆弱性対応の前後に一度だけAMIを作りたい
  • 対象インスタンスが作業ごとに変わる
  • 複数EC2を今すぐまとめてバックアップしたい
  • AMI名を自分たちのルールで付けたい

このような場合は、AWS Backupの定期バックアップよりも、LambdaでEC2 CreateImageを呼び出す方がシンプルに扱える場面があります。

今回の構成

Parameter Store
ec2-backup-target-list
= i-aaa,i-bbb,i-ccc

Lambda

  1. Parameter StoreからInstance ID一覧を取得
  2. EC2のNameタグを取得
  3. AMI名を生成
  4. EC2 CreateImageをNoReboot=Trueで実行
  5. AMIとSnapshotにタグを付与

EC2 AMI
20260520_basecamp-step1-ec2-1


作成するリソース

今回作成するリソースは以下です。

  1. Parameter Store
    ec2-backup-target-list

  2. CloudWatch Logs Log Group
    /aws/lambda/ec2-ami-on-demand-backup

  3. Lambda実行Role
    lambda-ec2-ami-backup-role

  4. Lambda関数
    ec2-ami-on-demand-backup

実装手順

今回の構築は、ローカル環境へのツールインストールなしで誰でも同じ手順で実施できるよう、
AWS Management Console内のCloudShellを使って構築します。
構築後のリソース確認は、AWS Management Consoleの各サービス画面から行います。

スクリーンショット 2026-05-22 16.42.08

1〜8まで順番にCloudShellでコマンドを入力してください。


1. CloudShellで変数を設定する

※注意
CloudShellは一定時間操作がないとセッションが切れます。
exportで設定した変数はメモリ上にしか存在しないため、セッションが切れる→メモリがリセット→変数が消える
しかし、セッションが切れる前に終わるなら一番シンプルなので、以下のコードを利用します。
セッションが切れたら再度exportしてください。

bash
export REGION="ap-northeast-1"
export ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
export PARAMETER_NAME="ec2-backup-target-list"
export LAMBDA_ROLE_NAME="lambda-ec2-ami-backup-role"
export LAMBDA_FUNCTION_NAME="ec2-ami-on-demand-backup"

echo "REGION=${REGION}"
echo "ACCOUNT_ID=${ACCOUNT_ID}"
echo "PARAMETER_NAME=${PARAMETER_NAME}"
echo "LAMBDA_ROLE_NAME=${LAMBDA_ROLE_NAME}"
echo "LAMBDA_FUNCTION_NAME=${LAMBDA_FUNCTION_NAME}"

2. Parameter Storeを作成する

bash
aws ssm put-parameter \
  --name "${PARAMETER_NAME}" \
  --type "StringList" \
  --value "i-aaaaaaaaa,i-bbbbbbbbb,i-ccccccccc" \
  --overwrite \
  --region "${REGION}"

Parameter Storeを確認

スクリーンショット 2026-05-22 16.51.08

以下の「編集」からターゲットインスタンスを変更できます。
"i-aaaaaaaaa,i-bbbbbbbbb,i-ccccccccc"には
Backupしたい複数のインスタンスIDを入力してください。

スクリーンショット 2026-05-22 16.51.35

スクリーンショット 2026-05-22 18.50.19


3. CloudWatch Logs Log Groupを作成する

bash
aws logs create-log-group \
  --log-group-name "/aws/lambda/${LAMBDA_FUNCTION_NAME}" \
  --region "${REGION}" || true

aws logs put-retention-policy \
  --log-group-name "/aws/lambda/${LAMBDA_FUNCTION_NAME}" \
  --retention-in-days 30 \
  --region "${REGION}"

CloudWatch Logs Log Groupを確認

スクリーンショット 2026-05-22 16.52.44


4. Lambda実行Roleを作成する

bash
cat > lambda-trust-policy.json <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowLambdaServiceToAssumeRole",
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF
bash
aws iam create-role \
  --role-name "${LAMBDA_ROLE_NAME}" \
  --assume-role-policy-document file://lambda-trust-policy.json

5. Lambda用IAM Policyを作成する

bash
cat > lambda-ami-backup-policy.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadOnlySpecificParameter",
      "Effect": "Allow",
      "Action": [
        "ssm:GetParameter"
      ],
      "Resource": "arn:aws:ssm:${REGION}:${ACCOUNT_ID}:parameter/${PARAMETER_NAME}"
    },
    {
      "Sid": "DescribeEc2AndImagesForAmiBackup",
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeInstances",
        "ec2:DescribeImages"
      ],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "aws:RequestedRegion": "${REGION}"
        }
      }
    },
    {
      "Sid": "CreateAmiAndTagsOnlyInTargetRegion",
      "Effect": "Allow",
      "Action": [
        "ec2:CreateImage",
        "ec2:CreateTags"
      ],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "aws:RequestedRegion": "${REGION}"
        }
      }
    },
    {
      "Sid": "WriteLogsOnlyToThisLambdaLogGroup",
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": [
        "arn:aws:logs:${REGION}:${ACCOUNT_ID}:log-group:/aws/lambda/${LAMBDA_FUNCTION_NAME}:*"
      ]
    }
  ]
}
EOF
bash
aws iam put-role-policy \
  --role-name "${LAMBDA_ROLE_NAME}" \
  --policy-name "lambda-ec2-ami-backup-minimum-policy" \
  --policy-document file://lambda-ami-backup-policy.json

Lambda実行Roleを確認

スクリーンショット 2026-05-22 16.55.28


6. Lambdaコードを作成する

bash
cat > lambda_function.py <<'EOF'
import os
import re
import boto3
from datetime import datetime
from zoneinfo import ZoneInfo
from botocore.exceptions import ClientError

def sanitize_name(value: str) -> str:
    value = value.strip()
    value = re.sub(r"[^A-Za-z0-9._-]", "_", value)
    value = re.sub(r"_+", "_", value)
    value = value.strip("_") or "noname"
    return value[:80]

def get_name_tag(instance):
    for tag in instance.get("Tags", []):
        if tag.get("Key") == "Name":
            return tag.get("Value", instance["InstanceId"])
    return instance["InstanceId"]

def ami_name_exists(ec2, ami_name):
    response = ec2.describe_images(
        Owners=["self"],
        Filters=[
            {
                "Name": "name",
                "Values": [ami_name]
            }
        ]
    )
    return len(response.get("Images", [])) > 0

def lambda_handler(event, context):
    parameter_name = os.environ["PARAMETER_NAME"]
    target_region = os.environ["TARGET_REGION"]
    timezone_name = os.environ.get("TIMEZONE", "Asia/Tokyo")
    no_reboot = os.environ.get("NO_REBOOT", "true").lower() == "true"

    ssm = boto3.client("ssm", region_name=target_region)
    ec2 = boto3.client("ec2", region_name=target_region)

    now = datetime.now(ZoneInfo(timezone_name))
    date_str = now.strftime("%Y%m%d")
    time_str = now.strftime("%H%M%S")

    print(f"Target region: {target_region}")
    print(f"Parameter name: {parameter_name}")
    print(f"Timezone: {timezone_name}")
    print(f"Date: {date_str}")
    print(f"NoReboot: {no_reboot}")

    response = ssm.get_parameter(
        Name=parameter_name,
        WithDecryption=False
    )

    instance_ids_text = response["Parameter"]["Value"]

    instance_ids = [
        instance_id.strip()
        for instance_id in instance_ids_text.split(",")
        if instance_id.strip()
    ]

    if not instance_ids:
        print("No instance IDs found.")
        return {
            "message": "No instance IDs found.",
            "images": []
        }

    print(f"Target instance IDs: {instance_ids}")

    results = []

    for instance_id in instance_ids:
        try:
            describe_response = ec2.describe_instances(
                InstanceIds=[instance_id]
            )

            reservations = describe_response.get("Reservations", [])
            if not reservations or not reservations[0].get("Instances"):
                raise Exception(f"Instance not found: {instance_id}")

            instance = reservations[0]["Instances"][0]

            raw_name_tag = get_name_tag(instance)
            safe_name_tag = sanitize_name(raw_name_tag)

            ami_name = f"{date_str}_{safe_name_tag}"

            if ami_name_exists(ec2, ami_name):
                ami_name = f"{date_str}_{safe_name_tag}_{time_str}"

            print(f"Creating AMI for instance: {instance_id}")
            print(f"Instance Name tag: {raw_name_tag}")
            print(f"AMI name: {ami_name}")

            create_response = ec2.create_image(
                InstanceId=instance_id,
                Name=ami_name,
                Description=f"On-demand AMI backup for {instance_id} ({raw_name_tag}) created by Lambda",
                NoReboot=no_reboot,
                TagSpecifications=[
                    {
                        "ResourceType": "image",
                        "Tags": [
                            {"Key": "Name", "Value": ami_name},
                            {"Key": "BackupType", "Value": "OnDemand"},
                            {"Key": "Source", "Value": "Lambda"},
                            {"Key": "InstanceId", "Value": instance_id},
                            {"Key": "OriginalInstanceName", "Value": raw_name_tag},
                            {"Key": "BackupDate", "Value": date_str},
                            {"Key": "NoReboot", "Value": str(no_reboot)}
                        ]
                    },
                    {
                        "ResourceType": "snapshot",
                        "Tags": [
                            {"Key": "Name", "Value": ami_name},
                            {"Key": "BackupType", "Value": "OnDemand"},
                            {"Key": "Source", "Value": "Lambda"},
                            {"Key": "InstanceId", "Value": instance_id},
                            {"Key": "OriginalInstanceName", "Value": raw_name_tag},
                            {"Key": "BackupDate", "Value": date_str},
                            {"Key": "NoReboot", "Value": str(no_reboot)}
                        ]
                    }
                ]
            )

            image_id = create_response["ImageId"]

            result = {
                "instance_id": instance_id,
                "instance_name": raw_name_tag,
                "ami_id": image_id,
                "ami_name": ami_name,
                "no_reboot": no_reboot,
                "status": "AMI_CREATION_STARTED"
            }

            print(f"AMI creation started: {result}")
            results.append(result)

        except ClientError as e:
            print(f"Failed to create AMI for {instance_id}: {e}")
            results.append({
                "instance_id": instance_id,
                "status": "FAILED",
                "error": str(e)
            })

        except Exception as e:
            print(f"Failed to process {instance_id}: {e}")
            results.append({
                "instance_id": instance_id,
                "status": "FAILED",
                "error": str(e)
            })

    return {
        "message": "AMI creation requests completed.",
        "images": results
    }
EOF

7. Lambda関数を作成する

bash
zip function.zip lambda_function.py

export LAMBDA_ROLE_ARN="arn:aws:iam::${ACCOUNT_ID}:role/${LAMBDA_ROLE_NAME}"

sleep 10

aws lambda create-function \
  --function-name "${LAMBDA_FUNCTION_NAME}" \
  --runtime python3.12 \
  --role "${LAMBDA_ROLE_ARN}" \
  --handler lambda_function.lambda_handler \
  --zip-file fileb://function.zip \
  --timeout 180 \
  --memory-size 128 \
  --environment "Variables={PARAMETER_NAME=${PARAMETER_NAME},TARGET_REGION=${REGION},TIMEZONE=Asia/Tokyo,NO_REBOOT=true}" \
  --region "${REGION}"

Lambda関数を確認

スクリーンショット 2026-05-22 19.11.47

スクリーンショット 2026-05-22 19.13.11


8. Lambdaを実行する

CloudShellのセッションが切れた後も、実行できるように
以下の必要なexportを再度追記しています。
export REGION="ap-northeast-1"
export LAMBDA_FUNCTION_NAME="ec2-ami-on-demand-backup"

bash
export REGION="ap-northeast-1"
export LAMBDA_FUNCTION_NAME="ec2-ami-on-demand-backup"

aws lambda invoke \
  --function-name "${LAMBDA_FUNCTION_NAME}" \
  --payload '{}' \
  --cli-binary-format raw-in-base64-out \
  --region "${REGION}" \
  response.json

cat response.json

EC2 AMI作成を確認

スクリーンショット 2026-05-22 19.55.49

スクリーンショット 2026-05-22 19.56.30

まとめ

今回は、Systems Manager Parameter StoreとLambdaを使って、複数EC2のAMIバックアップを一括作成する仕組みを紹介しました。
複数EC2を一括バックアップする方法はいくつかありますが、それぞれ得意な場面が異なります。

方法別の使い分け

方法 向いている場面 AMI命名
AWS Backup 毎日・毎週など、決まった対象を定期バックアップ 自動生成
SSM Automation GUIで手軽に実行 カスタム不可
Parameter Store + Lambda 毎回対象が変わる臨時作業・脆弱性対応 カスタム可

Parameter Store + Lambdaが特に活きる場面

脆弱性対応やパッチ作業では、こんな状況がよく起こります。

  • メンテナンス作業によって対象インスタンスが毎回異なる
  • AMI・スナップショット名を独自のルールで付けたい
  • 作業前に今すぐバックアップしたい

Parameter Storeなら、対象インスタンスIDをリストに登録して
Lambdaを1回実行するだけで完了します。
作業が終わればリストを消すだけで、管理もシンプルです。

おわりに

「毎回対象が変わる臨時バックアップ」 という、
コンソールで手作業すると漏れやミスが起きやすい作業を、
シンプルに自動化できる点に価値があると思っています。

こうした小さな自動化を積み重ねることで、
運用の標準化やIaC導入への第一歩にしてみてはいかがでしょうか?

この記事をシェアする

関連記事