SSM Automationを1年運用して気づいた設計の勘どころ|IaCと組み合わせると何が変わるか
「EC2のパッチ当てツールでしょ」と思ってたSSM Automation。本番運用1年で痛感したRunbook設計の失敗談、IaCとの組み合わせ方、深夜インシデントから抜け出すまでの話。
SSMオートメーション、最初は「EC2のパッチ当てツール」だと思ってた
正直に言うと、Systems Manager(SSM)Automationを使い始めた当初、「まあ、EC2のパッチ適用とかシャットダウンを自動化するやつでしょ」くらいの認識だった。で、実際に運用に組み込んで1年以上経った今、その認識がかなり甘かったと気づいている。
うちのチームでは今年頭から本番環境の運用オペレーション全般をSSM Automationに移行した。きっかけは単純で、深夜インシデント対応の際に「手作業の手順書をSlackに貼りながら複数人で対応する」という地獄から抜け出したかったから。インシデント対応のベストプラクティスでも書かれているように、インシデント時の手作業は人為ミスの温床になる。
ここでは、実際に設計・運用してわかったSSM Automationの勘どころを共有していく。細かいAPIリファレンスより、「ここで詰まった」「この設計にしてよかった」という話を中心に書いていく。
Automation Runbookの設計、最初にここを間違えると後で泣く
ステップの粒度問題
Runbookを設計するとき、「1 Runbook = 1 オペレーション」にしたくなるんだけど、これを貫くと後でメンテナンスが地獄になる。最初に作った「デプロイ前AMIバックアップ」Runbookが400行超えたとき、さすがにやばいと思って設計を見直した。
今のチームでは「Compositeパターン」を採用している。粒度の細かいRunbookを部品として用意して、それをCallAutomationアクションで組み合わせる構成だ。
# 部品:AMIスナップショット取得
description: "Create AMI from instance"
schemaVersion: '0.3'
parameters:
InstanceId:
type: String
ImageName:
type: String
default: 'backup-{{global:DATE}}'
mainSteps:
- name: createImage
action: aws:createImage
inputs:
InstanceId: '{{ InstanceId }}'
ImageName: '{{ ImageName }}'
NoReboot: true
outputs:
- Name: ImageId
Selector: $.ImageId
Type: String
# 親Runbook:デプロイ前処理
schemaVersion: '0.3'
parameters:
InstanceId:
type: String
Environment:
type: String
allowedValues: [prod, staging]
mainSteps:
- name: backupInstance
action: aws:executeAutomation
inputs:
DocumentName: MyOrg-CreateAMIBackup
Parameters:
InstanceId:
- '{{ InstanceId }}'
ImageName:
- 'pre-deploy-{{ global:DATE }}-{{ InstanceId }}'
outputs:
- Name: AmiId
Selector: $.Output
Type: String
- name: runHealthCheck
action: aws:executeAutomation
inputs:
DocumentName: MyOrg-HealthCheck
Parameters:
InstanceId:
- '{{ InstanceId }}'
このパターンにしてから、個別ステップのテストが格段に楽になった。「AMI作成だけ失敗してる」のか「ヘルスチェックで落ちてる」のかが実行履歴ですぐわかるし、Runbook自体の再利用もできる。個人的には、この「部品化」が一番最初にやっておくべき設計判断だったと思っている。
承認ステップの入れ方
本番環境でのオペレーションに承認フローを組み込みたいという要件、よくあると思う。SSM Automationにはaws:approveアクションが用意されているんだけど、タイムアウト設定をちゃんとしないと承認待ちのまま費用が発生し続けるので注意が必要だ。
- name: approveBeforeTerminate
action: aws:approve
timeoutSeconds: 3600 # 1時間でタイムアウト
onFailure: Abort
inputs:
NotificationArn: 'arn:aws:sns:ap-northeast-1:123456789012:ops-approval'
Message: |
本番インスタンス {{ InstanceId }} の停止を承認してください。
実行者: {{ automation:EXECUTION_ID }}
環境: {{ Environment }}
MinRequiredApprovals: 2 # 2名承認必須
Approvers:
- 'arn:aws:iam::123456789012:role/OpsLeadRole'
- 'arn:aws:iam::123456789012:user/taro.yamada'
MinRequiredApprovals: 2 にしておくと「誰か1人が寝ぼけてApproveしてしまった」事故を防げる。プロダクション環境の重要操作には必ずこれを入れるようにしている。導入前に1回だけ「深夜に寝ぼけた承認」をやらかしたことがあって、それ以来この設定は絶対に外さないと決めた。
IaCとの組み合わせ、CDKで管理するのが今の正解
SSM AutomationのドキュメントをAWSコンソールのGUIで作っていた時期もあったんだけど、CDKで管理するようにしてから運用が格段に楽になった。Terraformで3年分の失敗から学んだ話にも通じるけど、IaCで管理しないと「あのRunbook誰が変更したの?」問題が必ず起きる。GUIでポチポチ作ったドキュメントが誰の手を経て今の状態になったか、もはや誰も知らない——という状況、あなたのチームにもありませんか。
CDKでRunbookを管理する
import * as cdk from 'aws-cdk-lib';
import * as ssm from 'aws-cdk-lib/aws-ssm';
import * as fs from 'fs';
import * as path from 'path';
export class AutomationStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// RunbookドキュメントをYAMLファイルから読み込む
const runbookContent = fs.readFileSync(
path.join(__dirname, '../runbooks/pre-deploy-backup.yaml'),
'utf-8'
);
const automationDocument = new ssm.CfnDocument(this, 'PreDeployBackup', {
name: 'MyOrg-PreDeployBackup',
documentType: 'Automation',
documentFormat: 'YAML',
content: runbookContent,
updateMethod: 'NewVersion', // バージョン管理が自動で効く
tags: [
{ key: 'Environment', value: 'prod' },
{ key: 'ManagedBy', value: 'cdk' },
{ key: 'Team', value: 'platform' },
],
});
// EventBridgeでスケジュール実行
const rule = new events.Rule(this, 'WeeklyAmiBackup', {
schedule: events.Schedule.cron({
minute: '0',
hour: '2',
weekDay: 'SUN',
}),
});
rule.addTarget(
new targets.SsmAutomation('MyOrg-PreDeployBackup', {
role: automationRole,
input: events.RuleTargetInput.fromObject({
InstanceId: ['i-0123456789abcdef0'],
Environment: ['prod'],
}),
})
);
}
}
updateMethod: 'NewVersion'を指定しておくと、CDKがデプロイするたびに自動でバージョンが上がる。過去バージョンへのロールバックもaws ssm get-document --name MyOrg-PreDeployBackup --document-version 3で即座にできるので、変更管理がしやすくなった。
マルチアカウント・マルチリージョン構成の実態
うちの本番環境はAWS Organizations配下で6アカウントに分散している。この環境でSSM Automationを使うには、クロスアカウント実行の設計が必要になる。
graph TB
subgraph Management["Management Account"]
EB[EventBridge Scheduler]
CW[CloudWatch Logs]
end
subgraph SharedServices["Shared Services Account"]
SSM_HUB[SSM Automation Hub]
AR[Automation Role - Hub]
SNS[SNS - 承認通知]
end
subgraph ProdAccount["Production Account (ap-northeast-1)"]
subgraph VPC_Prod["VPC: 10.0.0.0/16"]
subgraph AZ1["AZ: ap-northeast-1a"]
EC2_1[EC2: app-server-1]
RDS_1[RDS Primary]
end
subgraph AZ2["AZ: ap-northeast-1c"]
EC2_2[EC2: app-server-2]
RDS_2[RDS Standby]
end
end
AR_PROD[Automation Role - Spoke]
SSM_AGENT1[SSM Agent]
SSM_AGENT2[SSM Agent]
end
subgraph StagingAccount["Staging Account"]
subgraph VPC_STG["VPC: 10.1.0.0/16"]
EC2_STG[EC2: stg-server]
end
AR_STG[Automation Role - Spoke]
end
EB -->|トリガー| SSM_HUB
SSM_HUB -->|AssumeRole| AR_PROD
SSM_HUB -->|AssumeRole| AR_STG
AR_PROD -->|制御| EC2_1
AR_PROD -->|制御| EC2_2
AR_STG -->|制御| EC2_STG
SSM_AGENT1 <-->|SSMエンドポイント| SSM_HUB
SSM_AGENT2 <-->|SSMエンドポイント| SSM_HUB
SSM_HUB -->|承認フロー| SNS
SSM_HUB -->|実行ログ| CW
EC2_1 --- RDS_1
RDS_1 -.->|レプリケーション| RDS_2
この構成のポイントは「Hub(SharedServicesアカウント)でドキュメントを一元管理して、各アカウントのRoleにAssumeRoleする」という設計だ。各アカウントのSpoke Roleには最小権限を付与している。最初はアカウントごとに個別にRunbookを管理しようとしたんだけど、それだと同じドキュメントが微妙に違うバージョンで各アカウントに散在するという状況になって、すぐ諦めてHub集約に切り替えた。
// Spoke アカウント側のIAM Role(CDKで管理)
const spokeRole = new iam.Role(this, 'AutomationSpokeRole', {
roleName: 'SSMAutomationSpokeRole',
assumedBy: new iam.ArnPrincipal(
`arn:aws:iam::${SHARED_SERVICES_ACCOUNT_ID}:role/SSMAutomationHubRole`
),
inlinePolicies: {
SpokePolicy: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
'ec2:DescribeInstances',
'ec2:CreateImage',
'ec2:StartInstances',
'ec2:StopInstances',
'ssm:SendCommand',
'ssm:GetCommandInvocation',
],
resources: ['*'],
conditions: {
StringEquals: {
'aws:RequestedRegion': 'ap-northeast-1',
},
},
}),
],
}),
},
});
リージョン条件(aws:RequestedRegion)を入れておくのがポイントで、意図しないリージョンへの操作を防げる。SOC2審査のCloudTrail・Config設計の観点でも証跡が整理しやすくなるし、監査対応でも「このRoleはap-northeast-1以外は触れません」と即答できるのは地味に心強い。
実際に運用してわかった、地味だけど重要なこと
エラーハンドリングのonFailure設計
各ステップのonFailureをちゃんと設計しないと、失敗時に後続ステップが動いて二次被害が起きる。特にデータ変更を伴うオペレーションでは必須だ。
mainSteps:
- name: stopInstance
action: aws:changeInstanceState
onFailure: step:notifyFailure # 失敗したら通知ステップへジャンプ
inputs:
InstanceIds:
- '{{ InstanceId }}'
DesiredState: stopped
- name: modifyEbsVolume
action: aws:executeAwsApi
onFailure: step:rollbackAndNotify
inputs:
Service: ec2
Api: ModifyVolume
VolumeId: '{{ VolumeId }}'
Size: '{{ NewSize }}'
- name: notifyFailure
action: aws:executeAwsApi
isEnd: true
inputs:
Service: sns
Api: Publish
TopicArn: '{{ NotificationTopicArn }}'
Message: 'Automation failed at stopInstance step. ExecutionId: {{ automation:EXECUTION_ID }}'
- name: rollbackAndNotify
action: aws:executeAutomation
isEnd: true
inputs:
DocumentName: MyOrg-RollbackEBS
Parameters:
VolumeId:
- '{{ VolumeId }}'
正直まだ検証中の部分もあって、ロールバック処理自体が失敗したときの多段エラーハンドリングをどこまで作り込むか悩んでいる。完璧なエラーハンドリングを目指すと無限に複雑になるので、「致命的な操作の前には必ずスナップショット」という原則を守ることで妥協している。これが本当に正解なのかはまだわからないんだけど、今のところ大きな事故は起きていない。
実行結果のモニタリング
実行ログをCloudWatch Logsに集約して、実行ステータスをメトリクスとして可視化している。以下がうちのチームの直近3ヶ月の実行結果だ。
xychart-beta
title "SSM Automation 月別実行結果(2026年Q1)"
x-axis ["1月", "2月", "3月"]
y-axis "実行回数" 0 --> 300
bar [210, 245, 280]
line [18, 12, 8]
棒グラフが総実行数、折れ線が失敗数。1月は18件失敗していたのが、エラーハンドリングを改善するにつれて3月は8件まで下がってきた。実行数自体は増えているのに失敗率が下がっているのは、割と素直に嬉しい。
CloudWatch Logsへの出力はRunbook側で明示的に設定する必要がある。デフォルトでは出力されないので、最初から必ず有効化しておくことを強く勧める。
import boto3
# Automation実行時にCloudWatchログ出力を有効化
client = boto3.client('ssm')
response = client.start_automation_execution(
DocumentName='MyOrg-PreDeployBackup',
Parameters={
'InstanceId': ['i-0123456789abcdef0'],
'Environment': ['prod'],
},
# CloudWatch Logsへの出力設定
CloudWatchOutputConfig={
'CloudWatchLogGroupName': '/aws/ssm/automation/MyOrg-PreDeployBackup',
'CloudWatchOutputEnabled': True,
},
)
print(f"Execution ID: {response['AutomationExecutionId']}")
これを設定しておくと、実行ログをCloudWatch Logs Insightsで横断検索できるようになる。「先週の全Automation実行のうち、EC2停止に関わるものを全部出して」みたいなクエリを後から投げられるのは地味に便利で、障害調査のときに「あの実行、実際には何をやってたんだっけ」がすぐ掘れるのはありがたい。
コストとパフォーマンスの実態
SSM Automationの料金は、API Stepの実行回数ベースになっている。うちの規模感と照らし合わせるとこんな感じだ。
| 実行タイプ | 料金(2026年現在) | うちの月次実績 |
|---|---|---|
| マネージドインスタンス上のAPIステップ | $0.00200/ステップ | 約2,800ステップ/月 |
| クロスアカウント実行の追加コスト | なし | — |
| カスタムRunbook(セルフサービス) | 無料 | — |
| AWS管理ドキュメント | 無料 | — |
月$5〜6程度で収まっている。Lambdaでゴリゴリ自動化スクリプトを書いていた頃と比べると、ランタイム管理コストも含めて圧倒的に安い。「自動化のために自動化の管理コストが増える」というループから抜け出せた感じがある。
イベント駆動での起動パターン
定期実行だけじゃなく、イベント駆動でAutomationを起動するパターンも使っている。例えば「EC2の状態がrunningになった瞬間にAMIを取得する」みたいな用途だ。
{
"source": ["aws.ec2"],
"detail-type": ["EC2 Instance State-change Notification"],
"detail": {
"state": ["running"],
"instance-id": ["i-0123456789abcdef0"]
}
}
EventBridgeのルールでこのパターンを拾って、Automationを起動する。イベント駆動アーキテクチャの実装と組み合わせると、インフラ操作もイベント駆動で繋げられるのが面白いところだ。「インスタンスが起動した瞬間に自動でバックアップが走る」という挙動は、初めて動かしたとき少し感動した。
まとめ
SSM Automationを本番環境で1年以上運用してきた知見をまとめると、こんな感じになる。
| 教訓 | 要点 |
|---|---|
| Runbookは部品化して組み合わせる | 1つに詰め込むと後でメンテが辛い |
| IaC(CDK/Terraform)で管理する | バージョン管理と変更履歴が追えることが重要 |
onFailureと承認ステップは最初に設計する | 後から追加するのは思ったより手間がかかる |
| マルチアカウントはHub集約構成にする | リージョン条件もIAMに入れておく |
| CloudWatch Logsへの出力は必須にする | 後から「あの実行どうなったっけ」を調べられる環境が運用を楽にする |
次のアクション: まずは既存の手作業手順書を1つ選んで、SSM Automation化してみるのがおすすめだ。完璧に自動化しようとせず、「承認ステップで人間が確認する」部分を残すところから始めると、チームの不安も少なく導入できる。皆さんの環境ではどんなオペレーションを自動化していますか?特にパッチ適用以外の用途で面白い使い方があればぜひ聞きたい。