Opus 4.6 をサポートした Kiro で、Step Functions + JSONata で Lambda レスなディスク監視を実装してみた
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 を選択しました。設計の壁打ち相手としては、より高い推論能力を持つモデルが適しています。
最初の要件はシンプルでした。
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







