I Tried Implementing Lambda-less Disk Monitoring with Step Functions + JSONata Using Kiro with Opus 4.6 Support

I Tried Implementing Lambda-less Disk Monitoring with Step Functions + JSONata Using Kiro with Opus 4.6 Support

Using Claude Opus 4.6 in Kiro CLI, I built Lambda-less disk exhaustion prediction monitoring with Step Functions SDK integration and JSONata, sending SNS alerts. Also covers pitfalls and Opus 4.6's JSONata gains.
2026.02.07

This page has been translated by machine translation. View original

The straightforward way to monitor EC2 disk usage is to set up a CloudWatch alarm for each instance. Once you have 10 or 20 instances, creating and managing alarms becomes cumbersome, and you have to change alarm settings every time an instance is added or removed.

Also, with CloudWatch alarms, making a prediction like "at this pace, the disk will be full in 3 days" requires building it yourself with Metric Math, which made things even more complicated.

This time, with the help of Kiro, which supports the new model Opus 4.6, I had the opportunity to try building a mechanism that predicts disk exhaustion on many EC2 instances in advance and sends notifications, using only the Step Functions SDK integration and JSONata built-in functions. I'll introduce it here.

Design Session with Kiro CLI

In Kiro CLI, you can easily switch the model you use with the /model command.

$ /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

This time I selected Claude Opus 4.6, which became available in February 2026. A model with stronger reasoning capability is well suited as a sounding board for design.

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

The initial requirement was simple.

Monitor HDD usage. Notify SNS when there is a possibility that the remaining HDD space will run out within 72 hours, reaching 95%. I want to achieve this without Lambda, using the Step Functions SDK integration and JSONata.

From here, through dialogue with Kiro, I refined the plan document over several iterations.

Evolution Process

v1 (basic design) was a simple configuration: retrieve trend data with GetMetricStatistics, predict exhaustion with JSONata, and notify via SNS when a threshold is exceeded.

In v2 (operational extension), I added tag-based dynamic instance retrieval, parallel processing with Map, CRITICAL / WARNING tiering, CloudFormation conversion (switchable between Express / Standard), and various parameterizations.

In v3 (multi-disk and multi-OS support), I introduced automatic detection of all disks via ListMetrics, evolving into a configuration that processes in parallel with a double Map structure (instance x disk).

In particular, the "exact Dimensions match problem" was an important discovery. CloudWatch's GetMetricStatistics returns no data unless you specify Dimensions that exactly match those used when the metric was stored. Inserting ListMetrics solved this problem, and it also made it possible to support Windows and Linux environments with multiple drives attached.

Issues Discovered at the Testing Stage

At the testing stage after design and implementation, several more issues were found.

  • Ghost disk problem: Detached volumes remain in ListMetrics. Narrowed down to only disks active in the last 3 hours with RecentlyActive: PT3H
  • JSONata corner cases: Added defensive logic for insufficient data, negative increments, and division by zero
  • Immediate determination of threshold exceedance: Changed the order so that if the current value exceeds the threshold, it is judged CRITICAL even when data is insufficient

Architecture

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 正常終了

The double Map structure processes instance x disk combinations in parallel. For 10 instances x 2 disks, the cost is about $0.22 per month.

Prediction Logic

Instead of the average rate of increase, it uses the rate of increase on the day with the largest increment over the past 14 days.

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

This is a worst-case determination that can also detect patterns of temporary spikes caused by batch processing and the like.

Implementation Pitfalls

The combination of Step Functions JSONata and CloudFormation had several pitfalls.

1. {% %} Is Required

In Step Functions' JSONata mode, JSONata expressions must be enclosed in {% %}. This is clearly stated in the official AWS documentation.

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

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

2. Type Issue with DefinitionSubstitutions

Values passed via DefinitionSubstitutions become strings. Fields that require a number (e.g., Period) need conversion with $number().

DefinitionSubstitutions:
  MetricPeriod: !Ref MetricPeriod

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

3. $sort Doesn't Work as Expected

In Step Functions' JSONata implementation, there were cases where the $sort comparison function did not work as expected when sorting Timestamp strings. I resolved it by switching to a method that gets the latest value with $reduce.

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

4. The and Operator Can't Be Used

In Step Functions' JSONata, the and operator causes an error. Use nested ternary operators as a substitute.

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

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

5. Single-Element Array Problem

An expression in JSONata that generates an array of objects may return an object (not an array) when there is only one element. Passing this to Map's Items causes a type error, so explicitly make it an array with [...].

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

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

Test Environment and Execution

Test EC2

I created the following all at once with a CloudFormation template.

  • t4g.nano / EBS 8GB gp3
  • Managed policies for SSM Core + CloudWatch Agent
  • CWAgent automatically started with default settings via UserData
  • Monitor:DiskCheck tag attached

Running the CRITICAL Test

I filled the disk to 97% with SSM RunCommand and manually executed the state machine.

# ディスクを埋める
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

During testing, I set MetricPeriod=300 (5 minutes) and LookbackDays=1 to shorten the wait time for data accumulation.

Test Results

{
  "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"
    }
  ]
}

The CRITICAL determination, instance information, and predicted values were all output correctly.

Cost

Item Monthly (10 instances x 2 disks)
CloudWatch API (ListMetrics + GetMetricStatistics) $0.22
Step Functions Express (state transitions + execution time) Less than $0.01
EventBridge Scheduler Within free tier
Total About $0.23/month

Summary

By combining the Step Functions SDK integration with JSONata, I was able to achieve Lambda-less disk exhaustion prediction monitoring. ListMetrics automatically detects all disks on each instance, GetMetricStatistics retrieves trend data, and JSONata completes everything up to the prediction calculation, so it is a general-purpose mechanism that does not depend on the OS or disk configuration.

JSONata on Step Functions can implement complex logic, but it has its own constraints, such as the and operator being unavailable and the $sort comparison function not working as expected. Furthermore, when embedding a state machine definition in CloudFormation, there are pitfalls specific to the combination, such as the {% %} syntax being mandatory and values passed via DefinitionSubstitutions becoming strings, which requires type conversion with $number(). There are many issues that cannot be fully grasped from the documentation alone, and the implementation difficulty is by no means low.

With earlier versions of Kiro, I also tried implementing Step Functions using JSONata, but the accuracy was insufficient in generating complex JSONata expressions and handling corner cases, and I sometimes ended up settling on the well-established JSONPath + Lambda configuration.

With the latest model Opus 4.6 used this time, the accuracy of JSONata expression generation has clearly improved, and I received precise support at each stage of design, implementation, and testing, which made me feel that its JSONata handling capability has improved.
If maintaining CloudWatch alarms or Lambda for monitoring is a challenge for you, please give an implementation with Step Functions and JSONata, supported by Kiro, a try.

Below, I include the full text of the plan document and the CloudFormation template created with Kiro.

plan.md - Implementation Plan 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 Template
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

Share this article

AWSのお困り事はクラスメソッドへ