SSM Automationを1年本番運用して痛感した設計の勘どころと失敗パターン

深夜2時にパッチ適用Runbookの静かな失敗を検知——そんな経験ありませんか?マルチアカウント構成で1年運用して見えたSSM Automationの罠と対処法をリアルにまとめました。

先日、本番のパッチ適用Runbookが静かに失敗し続けていた件を検知したのが深夜2時だった。アラートが飛んでこなかったのは僕の設計ミスで、正直冷や汗どころじゃなかった。あの経験を含めて、SSM Automationを1年以上本番運用してきた中で「これは最初から知りたかった」と思うことをまとめておく。

SSM Automationって地味に奥が深くて、ドキュメントを読んでいるだけじゃ運用上の罠が見えてこないんですよね。うちのチームは最初「ただのRunbook実行基盤でしょ」と舐めてかかったせいで、3ヶ月目くらいにガッツリやらかした。今回はそういうリアルな話を中心に書く。

SSM Automationで最初にやらかしたこと

まず自分たちの構成から話す。うちはマルチアカウント構成で、管理アカウントからクロスアカウントのAutomationを実行するパターンを採用している。構成はこんな感じ。

graph TB
    subgraph Management Account
        SSMConsole[SSM コンソール / API]
        AutomationDoc[Automation Runbook]
        EventBridge[EventBridge Scheduler]
        SNS[SNS アラート]
    end

    subgraph VPC - ap-northeast-1
        subgraph AZ-1a
            EC2_1[EC2 Web Server]
            SSMAgent1[SSM Agent]
        end
        subgraph AZ-1c
            EC2_2[EC2 App Server]
            SSMAgent2[SSM Agent]
        end
        VPCEndpoint[SSM VPC Endpoint]
    end

    subgraph Dev Account
        IAMRole[自動化用 IAM Role]
        S3Bucket[Runbook ログ S3]
    end

    EventBridge -->|トリガー| AutomationDoc
    AutomationDoc -->|クロスアカウント実行| IAMRole
    IAMRole -->|AssumeRole| SSMConsole
    SSMAgent1 -->|VPC経由| VPCEndpoint
    SSMAgent2 -->|VPC経由| VPCEndpoint
    VPCEndpoint --> SSMConsole
    AutomationDoc -->|ログ出力| S3Bucket
    AutomationDoc -->|失敗通知| SNS

やらかしたのは VPCエンドポイントの設定漏れだ。インターネット経由でSSM Agentと疎通できていると思い込んでいたのに、実は一部のEC2がプライベートサブネットに置かれていて、Automationが途中でタイムアウトしていた。しかもエラーメッセージが「Command execution timed out」というゆるい文言だったため、原因特定に2日かかった。

VPCエンドポイントは最低でも以下の3つが必要だ。これを知らずに詰まる人が多いので最初に書いておく。

エンドポイント用途
com.amazonaws.ap-northeast-1.ssmSSM API
com.amazonaws.ap-northeast-1.ssmmessagesSession Manager通信
com.amazonaws.ap-northeast-1.ec2messagesRun Commandレスポンス

Runbook設計のリアルな勘どころ

エラー処理は「onFailure」だけに頼るな

RunbookのYAMLでonFailure: Abortを書いておけば止まるから安心、と思ってた時期があった。でも実際に困るのは「途中のステップが成功したように見えてFailしてる」パターンだ。

例えばパッチ適用ステップで、Agentがタイムアウト直前に「成功」レスポンスを返すことがある。その場合onFailureは発火しない。だから自分は必ず検証ステップを挟むようにしている。

schemaVersion: '0.3'
description: 'EC2 Patch + Validation Runbook'
mainSteps:
  - name: ApplyPatches
    action: aws:runCommand
    inputs:
      DocumentName: AWS-RunPatchBaseline
      InstanceIds:
        - '{{ InstanceId }}'
      Parameters:
        Operation: Install
    nextStep: ValidatePatchCompliance
    onFailure: step:NotifyFailure

  - name: ValidatePatchCompliance
    action: aws:runCommand
    inputs:
      DocumentName: AWS-RunShellScript
      InstanceIds:
        - '{{ InstanceId }}'
      Parameters:
        commands:
          - |
            PATCH_STATE=$(aws ssm describe-instance-patch-states \
              --instance-ids $(curl -s http://169.254.169.254/latest/meta-data/instance-id) \
              --query 'InstancePatchStates[0].FailedCount' \
              --output text)
            if [ "$PATCH_STATE" != "0" ]; then
              echo "PATCH_VALIDATION_FAILED: FailedCount=$PATCH_STATE"
              exit 1
            fi
            echo "Patch validation passed"
    nextStep: NotifySuccess
    onFailure: step:NotifyFailure

  - name: NotifySuccess
    action: aws:executeAwsApi
    inputs:
      Service: sns
      Api: Publish
      TopicArn: '{{ SNSTopicArn }}'
      Message: 'Patch completed successfully for {{ InstanceId }}'
    isEnd: true

  - name: NotifyFailure
    action: aws:executeAwsApi
    inputs:
      Service: sns
      Api: Publish
      TopicArn: '{{ SNSTopicArn }}'
      Message: 'Patch FAILED for {{ InstanceId }}. Check execution ID: {{ automation:EXECUTION_ID }}'
    isEnd: true

地味だけどこの「通知ステップでautomation:EXECUTION_IDを送る」が超重要なんですよね。深夜に飛んできたアラートから即座に原因を調べられる。これがなかった時代は「あのRunbook失敗したらしいけどどれ?」って探し回ることになってた。実際に深夜2時の件もこれさえあれば30分は早く原因を特定できていたと思う。

パラメータ設計は最初にちゃんとやれ

Runbookのパラメータ設計を後から変えると、呼び出し元のEventBridgeルールやIaCコードも全部変えることになる。最初に多少時間をかけても、型とデフォルト値をきっちり定義しておくべきだ。

parameters:
  InstanceId:
    type: String
    description: 'Target EC2 Instance ID'
    # allowedPattern で不正な値を弾く
    allowedPattern: '^i-[0-9a-f]{8,17}$'
  Environment:
    type: String
    description: 'Deployment environment'
    allowedValues:
      - dev
      - stg
      - prd
    default: dev
  SNSTopicArn:
    type: String
    description: 'SNS Topic ARN for notifications'
    allowedPattern: '^arn:aws:sns:.*'
  DryRun:
    type: Boolean
    description: '本番変更前の確認実行モード'
    default: false

DryRunパラメータは個人的にかなり気に入っている設計で、本番環境で事前確認ができる。allowedPatternでARNの形式チェックをかけておくと、コピペミスによる誤爆を防げる。好みが分かれる部分かもしれないけど、うちのチームでは全RunbookにDryRunを入れることにした。

CDK/Terraformとの組み合わせが本番で効いた

Runbook自体をIaC管理しないと、「誰かがコンソールでポチポチ編集した」状態が生まれる。これが3ヶ月もすると本当に地獄になる。うちはCDKでRunbookのYAMLを管理している。

正直まだ検証中だけど、2026年からSSM AutomationドキュメントのParametersをAWS AppConfig経由で動的に注入する構成も試し始めた。環境ごとにパラメータストアの値が違う場合の管理がかなり楽になった。

CDKでのRunbook定義はこんな感じ。

import * as ssm from 'aws-cdk-lib/aws-ssm';
import * as fs from 'fs';
import * as path from 'path';

export class PatchAutomationStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Runbook本体はYAMLファイルで管理
    const runbookContent = fs.readFileSync(
      path.join(__dirname, '../runbooks/patch-runbook.yaml'),
      'utf8'
    );

    const patchRunbook = new ssm.CfnDocument(this, 'PatchRunbook', {
      name: 'CustomPatchAndValidate',
      documentType: 'Automation',
      documentFormat: 'YAML',
      content: runbookContent,
      // ドキュメントをバージョン管理
      updateMethod: 'NewVersion',
    });

    // EventBridgeでスケジュール実行
    const rule = new events.Rule(this, 'PatchSchedule', {
      schedule: events.Schedule.cron({
        minute: '0',
        hour: '2',
        weekDay: 'SUN',
      }),
    });

    rule.addTarget(
      new targets.SsmAutomation(patchRunbook.ref, {
        role: automationRole,
        input: events.RuleTargetInput.fromObject({
          InstanceId: [{ Ref: 'InstanceId' }],
          Environment: ['prd'],
          SNSTopicArn: [snsTopicArn.valueAsString],
          DryRun: ['false'],
        }),
      })
    );
  }
}

YAMLをコードとして管理するのか、CfnDocumentのcontentとして埋め込むのか、どちらが良いかは正直まだ迷っている。YAMLファイル分離のほうがレビューしやすいので今はこのスタイルにしているが、埋め込みのほうがスタック全体を1ファイルで把握できるというメリットもある。

IaCでの自動化といえば、Terraformで3年分の失敗から学んだ、State管理とAI検証の正解でも書いたけど、自動化基盤のState管理は早めに設計しないと後で泣く。SSM Automationも例外じゃない。

2026年に追加されたSSM Automation新機能をどう使ってるか

この1年でRunbook実行数は約5倍に膨れ上がった。チームへの浸透が進んだ証拠でもあるんだけど、同時に運用の複雑さも正直かなり増えた。

xychart-beta
    title "SSM Automation 実行数推移(月次)"
    x-axis ["2025-07", "2025-09", "2025-11", "2026-01", "2026-03", "2026-05"]
    x-axis-label "月"
    y-axis-label "Runbook実行数"
    bar [120, 180, 240, 310, 430, 580]
    line [120, 180, 240, 310, 430, 580]

EventBridge Pipesとの連携

2025年末ごろから本番導入したのが、EventBridge Pipesを介してSSM AutomationをトリガーするパターンだE。EC2のAMI更新イベントをきっかけに自動でRunbookを走らせる構成で、EventBridge Pipes・Schedulerで本番サーバーレスを回した話でも触れたけど、フィルタリングがめちゃくちゃ細かくできる。これは地味に便利で、以前は「このイベントだけ拾って走らせたい」という細かいユースケースにLambdaを挟んでいたところが、Pipesだけで完結するようになった。

# Lambda(Pipes Enrichment)でRunbookパラメータを動的生成する例
import boto3
import json

def handler(event, context):
    """
    EventBridge Pipesのエンリッチメント処理
    EC2 State Changeイベントからインスタンス情報を取得してRunbookに渡す
    """
    ec2 = boto3.client('ec2')
    ssm = boto3.client('ssm')
    
    # Pipesから渡ってきたイベント
    instance_id = event[0]['detail']['instance-id']
    
    # インスタンスのタグを取得
    response = ec2.describe_instances(InstanceIds=[instance_id])
    tags = response['Reservations'][0]['Instances'][0].get('Tags', [])
    
    env_tag = next(
        (t['Value'] for t in tags if t['Key'] == 'Environment'),
        'dev'
    )
    
    # SSM Parameter Storeから環境別SNS ARNを取得
    param = ssm.get_parameter(
        Name=f'/automation/{env_tag}/sns-topic-arn',
        WithDecryption=True
    )
    sns_topic_arn = param['Parameter']['Value']
    
    # RunbookへのPipes出力形式
    return [
        {
            'InstanceId': instance_id,
            'Environment': env_tag,
            'SNSTopicArn': sns_topic_arn,
            'DryRun': 'false' if env_tag == 'prd' else 'true'
        }
    ]

Automation実行の可観測性をどう担保するか

実行数が増えてきたとき困ったのが、「どのRunbookが今走っていて、何分かかっているか」を一目で見られる仕組みがないことだった。CloudWatch MetricsにAutomationExecutionStatusは出るんだけど、粒度が粗い。

うちは以下の方針で可観測性を確保している。

監視対象手段
実行開始・終了・失敗SNS経由でSlackに通知(NotifyFailureステップ)
実行時間の追跡CloudWatch Logsに構造化ログ出力 → Metric Filterでダッシュボード化
長時間実行の検知EventBridgeでRunning状態が30分以上続いたらアラート

「長時間実行検知」は、タイムアウトして黙って止まるRunbookの検知に地味に効いている。最初に冒頭で書いた深夜2時の件、あれもこの仕組みがあれば事前に気づけていた可能性が高い。インシデント対応のプロセスとして、インシデント対応の最新ベストプラクティス2026も参考になるので見てほしい。

セキュリティ観点では注意点が1つある。SSM Automationを通じて実行されたコマンドは全てCloudTrailに記録されるが、実際のコマンド内容はCloudTrailには出てこない。監査要件がある場合はS3バケットへのセッションログ保存設定が別途必要なので注意したい。IaC基盤のセキュリティ自動検証についてはCDK Aspects・Nag導入で痛感した、自動セキュリティ検証の現実的な話も読んでみてほしい。

マルチターゲット実行とRate Controlの設計

複数インスタンスへの並列実行、皆さんはどうしてます?aws:runCommandTargetsを使えば複数インスタンスを一括指定できるんだけど、全部に一気に当てると当然リスクがある。

  - name: PatchWithRateControl
    action: aws:runCommand
    inputs:
      DocumentName: AWS-RunPatchBaseline
      Targets:
        - Key: tag:PatchGroup
          Values:
            - '{{ PatchGroupName }}'
      Parameters:
        Operation: Install
        RebootOption: RebootIfNeeded
      # 同時実行数を制御
      MaxConcurrency: '20%'    # タグで絞ったインスタンスの20%ずつ実行
      MaxErrors: '10%'         # 10%以上エラーが出たら停止
      TimeoutSeconds: 3600

MaxConcurrencyを絶対値ではなくパーセンテージで指定するのがポイントだ。インスタンス数が変動する環境(ASGやECS on EC2)では絶対値だと意図しない挙動になることがある。

MaxErrorsも同じで、10台中1台失敗したら止まる設定と、100台中10台失敗したら止まる設定では意味が全然違う。うちは最初に絶対値で設定していて、スケールアウト時に止まらなくなった(台数が増えたのでエラー率が下がった)という事故を経験した。これはかなり気づきにくい罠なので注意してほしい。

Rate Controlの実測値

当然速さとリスクのトレードオフだけど、数字で見るとわかりやすい。

xychart-beta
    title "MaxConcurrency別パッチ適用完了時間(50インスタンス対象)"
    x-axis ["10%", "20%", "30%", "50%", "100%"]
    x-axis-label "MaxConcurrency"
    y-axis-label "完了時間(分)"
    bar [95, 52, 38, 25, 18]

本番は20〜30%が落としどころという感覚だ。100%にすると18分で終わるけど、万が一のときの影響範囲が怖すぎる。個人的には「完了時間が2倍になっても、影響範囲が5分の1になるなら十分割に合う」という考え方でいる。

まとめ

1年SSM Automationを本番で使ってきて、痛感した設計のポイントをまとめる。

  • VPCエンドポイントの3点セットは初日に設定する:後から気づくと原因特定に時間がかかる
  • 検証ステップを必ずRunbookに挟むonFailureだけでは「成功に見えた失敗」を検知できない
  • Runbook自体をIaC管理する:コンソール編集が入ると3ヶ月後に誰も把握できなくなる
  • MaxConcurrencyはパーセンテージで指定する:スケーラブルな環境での絶対値指定は罠
  • 実行IDをSNS通知に必ず含める:深夜の障害対応で「どのRunbookか」を素早く特定できる

次のアクションとして取り組んでほしいのは、まず既存のRunbookに「通知ステップ」が入っているか確認すること。入っていない場合はautomation:EXECUTION_IDを含む失敗通知を追加するだけで運用コストが大きく下がる。次にVPCエンドポイントの設定を確認し、最後にRunbookのIaC管理化を検討してほしい。この3つをやるだけで、深夜に呼び出される確率はかなり下がると思う。

正直まだ検証中の部分(AppConfig連携やEventBridge Pipesとの深い統合)もあるので、続きはまた書く予定だ。何かハマった経験があれば教えてほしい。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事