diff --git a/CHANGELOG.md b/CHANGELOG.md index d34a53f1c81..b07cc72d10e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ IMPROVEMENTS: * provider: Add support for Task Roles when running on ECS or CodeBuild [GH-1425] * resource/aws_instance: New `user_data_base64` attribute that allows non-UTF8 data (such as gzip) to be assigned to user-data without corruption [GH-850] * data-source/aws_vpc: Expose enable_dns_* in aws_vpc data_source [GH-1373] +* resource/aws_appautoscaling_policy: Add support for DynamoDB [GH-1650] * resource/aws_directory_service_directory: Add support for `tags` [GH-1398] * resource/aws_rds_cluster: Allow setting of rds cluster engine [GH-1415] * resource/aws_ssm_association: now supports update for `parameters`, `schedule_expression`,`output_location` [GH-1421] @@ -44,7 +45,7 @@ IMPROVEMENTS: * resource/aws_nat_gateway: Add tags support [GH-1625] * resource/aws_route53_record: Add support for Route53 multi-value answer routing policy [GH-1686] * resource/aws_instance: Read iops only when volume type is io1 [GH-1573] -* Allow RDS Cluster / Cluster instance to specify the engine [GH-1591] +* resource/aws_rds_cluster(+_instance) Allow specifying the engine [GH-1591] * resource/aws_cloudwatch_event_target: Add Input transformer for Cloudwatch Events [GH-1343] * resource/aws_directory_service_directory: Support Import functionality [GH-1732] diff --git a/aws/resource_aws_appautoscaling_policy.go b/aws/resource_aws_appautoscaling_policy.go index 9ea31f4b490..673026f5a29 100644 --- a/aws/resource_aws_appautoscaling_policy.go +++ b/aws/resource_aws_appautoscaling_policy.go @@ -5,10 +5,12 @@ import ( "fmt" "log" "strconv" + "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/applicationautoscaling" "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" ) @@ -153,6 +155,95 @@ func resourceAwsAppautoscalingPolicy() *schema.Resource { }, }, }, + "target_tracking_scaling_policy_configuration": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "customized_metric_specification": &schema.Schema{ + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + ConflictsWith: []string{"target_tracking_scaling_policy_configuration.0.predefined_metric_specification"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "dimensions": &schema.Schema{ + 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": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "namespace": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "statistic": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ValidateFunc: validateAppautoscalingCustomizedMetricSpecificationStatistic, + }, + "unit": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "predefined_metric_specification": &schema.Schema{ + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + ConflictsWith: []string{"target_tracking_scaling_policy_configuration.0.customized_metric_specification"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "predefined_metric_type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ValidateFunc: validateAppautoscalingPredefinedMetricSpecification, + }, + "resource_label": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateAppautoscalingPredefinedResourceLabel, + }, + }, + }, + }, + "disable_scale_in": &schema.Schema{ + Type: schema.TypeBool, + Default: false, + Optional: true, + }, + "scale_in_cooldown": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + "scale_out_cooldown": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + "target_value": &schema.Schema{ + Type: schema.TypeFloat, + Required: true, + }, + }, + }, + }, }, } } @@ -166,9 +257,20 @@ func resourceAwsAppautoscalingPolicyCreate(d *schema.ResourceData, meta interfac } log.Printf("[DEBUG] ApplicationAutoScaling PutScalingPolicy: %#v", params) - resp, err := conn.PutScalingPolicy(¶ms) + var resp *applicationautoscaling.PutScalingPolicyOutput + err = resource.Retry(1*time.Minute, func() *resource.RetryError { + var err error + resp, err = conn.PutScalingPolicy(¶ms) + if err != nil { + if isAWSErr(err, "FailedResourceAccessException", "is not authorized to perform") { + return resource.RetryableError(err) + } + return resource.NonRetryableError(fmt.Errorf("Error putting scaling policy: %s", err)) + } + return nil + }) if err != nil { - return fmt.Errorf("Error putting scaling policy: %s", err) + return err } d.Set("arn", resp.PolicyARN) @@ -198,6 +300,8 @@ func resourceAwsAppautoscalingPolicyRead(d *schema.ResourceData, meta interface{ d.Set("service_namespace", p.ServiceNamespace) d.Set("alarms", p.Alarms) d.Set("step_scaling_policy_configuration", flattenStepScalingPolicyConfiguration(p.StepScalingPolicyConfiguration)) + d.Set("target_tracking_scaling_policy_configuration", + flattenTargetTrackingScalingPolicyConfiguration(p.TargetTrackingScalingPolicyConfiguration)) return nil } @@ -304,6 +408,59 @@ func expandAppautoscalingStepAdjustments(configured []interface{}) ([]*applicati return adjustments, nil } +func expandAppautoscalingCustomizedMetricSpecification(configured []interface{}) *applicationautoscaling.CustomizedMetricSpecification { + spec := &applicationautoscaling.CustomizedMetricSpecification{} + + for _, raw := range configured { + data := raw.(map[string]interface{}) + if v, ok := data["metric_name"]; ok { + spec.MetricName = aws.String(v.(string)) + } + + if v, ok := data["namespace"]; ok { + spec.Namespace = aws.String(v.(string)) + } + + if v, ok := data["unit"].(string); ok && v != "" { + spec.Unit = aws.String(v) + } + + if v, ok := data["statistic"]; ok { + spec.Statistic = aws.String(v.(string)) + } + + if s, ok := data["dimensions"].(*schema.Set); ok && s.Len() > 0 { + dimensions := make([]*applicationautoscaling.MetricDimension, s.Len(), s.Len()) + for i, d := range s.List() { + dimension := d.(map[string]interface{}) + dimensions[i] = &applicationautoscaling.MetricDimension{ + Name: aws.String(dimension["name"].(string)), + Value: aws.String(dimension["value"].(string)), + } + } + spec.Dimensions = dimensions + } + } + return spec +} + +func expandAppautoscalingPredefinedMetricSpecification(configured []interface{}) *applicationautoscaling.PredefinedMetricSpecification { + spec := &applicationautoscaling.PredefinedMetricSpecification{} + + for _, raw := range configured { + data := raw.(map[string]interface{}) + + if v, ok := data["predefined_metric_type"]; ok { + spec.PredefinedMetricType = aws.String(v.(string)) + } + + if v, ok := data["resource_label"].(string); ok && v != "" { + spec.ResourceLabel = aws.String(v) + } + } + return spec +} + func getAwsAppautoscalingPutScalingPolicyInput(d *schema.ResourceData) (applicationautoscaling.PutScalingPolicyInput, error) { var params = applicationautoscaling.PutScalingPolicyInput{ PolicyName: aws.String(d.Get("name").(string)), @@ -363,6 +520,39 @@ func getAwsAppautoscalingPutScalingPolicyInput(d *schema.ResourceData) (applicat params.StepScalingPolicyConfiguration = expandStepScalingPolicyConfiguration(v.([]interface{})) } + if l, ok := d.GetOk("target_tracking_scaling_policy_configuration"); ok { + v := l.([]interface{}) + if len(v) < 1 { + return params, fmt.Errorf("Empty target_tracking_scaling_policy_configuration block") + } + ttspCfg := v[0].(map[string]interface{}) + cfg := &applicationautoscaling.TargetTrackingScalingPolicyConfiguration{ + TargetValue: aws.Float64(ttspCfg["target_value"].(float64)), + } + + if v, ok := ttspCfg["scale_in_cooldown"]; ok { + cfg.ScaleInCooldown = aws.Int64(int64(v.(int))) + } + + if v, ok := ttspCfg["scale_out_cooldown"]; ok { + cfg.ScaleOutCooldown = aws.Int64(int64(v.(int))) + } + + if v, ok := ttspCfg["disable_scale_in"]; ok { + cfg.DisableScaleIn = aws.Bool(v.(bool)) + } + + if v, ok := ttspCfg["customized_metric_specification"].([]interface{}); ok && len(v) > 0 { + cfg.CustomizedMetricSpecification = expandAppautoscalingCustomizedMetricSpecification(v) + } + + if v, ok := ttspCfg["predefined_metric_specification"].([]interface{}); ok && len(v) > 0 { + cfg.PredefinedMetricSpecification = expandAppautoscalingPredefinedMetricSpecification(v) + } + + params.TargetTrackingScalingPolicyConfiguration = cfg + } + return params, nil } @@ -466,6 +656,78 @@ func flattenAppautoscalingStepAdjustments(adjs []*applicationautoscaling.StepAdj return out } +func flattenTargetTrackingScalingPolicyConfiguration(cfg *applicationautoscaling.TargetTrackingScalingPolicyConfiguration) []interface{} { + if cfg == nil { + return []interface{}{} + } + + m := make(map[string]interface{}, 0) + m["target_value"] = *cfg.TargetValue + + if cfg.DisableScaleIn != nil { + m["disable_scale_in"] = *cfg.DisableScaleIn + } + if cfg.ScaleInCooldown != nil { + m["scale_in_cooldown"] = *cfg.ScaleInCooldown + } + if cfg.ScaleOutCooldown != nil { + m["scale_out_cooldown"] = *cfg.ScaleOutCooldown + } + if cfg.CustomizedMetricSpecification != nil { + m["customized_metric_specification"] = flattenCustomizedMetricSpecification(cfg.CustomizedMetricSpecification) + } + if cfg.PredefinedMetricSpecification != nil { + m["predefined_metric_specification"] = flattenPredefinedMetricSpecification(cfg.PredefinedMetricSpecification) + } + + return []interface{}{m} +} + +func flattenCustomizedMetricSpecification(cfg *applicationautoscaling.CustomizedMetricSpecification) []interface{} { + if cfg == nil { + return []interface{}{} + } + + m := map[string]interface{}{ + "metric_name": *cfg.MetricName, + "namespace": *cfg.Namespace, + "statistic": *cfg.Statistic, + } + + if len(cfg.Dimensions) > 0 { + m["dimensions"] = flattenMetricDimensions(cfg.Dimensions) + } + + if cfg.Unit != nil { + m["unit"] = *cfg.Unit + } + return []interface{}{m} +} + +func flattenMetricDimensions(ds []*applicationautoscaling.MetricDimension) []interface{} { + l := make([]interface{}, len(ds), len(ds)) + for i, d := range ds { + l[i] = map[string]interface{}{ + "name": *d.Name, + "value": *d.Value, + } + } + return l +} + +func flattenPredefinedMetricSpecification(cfg *applicationautoscaling.PredefinedMetricSpecification) []interface{} { + if cfg == nil { + return []interface{}{} + } + m := map[string]interface{}{ + "predefined_metric_type": *cfg.PredefinedMetricType, + } + if cfg.ResourceLabel != nil { + m["resource_label"] = *cfg.ResourceLabel + } + return []interface{}{m} +} + func resourceAwsAppautoscalingAdjustmentHash(v interface{}) int { var buf bytes.Buffer m := v.(map[string]interface{}) diff --git a/aws/resource_aws_appautoscaling_policy_test.go b/aws/resource_aws_appautoscaling_policy_test.go index 1ec3bbc2891..440e398427b 100644 --- a/aws/resource_aws_appautoscaling_policy_test.go +++ b/aws/resource_aws_appautoscaling_policy_test.go @@ -97,6 +97,31 @@ func TestAccAWSAppautoScalingPolicy_spotFleetRequest(t *testing.T) { }) } +// TODO: Add test for CustomizedMetricSpecification +// The field doesn't seem to be accessible for common AWS customers (yet?) +func TestAccAWSAppautoScalingPolicy_dynamoDb(t *testing.T) { + var policy applicationautoscaling.ScalingPolicy + + randPolicyName := fmt.Sprintf("test-appautoscaling-policy-%s", acctest.RandString(5)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAppautoscalingPolicyDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSAppautoscalingPolicyDynamoDB(randPolicyName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppautoscalingPolicyExists("aws_appautoscaling_policy.dynamo_test", &policy), + resource.TestCheckResourceAttr("aws_appautoscaling_policy.dynamo_test", "name", randPolicyName), + resource.TestCheckResourceAttr("aws_appautoscaling_policy.dynamo_test", "service_namespace", "dynamodb"), + resource.TestCheckResourceAttr("aws_appautoscaling_policy.dynamo_test", "scalable_dimension", "dynamodb:table:WriteCapacityUnits"), + ), + }, + }, + }) +} + func testAccCheckAWSAppautoscalingPolicyExists(n string, policy *applicationautoscaling.ScalingPolicy) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -415,3 +440,88 @@ resource "aws_appautoscaling_policy" "foobar_simple" { } `, randClusterName, randClusterName, randClusterName, randPolicyName) } + +func testAccAWSAppautoscalingPolicyDynamoDB( + randPolicyName string) string { + return fmt.Sprintf(` +resource "aws_dynamodb_table" "dynamodb_table_test" { + name = "%s" + read_capacity = 5 + write_capacity = 5 + hash_key = "FooKey" + attribute { + name = "FooKey" + type = "S" + } +} + +resource "aws_iam_role" "autoscale_role" { + assume_role_policy = < 1023 { + errors = append(errors, fmt.Errorf( + "%q cannot be greater than 1023 characters", k)) + } + return +} + func validateConfigRuleSourceOwner(v interface{}, k string) (ws []string, errors []error) { validOwners := []string{ "CUSTOM_LAMBDA", diff --git a/website/docs/r/appautoscaling_policy.html.markdown b/website/docs/r/appautoscaling_policy.html.markdown index f54ea6b5d1e..90d2742b271 100644 --- a/website/docs/r/appautoscaling_policy.html.markdown +++ b/website/docs/r/appautoscaling_policy.html.markdown @@ -47,9 +47,11 @@ The following arguments are supported: * `name` - (Required) The name of the policy. * `policy_type` - (Optional) Defaults to "StepScaling" because it is the only option available. * `resource_id` - (Required) The resource type and unique identifier string for the resource associated with the scaling policy. For Amazon ECS services, this value is the resource type, followed by the cluster name and service name, such as `service/default/sample-webapp`. For Amazon EC2 Spot fleet requests, the resource type is `spot-fleet-request`, and the identifier is the Spot fleet request ID; for example, `spot-fleet-request/sfr-73fbd2ce-aa30-494c-8788-1cee4EXAMPLE`. +For DynamoDB tables, this value is `table/nameOfTheTable`. * `scalable_dimension` - (Required) The scalable dimension of the scalable target. The scalable dimension contains the service namespace, resource type, and scaling property, such as `ecs:service:DesiredCount` for the desired task count of an Amazon ECS service, or `ec2:spot-fleet-request:TargetCapacity` for the target capacity of an Amazon EC2 Spot fleet request. -* `service_namespace` - (Required) The AWS service namespace of the scalable target. Valid values are `ecs` for Amazon ECS services and `ec2` Amazon EC2 Spot fleet requests. +* `service_namespace` - (Required) The AWS service namespace of the scalable target. Valid values are `ecs` for Amazon ECS services, `ec2` for Amazon EC2 Spot fleet requests and `dynamodb` for DynamoDB tables. * `step_scaling_policy_configuration` - (Optional) Step scaling policy configuration, requires `policy_type = "StepScaling"` (default). See supported fields below. +* `target_tracking_scaling_policy_configuration` - (Optional) A target tracking policy, requires `policy_type = "TargetTrackingScaling"`. See supported fields below. ## Nested fields @@ -78,6 +80,28 @@ The following arguments are supported: * `metric_interval_upper_bound` - (Optional) The upper bound for the difference between the alarm threshold and the CloudWatch metric. Without a value, AWS will treat this bound as infinity. The upper bound must be greater than the lower bound. * `scaling_adjustment` - (Required) The number of members by which to scale, when the adjustment bounds are breached. A positive value scales up. A negative value scales down. +### `target_tracking_scaling_policy_configuration` + +* `target_value` - (Optional) The target value for the metric. +* `disable_scale_in` - (Optional) Indicates whether scale in by the target tracking policy is disabled. If the value is true, scale in is disabled and the target tracking policy won't remove capacity from the scalable resource. Otherwise, scale in is enabled and the target tracking policy can remove capacity from the scalable resource. The default value is `false`. +* `scale_in_cooldown` - (Optional) The amount of time, in seconds, after a scale in activity completes before another scale in activity can start. +* `scale_out_cooldown` - (Optional) The amount of time, in seconds, after a scale out activity completes before another scale out activity can start. +* `customized_metric_specification` - (Optional) Reserved for future use. See supported fields below. +* `predefined_metric_specification` - (Optional) A predefined metric. See supported fields below. + +### `customized_metric_specification` + +* `dimensions` - (Optional) The dimensions of the metric. +* `metric_name` - (Optional) The name of the metric. +* `namespace` - (Optional) The namespace of the metric. +* `statistic` - (Optional) The statistic of the metric. +* `unit` - (Optional) The unit of the metric. + +### `predefined_metric_specification` + +* `predefined_metric_type` - (Required) The metric type. +* `resource_label` - (Optional) Reserved for future use. + ## Attribute Reference * `adjustment_type` - The scaling policy's adjustment type. * `arn` - The ARN assigned by AWS to the scaling policy. diff --git a/website/docs/r/dynamodb_table.html.markdown b/website/docs/r/dynamodb_table.html.markdown index 82708666919..c3c756e201f 100644 --- a/website/docs/r/dynamodb_table.html.markdown +++ b/website/docs/r/dynamodb_table.html.markdown @@ -10,6 +10,8 @@ description: |- Provides a DynamoDB table resource +~> **Note:** It is recommended to use `lifecycle` [`ignore_changes`](/docs/configuration/resources.html#ignore_changes) for `read_capacity` and/or `write_capacity` if there's [autoscaling policy](/docs/providers/aws/r/appautoscaling_policy.html) attached to the table. + ## Example Usage The following dynamodb table description models the table and GSI shown