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.ssm | SSM API |
| com.amazonaws.ap-northeast-1.ssmmessages | Session Manager通信 |
| com.amazonaws.ap-northeast-1.ec2messages | Run 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:runCommandのTargetsを使えば複数インスタンスを一括指定できるんだけど、全部に一気に当てると当然リスクがある。
- 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との深い統合)もあるので、続きはまた書く予定だ。何かハマった経験があれば教えてほしい。