SSM AutomationをIaC連携で設計し直したら深夜対応がなくなった話

深夜2時のEC2障害対応を機にSSM Automationを本格設計し直した実録。CDKとのRunbook構造化で1年運用してわかった、同じ失敗を繰り返さないための勘どころ。

SSM Automationを本番設計し直した話|IaC連携とRunbook構造化で運用が変わった2026年版

去年の夏、深夜2時に「EC2が応答しない」という連絡が来て、SSHセッションを繋ぎながらドキュメントを漁って手作業で対応した経験がある。その翌日に「もう同じことを手でやりたくない」と思って、Systems Manager Automationを本格的に設計し直した。あれから1年以上経って、実際に運用してわかった勘どころを書いておく。

正直、SSM Automationって「便利そうだけどとっつきにくい」という印象が最初はあった。AWS Systems Manager自体の概念が広すぎて、どこから手をつければいいかわからない。でもIaCと組み合わせ始めたら、急にパズルのピースが合った感じがした。

似たような観点で過去にSSM Automationを1年本番運用して気づいた設計の勘どころも書いたけど、今回はより設計レベルを掘り下げてIaC連携にフォーカスする。

SSM Automationを使う前に整理すべきこと

SSM Automationを導入するとき、最初に「どのRunbookを使うか」から考え始めて失敗した。本当は「何を自動化したいか」の整理が先で、そこから逆算してRunbookを設計するべきだった。

うちのチームでは最終的に以下の3つのカテゴリに分類して整理した。

カテゴリ具体例実行頻度承認フロー
定期メンテナンスOSパッチ適用、バックアップ検証週次・月次不要(自動)
インシデント対応EC2再起動、EBSスナップショット取得随時条件付き承認
デプロイ支援AMI焼き直し、設定ファイル配布リリース時要承認

この分類があると、Runbookの設計方針が一気にクリアになる。特に「承認フロー」の有無は、Runbook内のaws:approveステップをどこに入れるかに直結するので、最初に決めておくのが大事だった。

IaC(CDK)でAutomation Documentを管理する

最初はコンソールからポチポチDocumentを作ってたんだけど、これが地獄だった。変更履歴が追えない、環境間で差分が出る、誰がいつ変更したかわからない。3ヶ月でギブアップして全部CDKで管理することにした。

// lib/ssm-automation-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as ssm from 'aws-cdk-lib/aws-ssm';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';

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

    // Automation実行用のIAMロール
    const automationRole = new iam.Role(this, 'AutomationExecutionRole', {
      assumedBy: new iam.ServicePrincipal('ssm.amazonaws.com'),
      roleName: 'SSMAutomationExecutionRole',
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName(
          'AmazonSSMAutomationRole'
        ),
      ],
    });

    // EC2再起動RunbookをCDKで管理
    const ec2RestartDocument = new ssm.CfnDocument(this, 'EC2RestartRunbook', {
      name: 'Custom-EC2SafeRestart',
      documentType: 'Automation',
      documentFormat: 'YAML',
      content: {
        schemaVersion: '0.3',
        description: 'EC2インスタンスを安全に再起動するRunbook',
        assumeRole: automationRole.roleArn,
        parameters: {
          InstanceId: {
            type: 'String',
            description: '再起動対象のEC2インスタンスID',
          },
          ApproverArn: {
            type: 'String',
            description: '承認者のARN(SNSトピックまたはIAM ARN)',
          },
        },
        mainSteps: [
          {
            name: 'createSnapshot',
            action: 'aws:createImage',
            inputs: {
              InstanceId: '{{ InstanceId }}',
              ImageName: 'pre-restart-backup-{{ global:DATE_TIME }}',
              NoReboot: true,
            },
          },
          {
            name: 'requestApproval',
            action: 'aws:approve',
            timeoutSeconds: 3600,
            inputs: {
              NotificationArn: '{{ ApproverArn }}',
              Message: '{{ InstanceId }} の再起動を承認してください。スナップショット取得済み。',
              MinRequiredApprovals: 1,
              Approvers: ['{{ ApproverArn }}'],
            },
          },
          {
            name: 'stopInstance',
            action: 'aws:changeInstanceState',
            inputs: {
              InstanceIds: ['{{ InstanceId }}'],
              DesiredState: 'stopped',
            },
          },
          {
            name: 'startInstance',
            action: 'aws:changeInstanceState',
            inputs: {
              InstanceIds: ['{{ InstanceId }}'],
              DesiredState: 'running',
            },
          },
          {
            name: 'verifyInstanceRunning',
            action: 'aws:waitForAwsResourceProperty',
            inputs: {
              Service: 'ec2',
              Api: 'DescribeInstanceStatus',
              InstanceIds: ['{{ InstanceId }}'],
              PropertySelector: '$.InstanceStatuses[0].InstanceState.Name',
              DesiredValues: ['running'],
            },
          },
        ],
      },
    });

    // EventBridgeからの自動トリガー(CloudWatch Alarmと連携)
    new cdk.CfnOutput(this, 'DocumentName', {
      value: ec2RestartDocument.name || 'Custom-EC2SafeRestart',
    });
  }
}

これで変更がGitHubで追跡できるようになって、コードレビューも通るようになった。地味に便利なのは、CDK DiffでDocument内容の変更がテキスト差分として見えること。コンソール時代は「なんか動きが変わった気がするけど誰が何をしたかわからない」という状態が普通にあったので、これだけでも移行した甲斐があったと思う。

CDKでのインフラセキュリティ検証についてはCDK Aspects・Nag導入で痛感した、自動セキュリティ検証の現実的な話で詳しく書いているので、合わせて読んでもらえると。

Runbook設計で絶対やっておくべき3つのこと

1年運用してみて「最初からやっておけばよかった」と思う設計パターンが3つある。順番に見ていく。

1. エラーハンドリングを全ステップに入れる

SSM Automationのデフォルトは、ステップが失敗すると即座に全体が失敗になる。本番で困るのは「どこで失敗したのかわからない」状態だ。onFailureを明示的に設定することで、エラー時のロールバック処理を挟める。

mainSteps:
  - name: applyPatch
    action: aws:runCommand
    onFailure: step:rollbackPatch  # 失敗時にロールバックへ
    inputs:
      DocumentName: AWS-RunShellScript
      InstanceIds:
        - '{{ InstanceId }}'
      Parameters:
        commands:
          - 'sudo yum update -y --security'
  
  - name: verifyPatch
    action: aws:runCommand
    onFailure: step:rollbackPatch
    inputs:
      DocumentName: AWS-RunShellScript
      InstanceIds:
        - '{{ InstanceId }}'
      Parameters:
        commands:
          - 'rpm -qa --last | head -20'
          - 'systemctl is-active myapp'
  
  - name: patchSuccess
    action: aws:sleep
    inputs:
      Duration: PT1S
    isEnd: true
  
  - name: rollbackPatch
    action: aws:runCommand
    inputs:
      DocumentName: AWS-RunShellScript
      InstanceIds:
        - '{{ InstanceId }}'
      Parameters:
        commands:
          - 'sudo yum history undo last -y'
    isEnd: true

2. Rate ControlとConcurrencyを忘れない

複数EC2インスタンスに対してRunbookを走らせるとき、Rate Controlを設定しないと全インスタンスに同時に実行されて本番が死ぬ。これは実際にやらかした経験がある。冷や汗もんだった。

# AWS CLIでAutomation実行(Rate Control付き)
aws ssm start-automation-execution \
  --document-name "Custom-EC2SafeRestart" \
  --target-parameter-name "InstanceId" \
  --targets '[{"Key":"tag:Environment","Values":["production"]}]' \
  --max-concurrency "10%" \
  --max-errors "5%" \
  --parameters '{"ApproverArn":["arn:aws:sns:ap-northeast-1:123456789012:ops-approval"]}'

max-concurrency"10%"にすると、対象インスタンス総数の10%ずつ処理される。エラーがmax-errorsを超えると全体が停止する。この設定は本番環境では必須だと思う。個人的には「なぜデフォルトが無制限なんだ」と最初は思ったけど、まあ設計の自由度優先ってことなんだろうな。

3. 実行ログをCloudWatch Logsに集約する

デフォルトだとSSMの実行ログはコンソールからしか見えない。CloudWatch Logsに流しておくと、DatadogやGrafanaで可視化できるし、アラートも設定できる。

// CDKでAutomation実行時のログ設定
const logGroup = new logs.LogGroup(this, 'AutomationLogGroup', {
  logGroupName: '/aws/ssm/automation',
  retention: logs.RetentionDays.THREE_MONTHS,
  removalPolicy: cdk.RemovalPolicy.DESTROY,
});

// Automation実行ロールにCloudWatch Logs書き込み権限を付与
automationRole.addToPolicy(new iam.PolicyStatement({
  actions: [
    'logs:CreateLogGroup',
    'logs:CreateLogStream',
    'logs:PutLogEvents',
    'logs:DescribeLogGroups',
    'logs:DescribeLogStreams',
  ],
  resources: [logGroup.logGroupArn],
}));

これが地味に効いていて、「なんかRunbookが失敗したらしい」という通報が来たときに、CloudWatch Logsを開けばステップ単位で何が起きたか追えるようになった。コンソールのSSM画面だけだと検索性がつらい。

実際のアーキテクチャ構成

うちのチームで本番稼働しているSSM Automation基盤の構成図を載せておく。

graph TB
    subgraph Management["Management Account"]
        EB[EventBridge]
        SNS[SNS Topic\n承認通知]
        CW[CloudWatch Logs]
    end

    subgraph WorkloadVPC["Workload Account - VPC (10.0.0.0/16)"]
        subgraph AZ_A["AZ-a"]
            subgraph PrivSubA["Private Subnet (10.0.1.0/24)"]
                EC2A[EC2 Instance\nProd-App-01]
                EC2B[EC2 Instance\nProd-App-02]
            end
        end
        subgraph AZ_B["AZ-b"]
            subgraph PrivSubB["Private Subnet (10.0.2.0/24)"]
                EC2C[EC2 Instance\nProd-App-03]
                EC2D[EC2 Instance\nProd-App-04]
            end
        end
        SSME[SSM Endpoint\nVPCエンドポイント]
    end

    subgraph AutomationLayer["Automation Layer"]
        SSM_AUTO[SSM Automation\nRunbook Executor]
        PARAM[Parameter Store\n設定値管理]
        DOC[Automation Document\nCustom Runbooks]
    end

    subgraph IaC["IaC Pipeline"]
        GH[GitHub\nRunbook定義]
        CDK[CDK Pipelines\nデプロイ自動化]
    end

    GH -->|PR & Deploy| CDK
    CDK -->|Document作成・更新| DOC
    EB -->|スケジュール・アラーム| SSM_AUTO
    SSM_AUTO -->|実行| DOC
    SSM_AUTO -->|承認要求| SNS
    SNS -->|通知| OPS[Opsチーム]
    OPS -->|承認/拒否| SSM_AUTO
    SSM_AUTO -->|設定取得| PARAM
    SSM_AUTO -->|実行ログ| CW
    SSME -->|SSM通信| EC2A
    SSME -->|SSM通信| EC2B
    SSME -->|SSM通信| EC2C
    SSME -->|SSM通信| EC2D
    SSM_AUTO -->|コマンド送信| SSME

ポイントはいくつかある。まずEC2へのSSH接続を一切開けていない。Session ManagerとAutomationで全てのオペレーションが完結するように設計した。インシデント対応についてもインシデント対応の最新ベストプラクティス2026で書いた「本番SSHレスの運用」に近い考え方で、SSM経由の操作ログが全て残るので監査対応も楽になった。

VPCエンドポイントを使ってインターネット経由ではなくAWSネットワーク内で通信しているのも重要で、セキュリティグループでSSMエンドポイントへの443番のみを許可する最小権限構成にしている。

実際の運用で効果が出た数値

1年間の本番運用で、手作業対応がどう変化したかをまとめておく。

xychart-beta
    title "月次手作業対応時間の変化(時間)"
    x-axis ["2025-07", "2025-08", "2025-09", "2025-10", "2025-11", "2025-12", "2026-01", "2026-02", "2026-03", "2026-04", "2026-05", "2026-06"]
    y-axis "時間" 0 --> 80
    bar [68, 72, 45, 38, 30, 22, 18, 15, 12, 10, 9, 8]
    line [68, 72, 45, 38, 30, 22, 18, 15, 12, 10, 9, 8]

SSM Automation本格導入前(2025年7〜8月)は月約70時間が手作業対応に消えていた。段階的にRunbookを整備して、2026年6月時点では月8時間まで落ちた。正直もうちょっと劇的な削減を期待してたんだけど、「急ぎで手動対応しちゃった」という案件が地道に残り続けるのが現実ではある。

ここは好みが分かれるかもしれないけど、「100%自動化」より「80%自動化して残り20%は人間が判断する」のほうが安全だと思ってる。緊急時に自動化の枠組みに縛られて余計な時間を食うくらいなら、さっさと手で動かして後でRunbookに落とすほうが現実的だ。

EventBridgeとの組み合わせで定期自動化

定期メンテナンスのRunbookはEventBridgeのスケジュールと組み合わせると強力だった。

// 毎週日曜日深夜2時にパッチ適用を自動実行
const patchScheduleRule = new events.Rule(this, 'WeeklyPatchRule', {
  schedule: events.Schedule.cron({
    minute: '0',
    hour: '17', // UTC 17:00 = JST 02:00
    weekDay: 'SUN',
  }),
  description: '週次OSパッチ適用自動化',
});

// EventBridgeからSSM Automationを呼び出すターゲット
const ssmTarget = new targets.AwsApi({
  service: 'SSM',
  action: 'startAutomationExecution',
  parameters: {
    DocumentName: 'Custom-WeeklyOSPatch',
    TargetParameterName: 'InstanceId',
    Targets: [
      {
        Key: 'tag:PatchGroup',
        Values: ['sunday-maintenance'],
      },
    ],
    MaxConcurrency: '20%',
    MaxErrors: '10%',
  },
  policyStatement: new iam.PolicyStatement({
    actions: ['ssm:StartAutomationExecution'],
    resources: ['*'],
  }),
});

patchScheduleRule.addTarget(ssmTarget);

このパターン、最初は「EventBridgeからSSM Automationを直接呼び出せるの?」と半信半疑だったんだけど、targets.AwsApiを使えば任意のAWS APIを呼び出せる。2026年現在このアプローチが一番シンプルで、Lambda関数を挟む必要がないのがいい。

ただし注意点として、EventBridgeのターゲットに設定したIAMロールにSSM StartAutomationExecution権限が必要なのと、Automation側でassumeRoleとして別のロールを指定している場合はそちらへのPassRole権限も必要になる。ここで1時間ハマったので書いておく。

Parameter Storeとの連携で環境差分を吸収する

Runbook内でハードコーディングを避けるために、Parameter Storeを使って環境固有の値を外出しにしている。

# Runbook内でParameter Store参照
parameters:
  Environment:
    type: String
    allowedValues:
      - staging
      - production
mainSteps:
  - name: getConfig
    action: aws:executeAwsApi
    inputs:
      Service: ssm
      Api: GetParameter
      Name: '/automation/{{ Environment }}/approval-sns-arn'
      WithDecryption: true
    outputs:
      - Name: ApprovalArn
        Selector: $.Parameter.Value
        Type: String
  
  - name: requestApproval
    action: aws:approve
    inputs:
      NotificationArn: '{{ getConfig.ApprovalArn }}'
      Message: '{{ Environment }}環境でのオペレーション承認要求'
      MinRequiredApprovals: 1

これでRunbook自体はステージングと本番で同一のDocumentを使いまわせる。CDKでParameter Storeの値も管理しているので、環境ごとの設定変更もコードレビューが通る。メンテナンスコストが体感で半分くらいになった。

正直まだ検証中だけど、SecureString型のパラメータをRunbook内で参照する際の権限周りが少し複雑で、IAMロールの設計が完璧じゃないかもしれない。同じパターンを使ってるチームがあればどう対処してるか聞いてみたいところ。

まとめ

SSM Automationの設計で特に効いた知見を整理すると、以下の5点に集約できる。

  1. Runbookは必ずIaC(CDK/Terraform)で管理する — コンソール手作業でのDocument管理は変更追跡が不可能になる。GitHubでの差分レビューが品質を担保してくれる

  2. 承認フローとエラーハンドリングは設計フェーズで決めるaws:approveステップの位置とonFailureのロールバック設計は、後から変えると難しい。最初に決めておくことが大事

  3. Rate ControlとMax Errorsは絶対設定する — 特に本番マルチEC2環境では、これがないと「全台同時に処理して死亡」というパターンが発生する

  4. VPCエンドポイント経由でSSH廃止を目指す — Session Manager + Automationの組み合わせで、SSH開放なしの運用が2026年現在は現実的に可能

  5. Parameter StoreでRunbookの環境差分を吸収する — ステージングと本番でDocumentを共有できると、メンテナンスコストが大幅に下がる

次のアクションとして、まずは一番手作業が多い運用タスク(うちの場合はOSパッチ適用だった)から1つだけRunbookを作ってみるのがおすすめだ。完璧な設計を目指すより「1つ動くものを作る」が早い。EventBridgeとの連携まで通して動いた瞬間、「あー、これが自動化か」という感覚が得られると思う。

U

Untanbaby

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

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

関連記事