I Tried Implementing Lambda-less Disk Monitoring with Step Functions + JSONata Using Kiro with Opus 4.6 Support
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.
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:DiskChecktag 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
