SSM Automation、1年本番運用して気づいた設計の勘どころ

「後でやる」を繰り返した手作業オペミスをSSM Automationで潰すことになって1年。Runbook設計・IAM・IaC連携で実際にハマった失敗談と、そこから得た実践知見を正直に書きます。

SSM Automationと向き合うことになったきっかけ

うちのチームがSystems Manager Automation(以下、SSM Automation)を本格的に使い始めたのは、インシデント対応の自動化がきっかけだった。2024年末ごろ、EC2の定期パッチ適用が手作業で行われていて、オペミスが続発していた。「Runbookを書けばよい」という話が出るたびに誰かが「後でやる」とSlackに流して、そのまま忘れる……という負のループ。

転機はインシデント対応の改善に取り組んでいた時期と重なって、「手作業インシデントをシステムで潰そう」という機運が高まったこと。その流れでSSM Automationを本気で設計し直すことになった。今から振り返ると、最初の設計はかなり甘かったと思う。

1年間本番で運用してわかった設計の勘どころを、失敗談も交えて書く。正直まだ改善中の部分もあるけど、参考になれば。


SSM Automationの基本アーキテクチャと2026年時点の機能

まず、現時点の構成を整理しておく。2026年現在、SSM Automationは単なるRunbook実行エンジンにとどまらず、EventBridgeトリガー・OpsCenter統合・Change Calendarとの連携など、かなり「運用基盤」としての機能が充実してきた。

graph TB
  subgraph Control["管理アカウント"]
    EB[EventBridge Scheduler]
    CW[CloudWatch Alarms]
    CC[Change Calendar]
    OC[OpsCenter]
  end

  subgraph SSM["SSM Automation基盤"]
    direction TB
    DOC[Automation Document\n(Runbook)]
    EXE[Automation Execution]
    LOG[Execution Logs]
  end

  subgraph TargetVPC["対象VPC (AZ-a / AZ-c)"]
    subgraph AZa["AZ-a"]
      EC2a[EC2 Instance\n(Private Subnet)]
    end
    subgraph AZc["AZ-c"]
      EC2c[EC2 Instance\n(Private Subnet)]
    end
    RDS[(RDS Aurora\nCluster)]
  end

  subgraph Endpoints["VPCエンドポイント"]
    EP_SSM[com.amazonaws.\nssm]
    EP_EC2MSG[com.amazonaws.\nec2messages]
    EP_SSMMSG[com.amazonaws.\nssmmessages]
  end

  subgraph Logging["ログ・監査"]
    S3L[S3 Bucket\n(Execution Logs)]
    CWL[CloudWatch Logs]
    CT[CloudTrail]
  end

  EB -->|スケジュール起動| EXE
  CW -->|アラーム起動| EXE
  CC -->|メンテナンスウィンドウ確認| EXE
  OC -->|OpsItem連携| EXE
  EXE --> DOC
  DOC --> EC2a
  DOC --> EC2c
  DOC --> RDS
  EC2a <--> EP_SSM
  EC2c <--> EP_SSM
  EP_SSM <--> Endpoints
  EXE --> LOG
  LOG --> S3L
  LOG --> CWL
  EXE --> CT

この構成で痛感したのが、VPCエンドポイントの設定漏れが一番多いトラブル原因ということ。Privateサブネットにいるインスタンスに対してSSM経由で操作しようとして「エージェントが応答しない」状態になったことが何度かある。com.amazonaws.region.ssmec2messagesssmmessages の3つが揃っていないと動かないので、Checklistに入れておくのを強くすすめる。


Runbook設計で失敗した話と、その後の改善パターン

最初期に作ったRunbookを今見ると、正直しんどくなる。すべての処理を1つのDocumentに詰め込んで、エラーハンドリングも「失敗したら終了」くらいしか考えていなかった。

やらかしパターン1: モノリシックRunbook地獄

最初に作った「EC2パッチ適用Runbook」は1ファイルに20ステップ以上入れていた。途中でコケると最初からやり直しになる仕様だったため、14ステップ目で失敗するたびに13ステップ分の処理が無駄になる。これが週に何度も起きるので、地味にじわじわ辛かった。

改善後は単機能Runbookの組み合わせに切り替えた。

# runbook-patch-ec2.yml (CDKで生成するSSM Document)
description: 'EC2 Patch Application Orchestrator'
schemaVersion: '0.3'
parameters:
  InstanceIds:
    type: StringList
    description: 'Target EC2 instance IDs'
  SnapshotBeforePatch:
    type: String
    default: 'true'
    allowedValues:
      - 'true'
      - 'false'
mainSteps:
  - name: checkChangeCalendar
    action: aws:assertAwsResourceProperty
    onFailure: Abort
    inputs:
      Service: ssm
      Api: GetCalendarState
      CalendarNames:
        - 'arn:aws:ssm:ap-northeast-1:123456789012:document/MaintenanceCalendar'
      PropertySelector: '$.State'
      DesiredValues:
        - OPEN

  - name: createSnapshotIfRequired
    action: aws:executeAutomation
    onFailure: Abort
    inputs:
      DocumentName: 'MyOrg-CreateEBSSnapshot'
      Parameters:
        InstanceIds: '{{ InstanceIds }}'
    nextStep: runPatchBaseline

  - name: runPatchBaseline
    action: aws:runCommand
    onFailure: step:notifyFailure
    inputs:
      DocumentName: AWS-RunPatchBaseline
      InstanceIds: '{{ InstanceIds }}'
      Parameters:
        Operation: Install
        RebootOption: RebootIfNeeded

  - name: verifyPatchCompliance
    action: aws:executeAutomation
    inputs:
      DocumentName: 'MyOrg-VerifyPatchCompliance'
      Parameters:
        InstanceIds: '{{ InstanceIds }}'

  - name: notifyFailure
    action: aws:executeAwsApi
    inputs:
      Service: sns
      Api: Publish
      TopicArn: 'arn:aws:sns:ap-northeast-1:123456789012:ops-alert'
      Message: 'Patch automation failed. Check execution: {{ automation:EXECUTION_ID }}'
    isEnd: true

onFailure: step:notifyFailure でエラー時のフォールバックステップを指定できるのが地味に便利で、これを知らずに最初の半年を過ごしていたのが本当に悔やまれる。ドキュメントのどこかに書いてあったんだろうけど、見落としていた。

やらかしパターン2: IAMロールの設計が甘すぎた

Runbookに使うIAMロールに最初「AdministratorAccess」を雑につけていた。さすがにひどい。CDK NagやAspectsでセキュリティ検証を導入したときに全部指摘されて、穴があったら入りたい気持ちだった。

改善後のIAMロール設計はこんな感じ:

// CDK でRunbook用IAMロールを定義
const automationRole = new iam.Role(this, 'SSMAutomationRole', {
  assumedBy: new iam.ServicePrincipal('ssm.amazonaws.com'),
  description: 'Role for SSM Automation Runbooks',
  roleName: 'SSMAutomationExecutionRole',
});

// 最小権限原則: Patchに必要なものだけ
automationRole.addToPolicy(new iam.PolicyStatement({
  sid: 'AllowEC2Operations',
  actions: [
    'ec2:CreateSnapshot',
    'ec2:DescribeInstances',
    'ec2:DescribeSnapshots',
  ],
  resources: ['*'],
  conditions: {
    StringEquals: {
      'aws:RequestedRegion': 'ap-northeast-1',
    },
  },
}));

automationRole.addToPolicy(new iam.PolicyStatement({
  sid: 'AllowSSMRunCommand',
  actions: [
    'ssm:SendCommand',
    'ssm:GetCommandInvocation',
    'ssm:ListCommandInvocations',
  ],
  resources: [
    `arn:aws:ec2:*:${this.account}:instance/*`,
    'arn:aws:ssm:*:*:document/AWS-RunPatchBaseline',
    `arn:aws:ssm:*:${this.account}:document/MyOrg-*`,
  ],
}));

automationRole.addToPolicy(new iam.PolicyStatement({
  sid: 'AllowSNSPublish',
  actions: ['sns:Publish'],
  resources: [`arn:aws:sns:ap-northeast-1:${this.account}:ops-alert`],
}));

aws:RequestedRegion のConditionはAWS Organizationsと組み合わせると特に有効で、意図しないリージョンへの操作を防げる。Organizations SCPとの組み合わせ方はこちらの記事も参考になるかもしれない。


IaCとの組み合わせで変わったこと

SSM Automationのドキュメントをマネジメントコンソールで手作業で作っていた時期があって、あの頃は本当に「再現性がない」「誰が何を変えたかわからない」状態だった。CDKでDocument自体をコード管理するようにしてから、だいぶ改善した。

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

// RunbookをYAMLファイルとして管理し、CDKで登録
const patchRunbookContent = yaml.dump(
  yaml.load(fs.readFileSync('./runbooks/patch-ec2.yml', 'utf8'))
);

const patchRunbook = new ssm.CfnDocument(this, 'PatchRunbook', {
  name: 'MyOrg-PatchEC2Instances',
  documentType: 'Automation',
  documentFormat: 'YAML',
  content: patchRunbookContent,
  updateMethod: 'NewVersion', // 大事。Replace だとダウンタイムが生じることがある
  tags: [
    { key: 'ManagedBy', value: 'CDK' },
    { key: 'Team', value: 'Platform' },
    { key: 'Environment', value: 'Production' },
  ],
});

// EventBridge SchedulerでRunbookを定期実行
const schedulerRole = new iam.Role(this, 'SchedulerRole', {
  assumedBy: new iam.ServicePrincipal('scheduler.amazonaws.com'),
});

schedulerRole.addToPolicy(new iam.PolicyStatement({
  actions: ['ssm:StartAutomationExecution'],
  resources: [
    `arn:aws:ssm:ap-northeast-1:${this.account}:automation-definition/MyOrg-PatchEC2Instances:*`,
  ],
}));

const patchSchedule = new scheduler.CfnSchedule(this, 'PatchSchedule', {
  name: 'weekly-patch-schedule',
  scheduleExpression: 'cron(0 2 ? * SUN *)', // 毎週日曜2時
  flexibleTimeWindow: { mode: 'FLEXIBLE', maximumWindowInMinutes: 30 },
  target: {
    arn: 'arn:aws:ssm:ap-northeast-1::automation-definition/MyOrg-PatchEC2Instances',
    roleArn: schedulerRole.roleArn,
    input: JSON.stringify({
      InstanceIds: ['i-xxxxxxxxxxxx', 'i-yyyyyyyyyyyy'],
      SnapshotBeforePatch: 'true',
    }),
  },
});

updateMethod: 'NewVersion' の設定は地味に重要で、ReplaceにするとRunbook実行中にデプロイが走ったとき実行が中断されることがある。これも最初は知らなくてやらかした。

実行結果のログをS3に落として、Athenaでクエリできるようにもしている。

-- Athena: 過去30日間のAutomation実行失敗率を確認
SELECT
  date_trunc('day', from_iso8601_timestamp(executionStartTime)) AS execution_date,
  documentName,
  COUNT(*) AS total_executions,
  SUM(CASE WHEN status = 'Failed' THEN 1 ELSE 0 END) AS failed_count,
  ROUND(
    100.0 * SUM(CASE WHEN status = 'Failed' THEN 1 ELSE 0 END) / COUNT(*),
    2
  ) AS failure_rate_pct
FROM ssm_automation_logs
WHERE
  from_iso8601_timestamp(executionStartTime) >= current_date - interval '30' day
GROUP BY 1, 2
ORDER BY 1 DESC, failure_rate_pct DESC;

このクエリで月次レビューするようにしたら、特定のRunbookが週に2〜3回コケていることが発見されて、原因追跡につながった。地味だけど、個人的にはこのダッシュボードが一番費用対効果高かった。


1年運用してわかったベストプラクティスと、正直まだ悩んでいること

実測値:導入前後の比較

数字で見ると劇的に見えるけど、正直最初の3ヶ月は導入コストが高くて「本当に良くなってるのか?」と疑問に思っていた時期もある。Runbook設計を何度も書き直したり、IAMトラブルシューティングで丸1日潰したこともあった。それでも1年続けたら、こうなった。

xychart-beta
  title "SSM Automation導入前後の比較(月次平均)"
  x-axis ["手作業工数(h)", "インシデント件数", "パッチ適用失敗数", "対応時間(h)"]
  y-axis "値" 0 --> 80
  bar [68, 12, 8, 45]
  bar [14, 3, 1, 8]

※ 左が導入前、右が導入後(約1年後の平均値)

設計原則として落ち着いたもの

1. Runbookは単機能に保つ 1つのDocumentは1つの責務。パッチ適用、スナップショット作成、コンプライアンス確認はそれぞれ別Runbook。aws:executeAutomationでオーケストレーションする。最初はまとめたくなるけど、後悔するので我慢。

2. Change Calendarを必ず確認ステップに入れる 本番環境へのRunbook実行は必ずChange Calendarの状態確認ステップを先頭に置く。「誰かが手動でRunbookをトリガーした結果、リリースフリーズ期間中にパッチが当たった」という事故が1回あって、それ以来徹底している。

3. 実行ロールはRunbookごとに分ける 最初は全RunbookをまとめたIAMロールで済ませていたけど、最小権限の観点から1Runbook=1Roleに変えた。CDKでgrantXxx()メソッドを使うと楽に書ける。

4. 実行結果は必ずS3 + CloudWatch Logsに二重保存 SSM Automationのコンソールは過去90日分しか実行履歴が残らない。監査目的でS3とCloudWatch Logsの両方に出力する設定を入れると安心できる。

ツール比較:SSM Automation vs 他のアプローチ

どのツールを選ぶかで結構迷ったので、整理しておく。

項目SSM AutomationAWS LambdaEventBridge + Step Functions
EC2操作の容易さ△(SSM経由が必要)
マネコン視認性
実行履歴の管理○(90日)△(CloudWatch依存)
パラメータ管理◎(Parameter Store連携)
学習コスト△(高め)
マルチアカウント実行◎(built-in)
IaC管理のしやすさ○(CDK対応)

EC2の運用自動化においてはSSM Automationの強みは明確で、特にマルチアカウント実行がネイティブサポートされているのはかなり大きい。逆に複雑なデータ処理や条件分岐が多い場合はStep Functionsの方が向いているかもしれない。ここは正直、好みと用途次第だと思う。

まだ悩んでいること

正直まだ検証中なんだけど、マルチリージョンRunbookの実行順序制御が課題になってきた。東京リージョンと大阪リージョンで並列実行すると、ログの突き合わせが面倒で、今は手動でクエリを書いて確認している。EventBridge Pipesでうまく連携できないか試しているところ。

あとはTerraformとの共存も議論になっている。うちのチームはメインのインフラはTerraform、SSM DocumentはCDK、という微妙に分かれた構成で、統一されていないのが個人的にずっと気持ち悪い。皆さんのチームはどうしてます?


まとめ

1年間SSM Automationを本番で使い続けて、特に効果があったのはこの5点だった:

  1. RunbookはYAMLでGit管理・CDKでデプロイ — 「誰が何を変えたかわからない」問題が消えた
  2. 単機能Runbook + オーケストレーション分離 — 再利用性が上がり、デバッグが劇的に楽になった
  3. IAMロールはRunbookごとに最小権限で — セキュリティ審査で指摘されなくなった
  4. Change Calendarをガード条件に — リリースフリーズ中の誤操作がゼロになった
  5. 実行ログのS3 + Athena連携 — 運用問題の早期発見ができるようになった

まだ手作業でパッチ適用をやっているチームには、まず AWS-RunPatchBaseline を使った小さなRunbookを1つ作って、EventBridgeスケジューラーでトリガーするところから始めるのをおすすめしたい。IaC管理は後からでもキャッチアップできるので、「動くRunbookを1本作る」ことを優先した方がモチベーションが続く。

SSM Automationは地味だけど、運用自動化の核になるサービスだと思っている。まだ使ったことない方は、ぜひ試してみてほしい。

U

Untanbaby

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

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

関連記事