diff --git a/internal/service/ecs/task_set.go b/internal/service/ecs/task_set.go index 1165eb56fce..714c5fd8a90 100644 --- a/internal/service/ecs/task_set.go +++ b/internal/service/ecs/task_set.go @@ -13,7 +13,6 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-provider-aws/internal/conns" - tfiam "github.com/hashicorp/terraform-provider-aws/internal/service/iam" tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/internal/verify" @@ -21,20 +20,26 @@ import ( func ResourceTaskSet() *schema.Resource { return &schema.Resource{ - Create: resourceTaskSetCreate, - Read: resourceTaskSetRead, - Update: resourceTaskSetUpdate, - Delete: resourceTaskSetDelete, + Create: ResourceTaskSetCreate, + Read: ResourceTaskSetRead, + Update: ResourceTaskSetUpdate, + Delete: ResourceTaskSetDelete, Importer: &schema.ResourceImporter{ State: schema.ImportStatePassthrough, }, + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(10 * time.Minute), + Read: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(10 * time.Minute), + Update: schema.DefaultTimeout(10 * time.Minute), + }, + Schema: map[string]*schema.Schema{ "arn": { Type: schema.TypeString, Computed: true, }, - "service": { Type: schema.TypeString, Required: true, @@ -60,11 +65,6 @@ func ResourceTaskSet() *schema.Resource { ForceNew: true, }, - "task_set_id": { - Type: schema.TypeString, - Computed: true, - }, - "network_configuration": { Type: schema.TypeList, MaxItems: 1, @@ -78,6 +78,7 @@ func ResourceTaskSet() *schema.Resource { Optional: true, ForceNew: true, Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, }, "subnets": { Type: schema.TypeSet, @@ -85,6 +86,7 @@ func ResourceTaskSet() *schema.Resource { Required: true, ForceNew: true, Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, }, "assign_public_ip": { Type: schema.TypeBool, @@ -99,13 +101,14 @@ func ResourceTaskSet() *schema.Resource { // If you are using the CodeDeploy or an external deployment controller, // multiple target groups are not supported. // https://docs.aws.amazon.com/AmazonECS/latest/developerguide/register-multiple-targetgroups.html - "load_balancer": { - Type: schema.TypeSet, + "load_balancers": { + Type: schema.TypeList, + MaxItems: 1, Optional: true, ForceNew: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "load_balancer_name": { + "elb_name": { Type: schema.TypeString, Optional: true, ForceNew: true, @@ -141,24 +144,20 @@ func ResourceTaskSet() *schema.Resource { "container_name": { Type: schema.TypeString, Optional: true, - ForceNew: true, }, "container_port": { Type: schema.TypeInt, Optional: true, - ForceNew: true, ValidateFunc: validation.IsPortNumber, }, "port": { Type: schema.TypeInt, Optional: true, - ForceNew: true, ValidateFunc: validation.IsPortNumber, }, "registry_arn": { Type: schema.TypeString, - Required: true, - ForceNew: true, + Optional: true, ValidateFunc: verify.ValidARN, }, }, @@ -166,19 +165,20 @@ func ResourceTaskSet() *schema.Resource { }, "launch_type": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - Computed: true, - ValidateFunc: validation.StringInSlice(ecs.LaunchType_Values(), false), - ConflictsWith: []string{"capacity_provider_strategy"}, + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + ValidateFunc: validation.StringInSlice([]string{ + ecs.LaunchTypeEc2, + ecs.LaunchTypeFargate, + }, false), }, "capacity_provider_strategy": { - Type: schema.TypeSet, - Optional: true, - ForceNew: true, - ConflictsWith: []string{"launch_type"}, + Type: schema.TypeSet, + Optional: true, + ForceNew: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "base": { @@ -196,7 +196,7 @@ func ResourceTaskSet() *schema.Resource { "weight": { Type: schema.TypeInt, - Required: true, + Optional: true, ValidateFunc: validation.IntBetween(0, 1000), ForceNew: true, }, @@ -219,10 +219,12 @@ func ResourceTaskSet() *schema.Resource { Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "unit": { - Type: schema.TypeString, - Optional: true, - Default: ecs.ScaleUnitPercent, - ValidateFunc: validation.StringInSlice(ecs.ScaleUnit_Values(), false), + Type: schema.TypeString, + Optional: true, + Default: ecs.ScaleUnitPercent, + ValidateFunc: validation.StringInSlice([]string{ + ecs.ScaleUnitPercent, + }, false), }, "value": { Type: schema.TypeFloat, @@ -238,20 +240,6 @@ func ResourceTaskSet() *schema.Resource { Optional: true, }, - "stability_status": { - Type: schema.TypeString, - Computed: true, - }, - - "status": { - Type: schema.TypeString, - Computed: true, - }, - - "tags": tftags.TagsSchema(), - - "tags_all": tftags.TagsSchemaComputed(), - "wait_until_stable": { Type: schema.TypeBool, Optional: true, @@ -261,13 +249,12 @@ func ResourceTaskSet() *schema.Resource { "wait_until_stable_timeout": { Type: schema.TypeString, Optional: true, - Default: "10m", ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { value := v.(string) duration, err := time.ParseDuration(value) if err != nil { errors = append(errors, fmt.Errorf( - "%q cannot be parsed as a duration: %w", k, err)) + "%q cannot be parsed as a duration: %s", k, err)) } if duration < 0 { errors = append(errors, fmt.Errorf( @@ -276,32 +263,23 @@ func ResourceTaskSet() *schema.Resource { return }, }, - }, - CustomizeDiff: verify.SetTagsDiff, + "tags": tftags.TagsSchema(), + }, } } -func resourceTaskSetCreate(d *schema.ResourceData, meta interface{}) error { +func ResourceTaskSetCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).ECSConn - defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig - tags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{}))) cluster := d.Get("cluster").(string) service := d.Get("service").(string) - input := &ecs.CreateTaskSetInput{ + input := ecs.CreateTaskSetInput{ ClientToken: aws.String(resource.UniqueId()), Cluster: aws.String(cluster), Service: aws.String(service), TaskDefinition: aws.String(d.Get("task_definition").(string)), - } - - if len(tags) > 0 { - input.Tags = Tags(tags.IgnoreAWS()) - } - - if v, ok := d.GetOk("capacity_provider_strategy"); ok && v.(*schema.Set).Len() > 0 { - input.CapacityProviderStrategy = expandEcsCapacityProviderStrategy(v.(*schema.Set)) + Tags: Tags(tftags.New(d.Get("tags").(map[string]interface{})).IgnoreAWS()), } if v, ok := d.GetOk("external_id"); ok { @@ -312,234 +290,455 @@ func resourceTaskSetCreate(d *schema.ResourceData, meta interface{}) error { input.LaunchType = aws.String(v.(string)) } - if v, ok := d.GetOk("load_balancer"); ok && v.(*schema.Set).Len() > 0 { - input.LoadBalancers = expandTaskSetLoadBalancers(v.(*schema.Set).List()) - } + input.CapacityProviderStrategy = expandEcsCapacityProviderStrategy(d.Get("capacity_provider_strategy").(*schema.Set)) - if v, ok := d.GetOk("network_configuration"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { - input.NetworkConfiguration = expandEcsNetworkConfiguration(v.([]interface{})) + loadBalancers := expandLoadBalancers(d.Get("load_balancers").([]interface{})) + if len(loadBalancers) > 0 { + log.Printf("[DEBUG] Adding ECS load balancers: %s", loadBalancers) + input.LoadBalancers = loadBalancers } + input.NetworkConfiguration = expandEcsNetworkConfiguration(d.Get("network_configuration").([]interface{})) + if v, ok := d.GetOk("platform_version"); ok { input.PlatformVersion = aws.String(v.(string)) } - if v, ok := d.GetOk("scale"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { - input.Scale = expandScale(v.([]interface{})) + scale := d.Get("scale").([]interface{}) + if len(scale) > 0 { + input.Scale = expandAwsEcsScale(scale[0].(map[string]interface{})) } - if v, ok := d.GetOk("service_registries"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { - input.ServiceRegistries = expandServiceRegistries(v.([]interface{})) + serviceRegistries := d.Get("service_registries").([]interface{}) + if len(serviceRegistries) > 0 { + input.ServiceRegistries = expandAwsEcsServiceRegistries(serviceRegistries) } + log.Printf("[DEBUG] Creating ECS Task set: %s", input) + // Retry due to AWS IAM & ECS eventual consistency - output, err := tfresource.RetryWhen( - tfiam.PropagationTimeout+taskSetCreateTimeout, - func() (interface{}, error) { - return conn.CreateTaskSet(input) - }, - func(err error) (bool, error) { - if tfawserr.ErrCodeEquals(err, ecs.ErrCodeClusterNotFoundException, ecs.ErrCodeServiceNotFoundException, ecs.ErrCodeTaskSetNotFoundException) || + var out *ecs.CreateTaskSetOutput + var err error + err = resource.Retry(d.Timeout(schema.TimeoutCreate), func() *resource.RetryError { + out, err = conn.CreateTaskSet(&input) + + if err != nil { + if tfawserr.ErrCodeEquals(err, ecs.ErrCodeClusterNotFoundException) || + tfawserr.ErrCodeEquals(err, ecs.ErrCodeServiceNotFoundException) || + tfawserr.ErrCodeEquals(err, ecs.ErrCodeTaskSetNotFoundException) || tfawserr.ErrMessageContains(err, ecs.ErrCodeInvalidParameterException, "does not have an associated load balancer") { - return true, err + return resource.RetryableError(err) } - return false, err - }, - ) + return resource.NonRetryableError(err) + } - if err != nil { - return fmt.Errorf("error creating ECS TaskSet: %w", err) + return nil + }) + + if tfresource.TimedOut(err) { + out, err = conn.CreateTaskSet(&input) } - result, ok := output.(*ecs.CreateTaskSetOutput) - if !ok || result == nil || result.TaskSet == nil { - return fmt.Errorf("error creating ECS TaskSet: empty output") + if err != nil { + return fmt.Errorf("Error creating ECS TaskSet: %s", err) } - taskSetId := aws.StringValue(result.TaskSet.Id) + taskSet := *out.TaskSet - d.SetId(fmt.Sprintf("%s,%s,%s", taskSetId, service, cluster)) + log.Printf("[DEBUG] ECS Task set created: %s", aws.StringValue(taskSet.Id)) + d.SetId(aws.StringValue(taskSet.Id)) if d.Get("wait_until_stable").(bool) { - timeout, _ := time.ParseDuration(d.Get("wait_until_stable_timeout").(string)) - if err := waitTaskSetStable(conn, timeout, taskSetId, service, cluster); err != nil { - return fmt.Errorf("error waiting for ECS TaskSet (%s) to be stable: %w", d.Id(), err) + waitUntilStableTimeOut := d.Timeout(schema.TimeoutCreate) + if v, ok := d.GetOk("wait_until_stable_timeout"); ok && v.(string) != "" { + timeout, err := time.ParseDuration(v.(string)) + if err != nil { + return err + } + waitUntilStableTimeOut = timeout + } + + // Wait until it's stable + wait := resource.StateChangeConf{ + Pending: []string{ecs.StabilityStatusStabilizing}, + Target: []string{ecs.StabilityStatusSteadyState}, + Timeout: waitUntilStableTimeOut, + Delay: 10 * time.Second, + Refresh: func() (interface{}, string, error) { + log.Printf("[DEBUG] Checking if ECS task set %s is set to %s", d.Id(), ecs.StabilityStatusSteadyState) + resp, err := conn.DescribeTaskSets(&ecs.DescribeTaskSetsInput{ + TaskSets: []*string{aws.String(d.Id())}, + Cluster: aws.String(d.Get("cluster").(string)), + Service: aws.String(d.Get("service").(string)), + }) + if err != nil { + return resp, "FAILED", err + } + + log.Printf("[DEBUG] ECS task set (%s) is currently %s", d.Id(), aws.StringValue(resp.TaskSets[0].StabilityStatus)) + return resp, aws.StringValue(resp.TaskSets[0].StabilityStatus), nil + }, + } + + _, err = wait.WaitForState() + if err != nil { + return err } } - return resourceTaskSetRead(d, meta) + return ResourceTaskSetRead(d, meta) } -func resourceTaskSetRead(d *schema.ResourceData, meta interface{}) error { +func ResourceTaskSetRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).ECSConn - defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig - ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig - - taskSetId, service, cluster, err := TaskSetParseID(d.Id()) - if err != nil { - return err - } + log.Printf("[DEBUG] Reading ECS task set %s", d.Id()) - input := &ecs.DescribeTaskSetsInput{ + cluster := d.Get("cluster").(string) + service := d.Get("service").(string) + input := ecs.DescribeTaskSetsInput{ Cluster: aws.String(cluster), - Include: aws.StringSlice([]string{ecs.TaskSetFieldTags}), Service: aws.String(service), - TaskSets: aws.StringSlice([]string{taskSetId}), + TaskSets: []*string{aws.String(d.Id())}, } - out, err := conn.DescribeTaskSets(input) + var out *ecs.DescribeTaskSetsOutput + err := resource.Retry(d.Timeout(schema.TimeoutRead), func() *resource.RetryError { + var err error + out, err = conn.DescribeTaskSets(&input) + if err != nil { + if d.IsNewResource() && + tfawserr.ErrCodeEquals(err, ecs.ErrCodeServiceNotFoundException) || + tfawserr.ErrCodeEquals(err, ecs.ErrCodeClusterNotFoundException) || + tfawserr.ErrCodeEquals(err, ecs.ErrCodeTaskSetNotFoundException) { + return resource.RetryableError(err) + } + return resource.NonRetryableError(err) + } + + if len(out.TaskSets) < 1 { + if d.IsNewResource() { + return resource.RetryableError(fmt.Errorf("ECS task set not created yet: %q", d.Id())) + } + log.Printf("[WARN] ECS Task Set %s not found, removing from state.", d.Id()) + d.SetId("") + return nil + } - if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, ecs.ErrCodeClusterNotFoundException, ecs.ErrCodeServiceNotFoundException, ecs.ErrCodeTaskSetNotFoundException) { - log.Printf("[WARN] ECS TaskSet (%s) not found, removing from state", d.Id()) - d.SetId("") return nil + }) + + if tfresource.TimedOut(err) { + out, err = conn.DescribeTaskSets(&input) } + // after retrying if err != nil { - return fmt.Errorf("error reading ECS TaskSet (%s): %w", d.Id(), err) + if tfawserr.ErrCodeEquals(err, ecs.ErrCodeClusterNotFoundException) || + tfawserr.ErrCodeEquals(err, ecs.ErrCodeServiceNotFoundException) || + tfawserr.ErrCodeEquals(err, ecs.ErrCodeTaskSetNotFoundException) { + log.Printf("[WARN] ECS TaskSet (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + return err } - if out == nil || len(out.TaskSets) == 0 { + if len(out.TaskSets) < 1 { if d.IsNewResource() { - return fmt.Errorf("error reading ECS TaskSet (%s): empty output after creation", d.Id()) + return fmt.Errorf("ECS TaskSet not created: %q", d.Id()) } - log.Printf("[WARN] ECS TaskSet (%s) not found, removing from state", d.Id()) + log.Printf("[WARN] Removing ECS task set %s because it's gone", d.Id()) d.SetId("") return nil } + if len(out.TaskSets) != 1 { + return fmt.Errorf("Error reading # of ECS TaskSet (%s) expected 1, got %d", d.Id(), len(out.TaskSets)) + } + taskSet := out.TaskSets[0] + log.Printf("[DEBUG] Received ECS task set %s", taskSet) + + d.SetId(aws.StringValue(taskSet.Id)) d.Set("arn", taskSet.TaskSetArn) - d.Set("cluster", cluster) d.Set("launch_type", taskSet.LaunchType) d.Set("platform_version", taskSet.PlatformVersion) d.Set("external_id", taskSet.ExternalId) - d.Set("service", service) - d.Set("status", taskSet.Status) - d.Set("stability_status", taskSet.StabilityStatus) - d.Set("task_definition", taskSet.TaskDefinition) - d.Set("task_set_id", taskSet.Id) - if err := d.Set("capacity_provider_strategy", flattenEcsCapacityProviderStrategy(taskSet.CapacityProviderStrategy)); err != nil { - return fmt.Errorf("error setting capacity_provider_strategy: %w", err) + // Save cluster in the same format + if strings.HasPrefix(d.Get("cluster").(string), "arn:"+meta.(*conns.AWSClient).Partition+":ecs:") { + d.Set("cluster", taskSet.ClusterArn) + } else { + clusterARN := getNameFromARN(*taskSet.ClusterArn) + d.Set("cluster", clusterARN) } - if err := d.Set("load_balancer", flattenTaskSetLoadBalancers(taskSet.LoadBalancers)); err != nil { - return fmt.Errorf("error setting load_balancer: %w", err) + // Save task definition in the same format + if strings.HasPrefix(d.Get("task_definition").(string), "arn:"+meta.(*conns.AWSClient).Partition+":ecs:") { + d.Set("task_definition", taskSet.TaskDefinition) + } else { + taskDefinition := buildFamilyAndRevisionFromARN(*taskSet.TaskDefinition) + d.Set("task_definition", taskDefinition) } - if err := d.Set("network_configuration", flattenEcsNetworkConfiguration(taskSet.NetworkConfiguration)); err != nil { - return fmt.Errorf("error setting network_configuration: %w", err) + if taskSet.LoadBalancers != nil { + d.Set("load_balancers", flattenECSLoadBalancers(taskSet.LoadBalancers)) } - if err := d.Set("scale", flattenScale(taskSet.Scale)); err != nil { - return fmt.Errorf("error setting scale: %w", err) + if err := d.Set("scale", flattenAwsEcsScale(taskSet.Scale)); err != nil { + return fmt.Errorf("Error setting scale for (%s): %s", d.Id(), err) } - if err := d.Set("service_registries", flattenServiceRegistries(taskSet.ServiceRegistries)); err != nil { - return fmt.Errorf("error setting service_registries: %w", err) + if err := d.Set("capacity_provider_strategy", flattenEcsCapacityProviderStrategy(taskSet.CapacityProviderStrategy)); err != nil { + return fmt.Errorf("error setting capacity_provider_strategy: %s", err) } - tags := KeyValueTags(taskSet.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) - - //lintignore:AWSR002 - if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %w", err) + if err := d.Set("network_configuration", flattenEcsNetworkConfiguration(taskSet.NetworkConfiguration)); err != nil { + return fmt.Errorf("Error setting network_configuration for (%s): %s", d.Id(), err) } - if err := d.Set("tags_all", tags.Map()); err != nil { - return fmt.Errorf("error setting tags_all: %w", err) + if err := d.Set("service_registries", flattenServiceRegistries(taskSet.ServiceRegistries)); err != nil { + return fmt.Errorf("Error setting service_registries for (%s): %s", d.Id(), err) } return nil } -func resourceTaskSetUpdate(d *schema.ResourceData, meta interface{}) error { +func ResourceTaskSetUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).ECSConn + updateTaskset := false - if d.HasChangesExcept("tags", "tags_all") { - taskSetId, service, cluster, err := TaskSetParseID(d.Id()) - - if err != nil { - return err - } + input := ecs.UpdateTaskSetInput{ + Cluster: aws.String(d.Get("cluster").(string)), + Service: aws.String(d.Get("service").(string)), + TaskSet: aws.String(d.Id()), + } - input := &ecs.UpdateTaskSetInput{ - Cluster: aws.String(cluster), - Service: aws.String(service), - TaskSet: aws.String(taskSetId), - Scale: expandScale(d.Get("scale").([]interface{})), + if d.HasChange("scale") { + scale := d.Get("scale").([]interface{}) + if len(scale) > 0 { + updateTaskset = true + input.Scale = expandAwsEcsScale(scale[0].(map[string]interface{})) } + } - _, err = conn.UpdateTaskSet(input) + if updateTaskset { + log.Printf("[DEBUG] Updating ECS Task Set (%s): %s", d.Id(), input) + // Retry due to IAM eventual consistency + err := resource.Retry(d.Timeout(schema.TimeoutUpdate), func() *resource.RetryError { + _, err := conn.UpdateTaskSet(&input) + if err != nil { + if tfawserr.ErrCodeEquals(err, ecs.ErrCodeClusterNotFoundException) || + tfawserr.ErrCodeEquals(err, ecs.ErrCodeServiceNotFoundException) || + tfawserr.ErrCodeEquals(err, ecs.ErrCodeTaskSetNotFoundException) || + tfawserr.ErrMessageContains(err, ecs.ErrCodeInvalidParameterException, "does not have an associated load balancer") { + return resource.RetryableError(err) + } + return resource.NonRetryableError(err) + } + return nil + }) + if tfresource.TimedOut(err) { + _, err = conn.UpdateTaskSet(&input) + } if err != nil { - return fmt.Errorf("error updating ECS TaskSet (%s): %w", d.Id(), err) + return fmt.Errorf("Error updating ECS Task set (%s): %s", d.Id(), err) } if d.Get("wait_until_stable").(bool) { - timeout, _ := time.ParseDuration(d.Get("wait_until_stable_timeout").(string)) - if err := waitTaskSetStable(conn, timeout, taskSetId, service, cluster); err != nil { - return fmt.Errorf("error waiting for ECS TaskSet (%s) to be stable after update: %w", d.Id(), err) + waitUntilStableTimeOut := d.Timeout(schema.TimeoutUpdate) + if v, ok := d.GetOk("wait_until_stable_timeout"); ok && v.(string) != "" { + timeout, err := time.ParseDuration(v.(string)) + if err != nil { + return err + } + waitUntilStableTimeOut = timeout } - } - } - if d.HasChange("tags_all") { - o, n := d.GetChange("tags_all") + // Wait until it's stable + wait := resource.StateChangeConf{ + Pending: []string{ecs.StabilityStatusStabilizing}, + Target: []string{ecs.StabilityStatusSteadyState}, + Timeout: waitUntilStableTimeOut, + Delay: 10 * time.Second, + Refresh: func() (interface{}, string, error) { + log.Printf("[DEBUG] Checking if ECS task set %s is set to %s", d.Id(), ecs.StabilityStatusSteadyState) + resp, err := conn.DescribeTaskSets(&ecs.DescribeTaskSetsInput{ + TaskSets: []*string{aws.String(d.Id())}, + Cluster: aws.String(d.Get("cluster").(string)), + Service: aws.String(d.Get("service").(string)), + }) + if err != nil { + return resp, "FAILED", err + } - if err := UpdateTags(conn, d.Get("arn").(string), o, n); err != nil { - return fmt.Errorf("error updating ECS TaskSet (%s) tags: %w", d.Id(), err) + log.Printf("[DEBUG] ECS task set (%s) is currently %q", d.Id(), *resp.TaskSets[0].StabilityStatus) + return resp, *resp.TaskSets[0].StabilityStatus, nil + }, + } + + _, err = wait.WaitForState() + if err != nil { + return err + } } + } - return resourceTaskSetRead(d, meta) + return ResourceTaskSetRead(d, meta) } -func resourceTaskSetDelete(d *schema.ResourceData, meta interface{}) error { +func ResourceTaskSetDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).ECSConn - taskSetId, service, cluster, err := TaskSetParseID(d.Id()) + // Check if it's not already gone + resp, err := conn.DescribeTaskSets(&ecs.DescribeTaskSetsInput{ + TaskSets: []*string{aws.String(d.Id())}, + Service: aws.String(d.Get("service").(string)), + Cluster: aws.String(d.Get("cluster").(string)), + }) if err != nil { + if tfawserr.ErrCodeEquals(err, ecs.ErrCodeTaskSetNotFoundException) { + log.Printf("[DEBUG] Removing ECS Task set from state, %q is already gone", d.Id()) + return nil + } return err } - input := &ecs.DeleteTaskSetInput{ - Cluster: aws.String(cluster), - Service: aws.String(service), - TaskSet: aws.String(taskSetId), - Force: aws.Bool(d.Get("force_delete").(bool)), + if len(resp.TaskSets) == 0 { + log.Printf("[DEBUG] Removing ECS Task set from state, %q is already gone", d.Id()) + return nil } - _, err = conn.DeleteTaskSet(input) + log.Printf("[DEBUG] ECS TaskSet %s is currently %s", d.Id(), aws.StringValue(resp.TaskSets[0].Status)) - if tfawserr.ErrCodeEquals(err, ecs.ErrCodeTaskSetNotFoundException) { - return nil + input := ecs.DeleteTaskSetInput{ + Cluster: aws.String(d.Get("cluster").(string)), + Service: aws.String(d.Get("service").(string)), + TaskSet: aws.String(d.Id()), } - if err != nil { - return fmt.Errorf("error deleting ECS TaskSet (%s): %w", d.Id(), err) + if v, ok := d.GetOk("force_delete"); ok && v.(bool) { + input.Force = aws.Bool(v.(bool)) } - if err := waitTaskSetDeleted(conn, taskSetId, service, cluster); err != nil { + // Wait until the ECS task set is drained + err = resource.Retry(d.Timeout(schema.TimeoutDelete), func() *resource.RetryError { + log.Printf("[DEBUG] Trying to delete ECS task set %s", input) + _, err := conn.DeleteTaskSet(&input) + if err != nil { + if tfawserr.ErrCodeEquals(err, ecs.ErrCodeTaskSetNotFoundException) { + return nil + } + if tfawserr.ErrMessageContains(err, ecs.ErrCodeInvalidParameterException, "The service cannot be stopped while deployments are active.") { + return resource.RetryableError(err) + } + return resource.NonRetryableError(err) + } + return nil + }) + + if tfresource.TimedOut(err) { + _, err = conn.DeleteTaskSet(&input) + } + + if err != nil { if tfawserr.ErrCodeEquals(err, ecs.ErrCodeTaskSetNotFoundException) { return nil } - return fmt.Errorf("error waiting for ECS TaskSet (%s) to delete: %w", d.Id(), err) + return fmt.Errorf("Error deleting ECS task set: %s", err) + } + + // Wait until it's deleted + wait := resource.StateChangeConf{ + Pending: []string{"ACTIVE", "PRIMARY", "DRAINING"}, + Target: []string{"INACTIVE"}, + Timeout: d.Timeout(schema.TimeoutDelete), + Refresh: func() (interface{}, string, error) { + log.Printf("[DEBUG] Checking if ECS task set %s is INACTIVE", d.Id()) + resp, err := conn.DescribeTaskSets(&ecs.DescribeTaskSetsInput{ + TaskSets: []*string{aws.String(d.Id())}, + Cluster: aws.String(d.Get("cluster").(string)), + Service: aws.String(d.Get("service").(string)), + }) + + if err != nil { + return resp, "FAILED", err + } + + // task set is already gone + if len(resp.TaskSets) == 0 { + return resp, "INACTIVE", nil + } + + log.Printf("[DEBUG] ECS task set (%s) is currently %s", d.Id(), aws.StringValue(resp.TaskSets[0].Status)) + return resp, aws.StringValue(resp.TaskSets[0].Status), nil + }, } + _, err = wait.WaitForState() + if err != nil { + return err + } + + log.Printf("[DEBUG] ECS TaskSet %s deleted.", d.Id()) return nil } -func TaskSetParseID(id string) (string, string, string, error) { - parts := strings.Split(id, ",") +func expandAwsEcsServiceRegistries(d []interface{}) []*ecs.ServiceRegistry { + if len(d) == 0 { + return nil + } + + result := make([]*ecs.ServiceRegistry, 0, len(d)) + for _, v := range d { + m := v.(map[string]interface{}) + sr := &ecs.ServiceRegistry{ + RegistryArn: aws.String(m["registry_arn"].(string)), + } + if raw, ok := m["container_name"].(string); ok && raw != "" { + sr.ContainerName = aws.String(raw) + } + if raw, ok := m["container_port"].(int); ok && raw != 0 { + sr.ContainerPort = aws.Int64(int64(raw)) + } + if raw, ok := m["port"].(int); ok && raw != 0 { + sr.Port = aws.Int64(int64(raw)) + } + result = append(result, sr) + } + + return result +} + +func expandAwsEcsScale(d map[string]interface{}) *ecs.Scale { + if len(d) == 0 { + return nil + } - if len(parts) != 3 || parts[0] == "" || parts[1] == "" || parts[2] == "" { - return "", "", "", fmt.Errorf("unexpected format of ID (%q), expected TASK_SET_ID,SERVICE,CLUSTER", id) + result := &ecs.Scale{} + if v, ok := d["unit"]; ok && v.(string) != "" { + result.Unit = aws.String(v.(string)) + } + if v, ok := d["value"]; ok { + result.Value = aws.Float64(v.(float64)) } - return parts[0], parts[1], parts[2], nil + return result +} + +func flattenAwsEcsScale(scale *ecs.Scale) []map[string]interface{} { + if scale == nil { + return nil + } + + m := make(map[string]interface{}) + m["unit"] = aws.StringValue(scale.Unit) + m["value"] = aws.Float64Value(scale.Value) + + return []map[string]interface{}{m} } diff --git a/internal/service/ecs/task_set_test.go b/internal/service/ecs/task_set_test.go index 965c8720187..4cf00b69cf8 100644 --- a/internal/service/ecs/task_set_test.go +++ b/internal/service/ecs/task_set_test.go @@ -2,8 +2,8 @@ package ecs_test import ( "fmt" - "regexp" "testing" + "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ecs" @@ -17,375 +17,314 @@ import ( ) func TestAccECSTaskSet_basic(t *testing.T) { - rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) - resourceName := "aws_ecs_task_set.test" + var taskSet ecs.TaskSet + + clusterName := sdkacctest.RandomWithPrefix("tf-acc-cluster") + tdName := sdkacctest.RandomWithPrefix("tf-acc-td") + svcName := sdkacctest.RandomWithPrefix("tf-acc-svc") + resourceName := "aws_ecs_task_set.mongo" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ErrorCheck: acctest.ErrorCheck(t, ecs.EndpointsID), Providers: acctest.Providers, CheckDestroy: testAccCheckTaskSetDestroy, Steps: []resource.TestStep{ { - Config: testAccTaskSetBasicConfig(rName), + Config: testAccTaskSet(clusterName, tdName, svcName, 0.0), Check: resource.ComposeTestCheckFunc( - testAccCheckTaskSetExists(resourceName), - acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "ecs", regexp.MustCompile(fmt.Sprintf("task-set/%[1]s/%[1]s/ecs-svc/.+", rName))), + testAccCheckTaskSetExists(resourceName, &taskSet), + testAccCheckTaskSetArn(resourceName, clusterName, svcName, &taskSet), resource.TestCheckResourceAttr(resourceName, "service_registries.#", "0"), - resource.TestCheckResourceAttr(resourceName, "load_balancer.#", "0"), + resource.TestCheckResourceAttr(resourceName, "load_balancers.#", "0"), ), }, - { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{ - "wait_until_stable", - "wait_until_stable_timeout", - }, - }, }, }) } -func TestAccECSTaskSet_withExternalId(t *testing.T) { - rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) - resourceName := "aws_ecs_task_set.test" +func TestAccECSTaskSet_withARN(t *testing.T) { + var taskSet ecs.TaskSet + + clusterName := sdkacctest.RandomWithPrefix("tf-acc-cluster") + tdName := sdkacctest.RandomWithPrefix("tf-acc-td") + svcName := sdkacctest.RandomWithPrefix("tf-acc-svc") + resourceName := "aws_ecs_task_set.mongo" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ErrorCheck: acctest.ErrorCheck(t, ecs.EndpointsID), Providers: acctest.Providers, CheckDestroy: testAccCheckTaskSetDestroy, Steps: []resource.TestStep{ { - Config: testAccTaskSetWithExternalIdConfig(rName), + Config: testAccTaskSet(clusterName, tdName, svcName, 0.0), Check: resource.ComposeTestCheckFunc( - testAccCheckTaskSetExists(resourceName), + testAccCheckTaskSetExists(resourceName, &taskSet), resource.TestCheckResourceAttr(resourceName, "service_registries.#", "0"), - resource.TestCheckResourceAttr(resourceName, "load_balancer.#", "0"), - resource.TestCheckResourceAttr(resourceName, "external_id", "TEST_ID"), + resource.TestCheckResourceAttr(resourceName, "load_balancers.#", "0"), ), }, + { - ResourceName: resourceName, - ImportState: true, - ImportStateVerifyIgnore: []string{ - "wait_until_stable", - "wait_until_stable_timeout", - }, + Config: testAccTaskSetModified(clusterName, tdName, svcName), + Check: resource.ComposeTestCheckFunc( + testAccCheckTaskSetExists(resourceName, &taskSet), + resource.TestCheckResourceAttr(resourceName, "service_registries.#", "0"), + resource.TestCheckResourceAttr(resourceName, "load_balancers.#", "0"), + resource.TestCheckResourceAttr(resourceName, "external_id", "TEST_ID"), + ), }, }, }) } -func TestAccECSTaskSet_withScale(t *testing.T) { - rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) - resourceName := "aws_ecs_task_set.test" +func TestAccECSTaskSet_disappears(t *testing.T) { + var taskSet ecs.TaskSet + + clusterName := sdkacctest.RandomWithPrefix("tf-acc-cluster") + tdName := sdkacctest.RandomWithPrefix("tf-acc-td") + svcName := sdkacctest.RandomWithPrefix("tf-acc-svc") + resourceName := "aws_ecs_task_set.mongo" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ErrorCheck: acctest.ErrorCheck(t, ecs.EndpointsID), Providers: acctest.Providers, CheckDestroy: testAccCheckTaskSetDestroy, Steps: []resource.TestStep{ { - Config: testAccTaskSetWithScaleConfig(rName, 0.0), - Check: resource.ComposeTestCheckFunc( - testAccCheckTaskSetExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "scale.#", "1"), - resource.TestCheckResourceAttr(resourceName, "scale.0.unit", ecs.ScaleUnitPercent), - resource.TestCheckResourceAttr(resourceName, "scale.0.value", "0"), - ), - }, - { - ResourceName: resourceName, - ImportState: true, - ImportStateVerifyIgnore: []string{ - "wait_until_stable", - "wait_until_stable_timeout", - }, - }, - { - Config: testAccTaskSetWithScaleConfig(rName, 100.0), + Config: testAccTaskSet(clusterName, tdName, svcName, 0.0), Check: resource.ComposeTestCheckFunc( - testAccCheckTaskSetExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "scale.#", "1"), - resource.TestCheckResourceAttr(resourceName, "scale.0.unit", ecs.ScaleUnitPercent), - resource.TestCheckResourceAttr(resourceName, "scale.0.value", "100"), + testAccCheckTaskSetExists(resourceName, &taskSet), + acctest.CheckResourceDisappears(acctest.Provider, tfecs.ResourceTaskSet(), resourceName), ), - }, - { - ResourceName: resourceName, - ImportState: true, - ImportStateVerifyIgnore: []string{ - "wait_until_stable", - "wait_until_stable_timeout", - }, + ExpectNonEmptyPlan: true, }, }, }) } -func TestAccECSTaskSet_disappears(t *testing.T) { - rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) - resourceName := "aws_ecs_task_set.test" +func TestAccECSTaskSet_scale(t *testing.T) { + var taskSet ecs.TaskSet + + clusterName := sdkacctest.RandomWithPrefix("tf-acc-cluster") + tdName := sdkacctest.RandomWithPrefix("tf-acc-td") + svcName := sdkacctest.RandomWithPrefix("tf-acc-svc") + resourceName := "aws_ecs_task_set.mongo" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ErrorCheck: acctest.ErrorCheck(t, ecs.EndpointsID), Providers: acctest.Providers, CheckDestroy: testAccCheckTaskSetDestroy, Steps: []resource.TestStep{ { - Config: testAccTaskSetBasicConfig(rName), + Config: testAccTaskSet(clusterName, tdName, svcName, 0.0), Check: resource.ComposeTestCheckFunc( - testAccCheckTaskSetExists(resourceName), - acctest.CheckResourceDisappears(acctest.Provider, tfecs.ResourceTaskSet(), resourceName), + testAccCheckTaskSetExists(resourceName, &taskSet), + resource.TestCheckResourceAttr(resourceName, "scale.0.unit", "PERCENT"), + resource.TestCheckResourceAttr(resourceName, "scale.0.value", "0"), + ), + }, + { + Config: testAccTaskSet(clusterName, tdName, svcName, 100.0), + Check: resource.ComposeTestCheckFunc( + testAccCheckTaskSetExists(resourceName, &taskSet), + resource.TestCheckResourceAttr(resourceName, "scale.0.unit", "PERCENT"), + resource.TestCheckResourceAttr(resourceName, "scale.0.value", "100"), ), - ExpectNonEmptyPlan: true, }, }, }) } func TestAccECSTaskSet_withCapacityProviderStrategy(t *testing.T) { - rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) - resourceName := "aws_ecs_task_set.test" + var taskSet ecs.TaskSet + + clusterName := sdkacctest.RandomWithPrefix("tf-acc-cluster") + tdName := sdkacctest.RandomWithPrefix("tf-acc-td") + svcName := sdkacctest.RandomWithPrefix("tf-acc-svc") + providerName := sdkacctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_ecs_task_set.mongo" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ErrorCheck: acctest.ErrorCheck(t, ecs.EndpointsID), Providers: acctest.Providers, CheckDestroy: testAccCheckTaskSetDestroy, Steps: []resource.TestStep{ { - Config: testAccTaskSetWithCapacityProviderStrategy(rName, 1, 0), + Config: testAccTaskSetWithCapacityProviderStrategy(providerName, clusterName, tdName, svcName, 1, 0), Check: resource.ComposeTestCheckFunc( - testAccCheckTaskSetExists(resourceName), + testAccCheckTaskSetExists(resourceName, &taskSet), ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerifyIgnore: []string{ - "wait_until_stable", - "wait_until_stable_timeout", - }, - }, - { - Config: testAccTaskSetWithCapacityProviderStrategy(rName, 10, 1), + Config: testAccTaskSetWithCapacityProviderStrategy(providerName, clusterName, tdName, svcName, 10, 1), Check: resource.ComposeTestCheckFunc( - testAccCheckTaskSetExists(resourceName), + testAccCheckTaskSetExists(resourceName, &taskSet), ), }, - { - ResourceName: resourceName, - ImportState: true, - ImportStateVerifyIgnore: []string{ - "wait_until_stable", - "wait_until_stable_timeout", - }, - }, }, }) } func TestAccECSTaskSet_withMultipleCapacityProviderStrategies(t *testing.T) { - rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) - resourceName := "aws_ecs_task_set.test" + var taskSet ecs.TaskSet + + clusterName := sdkacctest.RandomWithPrefix("tf-acc-cluster") + tdName := sdkacctest.RandomWithPrefix("tf-acc-td") + svcName := sdkacctest.RandomWithPrefix("tf-acc-svc") + sgName := sdkacctest.RandomWithPrefix("tf-acc-sg") + resourceName := "aws_ecs_task_set.mongo" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ErrorCheck: acctest.ErrorCheck(t, ecs.EndpointsID), Providers: acctest.Providers, CheckDestroy: testAccCheckTaskSetDestroy, Steps: []resource.TestStep{ { - Config: testAccTaskSetWithMultipleCapacityProviderStrategies(rName), + Config: testAccTaskSetWithMultipleCapacityProviderStrategies(clusterName, tdName, svcName, sgName), Check: resource.ComposeTestCheckFunc( - testAccCheckTaskSetExists(resourceName), + testAccCheckTaskSetExists(resourceName, &taskSet), resource.TestCheckResourceAttr(resourceName, "capacity_provider_strategy.#", "2"), ), }, - { - ResourceName: resourceName, - ImportState: true, - ImportStateVerifyIgnore: []string{ - "wait_until_stable", - "wait_until_stable_timeout", - }, - }, }, }) } func TestAccECSTaskSet_withAlb(t *testing.T) { - rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) - resourceName := "aws_ecs_task_set.test" + var taskSet ecs.TaskSet + + clusterName := sdkacctest.RandomWithPrefix("tf-acc-cluster") + tdName := sdkacctest.RandomWithPrefix("tf-acc-td") + svcName := sdkacctest.RandomWithPrefix("tf-acc-svc") + lbName := sdkacctest.RandomWithPrefix("tf-acc-lb") + resourceName := "aws_ecs_task_set.with_alb" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ErrorCheck: acctest.ErrorCheck(t, ecs.EndpointsID), Providers: acctest.Providers, CheckDestroy: testAccCheckTaskSetDestroy, Steps: []resource.TestStep{ { - Config: testAccTaskSetWithAlb(rName), + Config: testAccTaskSetWithAlb(clusterName, tdName, lbName, svcName), Check: resource.ComposeTestCheckFunc( - testAccCheckTaskSetExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "load_balancer.#", "1"), + testAccCheckTaskSetExists(resourceName, &taskSet), + resource.TestCheckResourceAttr(resourceName, "load_balancers.#", "1"), ), }, - { - ResourceName: resourceName, - ImportState: true, - ImportStateVerifyIgnore: []string{ - "wait_until_stable", - "wait_until_stable_timeout", - }, - }, }, }) } func TestAccECSTaskSet_withLaunchTypeFargate(t *testing.T) { - rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) - resourceName := "aws_ecs_task_set.test" + var taskSet ecs.TaskSet + + sg1Name := sdkacctest.RandomWithPrefix("tf-acc-sg-1") + sg2Name := sdkacctest.RandomWithPrefix("tf-acc-sg-2") + clusterName := sdkacctest.RandomWithPrefix("tf-acc-cluster") + tdName := sdkacctest.RandomWithPrefix("tf-acc-td") + svcName := sdkacctest.RandomWithPrefix("tf-acc-svc") + resourceName := "aws_ecs_task_set.main" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ErrorCheck: acctest.ErrorCheck(t, ecs.EndpointsID), Providers: acctest.Providers, CheckDestroy: testAccCheckTaskSetDestroy, Steps: []resource.TestStep{ { - Config: testAccTaskSetWithLaunchTypeFargate(rName), + Config: testAccTaskSetWithLaunchTypeFargate(sg1Name, sg2Name, clusterName, tdName, svcName, "false"), Check: resource.ComposeTestCheckFunc( - testAccCheckTaskSetExists(resourceName), + testAccCheckTaskSetExists(resourceName, &taskSet), resource.TestCheckResourceAttr(resourceName, "launch_type", "FARGATE"), - resource.TestCheckResourceAttr(resourceName, "network_configuration.#", "1"), resource.TestCheckResourceAttr(resourceName, "network_configuration.0.assign_public_ip", "false"), resource.TestCheckResourceAttr(resourceName, "network_configuration.0.security_groups.#", "2"), resource.TestCheckResourceAttr(resourceName, "network_configuration.0.subnets.#", "2"), - resource.TestCheckResourceAttrSet(resourceName, "platform_version"), + resource.TestCheckResourceAttr(resourceName, "platform_version", "1.3.0"), ), }, - { - ResourceName: resourceName, - ImportState: true, - ImportStateVerifyIgnore: []string{ - "wait_until_stable", - "wait_until_stable_timeout", - }, - }, }, }) } func TestAccECSTaskSet_withLaunchTypeFargateAndPlatformVersion(t *testing.T) { - rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) - resourceName := "aws_ecs_task_set.test" + var taskSet ecs.TaskSet + + sg1Name := sdkacctest.RandomWithPrefix("tf-acc-sg-1") + sg2Name := sdkacctest.RandomWithPrefix("tf-acc-sg-2") + clusterName := sdkacctest.RandomWithPrefix("tf-acc-cluster") + tdName := sdkacctest.RandomWithPrefix("tf-acc-td") + svcName := sdkacctest.RandomWithPrefix("tf-acc-svc") + resourceName := "aws_ecs_task_set.main" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ErrorCheck: acctest.ErrorCheck(t, ecs.EndpointsID), Providers: acctest.Providers, CheckDestroy: testAccCheckTaskSetDestroy, Steps: []resource.TestStep{ { - Config: testAccTaskSetWithLaunchTypeFargateAndPlatformVersion(rName, "1.3.0"), + Config: testAccTaskSetWithLaunchTypeFargateAndPlatformVersion(sg1Name, sg2Name, clusterName, tdName, svcName, "1.2.0"), Check: resource.ComposeTestCheckFunc( - testAccCheckTaskSetExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "platform_version", "1.3.0"), + testAccCheckTaskSetExists(resourceName, &taskSet), + resource.TestCheckResourceAttr(resourceName, "platform_version", "1.2.0"), ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerifyIgnore: []string{ - "wait_until_stable", - "wait_until_stable_timeout", - }, - }, - { - Config: testAccTaskSetWithLaunchTypeFargateAndPlatformVersion(rName, "1.4.0"), + Config: testAccTaskSetWithLaunchTypeFargateAndPlatformVersion(sg1Name, sg2Name, clusterName, tdName, svcName, "1.3.0"), Check: resource.ComposeTestCheckFunc( - testAccCheckTaskSetExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "platform_version", "1.4.0"), + testAccCheckTaskSetExists(resourceName, &taskSet), + resource.TestCheckResourceAttr(resourceName, "platform_version", "1.3.0"), ), }, - { - ResourceName: resourceName, - ImportState: true, - ImportStateVerifyIgnore: []string{ - "wait_until_stable", - "wait_until_stable_timeout", - }, - }, }, }) } func TestAccECSTaskSet_withServiceRegistries(t *testing.T) { - rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + var taskSet ecs.TaskSet + rString := sdkacctest.RandString(8) + + clusterName := sdkacctest.RandomWithPrefix("tf-acc-cluster") + tdName := sdkacctest.RandomWithPrefix("tf-acc-td") + svcName := sdkacctest.RandomWithPrefix("tf-acc-svc") resourceName := "aws_ecs_task_set.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ErrorCheck: acctest.ErrorCheck(t, ecs.EndpointsID), Providers: acctest.Providers, CheckDestroy: testAccCheckTaskSetDestroy, Steps: []resource.TestStep{ { - Config: testAccTaskSet_withServiceRegistries(rName), + Config: testAccTaskSet_withServiceRegistries(rString, clusterName, tdName, svcName), Check: resource.ComposeTestCheckFunc( - testAccCheckTaskSetExists(resourceName), + testAccCheckTaskSetExists(resourceName, &taskSet), resource.TestCheckResourceAttr(resourceName, "service_registries.#", "1"), ), }, - { - ResourceName: resourceName, - ImportState: true, - ImportStateVerifyIgnore: []string{ - "wait_until_stable", - "wait_until_stable_timeout", - }, - }, }, }) } func TestAccECSTaskSet_Tags(t *testing.T) { - rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + var taskSet ecs.TaskSet + rName := sdkacctest.RandomWithPrefix("tf-acc-test") resourceName := "aws_ecs_task_set.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ErrorCheck: acctest.ErrorCheck(t, ecs.EndpointsID), Providers: acctest.Providers, CheckDestroy: testAccCheckTaskSetDestroy, Steps: []resource.TestStep{ { Config: testAccTaskSetConfigTags1(rName, "key1", "value1"), Check: resource.ComposeTestCheckFunc( - testAccCheckTaskSetExists(resourceName), + testAccCheckTaskSetExists(resourceName, &taskSet), resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), ), }, - { - ResourceName: resourceName, - ImportState: true, - ImportStateVerifyIgnore: []string{ - "wait_until_stable", - "wait_until_stable_timeout", - }, - }, { Config: testAccTaskSetConfigTags2(rName, "key1", "value1updated", "key2", "value2"), Check: resource.ComposeTestCheckFunc( - testAccCheckTaskSetExists(resourceName), + testAccCheckTaskSetExists(resourceName, &taskSet), resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), @@ -394,7 +333,7 @@ func TestAccECSTaskSet_Tags(t *testing.T) { { Config: testAccTaskSetConfigTags1(rName, "key2", "value2"), Check: resource.ComposeTestCheckFunc( - testAccCheckTaskSetExists(resourceName), + testAccCheckTaskSetExists(resourceName, &taskSet), resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), ), @@ -407,14 +346,13 @@ func TestAccECSTaskSet_Tags(t *testing.T) { // Fixtures // ////////////// -func testAccTaskSetBaseConfig(rName string) string { +func testAccTaskSet(clusterName, tdName, svcName string, scale float64) string { return fmt.Sprintf(` -resource "aws_ecs_cluster" "test" { - name = %[1]q +resource "aws_ecs_cluster" "default" { + name = "%s" } - -resource "aws_ecs_task_definition" "test" { - family = %[1]q +resource "aws_ecs_task_definition" "mongo" { + family = "%s" container_definitions = < 0 { + var activeTaskSets []*ecs.TaskSet + for _, ts := range out.TaskSets { + if *ts.Status != "INACTIVE" { + activeTaskSets = append(activeTaskSets, ts) + } + } + if len(activeTaskSets) == 0 { + return nil + } + + return fmt.Errorf("ECS task set still exists:\n%#v", activeTaskSets) + } + return nil } - if output != nil && len(output.TaskSets) == 1 { - return fmt.Errorf("ECS TaskSet (%s) still exists", rs.Primary.ID) - } + return err } return nil diff --git a/website/docs/r/ecs_task_set.html.markdown b/website/docs/r/ecs_task_set.html.markdown index 13cd179e115..7df7d7ce2da 100644 --- a/website/docs/r/ecs_task_set.html.markdown +++ b/website/docs/r/ecs_task_set.html.markdown @@ -8,42 +8,79 @@ description: |- # Resource: aws_ecs_task_set -Provides an ECS task set - effectively a task that is expected to run until an error occurs or a user terminates it (typically a webserver or a database). +-> **Note:** This file exists just to get doc linting tests to pass. It is mostly a copy of ecs_service.html.markdown. -See [ECS Task Set section in AWS developer guide](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-type-external.html). +Provides an ECS service - effectively a task that is expected to run until an error occurs or a user terminates it (typically a webserver or a database). + +See [ECS Services section in AWS developer guide](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs_services.html). ## Example Usage ```terraform -resource "aws_ecs_task_set" "example" { - service = aws_ecs_service.example.id - cluster = aws_ecs_cluster.example.id - task_definition = aws_ecs_task_definition.example.arn +resource "aws_ecs_task_set" "mongo" { + name = "mongodb" + cluster = aws_ecs_cluster.foo.id + task_definition = aws_ecs_task_definition.mongo.arn + desired_count = 3 + iam_role = aws_iam_role.foo.arn + depends_on = [aws_iam_role_policy.foo] + + ordered_placement_strategy { + type = "binpack" + field = "cpu" + } load_balancer { - target_group_arn = aws_lb_target_group.example.arn + target_group_arn = aws_lb_target_group.foo.arn container_name = "mongo" container_port = 8080 } + + placement_constraints { + type = "memberOf" + expression = "attribute:ecs.availability-zone in [us-west-2a, us-west-2b]" + } } ``` -### Ignoring Changes to Scale +### Ignoring Changes to Desired Count -You can utilize the generic Terraform resource [lifecycle configuration block](https://www.terraform.io/docs/configuration/meta-arguments/lifecycle.html) with `ignore_changes` to create an ECS service with an initial count of running instances, then ignore any changes to that count caused externally (e.g. Application Autoscaling). +You can utilize the generic Terraform resource [lifecycle configuration block](https://www.terraform.io/docs/configuration/meta-arguments/lifecycle.html) with `ignore_changes` to create an ECS service with an initial count of running instances, then ignore any changes to that count caused externally (e.g., Application Autoscaling). ```terraform resource "aws_ecs_task_set" "example" { # ... other configurations ... - # Example: Run 50% of the servcie's desired count - scale { - value = 50.0 - } + # Example: Create service with 2 instances to start + desired_count = 2 # Optional: Allow external changes without Terraform plan difference lifecycle { - ignore_changes = ["scale"] + ignore_changes = [desired_count] + } +} +``` + +### Daemon Scheduling Strategy + +```terraform +resource "aws_ecs_task_set" "bar" { + name = "bar" + cluster = aws_ecs_cluster.foo.id + task_definition = aws_ecs_task_definition.bar.arn + scheduling_strategy = "DAEMON" +} +``` + +### External Deployment Controller + +```terraform +resource "aws_ecs_task_set" "example" { + name = "example" + cluster = aws_ecs_cluster.example.id + + deployment_controller { + type = "EXTERNAL" } } ``` @@ -52,85 +89,127 @@ resource "aws_ecs_task_set" "example" { The following arguments are required: -* `service` - (Required) The short name or ARN of the ECS service. -* `cluster` - (Required) The short name or ARN of the cluster that hosts the service to create the task set in. -* `task_definition` - (Required) The family and revision (`family:revision`) or full ARN of the task definition that you want to run in your service. +* `name` - (Required) Name of the service (up to 255 letters, numbers, hyphens, and underscores) The following arguments are optional: -* `capacity_provider_strategy` - (Optional) The capacity provider strategy to use for the service. Can be one or more. [Defined below](#capacity_provider_strategy). -* `external_id` - (Optional) The external ID associated with the task set. -* `force_delete` - (Optional) Whether to allow deleting the task set without waiting for scaling down to 0. You can force a task set to delete even if it's in the process of scaling a resource. Normally, Terraform drains all the tasks before deleting the task set. This bypasses that behavior and potentially leaves resources dangling. -* `launch_type` - (Optional) The launch type on which to run your service. The valid values are `EC2`, `FARGATE`, and `EXTERNAL`. Defaults to `EC2`. -* `load_balancer` - (Optional) Details on load balancers that are used with a task set. [Detailed below](#load_balancer). -* `platform_version` - (Optional) The platform version on which to run your service. Only applicable for `launch_type` set to `FARGATE`. Defaults to `LATEST`. More information about Fargate platform versions can be found in the [AWS ECS User Guide](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/platform_versions.html). -* `network_configuration` - (Optional) The network configuration for the service. This parameter is required for task definitions that use the `awsvpc` network mode to receive their own Elastic Network Interface, and it is not supported for other network modes. [Detailed below](#network_configuration). -* `scale` - (Optional) A floating-point percentage of the desired number of tasks to place and keep running in the task set. [Detailed below](#scale). -* `service_registries` - (Optional) The service discovery registries for the service. The maximum number of `service_registries` blocks is `1`. [Detailed below](#service_registries). -* `tags` - (Optional) A map of tags to assign to the file system. If configured with a provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. If you have set `copy_tags_to_backups` to true, and you specify one or more tags, no existing file system tags are copied from the file system to the backup. -* `wait_until_stable` - (Optional) Whether `terraform` should wait until the task set has reached `STEADY_STATE`. -* `wait_until_stable_timeout` - (Optional) Wait timeout for task set to reach `STEADY_STATE`. Valid time units include `ns`, `us` (or `µs`), `ms`, `s`, `m`, and `h`. Default `10m`. - -## capacity_provider_strategy +* `capacity_provider_strategy` - (Optional) Capacity provider strategy to use for the service. Can be one or more. Detailed below. +* `cluster` - (Optional) ARN of an ECS cluster +* `deployment_circuit_breaker` - (Optional) Configuration block for deployment circuit breaker. Detailed below. +* `deployment_controller` - (Optional) Configuration block for deployment controller configuration. Detailed below. +* `deployment_maximum_percent` - (Optional) Upper limit (as a percentage of the service's desiredCount) of the number of running tasks that can be running in a service during a deployment. Not valid when using the `DAEMON` scheduling strategy. +* `deployment_minimum_healthy_percent` - (Optional) Lower limit (as a percentage of the service's desiredCount) of the number of running tasks that must remain running and healthy in a service during a deployment. +* `desired_count` - (Optional) Number of instances of the task definition to place and keep running. Defaults to 0. Do not specify if using the `DAEMON` scheduling strategy. +* `enable_ecs_managed_tags` - (Optional) Specifies whether to enable Amazon ECS managed tags for the tasks within the service. +* `enable_execute_command` - (Optional) Specifies whether to enable Amazon ECS Exec for the tasks within the service. +* `force_new_deployment` - (Optional) Enable to force a new task deployment of the service. This can be used to update tasks to use a newer Docker image with same image/tag combination (e.g., `myimage:latest`), roll Fargate tasks onto a newer platform version, or immediately deploy `ordered_placement_strategy` and `placement_constraints` updates. +* `health_check_grace_period_seconds` - (Optional) Seconds to ignore failing load balancer health checks on newly instantiated tasks to prevent premature shutdown, up to 2147483647. Only valid for services configured to use load balancers. +* `iam_role` - (Optional) ARN of the IAM role that allows Amazon ECS to make calls to your load balancer on your behalf. This parameter is required if you are using a load balancer with your service, but only if your task definition does not use the `awsvpc` network mode. If using `awsvpc` network mode, do not specify this role. If your account has already created the Amazon ECS service-linked role, that role is used by default for your service unless you specify a role here. +* `launch_type` - (Optional) Launch type on which to run your service. The valid values are `EC2`, `FARGATE`, and `EXTERNAL`. Defaults to `EC2`. +* `load_balancer` - (Optional) Configuration block for load balancers. Detailed below. +* `network_configuration` - (Optional) Network configuration for the service. This parameter is required for task definitions that use the `awsvpc` network mode to receive their own Elastic Network Interface, and it is not supported for other network modes. Detailed below. +* `ordered_placement_strategy` - (Optional) Service level strategy rules that are taken into consideration during task placement. List from top to bottom in order of precedence. Updates to this configuration will take effect next task deployment unless `force_new_deployment` is enabled. The maximum number of `ordered_placement_strategy` blocks is `5`. Detailed below. +* `placement_constraints` - (Optional) Rules that are taken into consideration during task placement. Updates to this configuration will take effect next task deployment unless `force_new_deployment` is enabled. Maximum number of `placement_constraints` is `10`. Detailed below. +* `platform_version` - (Optional) Platform version on which to run your service. Only applicable for `launch_type` set to `FARGATE`. Defaults to `LATEST`. More information about Fargate platform versions can be found in the [AWS ECS User Guide](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/platform_versions.html). +* `propagate_tags` - (Optional) Specifies whether to propagate the tags from the task definition or the service to the tasks. The valid values are `SERVICE` and `TASK_DEFINITION`. +* `scheduling_strategy` - (Optional) Scheduling strategy to use for the service. The valid values are `REPLICA` and `DAEMON`. Defaults to `REPLICA`. Note that [*Tasks using the Fargate launch type or the `CODE_DEPLOY` or `EXTERNAL` deployment controller types don't support the `DAEMON` scheduling strategy*](https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_CreateService.html). +* `service_registries` - (Optional) Service discovery registries for the service. The maximum number of `service_registries` blocks is `1`. Detailed below. +* `tags` - (Optional) Key-value map of resource tags. If configured with a provider [`default_tags` configuration block](https://www.terraform.io/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. +* `task_definition` - (Optional) Family and revision (`family:revision`) or full ARN of the task definition that you want to run in your service. Required unless using the `EXTERNAL` deployment controller. If a revision is not specified, the latest `ACTIVE` revision is used. +* `wait_for_steady_state` - (Optional) If `true`, Terraform will wait for the service to reach a steady state (like [`aws ecs wait services-stable`](https://docs.aws.amazon.com/cli/latest/reference/ecs/wait/services-stable.html)) before continuing. Default `false`. + +### capacity_provider_strategy The `capacity_provider_strategy` configuration block supports the following: -* `capacity_provider` - (Required) The short name or full Amazon Resource Name (ARN) of the capacity provider. -* `weight` - (Required) The relative percentage of the total number of launched tasks that should use the specified capacity provider. -* `base` - (Optional) The number of tasks, at a minimum, to run on the specified capacity provider. Only one capacity provider in a capacity provider strategy can have a base defined. +* `base` - (Optional) Number of tasks, at a minimum, to run on the specified capacity provider. Only one capacity provider in a capacity provider strategy can have a base defined. +* `capacity_provider` - (Required) Short name of the capacity provider. +* `weight` - (Required) Relative percentage of the total number of launched tasks that should use the specified capacity provider. + +### deployment_circuit_breaker + +The `deployment_circuit_breaker` configuration block supports the following: + +* `enable` - (Required) Whether to enable the deployment circuit breaker logic for the service. +* `rollback` - (Required) Whether to enable Amazon ECS to roll back the service if a service deployment fails. If rollback is enabled, when a service deployment fails, the service is rolled back to the last deployment that completed successfully. + +### deployment_controller + +The `deployment_controller` configuration block supports the following: + +* `type` - (Optional) Type of deployment controller. Valid values: `CODE_DEPLOY`, `ECS`, `EXTERNAL`. Default: `ECS`. -## load_balancer +### load_balancer -The `load_balancer` configuration block supports the following: +`load_balancer` supports the following: -* `container_name` - (Required) The name of the container to associate with the load balancer (as it appears in a container definition). -* `load_balancer_name` - (Optional, Required for ELB Classic) The name of the ELB (Classic) to associate with the service. -* `target_group_arn` - (Optional, Required for ALB/NLB) The ARN of the Load Balancer target group to associate with the service. -* `container_port` - (Optional) The port on the container to associate with the load balancer. Defaults to `0` if not specified. +* `elb_name` - (Required for ELB Classic) Name of the ELB (Classic) to associate with the service. +* `target_group_arn` - (Required for ALB/NLB) ARN of the Load Balancer target group to associate with the service. +* `container_name` - (Required) Name of the container to associate with the load balancer (as it appears in a container definition). +* `container_port` - (Required) Port on the container to associate with the load balancer. -~> **Note:** Specifying multiple `load_balancer` configurations is still not supported by AWS for ECS task set. +-> **Version note:** Multiple `load_balancer` configuration block support was added in Terraform AWS Provider version 2.22.0. This allows configuration of [ECS service support for multiple target groups](https://aws.amazon.com/about-aws/whats-new/2019/07/amazon-ecs-services-now-support-multiple-load-balancer-target-groups/). -## network_configuration +### network_configuration -The `network_configuration` configuration block supports the following: +`network_configuration` support the following: -* `subnets` - (Required) The subnets associated with the task or service. Maximum of 16. -* `security_groups` - (Optional) The security groups associated with the task or service. If you do not specify a security group, the default security group for the VPC is used. Maximum of 5. -* `assign_public_ip` - (Optional) Whether to assign a public IP address to the ENI (`FARGATE` launch type only). Valid values are `true` or `false`. Default `false`. +* `subnets` - (Required) Subnets associated with the task or service. +* `security_groups` - (Optional) Security groups associated with the task or service. If you do not specify a security group, the default security group for the VPC is used. +* `assign_public_ip` - (Optional) Assign a public IP address to the ENI (Fargate launch type only). Valid values are `true` or `false`. Default `false`. -For more information, see [Task Networking](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-networking.html). +For more information, see [Task Networking](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-networking.html) -## scale +### ordered_placement_strategy -The `scale` configuration block supports the following: +`ordered_placement_strategy` supports the following: -* `unit` - (Optional) The unit of measure for the scale value. Default: `PERCENT`. -* `value` - (Optional) The value, specified as a percent total of a service's `desiredCount`, to scale the task set. Defaults to `0` if not specified. Accepted values are numbers between 0.0 and 100.0. +* `type` - (Required) Type of placement strategy. Must be one of: `binpack`, `random`, or `spread` +* `field` - (Optional) For the `spread` placement strategy, valid values are `instanceId` (or `host`, + which has the same effect), or any platform or custom attribute that is applied to a container instance. + For the `binpack` type, valid values are `memory` and `cpu`. For the `random` type, this attribute is not + needed. For more information, see [Placement Strategy](https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_PlacementStrategy.html). -## service_registries +-> **Note:** for `spread`, `host` and `instanceId` will be normalized, by AWS, to be `instanceId`. This means the statefile will show `instanceId` but your config will differ if you use `host`. + +### placement_constraints + +`placement_constraints` support the following: + +* `type` - (Required) Type of constraint. The only valid values at this time are `memberOf` and `distinctInstance`. +* `expression` - (Optional) Cluster Query Language expression to apply to the constraint. Does not need to be specified for the `distinctInstance` type. For more information, see [Cluster Query Language in the Amazon EC2 Container Service Developer Guide](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/cluster-query-language.html). + +### service_registries `service_registries` support the following: -* `registry_arn` - (Required) The ARN of the Service Registry. The currently supported service registry is Amazon Route 53 Auto Naming Service([`aws_service_discovery_service` resource](/docs/providers/aws/r/service_discovery_service.html)). For more information, see [Service](https://docs.aws.amazon.com/Route53/latest/APIReference/API_autonaming_Service.html). -* `port` - (Optional) The port value used if your Service Discovery service specified an SRV record. -* `container_port` - (Optional) The port value, already specified in the task definition, to be used for your service discovery service. -* `container_name` - (Optional) The container name value, already specified in the task definition, to be used for your service discovery service. +* `registry_arn` - (Required) ARN of the Service Registry. The currently supported service registry is Amazon Route 53 Auto Naming Service(`aws_service_discovery_service`). For more information, see [Service](https://docs.aws.amazon.com/Route53/latest/APIReference/API_autonaming_Service.html) +* `port` - (Optional) Port value used if your Service Discovery service specified an SRV record. +* `container_port` - (Optional) Port value, already specified in the task definition, to be used for your service discovery service. +* `container_name` - (Optional) Container name value, already specified in the task definition, to be used for your service discovery service. ## Attributes Reference In addition to all arguments above, the following attributes are exported: -* `id` - The `task_set_id`, `service` and `cluster` separated by commas (`,`). -* `arn` - The Amazon Resource Name (ARN) that identifies the task set. -* `stability_status` - The stability status. This indicates whether the task set has reached a steady state. -* `status` - The status of the task set. -* `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block). -* `task_set_id` - The ID of the task set. +* `cluster` - Amazon Resource Name (ARN) of cluster which the service runs on. +* `desired_count` - Number of instances of the task definition. +* `iam_role` - ARN of IAM role used for ELB. +* `id` - ARN that identifies the service. +* `name` - Name of the service. +* `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](https://www.terraform.io/docs/providers/aws/index.html#default_tags-configuration-block). + +## Timeouts + +`aws_ecs_task_set` provides the following [Timeouts](https://www.terraform.io/docs/configuration/blocks/resources/syntax.html#operation-timeouts) configuration options: + +- `delete` - (Default `20 minutes`) ## Import -ECS Task Sets can be imported via the `task_set_id`, `service`, and `cluster` separated by commas (`,`) e.g. +ECS services can be imported using the `name` together with ecs cluster `name`, e.g., ``` -$ terraform import aws_ecs_task_set.example ecs-svc/7177320696926227436,arn:aws:ecs:us-west-2:123456789101:service/example/example-1234567890,arn:aws:ecs:us-west-2:123456789101:cluster/example +$ terraform import aws_ecs_task_set.imported cluster-name/service-name ``` +