From 9dc5c3acb6b1d34122d6976571169589522acad5 Mon Sep 17 00:00:00 2001 From: Kumarappan-A Date: Mon, 20 May 2019 23:36:26 -0700 Subject: [PATCH] resource/aws_ecs_task_set: Support ECS task set --- aws/provider.go | 1 + aws/resource_aws_ecs_task_set.go | 737 +++++ aws/resource_aws_ecs_task_set_test.go | 1069 ++++++ website/aws.erb | 3650 +++++++++++++++++++++ website/docs/r/ecs_task_set.html.markdown | 121 + 5 files changed, 5578 insertions(+) create mode 100644 aws/resource_aws_ecs_task_set.go create mode 100644 aws/resource_aws_ecs_task_set_test.go create mode 100644 website/aws.erb create mode 100644 website/docs/r/ecs_task_set.html.markdown diff --git a/aws/provider.go b/aws/provider.go index 1beb5e9c100..93d8fe696c1 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -728,6 +728,7 @@ func Provider() *schema.Provider { "aws_ecs_cluster": resourceAwsEcsCluster(), "aws_ecs_service": resourceAwsEcsService(), "aws_ecs_task_definition": resourceAwsEcsTaskDefinition(), + "aws_ecs_task_set": resourceAwsEcsTaskSet(), "aws_efs_access_point": resourceAwsEfsAccessPoint(), "aws_efs_backup_policy": resourceAwsEfsBackupPolicy(), "aws_efs_file_system": resourceAwsEfsFileSystem(), diff --git a/aws/resource_aws_ecs_task_set.go b/aws/resource_aws_ecs_task_set.go new file mode 100644 index 00000000000..f77d59d8b1c --- /dev/null +++ b/aws/resource_aws_ecs_task_set.go @@ -0,0 +1,737 @@ +package aws + +import ( + "fmt" + "log" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ecs" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "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/keyvaluetags" +) + +func resourceAwsEcsTaskSet() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsEcsTaskSetCreate, + Read: resourceAwsEcsTaskSetRead, + Update: resourceAwsEcsTaskSetUpdate, + Delete: resourceAwsEcsTaskSetDelete, + 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, + ForceNew: true, + }, + + "cluster": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "external_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + }, + + "task_definition": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "network_configuration": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "security_groups": { + Type: schema.TypeSet, + MaxItems: 5, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "subnets": { + Type: schema.TypeSet, + MaxItems: 16, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "assign_public_ip": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + }, + }, + }, + + // 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_balancers": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "elb_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "target_group_arn": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validateArn, + }, + "container_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "container_port": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validation.IsPortNumber, + }, + }, + }, + }, + + "service_registries": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "container_name": { + Type: schema.TypeString, + Optional: true, + }, + "container_port": { + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IsPortNumber, + }, + "port": { + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IsPortNumber, + }, + "registry_arn": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateArn, + }, + }, + }, + }, + + "launch_type": { + 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, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "base": { + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntBetween(0, 100000), + ForceNew: true, + }, + + "capacity_provider": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "weight": { + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntBetween(0, 1000), + ForceNew: true, + }, + }, + }, + }, + + "platform_version": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "scale": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "unit": { + Type: schema.TypeString, + Optional: true, + Default: ecs.ScaleUnitPercent, + ValidateFunc: validation.StringInSlice([]string{ + ecs.ScaleUnitPercent, + }, false), + }, + "value": { + Type: schema.TypeFloat, + Optional: true, + ValidateFunc: validation.FloatBetween(0.0, 100.0), + }, + }, + }, + }, + + "force_delete": { + Type: schema.TypeBool, + Optional: true, + }, + + "wait_until_stable": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + + "wait_until_stable_timeout": { + Type: schema.TypeString, + Optional: true, + 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: %s", k, err)) + } + if duration < 0 { + errors = append(errors, fmt.Errorf( + "%q must be greater than zero", k)) + } + return + }, + }, + + "tags": tagsSchema(), + }, + } +} + +func resourceAwsEcsTaskSetCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ecsconn + + cluster := d.Get("cluster").(string) + service := d.Get("service").(string) + input := ecs.CreateTaskSetInput{ + ClientToken: aws.String(resource.UniqueId()), + Cluster: aws.String(cluster), + Service: aws.String(service), + TaskDefinition: aws.String(d.Get("task_definition").(string)), + Tags: keyvaluetags.New(d.Get("tags").(map[string]interface{})).IgnoreAws().EcsTags(), + } + + if v, ok := d.GetOk("external_id"); ok { + input.ExternalId = aws.String(v.(string)) + } + + if v, ok := d.GetOk("launch_type"); ok { + input.LaunchType = aws.String(v.(string)) + } + + input.CapacityProviderStrategy = expandEcsCapacityProviderStrategy(d.Get("capacity_provider_strategy").(*schema.Set)) + + loadBalancers := expandEcsLoadBalancers(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)) + } + + scale := d.Get("scale").([]interface{}) + if len(scale) > 0 { + input.Scale = expandAwsEcsScale(scale[0].(map[string]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 + 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 isAWSErr(err, ecs.ErrCodeClusterNotFoundException, "") || + isAWSErr(err, ecs.ErrCodeServiceNotFoundException, "") || + isAWSErr(err, ecs.ErrCodeTaskSetNotFoundException, "") || + isAWSErr(err, ecs.ErrCodeInvalidParameterException, "does not have an associated load balancer") { + return resource.RetryableError(err) + } + return resource.NonRetryableError(err) + } + + return nil + }) + + if isResourceTimeoutError(err) { + out, err = conn.CreateTaskSet(&input) + } + + if err != nil { + return fmt.Errorf("Error creating ECS TaskSet: %s", err) + } + + taskSet := *out.TaskSet + + 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) { + 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 resourceAwsEcsTaskSetRead(d, meta) +} + +func resourceAwsEcsTaskSetRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ecsconn + + log.Printf("[DEBUG] Reading ECS task set %s", d.Id()) + + cluster := d.Get("cluster").(string) + service := d.Get("service").(string) + input := ecs.DescribeTaskSetsInput{ + Cluster: aws.String(cluster), + Service: aws.String(service), + TaskSets: []*string{aws.String(d.Id())}, + } + + 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() && + isAWSErr(err, ecs.ErrCodeServiceNotFoundException, "") || + isAWSErr(err, ecs.ErrCodeClusterNotFoundException, "") || + isAWSErr(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 + } + + return nil + }) + + if isResourceTimeoutError(err) { + out, err = conn.DescribeTaskSets(&input) + } + + // after retrying + if err != nil { + if isAWSErr(err, ecs.ErrCodeClusterNotFoundException, "") || + isAWSErr(err, ecs.ErrCodeServiceNotFoundException, "") || + isAWSErr(err, ecs.ErrCodeTaskSetNotFoundException, "") { + log.Printf("[WARN] ECS TaskSet (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + return err + } + + if len(out.TaskSets) < 1 { + if d.IsNewResource() { + return fmt.Errorf("ECS TaskSet not created: %q", 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("launch_type", taskSet.LaunchType) + d.Set("platform_version", taskSet.PlatformVersion) + d.Set("external_id", taskSet.ExternalId) + + // Save cluster in the same format + if strings.HasPrefix(d.Get("cluster").(string), "arn:"+meta.(*AWSClient).partition+":ecs:") { + d.Set("cluster", taskSet.ClusterArn) + } else { + clusterARN := getNameFromARN(*taskSet.ClusterArn) + d.Set("cluster", clusterARN) + } + + // Save task definition in the same format + if strings.HasPrefix(d.Get("task_definition").(string), "arn:"+meta.(*AWSClient).partition+":ecs:") { + d.Set("task_definition", taskSet.TaskDefinition) + } else { + taskDefinition := buildFamilyAndRevisionFromARN(*taskSet.TaskDefinition) + d.Set("task_definition", taskDefinition) + } + + if taskSet.LoadBalancers != nil { + d.Set("load_balancers", flattenEcsLoadBalancers(taskSet.LoadBalancers)) + } + + 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("capacity_provider_strategy", flattenEcsCapacityProviderStrategy(taskSet.CapacityProviderStrategy)); err != nil { + return fmt.Errorf("error setting capacity_provider_strategy: %s", 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("service_registries", flattenServiceRegistries(taskSet.ServiceRegistries)); err != nil { + return fmt.Errorf("Error setting service_registries for (%s): %s", d.Id(), err) + } + + return nil +} + +func resourceAwsEcsTaskSetUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ecsconn + updateTaskset := false + + input := ecs.UpdateTaskSetInput{ + Cluster: aws.String(d.Get("cluster").(string)), + Service: aws.String(d.Get("service").(string)), + TaskSet: aws.String(d.Id()), + } + + if d.HasChange("scale") { + scale := d.Get("scale").([]interface{}) + if len(scale) > 0 { + updateTaskset = true + input.Scale = expandAwsEcsScale(scale[0].(map[string]interface{})) + } + } + + 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 isAWSErr(err, ecs.ErrCodeClusterNotFoundException, "") || + isAWSErr(err, ecs.ErrCodeServiceNotFoundException, "") || + isAWSErr(err, ecs.ErrCodeTaskSetNotFoundException, "") || + isAWSErr(err, ecs.ErrCodeInvalidParameterException, "does not have an associated load balancer") { + return resource.RetryableError(err) + } + return resource.NonRetryableError(err) + } + return nil + }) + + if isResourceTimeoutError(err) { + _, err = conn.UpdateTaskSet(&input) + } + if err != nil { + return fmt.Errorf("Error updating ECS Task set (%s): %s", d.Id(), err) + } + + if d.Get("wait_until_stable").(bool) { + 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 + } + + // 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 %q", d.Id(), *resp.TaskSets[0].StabilityStatus) + return resp, *resp.TaskSets[0].StabilityStatus, nil + }, + } + + _, err = wait.WaitForState() + if err != nil { + return err + } + } + + } + + return resourceAwsEcsTaskSetRead(d, meta) +} + +func resourceAwsEcsTaskSetDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ecsconn + + // 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 isAWSErr(err, ecs.ErrCodeTaskSetNotFoundException, "") { + log.Printf("[DEBUG] Removing ECS Task set from state, %q is already gone", d.Id()) + return nil + } + return err + } + + if len(resp.TaskSets) == 0 { + log.Printf("[DEBUG] Removing ECS Task set from state, %q is already gone", d.Id()) + return nil + } + + log.Printf("[DEBUG] ECS TaskSet %s is currently %s", d.Id(), aws.StringValue(resp.TaskSets[0].Status)) + + input := ecs.DeleteTaskSetInput{ + Cluster: aws.String(d.Get("cluster").(string)), + Service: aws.String(d.Get("service").(string)), + TaskSet: aws.String(d.Id()), + } + + if v, ok := d.GetOk("force_delete"); ok && v.(bool) { + input.Force = aws.Bool(v.(bool)) + } + + // 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 isAWSErr(err, ecs.ErrCodeTaskSetNotFoundException, "") { + return nil + } + if isAWSErr(err, ecs.ErrCodeInvalidParameterException, "The service cannot be stopped while deployments are active.") { + return resource.RetryableError(err) + } + return resource.NonRetryableError(err) + } + return nil + }) + + if isResourceTimeoutError(err) { + _, err = conn.DeleteTaskSet(&input) + } + + if err != nil { + if isAWSErr(err, ecs.ErrCodeTaskSetNotFoundException, "") { + return nil + } + 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 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 + } + + 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 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/aws/resource_aws_ecs_task_set_test.go b/aws/resource_aws_ecs_task_set_test.go new file mode 100644 index 00000000000..09326f61354 --- /dev/null +++ b/aws/resource_aws_ecs_task_set_test.go @@ -0,0 +1,1069 @@ +package aws + +import ( + "fmt" + "testing" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ecs" + "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" +) + +func TestAccAwsEcsTaskSet_basic(t *testing.T) { + var taskSet ecs.TaskSet + + clusterName := acctest.RandomWithPrefix("tf-acc-cluster") + tdName := acctest.RandomWithPrefix("tf-acc-td") + svcName := acctest.RandomWithPrefix("tf-acc-svc") + resourceName := "aws_ecs_task_set.mongo" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSEcsTaskSetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSEcsTaskSet(clusterName, tdName, svcName, 0.0), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEcsTaskSetExists(resourceName, &taskSet), + testAccCheckAwsEcsTaskSetArn(resourceName, clusterName, svcName, &taskSet), + resource.TestCheckResourceAttr(resourceName, "service_registries.#", "0"), + resource.TestCheckResourceAttr(resourceName, "load_balancers.#", "0"), + ), + }, + }, + }) +} + +func TestAccAWSEcsTaskSet_withARN(t *testing.T) { + var taskSet ecs.TaskSet + + clusterName := acctest.RandomWithPrefix("tf-acc-cluster") + tdName := acctest.RandomWithPrefix("tf-acc-td") + svcName := acctest.RandomWithPrefix("tf-acc-svc") + resourceName := "aws_ecs_task_set.mongo" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSEcsTaskSetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSEcsTaskSet(clusterName, tdName, svcName, 0.0), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEcsTaskSetExists(resourceName, &taskSet), + resource.TestCheckResourceAttr(resourceName, "service_registries.#", "0"), + resource.TestCheckResourceAttr(resourceName, "load_balancers.#", "0"), + ), + }, + + { + Config: testAccAWSEcsTaskSetModified(clusterName, tdName, svcName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEcsTaskSetExists(resourceName, &taskSet), + resource.TestCheckResourceAttr(resourceName, "service_registries.#", "0"), + resource.TestCheckResourceAttr(resourceName, "load_balancers.#", "0"), + resource.TestCheckResourceAttr(resourceName, "external_id", "TEST_ID"), + ), + }, + }, + }) +} + +func TestAccAWSEcsTaskSet_disappears(t *testing.T) { + var taskSet ecs.TaskSet + + clusterName := acctest.RandomWithPrefix("tf-acc-cluster") + tdName := acctest.RandomWithPrefix("tf-acc-td") + svcName := acctest.RandomWithPrefix("tf-acc-svc") + resourceName := "aws_ecs_task_set.mongo" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSEcsTaskSetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSEcsTaskSet(clusterName, tdName, svcName, 0.0), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEcsTaskSetExists(resourceName, &taskSet), + testAccCheckResourceDisappears(testAccProvider, resourceAwsEcsTaskSet(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAWSEcsTaskSet_scale(t *testing.T) { + var taskSet ecs.TaskSet + + clusterName := acctest.RandomWithPrefix("tf-acc-cluster") + tdName := acctest.RandomWithPrefix("tf-acc-td") + svcName := acctest.RandomWithPrefix("tf-acc-svc") + resourceName := "aws_ecs_task_set.mongo" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSEcsTaskSetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSEcsTaskSet(clusterName, tdName, svcName, 0.0), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEcsTaskSetExists(resourceName, &taskSet), + resource.TestCheckResourceAttr(resourceName, "scale.0.unit", "PERCENT"), + resource.TestCheckResourceAttr(resourceName, "scale.0.value", "0"), + ), + }, + { + Config: testAccAWSEcsTaskSet(clusterName, tdName, svcName, 100.0), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEcsTaskSetExists(resourceName, &taskSet), + resource.TestCheckResourceAttr(resourceName, "scale.0.unit", "PERCENT"), + resource.TestCheckResourceAttr(resourceName, "scale.0.value", "100"), + ), + }, + }, + }) +} + +func TestAccAWSEcsTaskSet_withCapacityProviderStrategy(t *testing.T) { + var taskSet ecs.TaskSet + + clusterName := acctest.RandomWithPrefix("tf-acc-cluster") + tdName := acctest.RandomWithPrefix("tf-acc-td") + svcName := acctest.RandomWithPrefix("tf-acc-svc") + providerName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_ecs_task_set.mongo" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSEcsTaskSetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSEcsTaskSetWithCapacityProviderStrategy(providerName, clusterName, tdName, svcName, 1, 0), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEcsTaskSetExists(resourceName, &taskSet), + ), + }, + { + Config: testAccAWSEcsTaskSetWithCapacityProviderStrategy(providerName, clusterName, tdName, svcName, 10, 1), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEcsTaskSetExists(resourceName, &taskSet), + ), + }, + }, + }) +} + +func TestAccAWSEcsTaskSet_withMultipleCapacityProviderStrategies(t *testing.T) { + var taskSet ecs.TaskSet + + clusterName := acctest.RandomWithPrefix("tf-acc-cluster") + tdName := acctest.RandomWithPrefix("tf-acc-td") + svcName := acctest.RandomWithPrefix("tf-acc-svc") + sgName := acctest.RandomWithPrefix("tf-acc-sg") + resourceName := "aws_ecs_task_set.mongo" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSEcsTaskSetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSEcsTaskSetWithMultipleCapacityProviderStrategies(clusterName, tdName, svcName, sgName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEcsTaskSetExists(resourceName, &taskSet), + resource.TestCheckResourceAttr(resourceName, "capacity_provider_strategy.#", "2"), + ), + }, + }, + }) +} + +func TestAccAWSEcsTaskSet_withAlb(t *testing.T) { + var taskSet ecs.TaskSet + + clusterName := acctest.RandomWithPrefix("tf-acc-cluster") + tdName := acctest.RandomWithPrefix("tf-acc-td") + svcName := acctest.RandomWithPrefix("tf-acc-svc") + lbName := acctest.RandomWithPrefix("tf-acc-lb") + resourceName := "aws_ecs_task_set.with_alb" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSEcsTaskSetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSEcsTaskSetWithAlb(clusterName, tdName, lbName, svcName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEcsTaskSetExists(resourceName, &taskSet), + resource.TestCheckResourceAttr(resourceName, "load_balancers.#", "1"), + ), + }, + }, + }) +} + +func TestAccAWSEcsTaskSet_withLaunchTypeFargate(t *testing.T) { + var taskSet ecs.TaskSet + + sg1Name := acctest.RandomWithPrefix("tf-acc-sg-1") + sg2Name := acctest.RandomWithPrefix("tf-acc-sg-2") + clusterName := acctest.RandomWithPrefix("tf-acc-cluster") + tdName := acctest.RandomWithPrefix("tf-acc-td") + svcName := acctest.RandomWithPrefix("tf-acc-svc") + resourceName := "aws_ecs_task_set.main" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSEcsTaskSetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSEcsTaskSetWithLaunchTypeFargate(sg1Name, sg2Name, clusterName, tdName, svcName, "false"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEcsTaskSetExists(resourceName, &taskSet), + resource.TestCheckResourceAttr(resourceName, "launch_type", "FARGATE"), + 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.TestCheckResourceAttr(resourceName, "platform_version", "1.3.0"), + ), + }, + }, + }) +} + +func TestAccAWSEcsTaskSet_withLaunchTypeFargateAndPlatformVersion(t *testing.T) { + var taskSet ecs.TaskSet + + sg1Name := acctest.RandomWithPrefix("tf-acc-sg-1") + sg2Name := acctest.RandomWithPrefix("tf-acc-sg-2") + clusterName := acctest.RandomWithPrefix("tf-acc-cluster") + tdName := acctest.RandomWithPrefix("tf-acc-td") + svcName := acctest.RandomWithPrefix("tf-acc-svc") + resourceName := "aws_ecs_task_set.main" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSEcsTaskSetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSEcsTaskSetWithLaunchTypeFargateAndPlatformVersion(sg1Name, sg2Name, clusterName, tdName, svcName, "1.2.0"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEcsTaskSetExists(resourceName, &taskSet), + resource.TestCheckResourceAttr(resourceName, "platform_version", "1.2.0"), + ), + }, + { + Config: testAccAWSEcsTaskSetWithLaunchTypeFargateAndPlatformVersion(sg1Name, sg2Name, clusterName, tdName, svcName, "1.3.0"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEcsTaskSetExists(resourceName, &taskSet), + resource.TestCheckResourceAttr(resourceName, "platform_version", "1.3.0"), + ), + }, + }, + }) +} + +func TestAccAWSEcsTaskSet_withServiceRegistries(t *testing.T) { + var taskSet ecs.TaskSet + rString := acctest.RandString(8) + + clusterName := acctest.RandomWithPrefix("tf-acc-cluster") + tdName := acctest.RandomWithPrefix("tf-acc-td") + svcName := acctest.RandomWithPrefix("tf-acc-svc") + resourceName := "aws_ecs_task_set.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSEcsTaskSetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSEcsTaskSet_withServiceRegistries(rString, clusterName, tdName, svcName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEcsTaskSetExists(resourceName, &taskSet), + resource.TestCheckResourceAttr(resourceName, "service_registries.#", "1"), + ), + }, + }, + }) +} + +func TestAccAWSEcsTaskSet_Tags(t *testing.T) { + var taskSet ecs.TaskSet + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_ecs_task_set.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSEcsTaskSetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSEcsTaskSetConfigTags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEcsTaskSetExists(resourceName, &taskSet), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + Config: testAccAWSEcsTaskSetConfigTags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEcsTaskSetExists(resourceName, &taskSet), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccAWSEcsTaskSetConfigTags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEcsTaskSetExists(resourceName, &taskSet), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +////////////// +// Fixtures // +////////////// + +func testAccAWSEcsTaskSet(clusterName, tdName, svcName string, scale float64) string { + return fmt.Sprintf(` +resource "aws_ecs_cluster" "default" { + name = "%s" +} +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 + } + + return err + } + + return nil +} diff --git a/website/aws.erb b/website/aws.erb new file mode 100644 index 00000000000..2a9f45c1703 --- /dev/null +++ b/website/aws.erb @@ -0,0 +1,3650 @@ +<% wrap_layout :inner do %> + <% content_for :sidebar do %> + + <% end %> + <%= yield %> +<% end %> diff --git a/website/docs/r/ecs_task_set.html.markdown b/website/docs/r/ecs_task_set.html.markdown new file mode 100644 index 00000000000..a02cfeff5e4 --- /dev/null +++ b/website/docs/r/ecs_task_set.html.markdown @@ -0,0 +1,121 @@ +--- +subcategory: "ECS" +layout: "aws" +page_title: "AWS: aws_ecs_task_set" +description: |- + Provides an ECS task set. +--- + +# 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). + +See [ECS Task Set section in AWS developer guide](https://docs.amazonaws.cn/en_us/AmazonECS/latest/userguide/deployment-type-external.html). + +## Example Usage + +```hcl +resource "aws_ecs_task_set" "mongo" { + service = aws_ecs_service.foo.id + cluster = aws_ecs_cluster.foo.id + task_definition = aws_ecs_task_definition.mongo.arn + + load_balancer { + target_group_arn = aws_lb_target_group.foo.arn + container_name = "mongo" + container_port = 8080 + } +} +``` + +### Ignoring Changes to Scale + +You can utilize the generic Terraform resource [lifecycle configuration block](/docs/configuration/resources.html#lifecycle) 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). + +```hcl +resource "aws_ecs_task_set" "example" { + # ... other configurations ... + + # Example: Run 50% of the servcie's desired count + scale { + value = 50.0 + } + + # Optional: Allow external changes without Terraform plan difference + lifecycle { + ignore_changes = ["scale"] + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `service` - (Required) The name or ARN of the ECS service. +* `cluster` - (Required) The name or ARN of an ECS cluster. +* `external_id` - (Optional) The external ID associated with the task set. +* `task_definition` - (Required) The family and revision (`family:revision`) or full ARN of the task definition that you want to run in your service. +* `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. +* `load_balancer` - (Optional) A load balancer block. Load balancers documented below. +* `service_registries` - (Optional) The service discovery registries for the service. The maximum number of `service_registries` blocks is `1`. +* `launch_type` - (Optional) The launch type on which to run your service. The valid values are `EC2` and `FARGATE`. Defaults to `EC2`. +* `capacity_provider_strategy` - (Optional) The capacity provider strategy to use for the service. Can be one or more. Defined below. +* `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). +* `scale` - (Optional) A floating-point percentage of the desired number of tasks to place and keep running in the task set. `unit` determines the interpretation of this number (a percentage of the service's desired count). Accepted values are numbers between 0 and 100. +* `force_delete` - (Optional) Allows 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. +* `wait_until_stable` - (Optional) Apply will wait until the task set has reached `STEADY_STATE` +* `wait_until_stable_timeout` - (Optional) Wait timeout for task set to reach `STEADY_STATE`. Default `10m` +* `tags` - (Optional) Key-value map of resource tags + +## 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. + +## scale + +The `scale` configuration block supports the following: + +* `unit` - (Optional) The unit of measure for the scale value. Default: `PERCENT` +* `value` - (Required) The value, specified as a percent total of a service's `desiredCount`, to scale the task set. Accepted values are numbers between 0.0 and 100.0. + +## load_balancer + +`load_balancer` supports the following: + +* `elb_name` - (Required for ELB Classic) The name of the ELB (Classic) to associate with the service. +* `target_group_arn` - (Required for ALB/NLB) The ARN of the Load Balancer target group to associate with the service. +* `container_name` - (Required) The name of the container to associate with the load balancer (as it appears in a container definition). +* `container_port` - (Required) The port on the container to associate with the load balancer. + +-> **Note:** Multiple `load_balancer` configuration is still not supported by AWS for ECS task set. + +## network_configuration + +`network_configuration` support the following: + +* `subnets` - (Required) The subnets associated with the task or service. +* `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. +* `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) + +## 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`). 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. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The ID of the task set +* `arn` - The Amazon Resource Name (ARN) that identifies the task set