diff --git a/aws/internal/service/autoscalingplans/finder/finder.go b/aws/internal/service/autoscalingplans/finder/finder.go new file mode 100644 index 000000000000..90892a83c4f1 --- /dev/null +++ b/aws/internal/service/autoscalingplans/finder/finder.go @@ -0,0 +1,26 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/autoscalingplans" +) + +// ScalingPlan returns the scaling plan corresponding to the specified name and version. +// Returns nil and potentially an API error if no scaling plan is found. +func ScalingPlan(conn *autoscalingplans.AutoScalingPlans, scalingPlanName string, scalingPlanVersion int) (*autoscalingplans.ScalingPlan, error) { + input := &autoscalingplans.DescribeScalingPlansInput{ + ScalingPlanNames: aws.StringSlice([]string{scalingPlanName}), + ScalingPlanVersion: aws.Int64(int64(scalingPlanVersion)), + } + + output, err := conn.DescribeScalingPlans(input) + if err != nil { + return nil, err + } + + if output == nil || len(output.ScalingPlans) == 0 { + return nil, nil + } + + return output.ScalingPlans[0], nil +} diff --git a/aws/internal/service/autoscalingplans/waiter/status.go b/aws/internal/service/autoscalingplans/waiter/status.go new file mode 100644 index 000000000000..e1c7630c53f0 --- /dev/null +++ b/aws/internal/service/autoscalingplans/waiter/status.go @@ -0,0 +1,41 @@ +package waiter + +import ( + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/autoscalingplans" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/autoscalingplans/finder" +) + +const ( + scalingPlanStatusNotFound = "NotFound" + scalingPlanStatusUnknown = "Unknown" +) + +// ScalingPlanStatus fetches the ScalingPlan and its Status +func ScalingPlanStatus(conn *autoscalingplans.AutoScalingPlans, scalingPlanName string, scalingPlanVersion int) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + scalingPlan, err := finder.ScalingPlan(conn, scalingPlanName, scalingPlanVersion) + + if tfawserr.ErrCodeEquals(err, autoscalingplans.ErrCodeObjectNotFoundException) { + return nil, scalingPlanStatusNotFound, nil + } + + if err != nil { + return nil, scalingPlanStatusUnknown, err + } + + if scalingPlan == nil { + return nil, scalingPlanStatusNotFound, nil + } + + if statusMessage := aws.StringValue(scalingPlan.StatusMessage); statusMessage != "" { + log.Printf("[INFO] Auto Scaling Scaling Plan (%s/%d) status message: %s", scalingPlanName, scalingPlanVersion, statusMessage) + } + + return scalingPlan, aws.StringValue(scalingPlan.StatusCode), nil + } +} diff --git a/aws/internal/service/autoscalingplans/waiter/waiter.go b/aws/internal/service/autoscalingplans/waiter/waiter.go new file mode 100644 index 000000000000..bc47bf5b5451 --- /dev/null +++ b/aws/internal/service/autoscalingplans/waiter/waiter.go @@ -0,0 +1,76 @@ +package waiter + +import ( + "time" + + "github.com/aws/aws-sdk-go/service/autoscalingplans" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const ( + // Maximum amount of time to wait for a ScalingPlan to return Created + ScalingPlanCreatedTimeout = 5 * time.Minute + + // Maximum amount of time to wait for a ScalingPlan to return Deleted + ScalingPlanDeletedTimeout = 5 * time.Minute + + // Maximum amount of time to wait for a ScalingPlan to return Updated + ScalingPlanUpdatedTimeout = 5 * time.Minute +) + +// ScalingPlanCreated waits for a ScalingPlan to return Created +func ScalingPlanCreated(conn *autoscalingplans.AutoScalingPlans, scalingPlanName string, scalingPlanVersion int) (*autoscalingplans.ScalingPlan, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{autoscalingplans.ScalingPlanStatusCodeCreationInProgress}, + Target: []string{autoscalingplans.ScalingPlanStatusCodeActive, autoscalingplans.ScalingPlanStatusCodeActiveWithProblems}, + Refresh: ScalingPlanStatus(conn, scalingPlanName, scalingPlanVersion), + Timeout: ScalingPlanCreatedTimeout, + Delay: 10 * time.Second, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*autoscalingplans.ScalingPlan); ok { + return v, err + } + + return nil, err +} + +// ScalingPlanDeleted waits for a ScalingPlan to return Deleted +func ScalingPlanDeleted(conn *autoscalingplans.AutoScalingPlans, scalingPlanName string, scalingPlanVersion int) (*autoscalingplans.ScalingPlan, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{autoscalingplans.ScalingPlanStatusCodeDeletionInProgress}, + Target: []string{}, + Refresh: ScalingPlanStatus(conn, scalingPlanName, scalingPlanVersion), + Timeout: ScalingPlanDeletedTimeout, + Delay: 10 * time.Second, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*autoscalingplans.ScalingPlan); ok { + return v, err + } + + return nil, err +} + +// ScalingPlanUpdated waits for a ScalingPlan to return Updated +func ScalingPlanUpdated(conn *autoscalingplans.AutoScalingPlans, scalingPlanName string, scalingPlanVersion int) (*autoscalingplans.ScalingPlan, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{autoscalingplans.ScalingPlanStatusCodeUpdateInProgress}, + Target: []string{autoscalingplans.ScalingPlanStatusCodeActive, autoscalingplans.ScalingPlanStatusCodeActiveWithProblems}, + Refresh: ScalingPlanStatus(conn, scalingPlanName, scalingPlanVersion), + Timeout: ScalingPlanUpdatedTimeout, + Delay: 10 * time.Second, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*autoscalingplans.ScalingPlan); ok { + return v, err + } + + return nil, err +} diff --git a/aws/provider.go b/aws/provider.go index 34e5c2925841..8c3df08879a0 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -430,6 +430,7 @@ func Provider() *schema.Provider { "aws_autoscaling_notification": resourceAwsAutoscalingNotification(), "aws_autoscaling_policy": resourceAwsAutoscalingPolicy(), "aws_autoscaling_schedule": resourceAwsAutoscalingSchedule(), + "aws_autoscalingplans_scaling_plan": resourceAwsAutoScalingPlansScalingPlan(), "aws_backup_plan": resourceAwsBackupPlan(), "aws_backup_selection": resourceAwsBackupSelection(), "aws_backup_vault": resourceAwsBackupVault(), diff --git a/aws/provider_test.go b/aws/provider_test.go index b2ccb4b6a695..426f0565fecd 100644 --- a/aws/provider_test.go +++ b/aws/provider_test.go @@ -14,6 +14,7 @@ import ( "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/aws/endpoints" "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/service/organizations" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -571,6 +572,36 @@ func testAccOrganizationsEnabledPreCheck(t *testing.T) { } } +func testAccPreCheckIamServiceLinkedRole(t *testing.T, pathPrefix string) { + conn := testAccProvider.Meta().(*AWSClient).iamconn + + input := &iam.ListRolesInput{ + PathPrefix: aws.String(pathPrefix), + } + + var role *iam.Role + err := conn.ListRolesPages(input, func(page *iam.ListRolesOutput, lastPage bool) bool { + for _, r := range page.Roles { + role = r + break + } + + return !lastPage + }) + + if testAccPreCheckSkipError(err) { + t.Skipf("skipping tests: %s", err) + } + + if err != nil { + t.Fatalf("error listing IAM roles: %s", err) + } + + if role == nil { + t.Skipf("skipping tests; missing IAM service-linked role %s. Please create the role and retry", pathPrefix) + } +} + func testAccAlternateAccountProviderConfig() string { //lintignore:AT004 return fmt.Sprintf(` diff --git a/aws/resource_aws_autoscalingplans_scaling_plan.go b/aws/resource_aws_autoscalingplans_scaling_plan.go new file mode 100644 index 000000000000..df99d0517f26 --- /dev/null +++ b/aws/resource_aws_autoscalingplans_scaling_plan.go @@ -0,0 +1,786 @@ +package aws + +import ( + "fmt" + "log" + "regexp" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/autoscalingplans" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/autoscalingplans/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/autoscalingplans/waiter" +) + +func resourceAwsAutoScalingPlansScalingPlan() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsAutoScalingPlansScalingPlanCreate, + Read: resourceAwsAutoScalingPlansScalingPlanRead, + Update: resourceAwsAutoScalingPlansScalingPlanUpdate, + Delete: resourceAwsAutoScalingPlansScalingPlanDelete, + Importer: &schema.ResourceImporter{ + State: resourceAwsAutoScalingPlansScalingPlanImport, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.All( + validation.StringLenBetween(1, 128), + validation.StringMatch(regexp.MustCompile(`^[[:print:]]+$`), "must be printable"), + validation.StringDoesNotContainAny("|:/"), + ), + }, + + "application_source": { + Type: schema.TypeList, + Required: true, + MinItems: 1, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "cloudformation_stack_arn": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateArn, + ConflictsWith: []string{"application_source.0.tag_filter"}, + }, + + "tag_filter": { + Type: schema.TypeSet, + Optional: true, + MinItems: 0, + MaxItems: 50, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Required: true, + }, + + "values": { + Type: schema.TypeSet, + Optional: true, + MinItems: 0, + MaxItems: 50, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + }, + }, + ConflictsWith: []string{"application_source.0.cloudformation_stack_arn"}, + }, + }, + }, + }, + + "scaling_instruction": { + Type: schema.TypeSet, + Required: true, + MinItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "customized_load_metric_specification": { + Type: schema.TypeList, + Optional: true, + MinItems: 0, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "dimensions": { + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + + "metric_name": { + Type: schema.TypeString, + Required: true, + }, + + "namespace": { + Type: schema.TypeString, + Required: true, + }, + + "statistic": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + autoscalingplans.MetricStatisticSum, + }, false), + }, + + "unit": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + + "disable_dynamic_scaling": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + + "max_capacity": { + Type: schema.TypeInt, + Required: true, + }, + + "min_capacity": { + Type: schema.TypeInt, + Required: true, + }, + + "predefined_load_metric_specification": { + Type: schema.TypeList, + Optional: true, + MinItems: 0, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "predefined_load_metric_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(autoscalingplans.LoadMetricType_Values(), false), + }, + + "resource_label": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(1, 1023), + }, + }, + }, + }, + + "predictive_scaling_max_capacity_behavior": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(autoscalingplans.PredictiveScalingMaxCapacityBehavior_Values(), false), + }, + + "predictive_scaling_max_capacity_buffer": { + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntBetween(1, 100), + }, + + "predictive_scaling_mode": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(autoscalingplans.PredictiveScalingMode_Values(), false), + }, + + "resource_id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 1600), + }, + + "scalable_dimension": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(autoscalingplans.ScalableDimension_Values(), false), + }, + + "scaling_policy_update_behavior": { + Type: schema.TypeString, + Optional: true, + Default: autoscalingplans.ScalingPolicyUpdateBehaviorKeepExternalPolicies, + ValidateFunc: validation.StringInSlice(autoscalingplans.ScalingPolicyUpdateBehavior_Values(), false), + }, + + "scheduled_action_buffer_time": { + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntAtLeast(0), + }, + + "service_namespace": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(autoscalingplans.ServiceNamespace_Values(), false), + }, + + "target_tracking_configuration": { + Type: schema.TypeSet, + Required: true, + MinItems: 1, + MaxItems: 10, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "customized_scaling_metric_specification": { + Type: schema.TypeList, + Optional: true, + MinItems: 0, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "dimensions": { + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + + "metric_name": { + Type: schema.TypeString, + Required: true, + }, + + "namespace": { + Type: schema.TypeString, + Required: true, + }, + + "statistic": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(autoscalingplans.MetricStatistic_Values(), false), + }, + + "unit": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + + "disable_scale_in": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + + "estimated_instance_warmup": { + Type: schema.TypeInt, + Optional: true, + }, + + "predefined_scaling_metric_specification": { + Type: schema.TypeList, + Optional: true, + MinItems: 0, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "predefined_scaling_metric_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(autoscalingplans.ScalingMetricType_Values(), false), + }, + + "resource_label": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(1, 1023), + }, + }, + }, + }, + + "scale_in_cooldown": { + Type: schema.TypeInt, + Optional: true, + }, + + "scale_out_cooldown": { + Type: schema.TypeInt, + Optional: true, + }, + + "target_value": { + Type: schema.TypeFloat, + Required: true, + ValidateFunc: validation.FloatBetween(8.515920e-109, 1.174271e+108), + }, + }, + }, + }, + }, + }, + }, + + "scaling_plan_version": { + Type: schema.TypeInt, + Computed: true, + }, + }, + } +} + +func resourceAwsAutoScalingPlansScalingPlanCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).autoscalingplansconn + + scalingPlanName := d.Get("name").(string) + + input := &autoscalingplans.CreateScalingPlanInput{ + ApplicationSource: expandAutoScalingPlansApplicationSource(d.Get("application_source").([]interface{})), + ScalingInstructions: expandAutoScalingPlansScalingInstructions(d.Get("scaling_instruction").(*schema.Set)), + ScalingPlanName: aws.String(scalingPlanName), + } + + log.Printf("[DEBUG] Creating Auto Scaling Scaling Plan: %s", input) + output, err := conn.CreateScalingPlan(input) + if err != nil { + return fmt.Errorf("error creating Auto Scaling Scaling Plan: %w", err) + } + + scalingPlanVersion := int(aws.Int64Value(output.ScalingPlanVersion)) + d.SetId(autoScalingPlansScalingPlanId(scalingPlanName, scalingPlanVersion)) + d.Set("scaling_plan_version", scalingPlanVersion) + + _, err = waiter.ScalingPlanCreated(conn, scalingPlanName, scalingPlanVersion) + if err != nil { + return fmt.Errorf("error waiting for Auto Scaling Scaling Plan (%s) to be created: %w", d.Id(), err) + } + + return resourceAwsAutoScalingPlansScalingPlanRead(d, meta) +} + +func resourceAwsAutoScalingPlansScalingPlanRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).autoscalingplansconn + + scalingPlan, err := finder.ScalingPlan(conn, d.Get("name").(string), d.Get("scaling_plan_version").(int)) + if err != nil { + return fmt.Errorf("error reading Auto Scaling Scaling Plan (%s): %w", d.Id(), err) + } + if scalingPlan == nil { + log.Printf("[WARN] Auto Scaling Scaling Plan (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + err = d.Set("application_source", flattenAutoScalingPlansApplicationSource(scalingPlan.ApplicationSource)) + if err != nil { + return fmt.Errorf("error setting application_source: %w", err) + } + d.Set("name", scalingPlan.ScalingPlanName) + err = d.Set("scaling_instruction", flattenAutoScalingPlansScalingInstructions(scalingPlan.ScalingInstructions)) + if err != nil { + return fmt.Errorf("error setting application_source: %w", err) + } + d.Set("scaling_plan_version", int(aws.Int64Value(scalingPlan.ScalingPlanVersion))) + + return nil +} + +func resourceAwsAutoScalingPlansScalingPlanUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).autoscalingplansconn + + scalingPlanName := d.Get("name").(string) + scalingPlanVersion := d.Get("scaling_plan_version").(int) + + input := &autoscalingplans.UpdateScalingPlanInput{ + ApplicationSource: expandAutoScalingPlansApplicationSource(d.Get("application_source").([]interface{})), + ScalingInstructions: expandAutoScalingPlansScalingInstructions(d.Get("scaling_instruction").(*schema.Set)), + ScalingPlanName: aws.String(scalingPlanName), + ScalingPlanVersion: aws.Int64(int64(scalingPlanVersion)), + } + + log.Printf("[DEBUG] Updating Auto Scaling Scaling Plan: %s", input) + _, err := conn.UpdateScalingPlan(input) + if err != nil { + return fmt.Errorf("error updating Auto Scaling Scaling Plan (%s): %w", d.Id(), err) + } + + _, err = waiter.ScalingPlanUpdated(conn, scalingPlanName, scalingPlanVersion) + if err != nil { + return fmt.Errorf("error waiting for Auto Scaling Scaling Plan (%s) to be updated: %w", d.Id(), err) + } + + return resourceAwsAutoScalingPlansScalingPlanRead(d, meta) +} + +func resourceAwsAutoScalingPlansScalingPlanDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).autoscalingplansconn + + scalingPlanName := d.Get("name").(string) + scalingPlanVersion := d.Get("scaling_plan_version").(int) + + log.Printf("[DEBUG] Deleting Auto Scaling Scaling Plan: %s", d.Id()) + _, err := conn.DeleteScalingPlan(&autoscalingplans.DeleteScalingPlanInput{ + ScalingPlanName: aws.String(scalingPlanName), + ScalingPlanVersion: aws.Int64(int64(scalingPlanVersion)), + }) + if isAWSErr(err, autoscalingplans.ErrCodeObjectNotFoundException, "") { + return nil + } + if err != nil { + return fmt.Errorf("error deleting Auto Scaling Scaling Plan (%s): %w", d.Id(), err) + } + + _, err = waiter.ScalingPlanDeleted(conn, scalingPlanName, scalingPlanVersion) + if err != nil { + return fmt.Errorf("error waiting for Auto Scaling Scaling Plan (%s) to be deleted: %w", d.Id(), err) + } + + return nil +} + +func resourceAwsAutoScalingPlansScalingPlanImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + scalingPlanName := d.Id() + scalingPlanVersion := 1 + + d.SetId(autoScalingPlansScalingPlanId(scalingPlanName, scalingPlanVersion)) + d.Set("name", scalingPlanName) + d.Set("scaling_plan_version", scalingPlanVersion) + + return []*schema.ResourceData{d}, nil +} + +// Terraform resource ID. +func autoScalingPlansScalingPlanId(scalingPlanName string, scalingPlanVersion int) string { + return fmt.Sprintf("%s/%d", scalingPlanName, scalingPlanVersion) +} + +// +// ApplicationSource functions. +// + +func expandAutoScalingPlansApplicationSource(vApplicationSource []interface{}) *autoscalingplans.ApplicationSource { + if len(vApplicationSource) == 0 || vApplicationSource[0] == nil { + return nil + } + mApplicationSource := vApplicationSource[0].(map[string]interface{}) + + applicationSource := &autoscalingplans.ApplicationSource{} + + if v, ok := mApplicationSource["cloudformation_stack_arn"].(string); ok && v != "" { + applicationSource.CloudFormationStackARN = aws.String(v) + } + + if vTagFilters, ok := mApplicationSource["tag_filter"].(*schema.Set); ok && vTagFilters.Len() > 0 { + tagFilters := []*autoscalingplans.TagFilter{} + + for _, vTagFilter := range vTagFilters.List() { + tagFilter := &autoscalingplans.TagFilter{} + + mTagFilter := vTagFilter.(map[string]interface{}) + + if v, ok := mTagFilter["key"].(string); ok && v != "" { + tagFilter.Key = aws.String(v) + } + + if vValues, ok := mTagFilter["values"].(*schema.Set); ok && vValues.Len() > 0 { + tagFilter.Values = expandStringSet(vValues) + } + + tagFilters = append(tagFilters, tagFilter) + } + + applicationSource.TagFilters = tagFilters + } + + return applicationSource +} + +func flattenAutoScalingPlansApplicationSource(applicationSource *autoscalingplans.ApplicationSource) []interface{} { + if applicationSource == nil { + return []interface{}{} + } + + mApplicationSource := map[string]interface{}{ + "cloudformation_stack_arn": aws.StringValue(applicationSource.CloudFormationStackARN), + } + + if tagFilters := applicationSource.TagFilters; tagFilters != nil { + vTagFilters := []interface{}{} + + for _, tagFilter := range tagFilters { + mTagFilter := map[string]interface{}{ + "key": aws.StringValue(tagFilter.Key), + "values": flattenStringSet(tagFilter.Values), + } + + vTagFilters = append(vTagFilters, mTagFilter) + } + + mApplicationSource["tag_filter"] = vTagFilters + } + + return []interface{}{mApplicationSource} +} + +// +// ScalingInstruction functions. +// + +func expandAutoScalingPlansScalingInstructions(vScalingInstructions *schema.Set) []*autoscalingplans.ScalingInstruction { + scalingInstructions := []*autoscalingplans.ScalingInstruction{} + + for _, vScalingInstruction := range vScalingInstructions.List() { + mScalingInstruction := vScalingInstruction.(map[string]interface{}) + + scalingInstruction := &autoscalingplans.ScalingInstruction{} + + if v, ok := mScalingInstruction["disable_dynamic_scaling"].(bool); ok { + scalingInstruction.DisableDynamicScaling = aws.Bool(v) + } + if v, ok := mScalingInstruction["max_capacity"].(int); ok { + scalingInstruction.MaxCapacity = aws.Int64(int64(v)) + } + if v, ok := mScalingInstruction["min_capacity"].(int); ok { + scalingInstruction.MinCapacity = aws.Int64(int64(v)) + } + if v, ok := mScalingInstruction["predictive_scaling_max_capacity_behavior"].(string); ok && v != "" { + scalingInstruction.PredictiveScalingMaxCapacityBehavior = aws.String(v) + } + if v, ok := mScalingInstruction["predictive_scaling_max_capacity_buffer"].(int); ok && v > 0 { + scalingInstruction.PredictiveScalingMaxCapacityBuffer = aws.Int64(int64(v)) + } + if v, ok := mScalingInstruction["predictive_scaling_mode"].(string); ok && v != "" { + scalingInstruction.PredictiveScalingMode = aws.String(v) + } + if v, ok := mScalingInstruction["resource_id"].(string); ok && v != "" { + scalingInstruction.ResourceId = aws.String(v) + } + if v, ok := mScalingInstruction["scalable_dimension"].(string); ok && v != "" { + scalingInstruction.ScalableDimension = aws.String(v) + } + if v, ok := mScalingInstruction["scaling_policy_update_behavior"].(string); ok && v != "" { + scalingInstruction.ScalingPolicyUpdateBehavior = aws.String(v) + } + if v, ok := mScalingInstruction["scheduled_action_buffer_time"].(int); ok && v > 0 { + scalingInstruction.ScheduledActionBufferTime = aws.Int64(int64(v)) + } + if v, ok := mScalingInstruction["service_namespace"].(string); ok && v != "" { + scalingInstruction.ServiceNamespace = aws.String(v) + } + + if vCustomizedLoadMetricSpecification, ok := mScalingInstruction["customized_load_metric_specification"].([]interface{}); ok && len(vCustomizedLoadMetricSpecification) > 0 && vCustomizedLoadMetricSpecification[0] != nil { + mCustomizedLoadMetricSpecification := vCustomizedLoadMetricSpecification[0].(map[string]interface{}) + + customizedLoadMetricSpecification := &autoscalingplans.CustomizedLoadMetricSpecification{} + + if v, ok := mCustomizedLoadMetricSpecification["dimensions"].(map[string]interface{}); ok { + dimensions := []*autoscalingplans.MetricDimension{} + + for key, value := range v { + dimension := &autoscalingplans.MetricDimension{} + + dimension.Name = aws.String(key) + dimension.Value = aws.String(value.(string)) + + dimensions = append(dimensions, dimension) + } + + customizedLoadMetricSpecification.Dimensions = dimensions + } + if v, ok := mCustomizedLoadMetricSpecification["metric_name"].(string); ok && v != "" { + customizedLoadMetricSpecification.MetricName = aws.String(v) + } + if v, ok := mCustomizedLoadMetricSpecification["namespace"].(string); ok && v != "" { + customizedLoadMetricSpecification.Namespace = aws.String(v) + } + if v, ok := mCustomizedLoadMetricSpecification["statistic"].(string); ok && v != "" { + customizedLoadMetricSpecification.Statistic = aws.String(v) + } + if v, ok := mCustomizedLoadMetricSpecification["unit"].(string); ok && v != "" { + customizedLoadMetricSpecification.Unit = aws.String(v) + } + + scalingInstruction.CustomizedLoadMetricSpecification = customizedLoadMetricSpecification + } + + if vPredefinedLoadMetricSpecification, ok := mScalingInstruction["predefined_load_metric_specification"].([]interface{}); ok && len(vPredefinedLoadMetricSpecification) > 0 && vPredefinedLoadMetricSpecification[0] != nil { + mPredefinedLoadMetricSpecification := vPredefinedLoadMetricSpecification[0].(map[string]interface{}) + + predefinedLoadMetricSpecification := &autoscalingplans.PredefinedLoadMetricSpecification{} + + if v, ok := mPredefinedLoadMetricSpecification["predefined_load_metric_type"].(string); ok && v != "" { + predefinedLoadMetricSpecification.PredefinedLoadMetricType = aws.String(v) + } + if v, ok := mPredefinedLoadMetricSpecification["resource_label"].(string); ok && v != "" { + predefinedLoadMetricSpecification.ResourceLabel = aws.String(v) + } + + scalingInstruction.PredefinedLoadMetricSpecification = predefinedLoadMetricSpecification + } + + if vTargetTrackingConfigurations, ok := mScalingInstruction["target_tracking_configuration"].(*schema.Set); ok && vTargetTrackingConfigurations.Len() > 0 { + targetTrackingConfigurations := []*autoscalingplans.TargetTrackingConfiguration{} + + for _, vTargetTrackingConfiguration := range vTargetTrackingConfigurations.List() { + targetTrackingConfiguration := &autoscalingplans.TargetTrackingConfiguration{} + + mTargetTrackingConfiguration := vTargetTrackingConfiguration.(map[string]interface{}) + + if v, ok := mTargetTrackingConfiguration["disable_scale_in"].(bool); ok { + targetTrackingConfiguration.DisableScaleIn = aws.Bool(v) + } + if v, ok := mTargetTrackingConfiguration["estimated_instance_warmup"].(int); ok && v > 0 { + targetTrackingConfiguration.EstimatedInstanceWarmup = aws.Int64(int64(v)) + } + if v, ok := mTargetTrackingConfiguration["scale_in_cooldown"].(int); ok && v > 0 { + targetTrackingConfiguration.ScaleInCooldown = aws.Int64(int64(v)) + } + if v, ok := mTargetTrackingConfiguration["scale_out_cooldown"].(int); ok && v > 0 { + targetTrackingConfiguration.ScaleOutCooldown = aws.Int64(int64(v)) + } + if v, ok := mTargetTrackingConfiguration["target_value"].(float64); ok && v > 0.0 { + targetTrackingConfiguration.TargetValue = aws.Float64(v) + } + + if vCustomizedScalingMetricSpecification, ok := mTargetTrackingConfiguration["customized_scaling_metric_specification"].([]interface{}); ok && len(vCustomizedScalingMetricSpecification) > 0 && vCustomizedScalingMetricSpecification[0] != nil { + mCustomizedScalingMetricSpecification := vCustomizedScalingMetricSpecification[0].(map[string]interface{}) + + customizedScalingMetricSpecification := &autoscalingplans.CustomizedScalingMetricSpecification{} + + if v, ok := mCustomizedScalingMetricSpecification["dimensions"].(map[string]interface{}); ok { + dimensions := []*autoscalingplans.MetricDimension{} + + for key, value := range v { + dimension := &autoscalingplans.MetricDimension{} + + dimension.Name = aws.String(key) + dimension.Value = aws.String(value.(string)) + + dimensions = append(dimensions, dimension) + } + + customizedScalingMetricSpecification.Dimensions = dimensions + } + if v, ok := mCustomizedScalingMetricSpecification["metric_name"].(string); ok && v != "" { + customizedScalingMetricSpecification.MetricName = aws.String(v) + } + if v, ok := mCustomizedScalingMetricSpecification["namespace"].(string); ok && v != "" { + customizedScalingMetricSpecification.Namespace = aws.String(v) + } + if v, ok := mCustomizedScalingMetricSpecification["statistic"].(string); ok && v != "" { + customizedScalingMetricSpecification.Statistic = aws.String(v) + } + if v, ok := mCustomizedScalingMetricSpecification["unit"].(string); ok && v != "" { + customizedScalingMetricSpecification.Unit = aws.String(v) + } + + targetTrackingConfiguration.CustomizedScalingMetricSpecification = customizedScalingMetricSpecification + } + + if vPredefinedScalingMetricSpecification, ok := mTargetTrackingConfiguration["predefined_scaling_metric_specification"].([]interface{}); ok && len(vPredefinedScalingMetricSpecification) > 0 && vPredefinedScalingMetricSpecification[0] != nil { + mPredefinedScalingMetricSpecification := vPredefinedScalingMetricSpecification[0].(map[string]interface{}) + + predefinedScalingMetricSpecification := &autoscalingplans.PredefinedScalingMetricSpecification{} + + if v, ok := mPredefinedScalingMetricSpecification["predefined_scaling_metric_type"].(string); ok && v != "" { + predefinedScalingMetricSpecification.PredefinedScalingMetricType = aws.String(v) + } + if v, ok := mPredefinedScalingMetricSpecification["resource_label"].(string); ok && v != "" { + predefinedScalingMetricSpecification.ResourceLabel = aws.String(v) + } + + targetTrackingConfiguration.PredefinedScalingMetricSpecification = predefinedScalingMetricSpecification + } + + targetTrackingConfigurations = append(targetTrackingConfigurations, targetTrackingConfiguration) + } + + scalingInstruction.TargetTrackingConfigurations = targetTrackingConfigurations + } + + scalingInstructions = append(scalingInstructions, scalingInstruction) + } + + return scalingInstructions +} + +func flattenAutoScalingPlansScalingInstructions(scalingInstructions []*autoscalingplans.ScalingInstruction) []interface{} { + vScalingInstructions := []interface{}{} + + for _, scalingInstruction := range scalingInstructions { + mScalingInstruction := map[string]interface{}{ + "disable_dynamic_scaling": aws.BoolValue(scalingInstruction.DisableDynamicScaling), + "max_capacity": int(aws.Int64Value(scalingInstruction.MaxCapacity)), + "min_capacity": int(aws.Int64Value(scalingInstruction.MinCapacity)), + "predictive_scaling_max_capacity_behavior": aws.StringValue(scalingInstruction.PredictiveScalingMaxCapacityBehavior), + "predictive_scaling_max_capacity_buffer": int(aws.Int64Value(scalingInstruction.PredictiveScalingMaxCapacityBuffer)), + "predictive_scaling_mode": aws.StringValue(scalingInstruction.PredictiveScalingMode), + "resource_id": aws.StringValue(scalingInstruction.ResourceId), + "scalable_dimension": aws.StringValue(scalingInstruction.ScalableDimension), + "scaling_policy_update_behavior": aws.StringValue(scalingInstruction.ScalingPolicyUpdateBehavior), + "scheduled_action_buffer_time": int(aws.Int64Value(scalingInstruction.ScheduledActionBufferTime)), + "service_namespace": aws.StringValue(scalingInstruction.ServiceNamespace), + } + + if customizedLoadMetricSpecification := scalingInstruction.CustomizedLoadMetricSpecification; customizedLoadMetricSpecification != nil { + mDimensions := map[string]interface{}{} + for _, dimension := range customizedLoadMetricSpecification.Dimensions { + mDimensions[aws.StringValue(dimension.Name)] = aws.StringValue(dimension.Value) + } + + mScalingInstruction["customized_load_metric_specification"] = []interface{}{ + map[string]interface{}{ + "dimensions": mDimensions, + "metric_name": aws.StringValue(customizedLoadMetricSpecification.MetricName), + "namespace": aws.StringValue(customizedLoadMetricSpecification.Namespace), + "statistic": aws.StringValue(customizedLoadMetricSpecification.Statistic), + "unit": aws.StringValue(customizedLoadMetricSpecification.Unit), + }, + } + } + + if predefinedLoadMetricSpecification := scalingInstruction.PredefinedLoadMetricSpecification; predefinedLoadMetricSpecification != nil { + mScalingInstruction["predefined_load_metric_specification"] = []interface{}{ + map[string]interface{}{ + "predefined_load_metric_type": aws.StringValue(predefinedLoadMetricSpecification.PredefinedLoadMetricType), + "resource_label": aws.StringValue(predefinedLoadMetricSpecification.ResourceLabel), + }, + } + } + + if targetTrackingConfigurations := scalingInstruction.TargetTrackingConfigurations; targetTrackingConfigurations != nil { + vTargetTrackingConfigurations := []interface{}{} + + for _, targetTrackingConfiguration := range targetTrackingConfigurations { + mTargetTrackingConfiguration := map[string]interface{}{ + "disable_scale_in": aws.BoolValue(targetTrackingConfiguration.DisableScaleIn), + "estimated_instance_warmup": int(aws.Int64Value(targetTrackingConfiguration.EstimatedInstanceWarmup)), + "scale_in_cooldown": int(aws.Int64Value(targetTrackingConfiguration.ScaleInCooldown)), + "scale_out_cooldown": int(aws.Int64Value(targetTrackingConfiguration.ScaleOutCooldown)), + "target_value": aws.Float64Value(targetTrackingConfiguration.TargetValue), + } + + if customizedScalingMetricSpecification := targetTrackingConfiguration.CustomizedScalingMetricSpecification; customizedScalingMetricSpecification != nil { + mDimensions := map[string]interface{}{} + for _, dimension := range customizedScalingMetricSpecification.Dimensions { + mDimensions[aws.StringValue(dimension.Name)] = aws.StringValue(dimension.Value) + } + + mTargetTrackingConfiguration["customized_scaling_metric_specification"] = []interface{}{ + map[string]interface{}{ + "dimensions": mDimensions, + "metric_name": aws.StringValue(customizedScalingMetricSpecification.MetricName), + "namespace": aws.StringValue(customizedScalingMetricSpecification.Namespace), + "statistic": aws.StringValue(customizedScalingMetricSpecification.Statistic), + "unit": aws.StringValue(customizedScalingMetricSpecification.Unit), + }, + } + } + + if predefinedScalingMetricSpecification := targetTrackingConfiguration.PredefinedScalingMetricSpecification; predefinedScalingMetricSpecification != nil { + mTargetTrackingConfiguration["predefined_scaling_metric_specification"] = []interface{}{ + map[string]interface{}{ + "predefined_scaling_metric_type": aws.StringValue(predefinedScalingMetricSpecification.PredefinedScalingMetricType), + "resource_label": aws.StringValue(predefinedScalingMetricSpecification.ResourceLabel), + }, + } + } + + vTargetTrackingConfigurations = append(vTargetTrackingConfigurations, mTargetTrackingConfiguration) + } + + mScalingInstruction["target_tracking_configuration"] = vTargetTrackingConfigurations + } + + vScalingInstructions = append(vScalingInstructions, mScalingInstruction) + } + + return vScalingInstructions +} diff --git a/aws/resource_aws_autoscalingplans_scaling_plan_test.go b/aws/resource_aws_autoscalingplans_scaling_plan_test.go new file mode 100644 index 000000000000..b4f659d2ca32 --- /dev/null +++ b/aws/resource_aws_autoscalingplans_scaling_plan_test.go @@ -0,0 +1,464 @@ +package aws + +import ( + "fmt" + "log" + "reflect" + "sort" + "strconv" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/autoscalingplans" + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/autoscalingplans/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfawsresource" +) + +func init() { + resource.AddTestSweepers("aws_autoscalingplans_scaling_plan", &resource.Sweeper{ + Name: "aws_autoscalingplans_scaling_plan", + F: testSweepAutoScalingPlansScalingPlans, + }) +} + +func testSweepAutoScalingPlansScalingPlans(region string) error { + client, err := sharedClientForRegion(region) + if err != nil { + return fmt.Errorf("error getting client: %w", err) + } + conn := client.(*AWSClient).autoscalingplansconn + input := &autoscalingplans.DescribeScalingPlansInput{} + var sweeperErrs *multierror.Error + + for { + output, err := conn.DescribeScalingPlans(input) + if testSweepSkipSweepError(err) { + log.Printf("[WARN] Skipping Auto Scaling Scaling Plans sweep for %s: %s", region, err) + return sweeperErrs.ErrorOrNil() // In case we have completed some pages, but had errors + } + if err != nil { + sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error listing Auto Scaling Scaling Plans: %w", err)) + return sweeperErrs.ErrorOrNil() + } + + for _, scalingPlan := range output.ScalingPlans { + scalingPlanName := aws.StringValue(scalingPlan.ScalingPlanName) + scalingPlanVersion := int(aws.Int64Value(scalingPlan.ScalingPlanVersion)) + + r := resourceAwsAutoScalingPlansScalingPlan() + d := r.Data(nil) + d.SetId("????????????????") // ID not used in Delete. + d.Set("name", scalingPlanName) + d.Set("scaling_plan_version", scalingPlanVersion) + err = r.Delete(d, client) + + if err != nil { + log.Printf("[ERROR] %s", err) + sweeperErrs = multierror.Append(sweeperErrs, err) + continue + } + } + + if aws.StringValue(output.NextToken) == "" { + break + } + input.NextToken = output.NextToken + } + + return sweeperErrs.ErrorOrNil() +} + +func TestAccAwsAutoScalingPlansScalingPlan_basicDynamicScaling(t *testing.T) { + var scalingPlan autoscalingplans.ScalingPlan + resourceName := "aws_autoscalingplans_scaling_plan.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAutoScalingPlansScalingPlanDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAutoScalingPlansScalingPlanConfigBasicDynamicScaling(rName, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAutoScalingPlansScalingPlanExists(resourceName, &scalingPlan), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "scaling_plan_version", "1"), + resource.TestCheckResourceAttr(resourceName, "application_source.#", "1"), + resource.TestCheckResourceAttr(resourceName, "application_source.0.cloudformation_stack_arn", ""), + testAccCheckAutoScalingPlansApplicationSourceTags(&scalingPlan, map[string][]string{ + rName: {rName}, + }), + resource.TestCheckResourceAttr(resourceName, "scaling_instruction.#", "1"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "scaling_instruction.*", map[string]string{ + "customized_load_metric_specification.#": "0", + "disable_dynamic_scaling": "false", + "max_capacity": "3", + "min_capacity": "0", + "predefined_load_metric_specification.#": "0", + "resource_id": fmt.Sprintf("autoScalingGroup/%s", rName), + "scalable_dimension": "autoscaling:autoScalingGroup:DesiredCapacity", + "scaling_policy_update_behavior": "KeepExternalPolicies", + "service_namespace": "autoscaling", + "target_tracking_configuration.#": "1", + }), + ), + }, + { + ResourceName: resourceName, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + return rName, nil + }, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsAutoScalingPlansScalingPlan_basicPredictiveScaling(t *testing.T) { + var scalingPlan autoscalingplans.ScalingPlan + resourceName := "aws_autoscalingplans_scaling_plan.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckIamServiceLinkedRole(t, "/aws-service-role/autoscaling-plans") + }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAutoScalingPlansScalingPlanDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAutoScalingPlansScalingPlanConfigBasicPredictiveScaling(rName, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAutoScalingPlansScalingPlanExists(resourceName, &scalingPlan), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "scaling_plan_version", "1"), + resource.TestCheckResourceAttr(resourceName, "application_source.#", "1"), + resource.TestCheckResourceAttr(resourceName, "application_source.0.cloudformation_stack_arn", ""), + testAccCheckAutoScalingPlansApplicationSourceTags(&scalingPlan, map[string][]string{ + rName: {rName}, + }), + resource.TestCheckResourceAttr(resourceName, "scaling_instruction.#", "1"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "scaling_instruction.*", map[string]string{ + "customized_load_metric_specification.#": "0", + "disable_dynamic_scaling": "true", + "max_capacity": "3", + "min_capacity": "0", + "predefined_load_metric_specification.#": "1", + "predefined_load_metric_specification.0.predefined_load_metric_type": "ASGTotalCPUUtilization", + "predictive_scaling_max_capacity_behavior": "SetForecastCapacityToMaxCapacity", + "predictive_scaling_mode": "ForecastOnly", + "resource_id": fmt.Sprintf("autoScalingGroup/%s", rName), + "scalable_dimension": "autoscaling:autoScalingGroup:DesiredCapacity", + "scaling_policy_update_behavior": "KeepExternalPolicies", + "service_namespace": "autoscaling", + "target_tracking_configuration.#": "1", + }), + ), + }, + { + ResourceName: resourceName, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + return rName, nil + }, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsAutoScalingPlansScalingPlan_basicUpdate(t *testing.T) { + var scalingPlan autoscalingplans.ScalingPlan + resourceName := "aws_autoscalingplans_scaling_plan.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + rNameUpdated := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckIamServiceLinkedRole(t, "/aws-service-role/autoscaling-plans") + }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAutoScalingPlansScalingPlanDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAutoScalingPlansScalingPlanConfigBasicDynamicScaling(rName, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAutoScalingPlansScalingPlanExists(resourceName, &scalingPlan), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "scaling_plan_version", "1"), + resource.TestCheckResourceAttr(resourceName, "application_source.#", "1"), + resource.TestCheckResourceAttr(resourceName, "application_source.0.cloudformation_stack_arn", ""), + testAccCheckAutoScalingPlansApplicationSourceTags(&scalingPlan, map[string][]string{ + rName: {rName}, + }), + resource.TestCheckResourceAttr(resourceName, "scaling_instruction.#", "1"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "scaling_instruction.*", map[string]string{ + "customized_load_metric_specification.#": "0", + "disable_dynamic_scaling": "false", + "max_capacity": "3", + "min_capacity": "0", + "predefined_load_metric_specification.#": "0", + "resource_id": fmt.Sprintf("autoScalingGroup/%s", rName), + "scalable_dimension": "autoscaling:autoScalingGroup:DesiredCapacity", + "scaling_policy_update_behavior": "KeepExternalPolicies", + "service_namespace": "autoscaling", + "target_tracking_configuration.#": "1", + }), + ), + }, + { + Config: testAccAutoScalingPlansScalingPlanConfigBasicPredictiveScaling(rName, rNameUpdated), + Check: resource.ComposeTestCheckFunc( + testAccCheckAutoScalingPlansScalingPlanExists(resourceName, &scalingPlan), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "scaling_plan_version", "1"), + resource.TestCheckResourceAttr(resourceName, "application_source.#", "1"), + resource.TestCheckResourceAttr(resourceName, "application_source.0.cloudformation_stack_arn", ""), + testAccCheckAutoScalingPlansApplicationSourceTags(&scalingPlan, map[string][]string{ + rNameUpdated: {rNameUpdated}, + }), + resource.TestCheckResourceAttr(resourceName, "scaling_instruction.#", "1"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "scaling_instruction.*", map[string]string{ + "customized_load_metric_specification.#": "0", + "disable_dynamic_scaling": "true", + "max_capacity": "3", + "min_capacity": "0", + "predefined_load_metric_specification.#": "1", + "predefined_load_metric_specification.0.predefined_load_metric_type": "ASGTotalCPUUtilization", + "predictive_scaling_max_capacity_behavior": "SetForecastCapacityToMaxCapacity", + "predictive_scaling_mode": "ForecastOnly", + "resource_id": fmt.Sprintf("autoScalingGroup/%s", rName), + "scalable_dimension": "autoscaling:autoScalingGroup:DesiredCapacity", + "scaling_policy_update_behavior": "KeepExternalPolicies", + "service_namespace": "autoscaling", + "target_tracking_configuration.#": "1", + }), + ), + }, + { + ResourceName: resourceName, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + return rName, nil + }, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsAutoScalingPlansScalingPlan_disappears(t *testing.T) { + var scalingPlan autoscalingplans.ScalingPlan + resourceName := "aws_autoscalingplans_scaling_plan.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAutoScalingPlansScalingPlanDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAutoScalingPlansScalingPlanConfigBasicDynamicScaling(rName, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAutoScalingPlansScalingPlanExists(resourceName, &scalingPlan), + testAccCheckResourceDisappears(testAccProvider, resourceAwsAutoScalingPlansScalingPlan(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckAutoScalingPlansScalingPlanDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).autoscalingplansconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_autoscalingplans_scaling_plan" { + continue + } + + scalingPlanVersion, err := strconv.Atoi(rs.Primary.Attributes["scaling_plan_version"]) + if err != nil { + return err + } + + scalingPlan, err := finder.ScalingPlan(conn, rs.Primary.Attributes["name"], scalingPlanVersion) + if err != nil { + return err + } + if scalingPlan == nil { + continue + } + return fmt.Errorf("Auto Scaling Scaling Plan %s still exists", rs.Primary.ID) + } + + return nil +} + +func testAccCheckAutoScalingPlansScalingPlanExists(name string, v *autoscalingplans.ScalingPlan) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).autoscalingplansconn + + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + if rs.Primary.ID == "" { + return fmt.Errorf("No Auto Scaling Scaling Plan ID is set") + } + + scalingPlanVersion, err := strconv.Atoi(rs.Primary.Attributes["scaling_plan_version"]) + if err != nil { + return err + } + + scalingPlan, err := finder.ScalingPlan(conn, rs.Primary.Attributes["name"], scalingPlanVersion) + if err != nil { + return err + } + if scalingPlan == nil { + return fmt.Errorf("Auto Scaling Scaling Plan %s not found", rs.Primary.ID) + } + + *v = *scalingPlan + + return nil + } +} + +func testAccCheckAutoScalingPlansApplicationSourceTags(scalingPlan *autoscalingplans.ScalingPlan, expectedTagFilters map[string][]string) resource.TestCheckFunc { + return func(s *terraform.State) error { + for _, tagFilter := range scalingPlan.ApplicationSource.TagFilters { + key := aws.StringValue(tagFilter.Key) + values := aws.StringValueSlice(tagFilter.Values) + + expectedValues, ok := expectedTagFilters[key] + if !ok { + return fmt.Errorf("Scaling plan application source tag filter key %q not expected", key) + } + + sort.Strings(values) + sort.Strings(expectedValues) + if !reflect.DeepEqual(values, expectedValues) { + return fmt.Errorf("Scaling plan application source tag filter values %q, expected %q", values, expectedValues) + } + } + + return nil + } +} + +func testAccAutoScalingPlansScalingPlanConfigBase(rName, tagName string) string { + return composeConfig( + testAccLatestAmazonLinuxHvmEbsAmiConfig(), + testAccAvailableAZsNoOptInDefaultExcludeConfig(), + testAccAvailableEc2InstanceTypeForRegion("t3.micro", "t2.micro"), + fmt.Sprintf(` +resource "aws_launch_configuration" "test" { + image_id = data.aws_ami.amzn-ami-minimal-hvm-ebs.id + instance_type = data.aws_ec2_instance_type_offering.available.instance_type +} + +resource "aws_autoscaling_group" "test" { + name = %[1]q + + launch_configuration = aws_launch_configuration.test.name + availability_zones = [data.aws_availability_zones.available.names[0]] + + min_size = 0 + max_size = 3 + desired_capacity = 0 + + tags = [ + { + key = %[2]q + value = %[2]q + propagate_at_launch = true + }, + ] +} +`, rName, tagName)) +} + +func testAccAutoScalingPlansScalingPlanConfigBasicDynamicScaling(rName, tagName string) string { + return composeConfig( + testAccAutoScalingPlansScalingPlanConfigBase(rName, tagName), + fmt.Sprintf(` +resource "aws_autoscalingplans_scaling_plan" "test" { + name = %[1]q + + application_source { + tag_filter { + key = %[2]q + values = [%[2]q] + } + } + + scaling_instruction { + max_capacity = aws_autoscaling_group.test.max_size + min_capacity = aws_autoscaling_group.test.min_size + resource_id = format("autoScalingGroup/%%s", aws_autoscaling_group.test.name) + scalable_dimension = "autoscaling:autoScalingGroup:DesiredCapacity" + service_namespace = "autoscaling" + + target_tracking_configuration { + predefined_scaling_metric_specification { + predefined_scaling_metric_type = "ASGAverageCPUUtilization" + } + + target_value = 75 + } + } +} +`, rName, tagName)) +} + +func testAccAutoScalingPlansScalingPlanConfigBasicPredictiveScaling(rName, tagName string) string { + return composeConfig( + testAccAutoScalingPlansScalingPlanConfigBase(rName, tagName), + fmt.Sprintf(` +resource "aws_autoscalingplans_scaling_plan" "test" { + name = %[1]q + + application_source { + tag_filter { + key = %[2]q + values = [%[2]q] + } + } + + scaling_instruction { + disable_dynamic_scaling = true + + max_capacity = aws_autoscaling_group.test.max_size + min_capacity = aws_autoscaling_group.test.min_size + resource_id = format("autoScalingGroup/%%s", aws_autoscaling_group.test.name) + scalable_dimension = "autoscaling:autoScalingGroup:DesiredCapacity" + service_namespace = "autoscaling" + + target_tracking_configuration { + predefined_scaling_metric_specification { + predefined_scaling_metric_type = "ASGAverageCPUUtilization" + } + + target_value = 75 + } + + predictive_scaling_max_capacity_behavior = "SetForecastCapacityToMaxCapacity" + predictive_scaling_mode = "ForecastOnly" + + predefined_load_metric_specification { + predefined_load_metric_type = "ASGTotalCPUUtilization" + } + } +} +`, rName, tagName)) +} diff --git a/website/allowed-subcategories.txt b/website/allowed-subcategories.txt index 15d8db21d040..3acc31cc3043 100644 --- a/website/allowed-subcategories.txt +++ b/website/allowed-subcategories.txt @@ -9,6 +9,7 @@ AppSync Application Autoscaling Athena Autoscaling +Autoscaling Plans Backup Batch Budgets diff --git a/website/docs/r/autoscalingplans_scaling_plan.html.markdown b/website/docs/r/autoscalingplans_scaling_plan.html.markdown new file mode 100644 index 000000000000..73298f160d9e --- /dev/null +++ b/website/docs/r/autoscalingplans_scaling_plan.html.markdown @@ -0,0 +1,224 @@ +--- +subcategory: "Autoscaling Plans" +layout: "aws" +page_title: "AWS: aws_autoscalingplans_scaling_plan" +description: |- + Manages an AWS Auto Scaling scaling plan. +--- + +# Resource: aws_autoscalingplans_scaling_plan + +Manages an AWS Auto Scaling scaling plan. +More information can be found in the [AWS Auto Scaling User Guide](https://docs.aws.amazon.com/autoscaling/plans/userguide/what-is-aws-auto-scaling.html). + +~> **NOTE:** The AWS Auto Scaling service uses an AWS IAM service-linked role to manage predictive scaling of Amazon EC2 Auto Scaling groups. The service attempts to automatically create this role the first time a scaling plan with predictive scaling enabled is created. +An [`aws_iam_service_linked_role`](/docs/providers/aws/r/iam_service_linked_role.html) resource can be used to manually manage this role. +See the [AWS documentation](https://docs.aws.amazon.com/autoscaling/plans/userguide/aws-auto-scaling-service-linked-roles.html#create-service-linked-role-manual) for more details. + +## Example Usage + +### Basic Dynamic Scaling + +```hcl +data "aws_availability_zones" "available" {} + +resource "aws_autoscaling_group" "example" { + name_prefix = "example" + + launch_configuration = aws_launch_configuration.example.name + availability_zones = [data.aws_availability_zones.available.names[0]] + + min_size = 0 + max_size = 3 + + tags = [ + { + key = "application" + value = "example" + propagate_at_launch = true + }, + ] +} + +resource "aws_autoscalingplans_scaling_plan" "example" { + name = "example-dynamic-cost-optimization" + + application_source { + tag_filter { + key = "application" + values = ["example"] + } + } + + scaling_instruction { + max_capacity = 3 + min_capacity = 0 + resource_id = format("autoScalingGroup/%s", aws_autoscaling_group.example.name) + scalable_dimension = "autoscaling:autoScalingGroup:DesiredCapacity" + service_namespace = "autoscaling" + + target_tracking_configuration { + predefined_scaling_metric_specification { + predefined_scaling_metric_type = "ASGAverageCPUUtilization" + } + + target_value = 70 + } + } +} +``` + +### Basic Predictive Scaling + +```hcl +data "aws_availability_zones" "available" {} + +resource "aws_autoscaling_group" "example" { + name_prefix = "example" + + launch_configuration = aws_launch_configuration.example.name + availability_zones = [data.aws_availability_zones.available.names[0]] + + min_size = 0 + max_size = 3 + + tags = [ + { + key = "application" + value = "example" + propagate_at_launch = true + }, + ] +} + +resource "aws_autoscalingplans_scaling_plan" "example" { + name = "example-predictive-cost-optimization" + + application_source { + tag_filter { + key = "application" + values = ["example"] + } + } + + scaling_instruction { + disable_dynamic_scaling = true + + max_capacity = 3 + min_capacity = 0 + resource_id = format("autoScalingGroup/%s", aws_autoscaling_group.example.name) + scalable_dimension = "autoscaling:autoScalingGroup:DesiredCapacity" + service_namespace = "autoscaling" + + target_tracking_configuration { + predefined_scaling_metric_specification { + predefined_scaling_metric_type = "ASGAverageCPUUtilization" + } + + target_value = 70 + } + + predictive_scaling_max_capacity_behavior = "SetForecastCapacityToMaxCapacity" + predictive_scaling_mode = "ForecastAndScale" + + predefined_load_metric_specification { + predefined_load_metric_type = "ASGTotalCPUUtilization" + } + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the scaling plan. Names cannot contain vertical bars, colons, or forward slashes. +* `application_source` - (Required) A CloudFormation stack or set of tags. You can create one scaling plan per application source. +* `scaling_instruction` - (Required) The scaling instructions. More details can be found in the [AWS Auto Scaling API Reference](https://docs.aws.amazon.com/autoscaling/plans/APIReference/API_ScalingInstruction.html). + +The `application_source` object supports the following: + +* `cloudformation_stack_arn` - (Optional) The Amazon Resource Name (ARN) of a AWS CloudFormation stack. +* `tag_filter` - (Optional) A set of tags. + +The `tag_filter` object supports the following: + +* `key` - (Required) The tag key. +* `values` - (Optional) The tag values. + +The `scaling_instruction` object supports the following: + +* `max_capacity` - (Required) The maximum capacity of the resource. The exception to this upper limit is if you specify a non-default setting for `predictive_scaling_max_capacity_behavior`. +* `min_capacity` - (Required) The minimum capacity of the resource. +* `resource_id` - (Required) The ID of the resource. This string consists of the resource type and unique identifier. +* `scalable_dimension` - (Required) The scalable dimension associated with the resource. Valid values: `autoscaling:autoScalingGroup:DesiredCapacity`, `dynamodb:index:ReadCapacityUnits`, `dynamodb:index:WriteCapacityUnits`, `dynamodb:table:ReadCapacityUnits`, `dynamodb:table:WriteCapacityUnits`, `ecs:service:DesiredCount`, `ec2:spot-fleet-request:TargetCapacity`, `rds:cluster:ReadReplicaCount`. +* `service_namespace` - (Required) The namespace of the AWS service. Valid values: `autoscaling`, `dynamodb`, `ecs`, `ec2`, `rds`. +* `target_tracking_configuration` - (Required) The structure that defines new target tracking configurations. Each of these structures includes a specific scaling metric and a target value for the metric, along with various parameters to use with dynamic scaling. +More details can be found in the [AWS Auto Scaling API Reference](https://docs.aws.amazon.com/autoscaling/plans/APIReference/API_TargetTrackingConfiguration.html). +* `customized_load_metric_specification` - (Optional) The customized load metric to use for predictive scaling. You must specify either `customized_load_metric_specification` or `predefined_load_metric_specification` when configuring predictive scaling. +More details can be found in the [AWS Auto Scaling API Reference](https://docs.aws.amazon.com/autoscaling/plans/APIReference/API_CustomizedLoadMetricSpecification.html). +* `disable_dynamic_scaling` - (Optional) Boolean controlling whether dynamic scaling by AWS Auto Scaling is disabled. Defaults to `false`. +* `predefined_load_metric_specification` - (Optional) The predefined load metric to use for predictive scaling. You must specify either `predefined_load_metric_specification` or `customized_load_metric_specification` when configuring predictive scaling. +More details can be found in the [AWS Auto Scaling API Reference](https://docs.aws.amazon.com/autoscaling/plans/APIReference/API_PredefinedLoadMetricSpecification.html). +* `predictive_scaling_max_capacity_behavior`- (Optional) Defines the behavior that should be applied if the forecast capacity approaches or exceeds the maximum capacity specified for the resource. +Valid values: `SetForecastCapacityToMaxCapacity`, `SetMaxCapacityAboveForecastCapacity`, `SetMaxCapacityToForecastCapacity`. +* `predictive_scaling_max_capacity_buffer` - (Optional) The size of the capacity buffer to use when the forecast capacity is close to or exceeds the maximum capacity. +* `predictive_scaling_mode` - (Optional) The predictive scaling mode. Valid values: `ForecastAndScale`, `ForecastOnly`. +* `scaling_policy_update_behavior` - (Optional) Controls whether a resource's externally created scaling policies are kept or replaced. Valid values: `KeepExternalPolicies`, `ReplaceExternalPolicies`. Defaults to `KeepExternalPolicies`. +* `scheduled_action_buffer_time` - (Optional) The amount of time, in seconds, to buffer the run time of scheduled scaling actions when scaling out. + +The `customized_load_metric_specification` object supports the following: + +* `metric_name` - (Required) The name of the metric. +* `namespace` - (Required) The namespace of the metric. +* `statistic` - (Required) The statistic of the metric. Currently, the value must always be `Sum`. +* `dimensions` - (Optional) The dimensions of the metric. +* `unit` - (Optional) The unit of the metric. + +The `predefined_load_metric_specification` object supports the following: + +* `predefined_load_metric_type` - (Required) The metric type. Valid values: `ALBTargetGroupRequestCount`, `ASGTotalCPUUtilization`, `ASGTotalNetworkIn`, `ASGTotalNetworkOut`. +* `resource_label` - (Optional) Identifies the resource associated with the metric type. + +The `target_tracking_configuration` object supports the following: + +* `target_value` - (Required) The target value for the metric. +* `customized_scaling_metric_specification` - (Optional) A customized metric. You can specify either `customized_scaling_metric_specification` or `predefined_scaling_metric_specification`. +More details can be found in the [AWS Auto Scaling API Reference](https://docs.aws.amazon.com/autoscaling/plans/APIReference/API_CustomizedScalingMetricSpecification.html). +* `disable_scale_in` - (Optional) Boolean indicating whether scale in by the target tracking scaling policy is disabled. Defaults to `false`. +* `predefined_scaling_metric_specification` - (Optional) A predefined metric. You can specify either `predefined_scaling_metric_specification` or `customized_scaling_metric_specification`. +More details can be found in the [AWS Auto Scaling API Reference](https://docs.aws.amazon.com/autoscaling/plans/APIReference/API_PredefinedScalingMetricSpecification.html). +* `estimated_instance_warmup` - (Optional) The estimated time, in seconds, until a newly launched instance can contribute to the CloudWatch metrics. +This value is used only if the resource is an Auto Scaling group. +* `scale_in_cooldown` - (Optional) The amount of time, in seconds, after a scale in activity completes before another scale in activity can start. +This value is not used if the scalable resource is an Auto Scaling group. +* `scale_out_cooldown` - (Optional) The amount of time, in seconds, after a scale-out activity completes before another scale-out activity can start. +This value is not used if the scalable resource is an Auto Scaling group. + +The `customized_scaling_metric_specification` object supports the following: + +* `metric_name` - (Required) The name of the metric. +* `namespace` - (Required) The namespace of the metric. +* `statistic` - (Required) The statistic of the metric. Valid values: `Average`, `Maximum`, `Minimum`, `SampleCount`, `Sum`. +* `dimensions` - (Optional) The dimensions of the metric. +* `unit` - (Optional) The unit of the metric. + +The `predefined_scaling_metric_specification` object supports the following: + +* `predefined_scaling_metric_type` - (Required) The metric type. Valid values: `ALBRequestCountPerTarget`, `ASGAverageCPUUtilization`, `ASGAverageNetworkIn`, `ASGAverageNetworkOut`, `DynamoDBReadCapacityUtilization`, `DynamoDBWriteCapacityUtilization`, `ECSServiceAverageCPUUtilization`, `ECSServiceAverageMemoryUtilization`, `EC2SpotFleetRequestAverageCPUUtilization`, `EC2SpotFleetRequestAverageNetworkIn`, `EC2SpotFleetRequestAverageNetworkOut`, `RDSReaderAverageCPUUtilization`, `RDSReaderAverageDatabaseConnections`. +* `resource_label` - (Optional) Identifies the resource associated with the metric type. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The scaling plan identifier. +* `scaling_plan_version` - The version number of the scaling plan. This value is always 1. + +## Import + +Auto Scaling scaling plans can be imported using the `name`, e.g. + +``` +$ terraform import aws_autoscalingplans_scaling_plan.example MyScale1 +```