Merge pull request #4277 from ctiwald/ct/add-new-asg-policy-type

Add support for "StepScaling" autoscaling policies.
This commit is contained in:
Paul Stack 2016-03-30 21:06:39 -05:00
commit a8e0528784
5 changed files with 343 additions and 37 deletions

View File

@ -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(&params)
@ -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(&params)
@ -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())
}

View File

@ -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}"
}
`)

View File

@ -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 {

View File

@ -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{

View File

@ -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.