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化してみるのがおすすめだ。完璧に自動化しようとせず、「承認ステップで人間が確認する」部分を残すところから始めると、チームの不安も少なく導入できる。皆さんの環境ではどんなオペレーションを自動化していますか?特にパッチ適用以外の用途で面白い使い方があればぜひ聞きたい。

U

Untanbaby

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

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

関連記事