cfn-guardian 0.13.3 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +1 -0
- data/docs/alarm_templates.md +17 -1
- data/docs/anomaly_detection.md +74 -0
- data/docs/ecs_scheduled_tasks.md +99 -0
- data/docs/overview.md +3 -1
- data/lib/cfnguardian/compile.rb +37 -0
- data/lib/cfnguardian/display_formatter.rb +87 -14
- data/lib/cfnguardian/models/alarm.rb +40 -6
- data/lib/cfnguardian/models/event_subscription.rb +24 -0
- data/lib/cfnguardian/resources/base.rb +6 -0
- data/lib/cfnguardian/resources/ecs_scheduled_task.rb +30 -0
- data/lib/cfnguardian/stacks/resources.rb +32 -1
- data/lib/cfnguardian/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 97db5ada9a8fd070dbc9cbb851b3d0f1b0352db96014d9f29883c359a47b92a4
|
|
4
|
+
data.tar.gz: 8ce062a52661342ab8c78e0adccb357887234384e4ff2e1cd4307cb3066bd41b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b58d879093561d7f836e73b02aaceb6f3499209907ec0aaf4e66e272398ac53756b51d0820ef319cf89b7bfdeedd2fb44f57d7aee1ddcec904122af523a2ca64
|
|
7
|
+
data.tar.gz: 0e582fcd0bda145414d31888bde92e0b866a895c82e45841760085ee2ca9466281776750787c0d7c9973a96b227e69e200c79891d9246b4bd294f7b118155fab
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
data/docs/alarm_templates.md
CHANGED
|
@@ -141,4 +141,20 @@ Templates:
|
|
|
141
141
|
Period: 60
|
|
142
142
|
EvaluationPeriods: 10
|
|
143
143
|
DatapointsToAlarm: 6
|
|
144
|
-
```
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Anomaly Detection
|
|
147
|
+
|
|
148
|
+
Instead of a static `Threshold`, an alarm can compare a metric against a CloudWatch anomaly detection band by setting `AnomalyDetection: true` along with one of the anomaly-specific `ComparisonOperator` values. `StandardDeviation` (default `2`) controls how wide the expected band is.
|
|
149
|
+
|
|
150
|
+
```yaml
|
|
151
|
+
Templates:
|
|
152
|
+
Ec2Instance:
|
|
153
|
+
CPUUtilizationHigh:
|
|
154
|
+
AnomalyDetection: true
|
|
155
|
+
StandardDeviation: 2
|
|
156
|
+
ComparisonOperator: GreaterThanUpperThreshold
|
|
157
|
+
EvaluationPeriods: 3
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`Threshold` cannot be combined with `AnomalyDetection`. See [Anomaly Detection Alarms](anomaly_detection.md) for the full property reference, valid `ComparisonOperator` values, and more examples.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Anomaly Detection Alarms
|
|
2
|
+
|
|
3
|
+
Anomaly detection alarms use CloudWatch [anomaly detection](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Anomaly_Detection.html) to alarm on a metric's expected behaviour rather than a fixed number. CloudWatch builds a model of the metric's normal range (a "band") from up to two weeks of history and the alarm fires when the metric strays outside that band, instead of crossing a static `Threshold`.
|
|
4
|
+
|
|
5
|
+
## The Problem
|
|
6
|
+
|
|
7
|
+
A static `Threshold` works well when "too high" or "too low" is a known constant, but many metrics (queue depth, request count, CPU on a workload with a daily cycle) have a normal range that varies by time of day or day of week. A fixed threshold set high enough to avoid false alarms on the daily peak is too high to catch a real problem during the trough, and vice versa.
|
|
8
|
+
|
|
9
|
+
## How It Works
|
|
10
|
+
|
|
11
|
+
Instead of emitting a CloudWatch alarm with a static `Threshold`, an anomaly detection alarm emits the CloudFormation `Metrics` property (a list of `MetricDataQuery` objects) with:
|
|
12
|
+
|
|
13
|
+
1. A `MetricStat` entry (`m1`) that returns the raw metric, built from the alarm's usual `MetricName`, `Namespace`, `Dimensions`, `Period` and `Statistic`.
|
|
14
|
+
2. An `ANOMALY_DETECTION_BAND(m1, <StandardDeviation>)` expression entry (`ad1`) that computes the expected value band for that metric.
|
|
15
|
+
|
|
16
|
+
The alarm then sets `ThresholdMetricId: ad1` instead of a `Threshold`, and compares the metric against the band using one of the anomaly-specific `ComparisonOperator` values. This implicitly creates the underlying `AWS::CloudWatch::AnomalyDetector` model in CloudWatch - no separate CloudFormation resource is required.
|
|
17
|
+
|
|
18
|
+
## Configuration
|
|
19
|
+
|
|
20
|
+
Add `AnomalyDetection: true` and a `ComparisonOperator` to an alarm template. Optionally set `StandardDeviation` to control the width of the expected band.
|
|
21
|
+
|
|
22
|
+
### Properties
|
|
23
|
+
|
|
24
|
+
| Property | Required | Default | Description |
|
|
25
|
+
| --- | --- | --- | --- |
|
|
26
|
+
| `AnomalyDetection` | Yes | `false` | Set to `true` to alarm on an anomaly detection band instead of a static Threshold. |
|
|
27
|
+
| `StandardDeviation` | No | `2` | The width of the expected value band, in standard deviations. A larger number widens the band (fewer, larger anomalies alarm); a smaller number narrows it. |
|
|
28
|
+
| `ComparisonOperator` | Yes | - | Must be one of `GreaterThanUpperThreshold`, `LessThanLowerThreshold`, or `LessThanLowerOrGreaterThanUpperThreshold`. |
|
|
29
|
+
|
|
30
|
+
`Threshold` must not be set on an anomaly detection alarm - CloudFormation treats `Threshold` and `ThresholdMetricId` as mutually exclusive, and `cfn-guardian` will fail validation if both are supplied.
|
|
31
|
+
|
|
32
|
+
### Overriding Default Alarms
|
|
33
|
+
|
|
34
|
+
You can convert an existing default alarm to use anomaly detection by overriding it in the template:
|
|
35
|
+
|
|
36
|
+
```yaml
|
|
37
|
+
Templates:
|
|
38
|
+
Ec2Instance:
|
|
39
|
+
CPUUtilizationHigh:
|
|
40
|
+
AnomalyDetection: true
|
|
41
|
+
StandardDeviation: 2
|
|
42
|
+
ComparisonOperator: GreaterThanUpperThreshold
|
|
43
|
+
EvaluationPeriods: 3
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Creating New Alarms
|
|
47
|
+
|
|
48
|
+
You can also create new anomaly detection alarms that don't override any defaults. `MetricName` and `Namespace` are still required, the same as any other alarm, since they identify the metric CloudWatch builds the anomaly detection model from:
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
Templates:
|
|
52
|
+
SQSQueue:
|
|
53
|
+
ApproximateNumberOfMessagesVisibleAnomaly:
|
|
54
|
+
MetricName: ApproximateNumberOfMessagesVisible
|
|
55
|
+
Statistic: Average
|
|
56
|
+
AnomalyDetection: true
|
|
57
|
+
StandardDeviation: 3
|
|
58
|
+
ComparisonOperator: LessThanLowerOrGreaterThanUpperThreshold
|
|
59
|
+
EvaluationPeriods: 3
|
|
60
|
+
DatapointsToAlarm: 2
|
|
61
|
+
AlarmAction: Warning
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Choosing A Comparison Operator
|
|
65
|
+
|
|
66
|
+
- `GreaterThanUpperThreshold` - alarm only when the metric goes above the expected band (e.g. an unexpected spike in queue depth or error count).
|
|
67
|
+
- `LessThanLowerThreshold` - alarm only when the metric drops below the expected band (e.g. request count dropping to zero when traffic is expected).
|
|
68
|
+
- `LessThanLowerOrGreaterThanUpperThreshold` - alarm on either side of the band. This is the most common choice when you don't know in advance which direction is abnormal.
|
|
69
|
+
|
|
70
|
+
## Limitations
|
|
71
|
+
|
|
72
|
+
- **Training period**: CloudWatch needs data to learn the metric's normal pattern; a newly created anomaly detector can take some time (often a few hours, up to a few days for metrics with a weekly pattern) before the band is reliable.
|
|
73
|
+
- **Cannot combine with `SearchExpression`**: both features rely on the alarm's `Metrics` property, so `AnomalyDetection` and `SearchExpression` cannot be set on the same alarm.
|
|
74
|
+
- See the [CloudWatch anomaly detection documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Anomaly_Detection.html) for how the model is trained and how to inspect it in the console.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# ECS Scheduled Tasks
|
|
2
|
+
|
|
3
|
+
`ECSScheduledTask` monitors ECS tasks that run outside of an ECS service, for example tasks launched on a schedule by an EventBridge rule or EventBridge Scheduler. These tasks have no service to attach `ECSService` alarms to, so Guardian watches the `ECS Task State Change` events for the cluster instead and notifies when a task fails.
|
|
4
|
+
|
|
5
|
+
## Configuration
|
|
6
|
+
|
|
7
|
+
```yaml
|
|
8
|
+
Resources:
|
|
9
|
+
ECSScheduledTask:
|
|
10
|
+
# Id is the ECS cluster name. Monitors every standalone task in the cluster.
|
|
11
|
+
- Id: batch-jobs
|
|
12
|
+
# Optionally narrow to a single task definition family.
|
|
13
|
+
- Id: shared-cluster
|
|
14
|
+
TaskDefinitionFamily: report-export
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
| Key | Required | Description |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| `Id` | yes | ECS cluster name |
|
|
20
|
+
| `TaskDefinitionFamily` | no | Only match tasks from this task definition family, matched on the task definition ARN |
|
|
21
|
+
|
|
22
|
+
Every event subscription in this group, including custom ones, is scoped to:
|
|
23
|
+
|
|
24
|
+
- the cluster in `Id`
|
|
25
|
+
- tasks not started by an ECS service (the task's `group` doesn't start with `service:`), so service deployments don't trigger alerts
|
|
26
|
+
- the `TaskDefinitionFamily`, if set
|
|
27
|
+
|
|
28
|
+
If a scheduled task is launched with a custom `group` starting with `service:`, it won't be matched.
|
|
29
|
+
|
|
30
|
+
## Default Event Subscriptions
|
|
31
|
+
|
|
32
|
+
Both are sent to the `Events` topic by default.
|
|
33
|
+
|
|
34
|
+
| Name | Fires when |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `TaskFailed` | A task stops and any of its containers has a non-zero exit code |
|
|
37
|
+
| `TaskFailedToStart` | A task stops with stop code `TaskFailedToStart`, e.g. the image can't be pulled or a secret can't be fetched |
|
|
38
|
+
|
|
39
|
+
A container with no exit code (null or missing, e.g. one that never started) doesn't match. EventBridge matches `exitCode` against every container in the task, so a sidecar (such as a log router) that exits non-zero will also trigger `TaskFailed`.
|
|
40
|
+
|
|
41
|
+
## Overriding Defaults
|
|
42
|
+
|
|
43
|
+
Event subscriptions are configured under the `EventSubscriptions` key. See [event subscriptions](event_subscriptions.md).
|
|
44
|
+
|
|
45
|
+
```yaml
|
|
46
|
+
EventSubscriptions:
|
|
47
|
+
ECSScheduledTask:
|
|
48
|
+
# page on failed runs instead of sending to the Events topic
|
|
49
|
+
TaskFailed:
|
|
50
|
+
Topic: Critical
|
|
51
|
+
# disable the failed-to-start notification
|
|
52
|
+
TaskFailedToStart: false
|
|
53
|
+
# custom subscriptions default to the ECS Task State Change detail type
|
|
54
|
+
# and are scoped to the cluster and family like the defaults
|
|
55
|
+
TaskStoppedBySpot:
|
|
56
|
+
Detail:
|
|
57
|
+
stopCode: [SpotInterruption]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Alarms
|
|
61
|
+
|
|
62
|
+
There are no default alarms, as scheduled tasks only publish metrics while they run. If [Container Insights](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/ContainerInsights.html) is enabled on the cluster, alarms can be added under `Templates`. They use the `ECS/ContainerInsights` namespace with the `ClusterName` dimension, plus `TaskDefinitionFamily` if set, and treat missing data as not breaching.
|
|
63
|
+
|
|
64
|
+
Alarms are named `guardian-ECSScheduledTask-<Id>-<AlarmName>`, or `guardian-ECSScheduledTask-<Id>:<TaskDefinitionFamily>-<AlarmName>` when a family is set, so a cluster-wide entry and family entries on the same cluster each get their own alarm.
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
Templates:
|
|
68
|
+
ECSScheduledTask:
|
|
69
|
+
MemoryUtilizedHigh:
|
|
70
|
+
MetricName: MemoryUtilized
|
|
71
|
+
Statistic: Maximum
|
|
72
|
+
Threshold: 1800 # MiB
|
|
73
|
+
AlarmAction: Warning
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Missed Runs
|
|
77
|
+
|
|
78
|
+
The event subscriptions only fire when a task runs and fails. They won't fire if the schedule never launches a task. If `RunTask` itself fails (for example on a bad network configuration), ECS creates no task and emits no events. That shows up in the launcher's own metrics instead: `FailedInvocations` in the `AWS/Events` namespace for an EventBridge rule, or `TargetErrorCount` and `InvocationDroppedCount` in the `AWS/Scheduler` namespace for EventBridge Scheduler (Scheduler doesn't publish `FailedInvocations`).
|
|
79
|
+
|
|
80
|
+
To catch a missed run, add a `LogGroup` metric filter that counts a line the task logs on success, and alarm when the count is below 1 over the schedule interval. A metric filter with no matches publishes no data, so the alarm must treat missing data as breaching. See [log group metric filters](custom_checks/log_group_metric_filters.md).
|
|
81
|
+
|
|
82
|
+
```yaml
|
|
83
|
+
Resources:
|
|
84
|
+
LogGroup:
|
|
85
|
+
- Id: batch-jobs
|
|
86
|
+
MetricFilters:
|
|
87
|
+
- MetricName: ReportExportSucceeded
|
|
88
|
+
Pattern: '"export complete"'
|
|
89
|
+
|
|
90
|
+
Templates:
|
|
91
|
+
LogGroup:
|
|
92
|
+
ReportExportSucceeded:
|
|
93
|
+
ComparisonOperator: LessThanThreshold
|
|
94
|
+
Threshold: 1
|
|
95
|
+
Period: 86400 # the schedule interval, here daily
|
|
96
|
+
EvaluationPeriods: 1
|
|
97
|
+
TreatMissingData: breaching
|
|
98
|
+
AlarmAction: Warning
|
|
99
|
+
```
|
data/docs/overview.md
CHANGED
|
@@ -23,4 +23,6 @@
|
|
|
23
23
|
9. [Alarms for Custom Metrics](custom_metrics.md)
|
|
24
24
|
10. [Dimension Variables](variables.md)
|
|
25
25
|
11. [Search Expression Alarms](search_expressions.md)
|
|
26
|
-
12. [Alarm Tags](alarm_tags.md)
|
|
26
|
+
12. [Alarm Tags](alarm_tags.md)
|
|
27
|
+
13. [Anomaly Detection Alarms](anomaly_detection.md)
|
|
28
|
+
14. [ECS Scheduled Tasks](ecs_scheduled_tasks.md)
|
data/lib/cfnguardian/compile.rb
CHANGED
|
@@ -20,6 +20,7 @@ require 'cfnguardian/resources/dynamodb_table'
|
|
|
20
20
|
require 'cfnguardian/resources/ec2_instance'
|
|
21
21
|
require 'cfnguardian/resources/ecs_cluster'
|
|
22
22
|
require 'cfnguardian/resources/ecs_service'
|
|
23
|
+
require 'cfnguardian/resources/ecs_scheduled_task'
|
|
23
24
|
require 'cfnguardian/resources/eks_container_insights'
|
|
24
25
|
require 'cfnguardian/resources/elastic_file_system'
|
|
25
26
|
require 'cfnguardian/resources/elasticache_replication_group'
|
|
@@ -190,6 +191,22 @@ module CfnGuardian
|
|
|
190
191
|
@resources.each do |resource|
|
|
191
192
|
case resource.type
|
|
192
193
|
when 'Alarm'
|
|
194
|
+
unless [true, false].include?(resource.anomaly_detection)
|
|
195
|
+
@errors << "CfnGuardian::AlarmPropertyError - alarm #{resource.name} for resource #{resource.resource_id} has invalid AnomalyDetection value '#{resource.anomaly_detection.inspect}'. Must be a boolean (true or false)."
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
unless resource.standard_deviation.nil?
|
|
199
|
+
if !resource.standard_deviation.is_a?(Numeric) || !resource.standard_deviation.finite? || resource.standard_deviation <= 0
|
|
200
|
+
@errors << "CfnGuardian::AlarmPropertyError - alarm #{resource.name} for resource #{resource.resource_id} has invalid StandardDeviation '#{resource.standard_deviation}'. Must be a finite positive number."
|
|
201
|
+
elsif resource.anomaly_detection != true
|
|
202
|
+
@errors << "CfnGuardian::AlarmPropertyError - alarm #{resource.name} for resource #{resource.resource_id} sets StandardDeviation but AnomalyDetection is not true. StandardDeviation only applies to anomaly detection alarms; either remove it or set AnomalyDetection: true."
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
if resource.search_expression && resource.anomaly_detection == true
|
|
207
|
+
@errors << "CfnGuardian::AlarmPropertyError - alarm #{resource.name} for resource #{resource.resource_id} cannot set both SearchExpression and AnomalyDetection. They both rely on the mutually exclusive Metrics property."
|
|
208
|
+
end
|
|
209
|
+
|
|
193
210
|
if resource.search_expression
|
|
194
211
|
if !resource.search_expression.is_a?(String) || resource.search_expression.strip.empty?
|
|
195
212
|
@errors << "CfnGuardian::AlarmPropertyError - alarm #{resource.name} for resource #{resource.resource_id} has an invalid SearchExpression. Must be a non-empty string."
|
|
@@ -203,7 +220,27 @@ module CfnGuardian
|
|
|
203
220
|
@errors << "CfnGuardian::AlarmPropertyError - alarm #{resource.name} for resource #{resource.resource_id} has invalid SearchAggregation '#{resource.search_aggregation}'. Must be one of: #{valid_aggregations.join(', ')}."
|
|
204
221
|
end
|
|
205
222
|
end
|
|
223
|
+
elsif resource.anomaly_detection == true
|
|
224
|
+
valid_operators = %w(GreaterThanUpperThreshold LessThanLowerOrGreaterThanUpperThreshold LessThanLowerThreshold)
|
|
225
|
+
unless valid_operators.include?(resource.comparison_operator)
|
|
226
|
+
@errors << "CfnGuardian::AlarmPropertyError - alarm #{resource.name} for resource #{resource.resource_id} has invalid ComparisonOperator '#{resource.comparison_operator}' for an AnomalyDetection alarm. Must be one of: #{valid_operators.join(', ')}."
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
if resource.threshold_overridden
|
|
230
|
+
@errors << "CfnGuardian::AlarmPropertyError - alarm #{resource.name} for resource #{resource.resource_id} cannot set both Threshold and AnomalyDetection. Anomaly detection alarms use StandardDeviation to size the expected band instead of a static Threshold."
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
%w(metric_name namespace).each do |property|
|
|
234
|
+
if resource.send(property).nil?
|
|
235
|
+
@errors << "CfnGuardian::AlarmPropertyError - alarm #{resource.name} for resource #{resource.resource_id} has nil value for property #{property.to_camelcase}. This could be due to incorrect spelling of a default alarm name or missing property #{property.to_camelcase} on a new alarm."
|
|
236
|
+
end
|
|
237
|
+
end
|
|
206
238
|
else
|
|
239
|
+
anomaly_only_operators = %w(GreaterThanUpperThreshold LessThanLowerOrGreaterThanUpperThreshold LessThanLowerThreshold)
|
|
240
|
+
if anomaly_only_operators.include?(resource.comparison_operator)
|
|
241
|
+
@errors << "CfnGuardian::AlarmPropertyError - alarm #{resource.name} for resource #{resource.resource_id} has ComparisonOperator '#{resource.comparison_operator}' which requires AnomalyDetection to be true. Either set AnomalyDetection: true or use a static-threshold ComparisonOperator."
|
|
242
|
+
end
|
|
243
|
+
|
|
207
244
|
%w(metric_name namespace).each do |property|
|
|
208
245
|
if resource.send(property).nil?
|
|
209
246
|
@errors << "CfnGuardian::AlarmPropertyError - alarm #{resource.name} for resource #{resource.resource_id} has nil value for property #{property.to_camelcase}. This could be due to incorrect spelling of a default alarm name or missing property #{property.to_camelcase} on a new alarm."
|
|
@@ -14,6 +14,8 @@ module CfnGuardian
|
|
|
14
14
|
|
|
15
15
|
@alarms.each do |alarm|
|
|
16
16
|
alarm_name = CfnGuardian::CloudWatch.get_alarm_name(alarm)
|
|
17
|
+
use_anomaly_detection = alarm.anomaly_detection == true
|
|
18
|
+
|
|
17
19
|
rows = [
|
|
18
20
|
['ResourceId', alarm.resource_id],
|
|
19
21
|
['ResourceHash', alarm.resource_hash],
|
|
@@ -21,7 +23,8 @@ module CfnGuardian
|
|
|
21
23
|
['Enabled', alarm.enabled],
|
|
22
24
|
['MetricName', alarm.metric_name],
|
|
23
25
|
['Dimensions', alarm.dimensions],
|
|
24
|
-
|
|
26
|
+
# An anomaly detection alarm uses ThresholdMetricId/StandardDeviation instead of a static Threshold.
|
|
27
|
+
['Threshold', use_anomaly_detection ? nil : alarm.threshold],
|
|
25
28
|
['Period', alarm.period],
|
|
26
29
|
['EvaluationPeriods', alarm.evaluation_periods],
|
|
27
30
|
['ComparisonOperator', alarm.comparison_operator],
|
|
@@ -33,9 +36,11 @@ module CfnGuardian
|
|
|
33
36
|
['Unit', alarm.unit],
|
|
34
37
|
['AlarmAction', alarm.alarm_action],
|
|
35
38
|
['OkActionDisabled', alarm.ok_action_disabled],
|
|
36
|
-
['TreatMissingData', alarm.treat_missing_data]
|
|
39
|
+
['TreatMissingData', alarm.treat_missing_data],
|
|
40
|
+
['AnomalyDetection', use_anomaly_detection ? alarm.anomaly_detection : nil],
|
|
41
|
+
['StandardDeviation', use_anomaly_detection ? (alarm.standard_deviation || 2) : nil]
|
|
37
42
|
]
|
|
38
|
-
|
|
43
|
+
|
|
39
44
|
rows.select! {|row| !row[1].nil?}
|
|
40
45
|
|
|
41
46
|
resp << {
|
|
@@ -53,30 +58,65 @@ module CfnGuardian
|
|
|
53
58
|
@alarms.each do |alarm|
|
|
54
59
|
alarm_name = CfnGuardian::CloudWatch.get_alarm_name(alarm)
|
|
55
60
|
metric_alarm = metric_alarms.find {|ma| ma.alarm_name.include? alarm_name}
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
use_anomaly_detection = alarm.anomaly_detection == true
|
|
62
|
+
deployed_anomaly_detection = anomaly_detection_deployed?(metric_alarm)
|
|
63
|
+
|
|
64
|
+
# A real deployed anomaly detection alarm has no top-level MetricName/Statistic/
|
|
65
|
+
# Period/Unit/Dimensions - CloudWatch only populates those nested inside the raw
|
|
66
|
+
# metric's MetricStat (see #deployed_metric_stat). Fall back to the top-level fields
|
|
67
|
+
# when the deployed alarm isn't a metric-math alarm at all (e.g. local config wants
|
|
68
|
+
# anomaly detection but the deployed alarm is still a plain static alarm).
|
|
69
|
+
metric_stat = deployed_metric_stat(metric_alarm)
|
|
70
|
+
if metric_stat
|
|
71
|
+
deployed_metric_name = metric_stat.metric.metric_name
|
|
72
|
+
deployed_dimensions = (metric_stat.metric.dimensions || []).map {|dim| {dim.name.to_sym => dim.value}}.inject(:merge)
|
|
73
|
+
deployed_period = metric_stat.period
|
|
74
|
+
deployed_statistic = metric_stat.stat
|
|
75
|
+
deployed_unit = metric_stat.unit
|
|
76
|
+
else
|
|
77
|
+
deployed_metric_name = metric_alarm.metric_name
|
|
78
|
+
deployed_dimensions = (metric_alarm.dimensions || []).map {|dim| {dim.name.to_sym => dim.value}}.inject(:merge)
|
|
79
|
+
deployed_period = metric_alarm.period
|
|
80
|
+
deployed_statistic = metric_alarm.statistic
|
|
81
|
+
deployed_unit = metric_alarm.unit
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# An anomaly detection alarm's MetricStat.Stat holds whichever of Statistic/
|
|
85
|
+
# ExtendedStatistic cfn-guardian generated (see stacks/resources.rb#add_alarm), so
|
|
86
|
+
# compare that combined value here and suppress the separate ExtendedStatistic row
|
|
87
|
+
# below - the same way Threshold is already suppressed for anomaly alarms.
|
|
88
|
+
local_statistic = use_anomaly_detection ? (alarm.extended_statistic || alarm.statistic) : alarm.statistic
|
|
89
|
+
|
|
58
90
|
rows = [
|
|
59
91
|
['ResourceId', alarm.resource_id, alarm.resource_id],
|
|
60
92
|
['ResourceHash', alarm.resource_hash, alarm.resource_hash],
|
|
61
93
|
['ResourceName', alarm.resource_name, alarm.resource_name],
|
|
62
94
|
['Enabled', alarm.enabled, true],
|
|
63
|
-
['MetricName', alarm.metric_name,
|
|
64
|
-
['Dimensions', alarm.dimensions,
|
|
65
|
-
|
|
66
|
-
|
|
95
|
+
['MetricName', alarm.metric_name, deployed_metric_name],
|
|
96
|
+
['Dimensions', alarm.dimensions, deployed_dimensions],
|
|
97
|
+
# A correctly deployed anomaly detection alarm has no static Threshold (it uses
|
|
98
|
+
# ThresholdMetricId/Metrics instead), so comparing Threshold here would always show
|
|
99
|
+
# as different. Compare AnomalyDetection/StandardDeviation below instead.
|
|
100
|
+
['Threshold', use_anomaly_detection ? nil : alarm.threshold.to_f, use_anomaly_detection ? nil : metric_alarm.threshold],
|
|
101
|
+
['Period', alarm.period, deployed_period],
|
|
67
102
|
['EvaluationPeriods', alarm.evaluation_periods, metric_alarm.evaluation_periods],
|
|
68
103
|
['ComparisonOperator', alarm.comparison_operator, metric_alarm.comparison_operator],
|
|
69
|
-
['Statistic',
|
|
104
|
+
['Statistic', local_statistic, deployed_statistic],
|
|
70
105
|
['ActionsEnabled', alarm.actions_enabled, metric_alarm.actions_enabled],
|
|
71
106
|
['DatapointsToAlarm', alarm.datapoints_to_alarm, metric_alarm.datapoints_to_alarm],
|
|
72
|
-
['ExtendedStatistic', alarm.extended_statistic, metric_alarm.extended_statistic],
|
|
107
|
+
['ExtendedStatistic', use_anomaly_detection ? nil : alarm.extended_statistic, use_anomaly_detection ? nil : metric_alarm.extended_statistic],
|
|
73
108
|
['EvaluateLowSampleCountPercentile', alarm.evaluate_low_sample_count_percentile, metric_alarm.evaluate_low_sample_count_percentile],
|
|
74
|
-
['Unit', alarm.unit,
|
|
109
|
+
['Unit', alarm.unit, deployed_unit],
|
|
75
110
|
['TreatMissingData', alarm.treat_missing_data, metric_alarm.treat_missing_data],
|
|
76
111
|
['AlarmAction', alarm.alarm_action, alarm.alarm_action],
|
|
77
112
|
['OkActionDisabled', alarm.ok_action_disabled]
|
|
78
113
|
]
|
|
79
|
-
|
|
114
|
+
|
|
115
|
+
if use_anomaly_detection || deployed_anomaly_detection
|
|
116
|
+
rows << ['AnomalyDetection', use_anomaly_detection, deployed_anomaly_detection]
|
|
117
|
+
rows << ['StandardDeviation', use_anomaly_detection ? (alarm.standard_deviation || 2).to_f : nil, deployed_standard_deviation(metric_alarm)]
|
|
118
|
+
end
|
|
119
|
+
|
|
80
120
|
rows.select! {|row| !row[1].nil?}.each {|row| colour_compare_row(row)}
|
|
81
121
|
|
|
82
122
|
if has_config_difference?(rows)
|
|
@@ -148,7 +188,40 @@ module CfnGuardian
|
|
|
148
188
|
end
|
|
149
189
|
|
|
150
190
|
private
|
|
151
|
-
|
|
191
|
+
|
|
192
|
+
# An anomaly detection alarm is identified on the deployed side by having a
|
|
193
|
+
# ThresholdMetricId set (it references the ANOMALY_DETECTION_BAND expression in Metrics).
|
|
194
|
+
def anomaly_detection_deployed?(metric_alarm)
|
|
195
|
+
!metric_alarm.threshold_metric_id.nil? && !metric_alarm.threshold_metric_id.to_s.empty?
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# For an anomaly detection alarm, CloudWatch does not populate the top-level MetricName/
|
|
199
|
+
# Namespace/Statistic/Period/Unit/Dimensions fields on describe_alarms - those live nested
|
|
200
|
+
# inside the raw metric's MetricStat, referenced by the Metrics array entry cfn-guardian
|
|
201
|
+
# generates with Id 'm1' (see stacks/resources.rb#add_alarm). Returns nil when the deployed
|
|
202
|
+
# alarm has no such entry (e.g. it's a plain static alarm, not managed via Metrics/MetricStat).
|
|
203
|
+
def deployed_metric_stat(metric_alarm)
|
|
204
|
+
raw_metric = (metric_alarm.metrics || []).find {|m| m.id == 'm1' && !m.metric_stat.nil?}
|
|
205
|
+
raw_metric.nil? ? nil : raw_metric.metric_stat
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Extracts the StandardDeviation from the deployed ANOMALY_DETECTION_BAND(m1, <stddev>)
|
|
209
|
+
# expression referenced by ThresholdMetricId. This is approximate: it assumes the band
|
|
210
|
+
# expression is in the same shape cfn-guardian generates, and returns nil if it can't be
|
|
211
|
+
# found or parsed (e.g. an anomaly alarm not managed by cfn-guardian). The number pattern
|
|
212
|
+
# also matches scientific notation (e.g. "1.0e-06"), since add_alarm embeds Ruby's
|
|
213
|
+
# Float#to_s form of StandardDeviation and to_s switches to that notation for very
|
|
214
|
+
# small (or very large) finite values.
|
|
215
|
+
def deployed_standard_deviation(metric_alarm)
|
|
216
|
+
return nil unless anomaly_detection_deployed?(metric_alarm)
|
|
217
|
+
|
|
218
|
+
band_metric = (metric_alarm.metrics || []).find {|m| m.id == metric_alarm.threshold_metric_id}
|
|
219
|
+
return nil if band_metric.nil? || band_metric.expression.nil?
|
|
220
|
+
|
|
221
|
+
match = band_metric.expression.match(/ANOMALY_DETECTION_BAND\([^,]+,\s*(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\s*\)/)
|
|
222
|
+
match.nil? ? nil : match[1].to_f
|
|
223
|
+
end
|
|
224
|
+
|
|
152
225
|
def has_config_difference?(rows)
|
|
153
226
|
rows.each do |row|
|
|
154
227
|
unless row[1].eql?(row[2])
|
|
@@ -6,8 +6,9 @@ module CfnGuardian
|
|
|
6
6
|
class BaseAlarm
|
|
7
7
|
|
|
8
8
|
attr_reader :type,
|
|
9
|
-
:resource_hash
|
|
10
|
-
|
|
9
|
+
:resource_hash,
|
|
10
|
+
:threshold_overridden
|
|
11
|
+
|
|
11
12
|
attr_accessor :group,
|
|
12
13
|
:name,
|
|
13
14
|
:metric_name,
|
|
@@ -33,8 +34,10 @@ module CfnGuardian
|
|
|
33
34
|
:additional_notifiers,
|
|
34
35
|
:tags,
|
|
35
36
|
:search_expression,
|
|
36
|
-
:search_aggregation
|
|
37
|
-
|
|
37
|
+
:search_aggregation,
|
|
38
|
+
:anomaly_detection,
|
|
39
|
+
:standard_deviation
|
|
40
|
+
|
|
38
41
|
def initialize(resource)
|
|
39
42
|
@type = 'Alarm'
|
|
40
43
|
@group = nil
|
|
@@ -43,6 +46,7 @@ module CfnGuardian
|
|
|
43
46
|
@namespace = nil
|
|
44
47
|
@dimensions = nil
|
|
45
48
|
@threshold = 0
|
|
49
|
+
@threshold_overridden = false
|
|
46
50
|
@period = 60
|
|
47
51
|
@evaluation_periods = 1
|
|
48
52
|
@comparison_operator = 'GreaterThanThreshold'
|
|
@@ -64,12 +68,23 @@ module CfnGuardian
|
|
|
64
68
|
@tags = {}
|
|
65
69
|
@search_expression = nil
|
|
66
70
|
@search_aggregation = nil
|
|
71
|
+
@anomaly_detection = false
|
|
72
|
+
@standard_deviation = nil
|
|
67
73
|
end
|
|
68
|
-
|
|
74
|
+
|
|
69
75
|
def metric_name=(metric_name)
|
|
70
76
|
raise ArgumentError.new("metric_name '#{metric_name}' must be of type String, provided type '#{metric_name.class}'") unless metric_name.is_a?(String)
|
|
71
77
|
@metric_name=metric_name
|
|
72
|
-
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Internal-only marker used to track that Threshold was explicitly set via a
|
|
81
|
+
# config override (as opposed to a resource group's own default_alarms
|
|
82
|
+
# definition). This is intentionally not exposed as a public writer so a
|
|
83
|
+
# YAML config cannot set ThresholdOverridden directly and bypass the
|
|
84
|
+
# "can't combine Threshold and AnomalyDetection" validation.
|
|
85
|
+
def mark_threshold_overridden!
|
|
86
|
+
@threshold_overridden = true
|
|
87
|
+
end
|
|
73
88
|
end
|
|
74
89
|
|
|
75
90
|
class AcmAlarm < BaseAlarm
|
|
@@ -256,6 +271,25 @@ module CfnGuardian
|
|
|
256
271
|
end
|
|
257
272
|
end
|
|
258
273
|
|
|
274
|
+
class ECSScheduledTaskAlarm < BaseAlarm
|
|
275
|
+
def initialize(resource)
|
|
276
|
+
super(resource)
|
|
277
|
+
@group = 'ECSScheduledTask'
|
|
278
|
+
@namespace = 'ECS/ContainerInsights'
|
|
279
|
+
@dimensions = { ClusterName: resource['Id'] }
|
|
280
|
+
if resource.has_key?('TaskDefinitionFamily')
|
|
281
|
+
@dimensions[:TaskDefinitionFamily] = resource['TaskDefinitionFamily']
|
|
282
|
+
# keep family alarms distinct from the cluster-wide and other family alarms on the same cluster.
|
|
283
|
+
# ECS cluster and family names can't contain a colon, so cluster `a` with family `b`
|
|
284
|
+
# can't share an alarm name with a cluster called `a-b`
|
|
285
|
+
@resource_hash = Digest::MD5.hexdigest [resource['Id'], resource['TaskDefinitionFamily']].to_json
|
|
286
|
+
@resource_name = "#{resource['Id']}:#{resource['TaskDefinitionFamily']}" if @resource_name.nil?
|
|
287
|
+
end
|
|
288
|
+
# scheduled tasks only publish metrics while they are running
|
|
289
|
+
@treat_missing_data = 'notBreaching'
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
259
293
|
class EKSContainerInsightsClusterAlarm < BaseAlarm
|
|
260
294
|
def initialize(resource)
|
|
261
295
|
super(resource)
|
|
@@ -95,6 +95,30 @@ module CfnGuardian
|
|
|
95
95
|
end
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
+
class ECSScheduledTaskEventSubscription < BaseEventSubscription
|
|
99
|
+
def initialize(resource)
|
|
100
|
+
super(resource)
|
|
101
|
+
@source = 'aws.ecs'
|
|
102
|
+
@detail_type = 'ECS Task State Change'
|
|
103
|
+
@hash = Digest::MD5.hexdigest [resource['Id'], resource['TaskDefinitionFamily']].to_json
|
|
104
|
+
@scope = {
|
|
105
|
+
'clusterArn' => ["arn:aws:ecs:${AWS::Region}:${AWS::AccountId}:cluster/#{resource['Id']}"],
|
|
106
|
+
# tasks started by an ECS service have a group of service:<name>
|
|
107
|
+
'group' => [{ 'anything-but' => { 'prefix' => 'service:' } }]
|
|
108
|
+
}
|
|
109
|
+
if resource.has_key?('TaskDefinitionFamily')
|
|
110
|
+
# the trailing colon stops family `app` matching `app-v2`
|
|
111
|
+
@scope['taskDefinitionArn'] = [{ 'prefix' => "arn:aws:ecs:${AWS::Region}:${AWS::AccountId}:task-definition/#{resource['TaskDefinitionFamily']}:" }]
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Always scope to the resource's cluster and family, including custom and
|
|
116
|
+
# inherited subscriptions that set their own Detail.
|
|
117
|
+
def detail
|
|
118
|
+
return @detail.merge(@scope)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
98
122
|
class AcmEventSubscription < BaseEventSubscription; end
|
|
99
123
|
class ApiGatewayEventSubscription < BaseEventSubscription; end
|
|
100
124
|
class ApplicationTargetGroupEventSubscription < BaseEventSubscription; end
|
|
@@ -251,6 +251,12 @@ module CfnGuardian::Resource
|
|
|
251
251
|
logger.debug("overriding #{obj.type} property '#{attr}' with value #{value} for resource id: #{obj.resource_id}")
|
|
252
252
|
begin
|
|
253
253
|
obj.send("#{attr.to_underscore}=",value.clone)
|
|
254
|
+
# Track that Threshold was set via a config override (as opposed to a resource
|
|
255
|
+
# group's own default_alarms definition) so validate_resources can detect a
|
|
256
|
+
# config that sets both a static Threshold and AnomalyDetection. This uses a
|
|
257
|
+
# dedicated marker method (not a public threshold_overridden= writer) so a
|
|
258
|
+
# config can't set ThresholdOverridden directly to bypass that validation.
|
|
259
|
+
obj.mark_threshold_overridden! if attr.to_underscore == 'threshold' && obj.respond_to?(:mark_threshold_overridden!)
|
|
254
260
|
rescue NoMethodError => e
|
|
255
261
|
if !e.message.match?(/inherit/)
|
|
256
262
|
logger.warn "Unknown property '#{attr}' for type: #{obj.type} and resource id: #{obj.resource_id}"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module CfnGuardian::Resource
|
|
2
|
+
# Monitors ECS tasks that run outside of an ECS service, such as tasks launched
|
|
3
|
+
# by an EventBridge rule or EventBridge Scheduler. These tasks have no service
|
|
4
|
+
# metrics, so failures are detected from the ECS Task State Change events.
|
|
5
|
+
# The cluster and task definition family scope is applied by
|
|
6
|
+
# ECSScheduledTaskEventSubscription, so custom subscriptions get it too.
|
|
7
|
+
class ECSScheduledTask < Base
|
|
8
|
+
|
|
9
|
+
def default_event_subscriptions()
|
|
10
|
+
event_subscription = CfnGuardian::Models::ECSScheduledTaskEventSubscription.new(@resource)
|
|
11
|
+
event_subscription.name = 'TaskFailed'
|
|
12
|
+
event_subscription.detail = {
|
|
13
|
+
'lastStatus' => ['STOPPED'],
|
|
14
|
+
# numeric matching skips containers with a null or missing exit code,
|
|
15
|
+
# which `anything-but: 0` would match
|
|
16
|
+
'containers' => { 'exitCode' => [{ 'numeric' => ['>', 0] }] }
|
|
17
|
+
}
|
|
18
|
+
@event_subscriptions.push(event_subscription)
|
|
19
|
+
|
|
20
|
+
event_subscription = CfnGuardian::Models::ECSScheduledTaskEventSubscription.new(@resource)
|
|
21
|
+
event_subscription.name = 'TaskFailedToStart'
|
|
22
|
+
event_subscription.detail = {
|
|
23
|
+
'lastStatus' => ['STOPPED'],
|
|
24
|
+
'stopCode' => ['TaskFailedToStart']
|
|
25
|
+
}
|
|
26
|
+
@event_subscriptions.push(event_subscription)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -34,6 +34,7 @@ module CfnGuardian
|
|
|
34
34
|
actions = alarm.alarm_action.kind_of?(Array) ? alarm.alarm_action.map{|action| Ref(action)} : [Ref(alarm.alarm_action)]
|
|
35
35
|
actions.concat alarm.maintenance_groups.map {|mg| Ref(mg)} if alarm.maintenance_groups.any?
|
|
36
36
|
use_search = alarm.search_expression.is_a?(String) && !alarm.search_expression.strip.empty?
|
|
37
|
+
use_anomaly_detection = alarm.anomaly_detection == true
|
|
37
38
|
|
|
38
39
|
@template.declare do
|
|
39
40
|
CloudWatch_Alarm("#{alarm.resource_hash}#{alarm.group}#{alarm.name.gsub(/[^0-9a-zA-Z]/i, '')}#{alarm.type}"[0..255]) do
|
|
@@ -42,7 +43,9 @@ module CfnGuardian
|
|
|
42
43
|
AlarmName CfnGuardian::CloudWatch.get_alarm_name(alarm)
|
|
43
44
|
ComparisonOperator alarm.comparison_operator
|
|
44
45
|
EvaluationPeriods alarm.evaluation_periods
|
|
45
|
-
Threshold
|
|
46
|
+
# Threshold and ThresholdMetricId are mutually exclusive on AWS::CloudWatch::Alarm -
|
|
47
|
+
# an anomaly detection alarm uses ThresholdMetricId (set below) instead of a static Threshold.
|
|
48
|
+
Threshold alarm.threshold unless use_anomaly_detection
|
|
46
49
|
AlarmActions actions
|
|
47
50
|
OKActions actions unless alarm.ok_action_disabled
|
|
48
51
|
TreatMissingData alarm.treat_missing_data unless alarm.treat_missing_data.nil?
|
|
@@ -63,6 +66,34 @@ module CfnGuardian
|
|
|
63
66
|
ReturnData: true
|
|
64
67
|
}
|
|
65
68
|
]
|
|
69
|
+
elsif use_anomaly_detection
|
|
70
|
+
band_width = alarm.standard_deviation || 2
|
|
71
|
+
metric_stat = {
|
|
72
|
+
Metric: {
|
|
73
|
+
Namespace: alarm.namespace,
|
|
74
|
+
MetricName: alarm.metric_name
|
|
75
|
+
},
|
|
76
|
+
Period: alarm.period,
|
|
77
|
+
Stat: alarm.extended_statistic.nil? ? alarm.statistic : alarm.extended_statistic
|
|
78
|
+
}
|
|
79
|
+
metric_stat[:Metric][:Dimensions] = alarm.dimensions.map {|k,v| {Name: k, Value: v}} unless alarm.dimensions.nil?
|
|
80
|
+
metric_stat[:Unit] = alarm.unit unless alarm.unit.nil?
|
|
81
|
+
|
|
82
|
+
Metrics [
|
|
83
|
+
{
|
|
84
|
+
Id: 'm1',
|
|
85
|
+
MetricStat: metric_stat,
|
|
86
|
+
ReturnData: true
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
Id: 'ad1',
|
|
90
|
+
Expression: "ANOMALY_DETECTION_BAND(m1, #{band_width})",
|
|
91
|
+
Label: "#{alarm.metric_name} (expected)",
|
|
92
|
+
ReturnData: true
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
ThresholdMetricId 'ad1'
|
|
96
|
+
EvaluateLowSampleCountPercentile alarm.evaluate_low_sample_count_percentile unless alarm.evaluate_low_sample_count_percentile.nil?
|
|
66
97
|
else
|
|
67
98
|
Dimensions alarm.dimensions.map {|k,v| {Name: k, Value: v}} unless alarm.dimensions.nil?
|
|
68
99
|
Statistic alarm.statistic if alarm.extended_statistic.nil?
|
data/lib/cfnguardian/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cfn-guardian
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.15.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Guslington
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-11-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: thor
|
|
@@ -312,6 +312,7 @@ files:
|
|
|
312
312
|
- cfn-guardian.gemspec
|
|
313
313
|
- docs/alarm_tags.md
|
|
314
314
|
- docs/alarm_templates.md
|
|
315
|
+
- docs/anomaly_detection.md
|
|
315
316
|
- docs/cli.md
|
|
316
317
|
- docs/composite_alarms.md
|
|
317
318
|
- docs/custom_checks/azure_file_check.md
|
|
@@ -326,6 +327,7 @@ files:
|
|
|
326
327
|
- docs/custom_checks/tls.md
|
|
327
328
|
- docs/custom_checks/websocket.md
|
|
328
329
|
- docs/custom_metrics.md
|
|
330
|
+
- docs/ecs_scheduled_tasks.md
|
|
329
331
|
- docs/event_subscriptions.md
|
|
330
332
|
- docs/maintenance_mode.md
|
|
331
333
|
- docs/notifiers.md
|
|
@@ -368,6 +370,7 @@ files:
|
|
|
368
370
|
- lib/cfnguardian/resources/dynamodb_table.rb
|
|
369
371
|
- lib/cfnguardian/resources/ec2_instance.rb
|
|
370
372
|
- lib/cfnguardian/resources/ecs_cluster.rb
|
|
373
|
+
- lib/cfnguardian/resources/ecs_scheduled_task.rb
|
|
371
374
|
- lib/cfnguardian/resources/ecs_service.rb
|
|
372
375
|
- lib/cfnguardian/resources/eks_container_insights.rb
|
|
373
376
|
- lib/cfnguardian/resources/elastic_file_system.rb
|