diff --git a/builtin/providers/aws/resource_aws_autoscaling_policy.go b/builtin/providers/aws/resource_aws_autoscaling_policy.go index 07e94feb8..5ec4136bd 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_policy.go +++ b/builtin/providers/aws/resource_aws_autoscaling_policy.go @@ -1,11 +1,13 @@ package aws import ( + "bytes" "fmt" "log" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/autoscaling" + "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/schema" ) @@ -35,17 +37,59 @@ func resourceAwsAutoscalingPolicy() *schema.Resource { Required: true, ForceNew: true, }, + "policy_type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "SimpleScaling", // preserve AWS's default to make validation easier. + }, "cooldown": &schema.Schema{ Type: schema.TypeInt, Optional: true, }, - "min_adjustment_step": &schema.Schema{ + "estimated_instance_warmup": &schema.Schema{ Type: schema.TypeInt, Optional: true, }, - "scaling_adjustment": &schema.Schema{ + "metric_aggregation_type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "min_adjustment_magnitude": &schema.Schema{ Type: schema.TypeInt, - Required: true, + Optional: true, + }, + "min_adjustment_step": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Deprecated: "Use min_adjustment_magnitude instead.", + ConflictsWith: []string{"min_adjustment_magnitude"}, + }, + "scaling_adjustment": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ConflictsWith: []string{"step_adjustment"}, + }, + "step_adjustment": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ConflictsWith: []string{"scaling_adjustment"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "metric_interval_lower_bound": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "metric_interval_upper_bound": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "scaling_adjustment": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + }, + }, + Set: resourceAwsAutoscalingScalingAdjustmentHash, }, }, } @@ -54,7 +98,10 @@ func resourceAwsAutoscalingPolicy() *schema.Resource { func resourceAwsAutoscalingPolicyCreate(d *schema.ResourceData, meta interface{}) error { autoscalingconn := meta.(*AWSClient).autoscalingconn - params := getAwsAutoscalingPutScalingPolicyInput(d) + params, err := getAwsAutoscalingPutScalingPolicyInput(d) + if err != nil { + return err + } log.Printf("[DEBUG] AutoScaling PutScalingPolicy: %#v", params) resp, err := autoscalingconn.PutScalingPolicy(¶ms) @@ -84,10 +131,15 @@ func resourceAwsAutoscalingPolicyRead(d *schema.ResourceData, meta interface{}) d.Set("adjustment_type", p.AdjustmentType) d.Set("autoscaling_group_name", p.AutoScalingGroupName) d.Set("cooldown", p.Cooldown) + d.Set("estimated_instance_warmup", p.EstimatedInstanceWarmup) + d.Set("metric_aggregation_type", p.MetricAggregationType) + d.Set("policy_type", p.PolicyType) + d.Set("min_adjustment_magnitude", p.MinAdjustmentMagnitude) d.Set("min_adjustment_step", p.MinAdjustmentStep) d.Set("arn", p.PolicyARN) d.Set("name", p.PolicyName) d.Set("scaling_adjustment", p.ScalingAdjustment) + d.Set("step_adjustment", flattenStepAdjustments(p.StepAdjustments)) return nil } @@ -95,7 +147,10 @@ func resourceAwsAutoscalingPolicyRead(d *schema.ResourceData, meta interface{}) func resourceAwsAutoscalingPolicyUpdate(d *schema.ResourceData, meta interface{}) error { autoscalingconn := meta.(*AWSClient).autoscalingconn - params := getAwsAutoscalingPutScalingPolicyInput(d) + params, inputErr := getAwsAutoscalingPutScalingPolicyInput(d) + if inputErr != nil { + return inputErr + } log.Printf("[DEBUG] Autoscaling Update Scaling Policy: %#v", params) _, err := autoscalingconn.PutScalingPolicy(¶ms) @@ -128,8 +183,10 @@ func resourceAwsAutoscalingPolicyDelete(d *schema.ResourceData, meta interface{} return nil } -// PutScalingPolicy seems to require all params to be resent, so create and update can share this common function -func getAwsAutoscalingPutScalingPolicyInput(d *schema.ResourceData) autoscaling.PutScalingPolicyInput { +// PutScalingPolicy can safely resend all parameters without destroying the +// resource, so create and update can share this common function. It will error +// if certain mutually exclusive values are set. +func getAwsAutoscalingPutScalingPolicyInput(d *schema.ResourceData) (autoscaling.PutScalingPolicyInput, error) { var params = autoscaling.PutScalingPolicyInput{ AutoScalingGroupName: aws.String(d.Get("autoscaling_group_name").(string)), PolicyName: aws.String(d.Get("name").(string)), @@ -143,15 +200,59 @@ func getAwsAutoscalingPutScalingPolicyInput(d *schema.ResourceData) autoscaling. params.Cooldown = aws.Int64(int64(v.(int))) } + if v, ok := d.GetOk("estimated_instance_warmup"); ok { + params.EstimatedInstanceWarmup = aws.Int64(int64(v.(int))) + } + + if v, ok := d.GetOk("metric_aggregation_type"); ok { + params.MetricAggregationType = aws.String(v.(string)) + } + + if v, ok := d.GetOk("policy_type"); ok { + params.PolicyType = aws.String(v.(string)) + } + if v, ok := d.GetOk("scaling_adjustment"); ok { params.ScalingAdjustment = aws.Int64(int64(v.(int))) } + if v, ok := d.GetOk("step_adjustment"); ok { + steps, err := expandStepAdjustments(v.(*schema.Set).List()) + if err != nil { + return params, fmt.Errorf("metric_interval_lower_bound and metric_interval_upper_bound must be strings!") + } + params.StepAdjustments = steps + } + + if v, ok := d.GetOk("min_adjustment_magnitude"); ok { + params.MinAdjustmentMagnitude = aws.Int64(int64(v.(int))) + } + if v, ok := d.GetOk("min_adjustment_step"); ok { params.MinAdjustmentStep = aws.Int64(int64(v.(int))) } - return params + // Validate our final input to confirm it won't error when sent to AWS. + // First, SimpleScaling policy types... + if *params.PolicyType == "SimpleScaling" && params.StepAdjustments != nil { + return params, fmt.Errorf("SimpleScaling policy types cannot use step_adjustments!") + } + if *params.PolicyType == "SimpleScaling" && params.MetricAggregationType != nil { + return params, fmt.Errorf("SimpleScaling policy types cannot use metric_aggregation_type!") + } + if *params.PolicyType == "SimpleScaling" && params.EstimatedInstanceWarmup != nil { + return params, fmt.Errorf("SimpleScaling policy types cannot use estimated_instance_warmup!") + } + + // Second, StepScaling policy types... + if *params.PolicyType == "StepScaling" && params.ScalingAdjustment != nil { + return params, fmt.Errorf("StepScaling policy types cannot use scaling_adjustment!") + } + if *params.PolicyType == "StepScaling" && params.Cooldown != nil { + return params, fmt.Errorf("StepScaling policy types cannot use cooldown!") + } + + return params, nil } func getAwsAutoscalingPolicy(d *schema.ResourceData, meta interface{}) (*autoscaling.ScalingPolicy, error) { @@ -179,3 +280,17 @@ func getAwsAutoscalingPolicy(d *schema.ResourceData, meta interface{}) (*autosca // policy not found return nil, nil } + +func resourceAwsAutoscalingScalingAdjustmentHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + if v, ok := m["metric_interval_lower_bound"]; ok { + buf.WriteString(fmt.Sprintf("%f-", v)) + } + if v, ok := m["metric_interval_upper_bound"]; ok { + buf.WriteString(fmt.Sprintf("%f-", v)) + } + buf.WriteString(fmt.Sprintf("%d-", m["scaling_adjustment"].(int))) + + return hashcode.String(buf.String()) +} diff --git a/builtin/providers/aws/resource_aws_autoscaling_policy_test.go b/builtin/providers/aws/resource_aws_autoscaling_policy_test.go index 6d402de85..1fd8567d4 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_policy_test.go +++ b/builtin/providers/aws/resource_aws_autoscaling_policy_test.go @@ -21,9 +21,20 @@ func TestAccAWSAutoscalingPolicy_basic(t *testing.T) { resource.TestStep{ Config: testAccAWSAutoscalingPolicyConfig, Check: resource.ComposeTestCheckFunc( - testAccCheckScalingPolicyExists("aws_autoscaling_policy.foobar", &policy), - resource.TestCheckResourceAttr("aws_autoscaling_policy.foobar", "adjustment_type", "ChangeInCapacity"), - resource.TestCheckResourceAttr("aws_autoscaling_policy.foobar", "cooldown", "300"), + testAccCheckScalingPolicyExists("aws_autoscaling_policy.foobar_simple", &policy), + resource.TestCheckResourceAttr("aws_autoscaling_policy.foobar_simple", "adjustment_type", "ChangeInCapacity"), + resource.TestCheckResourceAttr("aws_autoscaling_policy.foobar_simple", "policy_type", "SimpleScaling"), + resource.TestCheckResourceAttr("aws_autoscaling_policy.foobar_simple", "cooldown", "300"), + resource.TestCheckResourceAttr("aws_autoscaling_policy.foobar_simple", "name", "foobar_simple"), + resource.TestCheckResourceAttr("aws_autoscaling_policy.foobar_simple", "scaling_adjustment", "2"), + resource.TestCheckResourceAttr("aws_autoscaling_policy.foobar_simple", "autoscaling_group_name", "terraform-test-foobar5"), + testAccCheckScalingPolicyExists("aws_autoscaling_policy.foobar_step", &policy), + resource.TestCheckResourceAttr("aws_autoscaling_policy.foobar_step", "adjustment_type", "ChangeInCapacity"), + resource.TestCheckResourceAttr("aws_autoscaling_policy.foobar_step", "policy_type", "StepScaling"), + resource.TestCheckResourceAttr("aws_autoscaling_policy.foobar_step", "name", "foobar_step"), + resource.TestCheckResourceAttr("aws_autoscaling_policy.foobar_step", "metric_aggregation_type", "Minimum"), + resource.TestCheckResourceAttr("aws_autoscaling_policy.foobar_step", "estimated_instance_warmup", "200"), + resource.TestCheckResourceAttr("aws_autoscaling_policy.foobar_step", "autoscaling_group_name", "terraform-test-foobar5"), ), }, }, @@ -82,33 +93,47 @@ func testAccCheckAWSAutoscalingPolicyDestroy(s *terraform.State) error { var testAccAWSAutoscalingPolicyConfig = fmt.Sprintf(` resource "aws_launch_configuration" "foobar" { - name = "terraform-test-foobar5" - image_id = "ami-21f78e11" - instance_type = "t1.micro" + name = "terraform-test-foobar5" + image_id = "ami-21f78e11" + instance_type = "t1.micro" } resource "aws_autoscaling_group" "foobar" { - availability_zones = ["us-west-2a"] - name = "terraform-test-foobar5" - max_size = 5 - min_size = 2 - health_check_grace_period = 300 - health_check_type = "ELB" - force_delete = true - termination_policies = ["OldestInstance"] - launch_configuration = "${aws_launch_configuration.foobar.name}" - tag { - key = "Foo" - value = "foo-bar" - propagate_at_launch = true - } + availability_zones = ["us-west-2a"] + name = "terraform-test-foobar5" + max_size = 5 + min_size = 2 + health_check_grace_period = 300 + health_check_type = "ELB" + force_delete = true + termination_policies = ["OldestInstance"] + launch_configuration = "${aws_launch_configuration.foobar.name}" + tag { + key = "Foo" + value = "foo-bar" + propagate_at_launch = true + } } -resource "aws_autoscaling_policy" "foobar" { - name = "foobar" - scaling_adjustment = 4 - adjustment_type = "ChangeInCapacity" - cooldown = 300 - autoscaling_group_name = "${aws_autoscaling_group.foobar.name}" +resource "aws_autoscaling_policy" "foobar_simple" { + name = "foobar_simple" + adjustment_type = "ChangeInCapacity" + cooldown = 300 + policy_type = "SimpleScaling" + scaling_adjustment = 2 + autoscaling_group_name = "${aws_autoscaling_group.foobar.name}" +} + +resource "aws_autoscaling_policy" "foobar_step" { + name = "foobar_step" + adjustment_type = "ChangeInCapacity" + policy_type = "StepScaling" + estimated_instance_warmup = 200 + metric_aggregation_type = "Minimum" + step_adjustment { + scaling_adjustment = 1 + metric_interval_lower_bound = 2.0 + } + autoscaling_group_name = "${aws_autoscaling_group.foobar.name}" } `) diff --git a/builtin/providers/aws/structure.go b/builtin/providers/aws/structure.go index d12290992..72766c026 100644 --- a/builtin/providers/aws/structure.go +++ b/builtin/providers/aws/structure.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "sort" + "strconv" "strings" "github.com/aws/aws-sdk-go/aws" @@ -305,6 +306,58 @@ func flattenAccessLog(l *elb.AccessLog) []map[string]interface{} { return result } +// Takes the result of flatmap.Expand for an array of step adjustments and +// returns a []*autoscaling.StepAdjustment. +func expandStepAdjustments(configured []interface{}) ([]*autoscaling.StepAdjustment, error) { + var adjustments []*autoscaling.StepAdjustment + + // Loop over our configured step adjustments and create an array + // of aws-sdk-go compatible objects. We're forced to convert strings + // to floats here because there's no way to detect whether or not + // an uninitialized, optional schema element is "0.0" deliberately. + // With strings, we can test for "", which is definitely an empty + // struct value. + for _, raw := range configured { + data := raw.(map[string]interface{}) + a := &autoscaling.StepAdjustment{ + ScalingAdjustment: aws.Int64(int64(data["scaling_adjustment"].(int))), + } + if data["metric_interval_lower_bound"] != "" { + bound := data["metric_interval_lower_bound"] + switch bound := bound.(type) { + case string: + f, err := strconv.ParseFloat(bound, 64) + if err != nil { + return nil, fmt.Errorf( + "metric_interval_lower_bound must be a float value represented as a string") + } + a.MetricIntervalLowerBound = aws.Float64(f) + default: + return nil, fmt.Errorf( + "metric_interval_lower_bound isn't a string. This is a bug. Please file an issue.") + } + } + if data["metric_interval_upper_bound"] != "" { + bound := data["metric_interval_upper_bound"] + switch bound := bound.(type) { + case string: + f, err := strconv.ParseFloat(bound, 64) + if err != nil { + return nil, fmt.Errorf( + "metric_interval_upper_bound must be a float value represented as a string") + } + a.MetricIntervalUpperBound = aws.Float64(f) + default: + return nil, fmt.Errorf( + "metric_interval_upper_bound isn't a string. This is a bug. Please file an issue.") + } + } + adjustments = append(adjustments, a) + } + + return adjustments, nil +} + // Flattens a health check into something that flatmap.Flatten() // can handle func flattenHealthCheck(check *elb.HealthCheck) []map[string]interface{} { @@ -564,6 +617,24 @@ func flattenAttachment(a *ec2.NetworkInterfaceAttachment) map[string]interface{} return att } +// Flattens step adjustments into a list of map[string]interface. +func flattenStepAdjustments(adjustments []*autoscaling.StepAdjustment) []map[string]interface{} { + result := make([]map[string]interface{}, 0, len(adjustments)) + for _, raw := range adjustments { + a := map[string]interface{}{ + "scaling_adjustment": *raw.ScalingAdjustment, + } + if raw.MetricIntervalUpperBound != nil { + a["metric_interval_upper_bound"] = *raw.MetricIntervalUpperBound + } + if raw.MetricIntervalLowerBound != nil { + a["metric_interval_lower_bound"] = *raw.MetricIntervalLowerBound + } + result = append(result, a) + } + return result +} + func flattenResourceRecords(recs []*route53.ResourceRecord) []string { strs := make([]string, 0, len(recs)) for _, r := range recs { diff --git a/builtin/providers/aws/structure_test.go b/builtin/providers/aws/structure_test.go index 74e7ca206..aa656710d 100644 --- a/builtin/providers/aws/structure_test.go +++ b/builtin/providers/aws/structure_test.go @@ -526,6 +526,33 @@ func TestexpandElasticacheParameters(t *testing.T) { } } +func TestExpandStepAdjustments(t *testing.T) { + expanded := []interface{}{ + map[string]interface{}{ + "metric_interval_lower_bound": "1.0", + "metric_interval_upper_bound": "2.0", + "scaling_adjustment": 1, + }, + } + parameters, err := expandStepAdjustments(expanded) + if err != nil { + t.Fatalf("bad: %#v", err) + } + + expected := &autoscaling.StepAdjustment{ + MetricIntervalLowerBound: aws.Float64(1.0), + MetricIntervalUpperBound: aws.Float64(2.0), + ScalingAdjustment: aws.Int64(int64(1)), + } + + if !reflect.DeepEqual(parameters[0], expected) { + t.Fatalf( + "Got:\n\n%#v\n\nExpected:\n\n%#v\n", + parameters[0], + expected) + } +} + func TestFlattenParameters(t *testing.T) { cases := []struct { Input []*rds.Parameter @@ -728,6 +755,30 @@ func TestFlattenAttachment(t *testing.T) { } } +func TestflattenStepAdjustments(t *testing.T) { + expanded := []*autoscaling.StepAdjustment{ + &autoscaling.StepAdjustment{ + MetricIntervalLowerBound: aws.Float64(1.0), + MetricIntervalUpperBound: aws.Float64(2.0), + ScalingAdjustment: aws.Int64(int64(1)), + }, + } + + result := flattenStepAdjustments(expanded)[0] + if result == nil { + t.Fatal("expected result to have value, but got nil") + } + if result["metric_interval_lower_bound"] != float64(1.0) { + t.Fatalf("expected metric_interval_lower_bound to be 1.0, but got %d", result["metric_interval_lower_bound"]) + } + if result["metric_interval_upper_bound"] != float64(2.0) { + t.Fatalf("expected metric_interval_upper_bound to be 1.0, but got %d", result["metric_interval_upper_bound"]) + } + if result["scaling_adjustment"] != int64(1) { + t.Fatalf("expected scaling_adjustment to be 1, but got %d", result["scaling_adjustment"]) + } +} + func TestFlattenResourceRecords(t *testing.T) { expanded := []*route53.ResourceRecord{ &route53.ResourceRecord{ diff --git a/website/source/docs/providers/aws/r/autoscaling_policy.html.markdown b/website/source/docs/providers/aws/r/autoscaling_policy.html.markdown index 82671e698..e816333c2 100644 --- a/website/source/docs/providers/aws/r/autoscaling_policy.html.markdown +++ b/website/source/docs/providers/aws/r/autoscaling_policy.html.markdown @@ -44,10 +44,54 @@ The following arguments are supported: * `name` - (Required) The name of the policy. * `autoscaling_group_name` - (Required) The name or ARN of the group. -* `adjustment_type` - (Required) Specifies whether the `scaling_adjustment` is an absolute number or a percentage of the current capacity. Valid values are `ChangeInCapacity`, `ExactCapacity`, and `PercentChangeInCapacity`. -* `scaling_adjustment` - (Required) The number of instances by which to scale. `adjustment_type` determines the interpretation of this number (e.g., as an absolute number or as a percentage of the existing Auto Scaling group size). A positive increment adds to the current capacity and a negative value removes from the current capacity. +* `adjustment_type` - (Required) Specifies whether the adjustment is an absolute number or a percentage of the current capacity. Valid values are `ChangeInCapacity`, `ExactCapacity`, and `PercentChangeInCapacity`. +* `policy_type` - (Optional) The policy type, either "SimpleScaling" or "StepScaling". If this value isn't provided, AWS will default to "SimpleScaling." + +The following arguments are only available to "SimpleScaling" type policies: + * `cooldown` - (Optional) The amount of time, in seconds, after a scaling activity completes and before the next scaling activity can start. -* `min_adjustment_step` - (Optional) Used with `adjustment_type` with the value `PercentChangeInCapacity`, the scaling policy changes the `desired_capacity` of the Auto Scaling group by at least the number of instances specified in the value. +* `scaling_adjustment` - (Optional) The number of instances by which to scale. `adjustment_type` determines the interpretation of this number (e.g., as an absolute number or as a percentage of the existing Auto Scaling group size). A positive increment adds to the current capacity and a negative value removes from the current capacity. + +The following arguments are only available to "StepScaling" type policies: + +* `metric_aggregation_type` - (Optional) The aggregation type for the policy's metrics. Valid values are "Minimum", "Maximum", and "Average". Without a value, AWS will treat the aggregation type as "Average". +* `estimated_instance_warmup` - (Optional) The estimated time, in seconds, until a newly launched instance will contribute CloudWatch metrics. Without a value, AWS will default to the group's specified cooldown period. +* `step_adjustments` - (Optional) A set of adjustments that manage +group scaling. These have the following structure: + +``` +step_adjustment { + scaling_adjustment = -1 + metric_interval_lower_bound = 1.0 + metric_interval_upper_bound = 2.0 +} +step_adjustment { + scaling_adjustment = 1 + metric_interval_lower_bound = 2.0 + metric_interval_upper_bound = 3.0 +} +``` + +The following fields are available in step adjustments: + +* `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. +* `metric_interval_lower_bound` - (Optional) The lower bound for the +difference between the alarm threshold and the CloudWatch metric. +Without a value, AWS will treat this bound as infinity. +* `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. + +The following arguments are supported for backwards compatibility but should not be used: + +* `min_adjustment_step` - (Optional) Use `min_adjustment_magnitude` instead. ## Attribute Reference * `arn` - The ARN assigned by AWS to the scaling policy. +* `name` - The scaling policy's name. +* `autoscaling_group_name` - The scaling policy's assigned autoscaling group. +* `adjustment_type` - The scaling policy's adjustment type. +* `policy_type` - The scaling policy's type.