Opus 4.6 をサポートした Kiro で、Step Functions + JSONata で Lambda レスなディスク監視を実装してみた

Opus 4.6 をサポートした Kiro で、Step Functions + JSONata で Lambda レスなディスク監視を実装してみた

Kiro CLI の最新モデル Claude Opus 4.6 を活用し、Step Functions の SDK 統合と JSONata で Lambda レスなディスク 枯渇予測監視を実装しました。CloudWatch Agent のメトリクスから過去のトレンドを分析し、ディスクフルを事前に予測して SNS で通知します。実装のハマりどころや Opus 4.6 による JSONata 処理能力の向上についても紹介します。
2026.02.07

EC2 のディスク使用量監視、素直にやると CloudWatch アラームをインスタンスごとに設定することになります。10台、20台と増えてくると、アラームの作成・管理が煩雑になり、インスタンスの追加・削除のたびにアラーム設定を変更する運用が発生します。

また CloudWatch アラームでは「このペースだと3日後にディスクフルになる」という予測のためには、Metric Math で自分で組む必要があり、より複雑化する課題がありました。

今回、新しいモデル Opus 4.6 をサポートした Kiro の支援を受け、Step Functions の SDK 統合と
JSONata の組み込み関数だけで、多数の EC2 インスタンスのディスク枯渇を事前に予測して通知する仕組みを構築できないか試みる機会がありましたので、紹介します

Kiro CLI との設計セッション

Kiro CLI では /model コマンドで利用するモデルを簡単に切り替えられます。

$ /model

 Press (Up/Down) to navigate - Enter to select model
  auto | 1x credit | Models chosen by task for optimal usage and consistent quality
> claude-opus-4.6 (current) | 2.2x credit | Experimental preview of Claude Opus 4.6
  claude-opus-4.5 | 2.2x credit | The latest Claude Opus model
  claude-sonnet-4.5 | 1.3x credit | The latest Claude Sonnet model
  claude-sonnet-4 | 1.3x credit | Hybrid reasoning and coding for regular use
  claude-haiku-4.5 | 0.4x credit | The latest Claude Haiku model

今回は 2026年2月に利用可能になった Claude Opus 4.6 を選択しました。設計の壁打ち相手としては、より高い推論能力を持つモデルが適しています。

https://dev.classmethod.jp/articles/amazon-bedrock-claude-opus-4-5-opus-4-6-check/

最初の要件はシンプルでした。

HDD使用率を監視。72時間以内にHDD残量が枯渇、95%に達する可能性が出た場合にSNSに通知。StepFunctionsのSDK統合とJSONATAを利用してLambdaレスで実現したい。

ここから Kiro との対話を通じて、計画書を複数回にわたってブラッシュアップしました。

進化の過程

v1(基本設計)では、GetMetricStatistics でトレンドデータを取得し、JSONata で枯渇予測、閾値超過で SNS 通知というシンプルな構成でした。

v2(運用拡張)で、タグベースの動的インスタンス取得、Map による並列処理、CRITICAL / WARNING の階層化、CloudFormation 化(Express / Standard 切替)、各種パラメータ化を追加しました。

v3(マルチディスク・マルチOS対応)では、ListMetrics による全ディスク自動検出を導入し、二重 Map 構造(インスタンス x ディスク)で並列処理する構成に進化しました。

特に「Dimensions 完全一致問題」は重要な発見でした。CloudWatch の GetMetricStatistics は、メトリクス保存時と完全に一致する Dimensions を指定しないとデータを返しません。ListMetrics を挟むことでこの問題を解決し、Windows や複数ドライブをアタッチした Linux 環境にも対応できました。

テスト段階で発覚した課題

設計・実装後のテスト段階で、さらにいくつかの課題が見つかりました。

  • 亡霊ディスク問題: デタッチ済みボリュームが ListMetrics に残る。RecentlyActive: PT3H で直近3時間のアクティブなディスクのみに絞り込み
  • JSONata のコーナーケース: データ不足、負の増分、ゼロ除算に対する防御的ロジックを追加
  • 閾値超過の即時判定: 現在値が閾値を超えていればデータ不足でも CRITICAL と判定するよう順序を変更

アーキテクチャ

EventBridge Scheduler (rate(1 hour))
    |
    v
Step Functions (EXPRESS / STANDARD 切替可)
    |
    +-- EC2 DescribeInstances (タグフィルタ)
    |
    +-- Map (インスタンスごと)
    |   +-- CloudWatch ListMetrics (全ディスク検出)
    |   +-- Map (ディスクごと)
    |       +-- CloudWatch GetMetricStatistics (過去14日分)
    |       +-- JSONata (予測計算 & 判定)
    |
    +-- JSONata (CRITICAL/WARNING/OK 集約レポート生成)
    |
    +-- Choice -- SNS Publish or 正常終了

二重 Map 構造で、インスタンス x ディスクの組み合わせを並列処理します。10台 x 2ディスクで月額約 $0.22 です。

予測ロジック

平均増加率ではなく、過去14日間で最も増分が大きかった日の増加率を使います。

1. 過去14日分の日次平均データを取得(14データポイント)
2. 隣接日の差分から、負の増分(ディスク掃除等)を除外
3. 正の増分の最大値を取得
4. daysUntilFull = (100 - 現在の使用率) / 最大日次増分

バッチ処理等で一時的に急増するパターンも検知できるワーストケース判定です。

実装のハマりどころ

Step Functions の JSONata + CloudFormation の組み合わせには、いくつかのハマりどころがありました。

1. {% %} は必須

Step Functions の JSONata モードでは、JSONata 式を {% %} で囲む必要があります。AWS 公式ドキュメントに明記されています。

# OK: 正しい
Output: "{% $states.input.Reservations[].Instances[] %}"

# NG: 動かない
Output: "$states.input.Reservations[].Instances[]"

2. DefinitionSubstitutions の型問題

DefinitionSubstitutions で渡した値は文字列になります。数値が必要なフィールド(例: Period)では $number() で変換が必要です。

DefinitionSubstitutions:
  MetricPeriod: !Ref MetricPeriod

# ステートマシン内
Period: "{% $number('${MetricPeriod}') %}"

3. $sort が期待通りに動かない

Step Functions の JSONata 実装では、$sort の比較関数が Timestamp 文字列のソートで期待通りに動かないケースがありました。$reduce で最新値を取得する方式に変更して解決しました。

# $sort の代わりに $reduce で最新データポイントを取得
$latest := $reduce($dp, function($acc, $v) {
  $v.Timestamp > $acc.Timestamp ? $v : $acc
})

4. and 演算子が使えない

Step Functions の JSONata では and 演算子がエラーになります。ネストした三項演算子で代替します。

# NG: エラー
$a != null and $a <= 3 ? 'WARNING' : 'OK'

# OK: 動く
$a != null ? ($a <= 3 ? 'WARNING' : 'OK') : 'OK'

5. 単一要素の配列問題

JSONata でオブジェクトの配列を生成する式が、要素が1つの場合にオブジェクト(配列ではなく)を返すことがあります。Map の Items に渡すと型エラーになるため、[...] で明示的に配列化します。

# NG: 1要素の場合にオブジェクトになる
$states.input.disks.{ 'dimensions': dimensions }

# OK: 常に配列
[$states.input.disks.{ 'dimensions': dimensions }]

テスト環境と実施

テスト用 EC2

CloudFormation テンプレートで以下を一括作成しました。

  • t4g.nano / EBS 8GB gp3
  • SSM Core + CloudWatch Agent のマネージドポリシー
  • UserData で CWAgent を標準設定で自動起動
  • Monitor:DiskCheck タグ付与

CRITICAL テストの実施

SSM RunCommand でディスクを97%まで埋めて、ステートマシンを手動実行しました。

# ディスクを埋める
aws ssm send-command \
  --instance-ids i-0123456789abcdef0 \
  --document-name "AWS-RunShellScript" \
  --parameters 'commands=["dd if=/dev/zero of=/var/fill_disk bs=1M count=5600"]'

# ステートマシン実行
aws stepfunctions start-execution \
  --state-machine-arn arn:aws:states:ap-northeast-1:xxxxxxxxxxxx:stateMachine:disk-usage-monitor

テスト時は MetricPeriod=300(5分)、LookbackDays=1 に設定して、データ蓄積の待ち時間を短縮しました。

テスト結果

{
  "subject": "Disk Usage Alert",
  "timestamp": "2026-02-07T08:47:49.163Z",
  "summary": {
    "totalInstances": 1,
    "totalDisks": 1,
    "criticalCount": 1,
    "warningCount": 0,
    "okCount": 0
  },
  "critical": [
    {
      "instanceId": "i-0123456789abcdef0",
      "instanceName": "disk-usage-monitor-test-env-test",
      "path": "/",
      "device": "nvme0n1p1",
      "fstype": "xfs",
      "currentUsage": 96.1,
      "maxDailyIncrease": 71.46,
      "daysUntilFull": 0.1,
      "worstDay": "2026-02-07"
    }
  ]
}

CRITICAL 判定、インスタンス情報、予測値すべてが正しく出力されました。

コスト

項目 月額(10台×2ディスク)
CloudWatch API(ListMetrics + GetMetricStatistics) $0.22
Step Functions Express(状態遷移 + 実行時間) $0.01 未満
EventBridge Scheduler 無料枠内
合計 約 $0.23/月

まとめ

Step Functions の SDK 統合と JSONata を組み合わせることで、Lambda レスなディスク枯渇予測監視を実現できました。ListMetrics で各インスタンスの全ディスクを自動検出し、GetMetricStatistics でトレンドデータを取得、JSONata で予測計算まで完結するため、OS やディスク構成に依存しない汎用的な仕組みになっています。

Step Functions 上の JSONata は複雑なロジックが実装可能ですが、and 演算子が使えない、$sort の比較関数が期待通りに動かないなど、固有の制約があります。さらに CloudFormation にステートマシン定義を埋め込む際にも、{% %} 構文が必須であることや、DefinitionSubstitutions で渡した値が文字列になるため $number() での型変換が必要になるなど、組み合わせ特有のハマりどころがあります。ドキュメントだけでは把握しきれない問題が多く、実装の難易度は決して低くありません。

以前の Kiro でも JSONata を利用した Step Functions の実装を試みたことがありますが、JSONata の複雑な式生成やコーナーケースの対処で精度が足りず、結局は枯れた JSONPath + Lambda の構成に落ち着くことがありました。

今回利用した最新モデルの Opus 4.6 では JSONata 式の生成精度が明らかに向上しており、設計・実装・テストの各段階で的確な支援を得られ、JSONata の処理能力が向上していることが実感できました。
監視用途の CloudWatch アラームや Lambda の維持管理が課題となっている場合、Kiro の支援を受けた Step Functions、JSONata での実装もお試し頂ければと思います。

以下に、Kiro と作成した計画書と CloudFormation テンプレートの全文を掲載します。

plan.md - 実装計画書 v4
# ディスク使用量監視 Step Functions 実装計画書 v4

## 変更履歴

| バージョン | 変更内容 |
|---|---|
| v3 | 初版。ListMetrics による全ディスク自動検出、二重 Map 構造 |
| v4 | 実装との差分を反映。空配列ガード、予測ロジック改善、MetricPeriod パラメータ追加 |

## 1. 概要

CloudWatch Agent が収集する HDD 使用率カスタムメトリクスを Step Functions(SDK 統合 + JSONata)で監視し、ディスク枯渇リスクを検知して SNS に構造化 JSON で通知する。Lambda レスで実現する。

監視ロジック:
- ListMetrics で各インスタンスの全ディスク(Dimension 組み合わせ)を自動検出
- 過去14日間の日次データから最大日次増分を特定
- その増分が継続した場合に3日以内にディスクフル(100%)に到達するかを判定
- 現在値が閾値(デフォルト95%)以上の場合は即時アラート

## 2. 前提条件

- CloudWatch Agent が対象 EC2 に設定済み(名前空間: `CWAgent`、メトリクス: `disk_used_percent`- 監視対象 EC2 に識別用タグが付与されている(例: `Monitor:DiskCheck`- SNS トピックが作成済み
- CWAgent の Dimensions 設定(append_dimensions 等)は問わない。ListMetrics で自動検出するため

## 3. アーキテクチャ

EventBridge Scheduler (定期実行: rate(1 hour))
    |
    v
Step Functions (デフォルト: EXPRESS / 引数で STANDARD 切替可)
    |
    +-- [1] EC2 DescribeInstances (タグフィルタ + running のみ)
    |
    +-- [2] Pass/JSONata (インスタンスID・名前を配列に整形)
    |
    +-- [2.5] Choice (インスタンス0件なら正常終了)
    |
    +-- [3] Map (並列: インスタンスごと)
    |       |
    |       +-- [3-1] CloudWatch ListMetrics (全ディスクの Dimension 組み合わせを取得)
    |       |
    |       +-- [3-2] Pass/JSONata (Dimension 組み合わせを配列に整形)
    |       |
    |       +-- [3-2.5] Choice (ディスク0件なら空結果で終了)
    |       |
    |       +-- [3-3] Map (並列: ディスクごと)
    |       |       +-- CloudWatch GetMetricStatistics (過去14日分、Period はパラメータ)
    |       |       +-- Pass/JSONata (全ペア比較で最大増分算出、枯渇リスク判定)
    |       |
    |       +-- Output で instanceId/instanceName/results を集約
    |
    +-- [4] Pass/JSONata (CRITICAL/WARNING/OK に振り分け、レポート生成)
    |
    +-- [5] Choice -- CRITICAL or WARNING あり -- SNS Publish
                   -- なし -- 正常終了

Express モード時は CloudWatch Logs に出力(14日保持)

## 4. ステートマシン定義

| ステート | タイプ | 処理内容 |
|---|---|---|
| GetInstances | Task (SDK: EC2) | DescribeInstances。タグフィルタ + instance-state-name=running で監視対象を取得 |
| ExtractInstanceList | Pass (JSONata) | インスタンスID・Name タグを配列に整形 |
| CheckHasInstances | Choice | インスタンス0件なら NoAction へ。Map の空配列エラーを防止 |
| CheckEachInstance | Map (並列) | 各インスタンスに対して以下を実行(外側 Map)。ToleratedFailurePercentage: 100 |
| -- ListDiskMetrics | Task (SDK: CloudWatch) | ListMetrics で該当インスタンスの全ディスク Dimension を取得 |
| -- ExtractDimensions | Pass (JSONata) | Dimension 組み合わせを配列に整形。metrics が null/空の場合は空配列を返す |
| -- CheckHasDisks | Choice | ディスク0件なら NoDisksFound へ。Map の空配列エラーを防止 |
| -- NoDisksFound | Pass | 空の results を返して終了 |
| -- CheckEachDisk | Map (並列) | 各ディスクに対して以下を実行(内側 Map)。ToleratedFailurePercentage: 100 |
| ---- GetDiskMetrics | Task (SDK: CloudWatch) | GetMetricStatistics で過去 LookbackDays 分を取得。Period は MetricPeriod パラメータ |
| ---- EvaluateRisk | Pass (JSONata) | 全ペア比較で最大増分を算出、枯渇リスク判定 |
| AggregateReport | Pass (JSONata) | CRITICAL/WARNING/OK に振り分け、JSON レポート生成 |
| HasAlerts | Choice | criticalCount > 0 or warningCount > 0 |
| SendReport | Task (SDK: SNS) | JSON レポートを Publish。Subject: "Disk Usage Alert" |
| NoAction | Succeed | 正常終了 |

## 5. CloudWatch API 仕様

### 5.1 ListMetrics(ディスク自動検出)

インスタンスあたり1回。そのインスタンスが持つ全ディスクの Dimension 組み合わせを取得。

| パラメータ ||
|---|---|
| Namespace | CWAgent |
| MetricName | disk_used_percent |
| Dimensions | [{"Name": "InstanceId", "Value": "<対象ID>"}] |
| RecentlyActive | PT3H |

RecentlyActive: PT3H により、直近3時間以内にデータ送信があるディスクのみを返す。

### 5.2 GetMetricStatistics(トレンドデータ取得)

ディスクあたり1回。ListMetrics で取得した完全な Dimensions をそのまま指定。

| パラメータ ||
|---|---|
| Namespace | CWAgent |
| MetricName | disk_used_percent |
| Period | MetricPeriod パラメータ(デフォルト: 86400秒 = 1日) |
| Statistics | ["Average"] |
| StartTime | 現在 - LookbackDays |
| EndTime | 現在 |
| Dimensions | ListMetrics のレスポンスをそのまま使用 |

### 5.3 コスト見積もり

10台 x 平均2ディスク/台の場合:
- ListMetrics: 10回/実行
- GetMetricStatistics: 20回/実行
- 合計: 30回/実行 x 24回/日 x 30日 = 21,600回/月 -> 約 $0.22/月

## 6. 予測ロジック(JSONata)

### 6.1 処理フロー

1. $reduce で最新データポイントを取得($sort は Step Functions の JSONata で不安定なため不使用)
2. データポイントが2未満 -> 予測不能(INSUFFICIENT_DATA)
3. 全ペア比較($map + $reduce)で、任意の2点間の最大増分とその日付を算出
4. 最大増分が0以下 -> 増加傾向なし(daysUntilFull: null)
5. daysUntilFull = (100 - currentUsage) / maxDailyIncrease

### 6.2 判定基準

| 条件 | ステータス | 優先度 |
|---|---|---|
| currentUsage >= thresholdPercent | CRITICAL | 1(最優先) |
| データポイント < 2 | INSUFFICIENT_DATA | 2 |
| daysUntilFull <= predictionDays | WARNING | 3 |
| 上記以外 | OK | 4 |

### 6.3 コーナーケース対応

| ケース | 挙動 |
|---|---|
| 新規インスタンス(データ1点以下) | INSUFFICIENT_DATA。アラート対象外 |
| ディスク掃除で使用率が下がった日がある | 全ペア比較で正の増分のみを対象 |
| 全日で使用率が横ばいまたは減少 | 正の増分なし -> daysUntilFull: null -> OK |
| 現在値が既に100% | CRITICAL(閾値超過で即時判定、データ不足でも発動) |
| インスタンスにディスクメトリクスなし | 空の results を返す(NoDisksFound ステート) |
| 監視対象インスタンスが0台 | CheckHasInstances で正常終了 |

### 6.4 EvaluateRisk 出力フィールド

| フィールド || 説明 |
|---|---|---|
| path | string | マウントパス(例: /, /data, C:) |
| device | string | デバイス名(例: nvme0n1p1, C:) |
| fstype | string | ファイルシステム(例: xfs, NTFS) |
| currentUsage | number | 現在の使用率(小数点1桁) |
| maxDailyIncrease | number/null | 最大日次増分(小数点2桁)。データ不足時は null |
| daysUntilFull | number/null | 100%到達までの残日数(小数点1桁)。増加傾向なしは null |
| worstDay | string/null | 最大増分が発生した日付(YYYY-MM-DD)。増加傾向なしは null |
| status | string | CRITICAL / WARNING / INSUFFICIENT_DATA / OK |

## 7. SNS 通知メッセージ

Subject: Disk Usage Alert

json
{
  "subject": "Disk Usage Alert",
  "timestamp": "2026-02-07T08:47:49.163Z",
  "summary": {
    "totalInstances": 10,
    "totalDisks": 18,
    "criticalCount": 1,
    "warningCount": 2,
    "okCount": 15
  },
  "critical": [
    {
      "instanceId": "i-0123456789abcdef0",
      "instanceName": "db-server",
      "path": "/data",
      "device": "nvme1n1",
      "fstype": "xfs",
      "currentUsage": 96.2,
      "maxDailyIncrease": 2.8,
      "daysUntilFull": 1.4,
      "worstDay": "2026-02-03"
    }
  ],
  "warning": [
    {
      "instanceId": "i-0abcdef1234567890",
      "instanceName": "app-server",
      "path": "/",
      "device": "nvme0n1p1",
      "fstype": "xfs",
      "currentUsage": 87.3,
      "maxDailyIncrease": 5.1,
      "daysUntilFull": 2.5,
      "worstDay": "2026-02-05"
    }
  ]
}

## 8. CloudFormation パラメータ

| パラメータ | デフォルト | 説明 |
|---|---|---|
| StateMachineType | EXPRESS | EXPRESS or STANDARD |
| TagKey | Monitor | 監視対象タグキー |
| TagValue | DiskCheck | 監視対象タグ値 |
| ThresholdPercent | 95 | CRITICAL 判定閾値(%) |
| PredictionDays | 3 | WARNING 判定日数 |
| LookbackDays | 14 | トレンド分析の過去参照日数 |
| MetricPeriod | 86400 | メトリクス集約期間(秒)。テスト時は 300 に設定 |
| SnsTopicArn | (必須) | 通知先 SNS トピック ARN |
| ScheduleExpression | rate(1 hour) | 実行スケジュール |
template.yaml - CloudFormation テンプレート
AWSTemplateFormatVersion: '2010-09-09'
Description: Disk Usage Monitor - Step Functions (Lambdaless / JSONata)

Parameters:
  StateMachineType:
    Type: String
    Default: EXPRESS
    AllowedValues: [EXPRESS, STANDARD]
  TagKey:
    Type: String
    Default: Monitor
  TagValue:
    Type: String
    Default: DiskCheck
  ThresholdPercent:
    Type: Number
    Default: 95
  PredictionDays:
    Type: Number
    Default: 3
  LookbackDays:
    Type: Number
    Default: 14
  SnsTopicArn:
    Type: String
    AllowedPattern: arn:aws:sns:.+
  MetricPeriod:
    Type: Number
    Default: 86400
    Description: Metric aggregation period in seconds (86400=daily, 300=5min for testing)
  ScheduleExpression:
    Type: String
    Default: rate(1 hour)

Conditions:
  IsExpress: !Equals [!Ref StateMachineType, EXPRESS]

Resources:
  StateMachineLogGroup:
    Type: AWS::Logs::LogGroup
    Condition: IsExpress
    Properties:
      LogGroupName: !Sub /aws/vendedlogs/states/${AWS::StackName}
      RetentionInDays: 14

  StateMachineRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: states.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: DiskMonitorPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - ec2:DescribeInstances
                  - cloudwatch:ListMetrics
                  - cloudwatch:GetMetricStatistics
                Resource: '*'
              - Effect: Allow
                Action: sns:Publish
                Resource: !Ref SnsTopicArn
              - !If
                - IsExpress
                - Effect: Allow
                  Action:
                    - logs:CreateLogDelivery
                    - logs:GetLogDelivery
                    - logs:UpdateLogDelivery
                    - logs:DeleteLogDelivery
                    - logs:ListLogDeliveries
                    - logs:PutResourcePolicy
                    - logs:DescribeResourcePolicies
                    - logs:DescribeLogGroups
                  Resource: '*'
                - !Ref AWS::NoValue

  SchedulerRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: scheduler.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: InvokeStateMachine
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - states:StartExecution
                  - states:StartSyncExecution
                Resource: !GetAtt StateMachine.Arn

  StateMachine:
    Type: AWS::StepFunctions::StateMachine
    Properties:
      StateMachineName: !Sub ${AWS::StackName}
      StateMachineType: !Ref StateMachineType
      RoleArn: !GetAtt StateMachineRole.Arn
      LoggingConfiguration: !If
        - IsExpress
        - Level: ALL
          IncludeExecutionData: true
          Destinations:
            - CloudWatchLogsLogGroup:
                LogGroupArn: !GetAtt StateMachineLogGroup.Arn
        - !Ref AWS::NoValue
      DefinitionSubstitutions:
        TagKey: !Ref TagKey
        TagValue: !Ref TagValue
        ThresholdPercent: !Ref ThresholdPercent
        PredictionDays: !Ref PredictionDays
        LookbackDays: !Ref LookbackDays
        MetricPeriod: !Ref MetricPeriod
        SnsTopicArn: !Ref SnsTopicArn
      Definition:
        QueryLanguage: JSONata
        Comment: Disk Usage Monitor - Lambdaless
        StartAt: GetInstances
        States:
          GetInstances:
            Type: Task
            Resource: arn:aws:states:::aws-sdk:ec2:describeInstances
            Arguments:
              Filters:
                - Name: "tag:${TagKey}"
                  Values:
                    - "${TagValue}"
                - Name: instance-state-name
                  Values:
                    - running
            Next: ExtractInstanceList

          ExtractInstanceList:
            Type: Pass
            Output: >-
              {% [$states.input.Reservations[].Instances[].{
                'instanceId': InstanceId,
                'instanceName': Tags[Key='Name'].Value
              }] %}
            Next: CheckHasInstances

          CheckHasInstances:
            Type: Choice
            Choices:
              - Condition: "{% $count($states.input) > 0 %}"
                Next: CheckEachInstance
            Default: NoAction

          CheckEachInstance:
            Type: Map
            Items: "{% $states.input %}"
            MaxConcurrency: 10
            ToleratedFailurePercentage: 100
            ItemProcessor:
              ProcessorConfig:
                Mode: INLINE
              StartAt: ListDiskMetrics
              States:
                ListDiskMetrics:
                  Type: Task
                  Resource: arn:aws:states:::aws-sdk:cloudwatch:listMetrics
                  Arguments:
                    Namespace: CWAgent
                    MetricName: disk_used_percent
                    Dimensions:
                      - Name: InstanceId
                        Value: "{% $states.input.instanceId %}"
                    RecentlyActive: PT3H
                  Output:
                    instanceId: "{% $states.input.instanceId %}"
                    instanceName: "{% $states.input.instanceName %}"
                    metrics: "{% $states.result.Metrics %}"
                  Next: ExtractDimensions

                ExtractDimensions:
                  Type: Pass
                  Output:
                    instanceId: "{% $states.input.instanceId %}"
                    instanceName: "{% $states.input.instanceName %}"
                    disks: >-
                      {% $states.input.metrics
                        ? [$states.input.metrics.{ 'dimensions': Dimensions }]
                        : [] %}
                  Next: CheckHasDisks

                CheckHasDisks:
                  Type: Choice
                  Choices:
                    - Condition: "{% $count($states.input.disks) > 0 %}"
                      Next: CheckEachDisk
                  Default: NoDisksFound

                NoDisksFound:
                  Type: Pass
                  Output:
                    instanceId: "{% $states.input.instanceId %}"
                    instanceName: "{% $states.input.instanceName %}"
                    results: []
                  End: true

                CheckEachDisk:
                  Type: Map
                  Items: >-
                    {% [$states.input.disks.{
                      'dimensions': dimensions,
                      'instanceId': $states.input.instanceId,
                      'instanceName': $states.input.instanceName
                    }] %}
                  MaxConcurrency: 5
                  ToleratedFailurePercentage: 100
                  ItemProcessor:
                    ProcessorConfig:
                      Mode: INLINE
                    StartAt: GetDiskMetrics
                    States:
                      GetDiskMetrics:
                        Type: Task
                        Resource: arn:aws:states:::aws-sdk:cloudwatch:getMetricStatistics
                        Arguments:
                          Namespace: CWAgent
                          MetricName: disk_used_percent
                          Dimensions: "{% $states.input.dimensions %}"
                          StartTime: >-
                            {% $fromMillis($millis() - ${LookbackDays} * 86400000) %}
                          EndTime: "{% $fromMillis($millis()) %}"
                          Period: "{% $number('${MetricPeriod}') %}"
                          Statistics:
                            - Average
                        Output:
                          dimensions: "{% $states.input.dimensions %}"
                          datapoints: "{% $states.result.Datapoints %}"
                        Next: EvaluateRisk

                      EvaluateRisk:
                        Type: Pass
                        Output: >-
                          {% (
                            $dims := $states.input.dimensions;
                            $path := ($dims[Name='path']).Value;
                            $device := ($dims[Name='device']).Value;
                            $fstype := ($dims[Name='fstype']).Value;
                            $dp := $states.input.datapoints;
                            $cnt := $count($dp);
                            $latest := $cnt > 0
                              ? $reduce($dp, function($acc, $v) {
                                  $v.Timestamp > $acc.Timestamp ? $v : $acc
                                })
                              : {'Average': 0};
                            $current := $latest.Average;
                            $prediction := $cnt < 2
                              ? { 'daysUntilFull': null, 'maxDailyIncrease': null, 'worstDay': null }
                              : (
                                $maxes := $map($dp, function($a) {
                                  $reduce($dp, function($best, $b) {
                                    ($a.Timestamp > $b.Timestamp)
                                      ? ( $d := $a.Average - $b.Average;
                                          $d > $best.delta
                                            ? {'delta': $d, 'date': $a.Timestamp}
                                            : $best )
                                      : $best
                                  }, {'delta': 0, 'date': ''})
                                });
                                $worst := $reduce($maxes, function($acc, $m) {
                                  $m.delta > $acc.delta ? $m : $acc
                                });
                                $worst.delta > 0
                                  ? {
                                    'daysUntilFull': $round((100 - $current) / $worst.delta, 1),
                                    'maxDailyIncrease': $round($worst.delta, 2),
                                    'worstDay': $substringBefore($worst.date, 'T')
                                  }
                                  : { 'daysUntilFull': null, 'maxDailyIncrease': 0, 'worstDay': null }
                              );
                            $status := $current >= ${ThresholdPercent}
                              ? 'CRITICAL'
                              : $cnt < 2
                                ? 'INSUFFICIENT_DATA'
                                : $prediction.daysUntilFull != null
                                  ? ($prediction.daysUntilFull <= ${PredictionDays}
                                      ? 'WARNING' : 'OK')
                                  : 'OK';
                            {
                              'path': $path,
                              'device': $device,
                              'fstype': $fstype,
                              'currentUsage': $round($current, 1),
                              'maxDailyIncrease': $prediction.maxDailyIncrease,
                              'daysUntilFull': $prediction.daysUntilFull,
                              'worstDay': $prediction.worstDay,
                              'status': $status
                            }
                          ) %}
                        End: true
                  Output:
                    instanceId: "{% $states.input.instanceId %}"
                    instanceName: "{% $states.input.instanceName %}"
                    results: "{% $states.result %}"
                  End: true
            Next: AggregateReport

          AggregateReport:
            Type: Pass
            Output: >-
              {% (
                $all := $states.input;
                $allDisks := $all.results[];
                {
                  'subject': 'Disk Usage Alert',
                  'timestamp': $now(),
                  'summary': {
                    'totalInstances': $count($all),
                    'totalDisks': $count($allDisks),
                    'criticalCount': $count($filter($allDisks, function($d) { $d.status = 'CRITICAL' })),
                    'warningCount': $count($filter($allDisks, function($d) { $d.status = 'WARNING' })),
                    'okCount': $count($filter($allDisks, function($d) { $d.status = 'OK' }))
                  },
                  'critical': $all.(
                    $inst := $;
                    results[status='CRITICAL'].{
                      'instanceId': $inst.instanceId,
                      'instanceName': $inst.instanceName,
                      'path': path, 'device': device, 'fstype': fstype,
                      'currentUsage': currentUsage,
                      'maxDailyIncrease': maxDailyIncrease,
                      'daysUntilFull': daysUntilFull,
                      'worstDay': worstDay
                    }
                  )[],
                  'warning': $all.(
                    $inst := $;
                    results[status='WARNING'].{
                      'instanceId': $inst.instanceId,
                      'instanceName': $inst.instanceName,
                      'path': path, 'device': device, 'fstype': fstype,
                      'currentUsage': currentUsage,
                      'maxDailyIncrease': maxDailyIncrease,
                      'daysUntilFull': daysUntilFull,
                      'worstDay': worstDay
                    }
                  )[]
                }
              ) %}
            Next: HasAlerts

          HasAlerts:
            Type: Choice
            Choices:
              - Condition: >-
                  {% $states.input.summary.criticalCount > 0
                    or $states.input.summary.warningCount > 0 %}
                Next: SendReport
            Default: NoAction

          SendReport:
            Type: Task
            Resource: arn:aws:states:::aws-sdk:sns:publish
            Arguments:
              TopicArn: "${SnsTopicArn}"
              Subject: "{% $states.input.subject %}"
              Message: "{% $string($states.input) %}"
            End: true

          NoAction:
            Type: Succeed

  Scheduler:
    Type: AWS::Scheduler::Schedule
    Properties:
      Name: !Sub ${AWS::StackName}-scheduler
      ScheduleExpression: !Ref ScheduleExpression
      FlexibleTimeWindow:
        Mode: 'OFF'
      Target:
        Arn: !GetAtt StateMachine.Arn
        RoleArn: !GetAtt SchedulerRole.Arn
        Input: '{}'

Outputs:
  StateMachineArn:
    Value: !GetAtt StateMachine.Arn
  StateMachineType:
    Value: !Ref StateMachineType
  LogGroupName:
    Condition: IsExpress
    Value: !Ref StateMachineLogGroup

この記事をシェアする

FacebookHatena blogX

関連記事