diff --git a/.changelog/28560.txt b/.changelog/28560.txt new file mode 100644 index 00000000000..e982bd00133 --- /dev/null +++ b/.changelog/28560.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_autoscaling_policy: Add `metrics` to the `target_tracking_configuration.customized_metric_specification` configuration block in support of [metric math](https://docs.aws.amazon.com/autoscaling/ec2/userguide/ec2-auto-scaling-target-tracking-metric-math.html) +``` diff --git a/internal/service/autoscaling/policy.go b/internal/service/autoscaling/policy.go index c201ccd397f..75ba6786191 100644 --- a/internal/service/autoscaling/policy.go +++ b/internal/service/autoscaling/policy.go @@ -263,8 +263,9 @@ func ResourcePolicy() *schema.Resource { Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "metric_dimension": { - Type: schema.TypeList, - Optional: true, + Type: schema.TypeList, + Optional: true, + ConflictsWith: []string{"target_tracking_configuration.0.customized_metric_specification.0.metrics"}, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { @@ -279,20 +280,104 @@ func ResourcePolicy() *schema.Resource { }, }, "metric_name": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"target_tracking_configuration.0.customized_metric_specification.0.metrics"}, + }, + "metrics": { + Type: schema.TypeSet, + Optional: true, + ConflictsWith: []string{"target_tracking_configuration.0.customized_metric_specification.0.metric_dimension", "target_tracking_configuration.0.customized_metric_specification.0.metric_name", "target_tracking_configuration.0.customized_metric_specification.0.namespace", "target_tracking_configuration.0.customized_metric_specification.0.statistic", "target_tracking_configuration.0.customized_metric_specification.0.unit"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "expression": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(1, 2047), + }, + "id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 255), + }, + "label": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(1, 2047), + }, + "metric_stat": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "metric": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "dimensions": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "value": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + "metric_name": { + Type: schema.TypeString, + Required: true, + }, + "namespace": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + "stat": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 100), + }, + "unit": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "return_data": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + }, + }, }, "namespace": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"target_tracking_configuration.0.customized_metric_specification.0.metrics"}, }, "statistic": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"target_tracking_configuration.0.customized_metric_specification.0.metrics"}, }, "unit": { - Type: schema.TypeString, - Optional: true, + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"target_tracking_configuration.0.customized_metric_specification.0.metrics"}, }, }, }, @@ -714,32 +799,89 @@ func expandTargetTrackingConfiguration(configs []interface{}) *autoscaling.Targe } if v, ok := config["customized_metric_specification"]; ok && len(v.([]interface{})) > 0 { spec := v.([]interface{})[0].(map[string]interface{}) - customSpec := &autoscaling.CustomizedMetricSpecification{ - Namespace: aws.String(spec["namespace"].(string)), - MetricName: aws.String(spec["metric_name"].(string)), - Statistic: aws.String(spec["statistic"].(string)), - } - if val, ok := spec["unit"]; ok && len(val.(string)) > 0 { - customSpec.Unit = aws.String(val.(string)) - } - if val, ok := spec["metric_dimension"]; ok { - dims := val.([]interface{}) - metDimList := make([]*autoscaling.MetricDimension, len(dims)) - for i := range metDimList { - dim := dims[i].(map[string]interface{}) - md := &autoscaling.MetricDimension{ - Name: aws.String(dim["name"].(string)), - Value: aws.String(dim["value"].(string)), + customSpec := &autoscaling.CustomizedMetricSpecification{} + if val, ok := spec["metrics"].(*schema.Set); ok && val.Len() > 0 { + customSpec.Metrics = expandTargetTrackingMetricDataQueries(val.List()) + } else { + customSpec.Namespace = aws.String(spec["namespace"].(string)) + customSpec.MetricName = aws.String(spec["metric_name"].(string)) + customSpec.Statistic = aws.String(spec["statistic"].(string)) + if val, ok := spec["unit"]; ok && len(val.(string)) > 0 { + customSpec.Unit = aws.String(val.(string)) + } + if val, ok := spec["metric_dimension"]; ok { + dims := val.([]interface{}) + metDimList := make([]*autoscaling.MetricDimension, len(dims)) + for i := range metDimList { + dim := dims[i].(map[string]interface{}) + md := &autoscaling.MetricDimension{ + Name: aws.String(dim["name"].(string)), + Value: aws.String(dim["value"].(string)), + } + metDimList[i] = md } - metDimList[i] = md + customSpec.Dimensions = metDimList } - customSpec.Dimensions = metDimList } result.CustomizedMetricSpecification = customSpec } return result } +func expandTargetTrackingMetricDataQueries(metricDataQuerySlices []interface{}) []*autoscaling.TargetTrackingMetricDataQuery { + if metricDataQuerySlices == nil || len(metricDataQuerySlices) < 1 { + return nil + } + metricDataQueries := make([]*autoscaling.TargetTrackingMetricDataQuery, len(metricDataQuerySlices)) + + for i := range metricDataQueries { + metricDataQueryFlat := metricDataQuerySlices[i].(map[string]interface{}) + metricDataQuery := &autoscaling.TargetTrackingMetricDataQuery{ + Id: aws.String(metricDataQueryFlat["id"].(string)), + } + if val, ok := metricDataQueryFlat["metric_stat"]; ok && len(val.([]interface{})) > 0 { + metricStatSpec := val.([]interface{})[0].(map[string]interface{}) + metricSpec := metricStatSpec["metric"].([]interface{})[0].(map[string]interface{}) + metric := &autoscaling.Metric{ + MetricName: aws.String(metricSpec["metric_name"].(string)), + Namespace: aws.String(metricSpec["namespace"].(string)), + } + if v, ok := metricSpec["dimensions"]; ok { + dims := v.(*schema.Set).List() + dimList := make([]*autoscaling.MetricDimension, len(dims)) + for i := range dimList { + dim := dims[i].(map[string]interface{}) + md := &autoscaling.MetricDimension{ + Name: aws.String(dim["name"].(string)), + Value: aws.String(dim["value"].(string)), + } + dimList[i] = md + } + metric.Dimensions = dimList + } + metricStat := &autoscaling.TargetTrackingMetricStat{ + Metric: metric, + Stat: aws.String(metricStatSpec["stat"].(string)), + } + if v, ok := metricStatSpec["unit"]; ok && len(v.(string)) > 0 { + metricStat.Unit = aws.String(v.(string)) + } + metricDataQuery.MetricStat = metricStat + } + if val, ok := metricDataQueryFlat["expression"]; ok && val.(string) != "" { + metricDataQuery.Expression = aws.String(val.(string)) + } + if val, ok := metricDataQueryFlat["label"]; ok && val.(string) != "" { + metricDataQuery.Label = aws.String(val.(string)) + } + if val, ok := metricDataQueryFlat["return_data"]; ok { + metricDataQuery.ReturnData = aws.Bool(val.(bool)) + } + metricDataQueries[i] = metricDataQuery + } + return metricDataQueries +} + func expandPredictiveScalingConfig(predictiveScalingConfigSlice []interface{}) *autoscaling.PredictiveScalingConfiguration { if predictiveScalingConfigSlice == nil || len(predictiveScalingConfigSlice) < 1 { return nil @@ -923,28 +1065,77 @@ func flattenTargetTrackingConfiguration(config *autoscaling.TargetTrackingConfig } if config.CustomizedMetricSpecification != nil { spec := map[string]interface{}{} - spec["metric_name"] = aws.StringValue(config.CustomizedMetricSpecification.MetricName) - spec["namespace"] = aws.StringValue(config.CustomizedMetricSpecification.Namespace) - spec["statistic"] = aws.StringValue(config.CustomizedMetricSpecification.Statistic) - if config.CustomizedMetricSpecification.Unit != nil { - spec["unit"] = aws.StringValue(config.CustomizedMetricSpecification.Unit) - } - if config.CustomizedMetricSpecification.Dimensions != nil { - dimSpec := make([]interface{}, len(config.CustomizedMetricSpecification.Dimensions)) - for i := range dimSpec { - dim := map[string]interface{}{} - rawDim := config.CustomizedMetricSpecification.Dimensions[i] - dim["name"] = aws.StringValue(rawDim.Name) - dim["value"] = aws.StringValue(rawDim.Value) - dimSpec[i] = dim + if config.CustomizedMetricSpecification.Metrics != nil { + spec["metrics"] = flattenTargetTrackingMetricDataQueries(config.CustomizedMetricSpecification.Metrics) + } else { + spec["metric_name"] = aws.StringValue(config.CustomizedMetricSpecification.MetricName) + spec["namespace"] = aws.StringValue(config.CustomizedMetricSpecification.Namespace) + spec["statistic"] = aws.StringValue(config.CustomizedMetricSpecification.Statistic) + if config.CustomizedMetricSpecification.Unit != nil { + spec["unit"] = aws.StringValue(config.CustomizedMetricSpecification.Unit) + } + if config.CustomizedMetricSpecification.Dimensions != nil { + dimSpec := make([]interface{}, len(config.CustomizedMetricSpecification.Dimensions)) + for i := range dimSpec { + dim := map[string]interface{}{} + rawDim := config.CustomizedMetricSpecification.Dimensions[i] + dim["name"] = aws.StringValue(rawDim.Name) + dim["value"] = aws.StringValue(rawDim.Value) + dimSpec[i] = dim + } + spec["metric_dimension"] = dimSpec } - spec["metric_dimension"] = dimSpec } result["customized_metric_specification"] = []map[string]interface{}{spec} } return []interface{}{result} } +func flattenTargetTrackingMetricDataQueries(metricDataQueries []*autoscaling.TargetTrackingMetricDataQuery) []interface{} { + metricDataQueriesSpec := make([]interface{}, len(metricDataQueries)) + for i := range metricDataQueriesSpec { + metricDataQuery := map[string]interface{}{} + rawMetricDataQuery := metricDataQueries[i] + metricDataQuery["id"] = aws.StringValue(rawMetricDataQuery.Id) + if rawMetricDataQuery.Expression != nil { + metricDataQuery["expression"] = aws.StringValue(rawMetricDataQuery.Expression) + } + if rawMetricDataQuery.Label != nil { + metricDataQuery["label"] = aws.StringValue(rawMetricDataQuery.Label) + } + if rawMetricDataQuery.MetricStat != nil { + metricStatSpec := map[string]interface{}{} + rawMetricStat := rawMetricDataQuery.MetricStat + rawMetric := rawMetricStat.Metric + metricSpec := map[string]interface{}{} + if rawMetric.Dimensions != nil { + dimSpec := make([]interface{}, len(rawMetric.Dimensions)) + for i := range dimSpec { + dim := map[string]interface{}{} + rawDim := rawMetric.Dimensions[i] + dim["name"] = aws.StringValue(rawDim.Name) + dim["value"] = aws.StringValue(rawDim.Value) + dimSpec[i] = dim + } + metricSpec["dimensions"] = dimSpec + } + metricSpec["metric_name"] = aws.StringValue(rawMetric.MetricName) + metricSpec["namespace"] = aws.StringValue(rawMetric.Namespace) + metricStatSpec["metric"] = []map[string]interface{}{metricSpec} + metricStatSpec["stat"] = aws.StringValue(rawMetricStat.Stat) + if rawMetricStat.Unit != nil { + metricStatSpec["unit"] = aws.StringValue(rawMetricStat.Unit) + } + metricDataQuery["metric_stat"] = []map[string]interface{}{metricStatSpec} + } + if rawMetricDataQuery.ReturnData != nil { + metricDataQuery["return_data"] = aws.BoolValue(rawMetricDataQuery.ReturnData) + } + metricDataQueriesSpec[i] = metricDataQuery + } + return metricDataQueriesSpec +} + func flattenPredictiveScalingConfig(predictiveScalingConfig *autoscaling.PredictiveScalingConfiguration) []map[string]interface{} { predictiveScalingConfigFlat := map[string]interface{}{} if predictiveScalingConfig == nil { diff --git a/internal/service/autoscaling/policy_test.go b/internal/service/autoscaling/policy_test.go index 86921ee329e..7ce005ea3c1 100644 --- a/internal/service/autoscaling/policy_test.go +++ b/internal/service/autoscaling/policy_test.go @@ -469,6 +469,34 @@ func TestAccAutoScalingPolicy_TargetTrack_custom(t *testing.T) { }) } +func TestAccAutoScalingPolicy_TargetTrack_metricMath(t *testing.T) { + ctx := acctest.Context(t) + var v autoscaling.ScalingPolicy + resourceName := "aws_autoscaling_policy.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, autoscaling.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckPolicyDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccPolicyConfig_targetTrackingMetricMath(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckScalingPolicyExists(ctx, resourceName, &v), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccPolicyImportStateIdFunc(resourceName), + ImportStateVerify: true, + }, + }, + }) +} + func TestAccAutoScalingPolicy_zeroValue(t *testing.T) { ctx := acctest.Context(t) var v1, v2 autoscaling.ScalingPolicy @@ -923,6 +951,65 @@ resource "aws_autoscaling_policy" "test" { `, rName)) } +func testAccPolicyConfig_targetTrackingMetricMath(rName string) string { + return acctest.ConfigCompose(testAccPolicyConfigBase(rName), fmt.Sprintf(` +resource "aws_autoscaling_policy" "test" { + name = "%[1]s-tracking" + policy_type = "TargetTrackingScaling" + autoscaling_group_name = aws_autoscaling_group.test.name + + target_tracking_configuration { + customized_metric_specification { + metrics { + id = "m1" + expression = "TIME_SERIES(20)" + return_data = false + } + metrics { + id = "m2" + metric_stat { + metric { + namespace = "foo" + metric_name = "bar" + } + unit = "Percent" + stat = "Sum" + } + return_data = false + } + metrics { + id = "m3" + metric_stat { + metric { + namespace = "foo" + metric_name = "bar" + dimensions { + name = "x" + value = "y" + } + dimensions { + name = "y" + value = "x" + } + } + unit = "Percent" + stat = "Sum" + } + return_data = false + } + metrics { + id = "e1" + expression = "m1 + m2 + m3" + return_data = true + } + } + + target_value = 12.3 + } +} +`, rName)) +} + func testAccPolicyConfig_zeroValue(rName string) string { return acctest.ConfigCompose(testAccPolicyConfigBase(rName), fmt.Sprintf(` resource "aws_autoscaling_policy" "test_simple" { diff --git a/website/docs/r/autoscaling_policy.html.markdown b/website/docs/r/autoscaling_policy.html.markdown index aa29813d1fd..1e4445f7278 100644 --- a/website/docs/r/autoscaling_policy.html.markdown +++ b/website/docs/r/autoscaling_policy.html.markdown @@ -41,6 +41,59 @@ resource "aws_autoscaling_group" "bar" { } ``` +### Create target tarcking scaling policy using metric math + +```terraform +resource "aws_autoscaling_policy" "example" { + autoscaling_group_name = "my-test-asg" + name = "foo" + policy_type = "TargetTrackingScaling" + target_tracking_configuration { + target_value = 100 + customized_metric_specification { + metrics { + label = "Get the queue size (the number of messages waiting to be processed)" + id = "m1" + metric_stat { + metric { + namespace = "AWS/SQS" + metric_name = "ApproximateNumberOfMessagesVisible" + dimensions { + name = "QueueName" + value = "my-queue" + } + } + stat = "Sum" + } + return_data = false + } + metrics { + label = "Get the group size (the number of InService instances)" + id = "m2" + metric_stat { + metric { + namespace = "AWS/AutoScaling" + metric_name = "GroupInServiceInstances" + dimensions { + name = "AutoScalingGroupName" + value = "my-asg" + } + } + stat = "Average" + } + return_data = false + } + metrics { + label = "Calculate the backlog per instance" + id = "e1" + expression = "m1 / m2" + return_data = true + } + } + } +} +``` + ### Create predictive scaling policy using customized metrics ```terraform @@ -214,10 +267,11 @@ The following arguments are supported: The following arguments are supported: * `metric_dimension` - (Optional) Dimensions of the metric. -* `metric_name` - (Required) Name of the metric. -* `namespace` - (Required) Namespace of the metric. -* `statistic` - (Required) Statistic of the metric. +* `metric_name` - (Optional) Name of the metric. +* `namespace` - (Optional) Namespace of the metric. +* `statistic` - (Optional) Statistic of the metric. * `unit` - (Optional) Unit of the metric. +* `metrics` - (Optional) Metrics to include, as a metric data query. #### metric_dimension @@ -226,6 +280,39 @@ The following arguments are supported: * `name` - (Required) Name of the dimension. * `value` - (Required) Value of the dimension. +#### metrics + +The following arguments are supported: + +* `expression` - (Optional) Math expression used on the returned metric. You must specify either `expression` or `metric_stat`, but not both. +* `id` - (Required) Short name for the metric used in target tracking scaling policy. +* `label` - (Optional) Human-readable label for this metric or expression. +* `metric_stat` - (Optional) Structure that defines CloudWatch metric to be used in target tracking scaling policy. You must specify either `expression` or `metric_stat`, but not both. +* `return_data` - (Optional) Boolean that indicates whether to return the timestamps and raw data values of this metric, the default is true + +##### metric_stat + +The following arguments are supported: + +* `metric` - (Required) Structure that defines the CloudWatch metric to return, including the metric name, namespace, and dimensions. +* `stat` - (Required) Statistic of the metrics to return. +* `unit` - (Optional) Unit of the metrics to return. + +##### metric + +The following arguments are supported: + +* `dimensions` - (Optional) Dimensions of the metric. +* `metric_name` - (Required) Name of the metric. +* `namespace` - (Required) Namespace of the metric. + +###### dimensions + +The following arguments are supported: + +* `name` - (Required) Name of the dimension. +* `value` - (Required) Value of the dimension. + ### predictive_scaling_configuration The following arguments are supported: @@ -269,30 +356,35 @@ The following arguments are supported: * `resource_label` - (Required) Label that uniquely identifies a specific Application Load Balancer target group from which to determine the request count served by your Auto Scaling group. ##### customized_scaling_metric_specification + The following arguments are supported: * `metric_data_queries` - (Required) List of up to 10 structures that defines custom scaling metric in predictive scaling policy ##### customized_load_metric_specification + The following arguments are supported: * `metric_data_queries` - (Required) List of up to 10 structures that defines custom load metric in predictive scaling policy ##### customized_capacity_metric_specification + The following arguments are supported: * `metric_data_queries` - (Required) List of up to 10 structures that defines custom capacity metric in predictive scaling policy ##### metric_data_queries + The following arguments are supported: * `expression` - (Optional) Math expression used on the returned metric. You must specify either `expression` or `metric_stat`, but not both. * `id` - (Required) Short name for the metric used in predictive scaling policy. -* `metric_stat` - (Optional) Structure that defines CloudWatch metric to be used in predictive scaling policy. You must specify either `expression` or `metric_stat`, but not both. * `label` - (Optional) Human-readable label for this metric or expression. -* `return_data` - (Optional) Boolean that indicates whether to return the timestamps and raw data values of this metric, the default it true +* `metric_stat` - (Optional) Structure that defines CloudWatch metric to be used in predictive scaling policy. You must specify either `expression` or `metric_stat`, but not both. +* `return_data` - (Optional) Boolean that indicates whether to return the timestamps and raw data values of this metric, the default is true ##### metric_stat + The following arguments are supported: * `metric` - (Required) Structure that defines the CloudWatch metric to return, including the metric name, namespace, and dimensions. @@ -300,6 +392,7 @@ The following arguments are supported: * `unit` - (Optional) Unit of the metrics to return. ##### metric + The following arguments are supported: * `dimensions` - (Optional) Dimensions of the metric. @@ -307,6 +400,7 @@ The following arguments are supported: * `namespace` - (Required) Namespace of the metric. ##### dimensions + The following arguments are supported: * `name` - (Required) Name of the dimension.