github.com/pbthorste/terraform@v0.8.6-0.20170127005045-deb56bd93da2/builtin/providers/aws/resource_aws_ecs_service.go (about)

     1  package aws
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"log"
     7  	"regexp"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/aws/aws-sdk-go/aws"
    12  	"github.com/aws/aws-sdk-go/aws/awserr"
    13  	"github.com/aws/aws-sdk-go/service/ecs"
    14  	"github.com/hashicorp/terraform/helper/hashcode"
    15  	"github.com/hashicorp/terraform/helper/resource"
    16  	"github.com/hashicorp/terraform/helper/schema"
    17  )
    18  
    19  var taskDefinitionRE = regexp.MustCompile("^([a-zA-Z0-9_-]+):([0-9]+)$")
    20  
    21  func resourceAwsEcsService() *schema.Resource {
    22  	return &schema.Resource{
    23  		Create: resourceAwsEcsServiceCreate,
    24  		Read:   resourceAwsEcsServiceRead,
    25  		Update: resourceAwsEcsServiceUpdate,
    26  		Delete: resourceAwsEcsServiceDelete,
    27  
    28  		Schema: map[string]*schema.Schema{
    29  			"name": {
    30  				Type:     schema.TypeString,
    31  				Required: true,
    32  				ForceNew: true,
    33  			},
    34  
    35  			"cluster": {
    36  				Type:     schema.TypeString,
    37  				Optional: true,
    38  				Computed: true,
    39  				ForceNew: true,
    40  			},
    41  
    42  			"task_definition": {
    43  				Type:     schema.TypeString,
    44  				Required: true,
    45  			},
    46  
    47  			"desired_count": {
    48  				Type:     schema.TypeInt,
    49  				Optional: true,
    50  			},
    51  
    52  			"iam_role": {
    53  				Type:     schema.TypeString,
    54  				ForceNew: true,
    55  				Optional: true,
    56  			},
    57  
    58  			"deployment_maximum_percent": {
    59  				Type:     schema.TypeInt,
    60  				Optional: true,
    61  				Default:  200,
    62  			},
    63  
    64  			"deployment_minimum_healthy_percent": {
    65  				Type:     schema.TypeInt,
    66  				Optional: true,
    67  				Default:  100,
    68  			},
    69  
    70  			"load_balancer": {
    71  				Type:     schema.TypeSet,
    72  				Optional: true,
    73  				ForceNew: true,
    74  				MaxItems: 1,
    75  				Elem: &schema.Resource{
    76  					Schema: map[string]*schema.Schema{
    77  						"elb_name": {
    78  							Type:     schema.TypeString,
    79  							Optional: true,
    80  							ForceNew: true,
    81  						},
    82  
    83  						"target_group_arn": {
    84  							Type:     schema.TypeString,
    85  							Optional: true,
    86  							ForceNew: true,
    87  						},
    88  
    89  						"container_name": {
    90  							Type:     schema.TypeString,
    91  							Required: true,
    92  							ForceNew: true,
    93  						},
    94  
    95  						"container_port": {
    96  							Type:     schema.TypeInt,
    97  							Required: true,
    98  							ForceNew: true,
    99  						},
   100  					},
   101  				},
   102  				Set: resourceAwsEcsLoadBalancerHash,
   103  			},
   104  
   105  			"placement_strategy": {
   106  				Type:     schema.TypeSet,
   107  				Optional: true,
   108  				ForceNew: true,
   109  				MaxItems: 5,
   110  				Elem: &schema.Resource{
   111  					Schema: map[string]*schema.Schema{
   112  						"type": {
   113  							Type:     schema.TypeString,
   114  							ForceNew: true,
   115  							Required: true,
   116  						},
   117  						"field": {
   118  							Type:     schema.TypeString,
   119  							ForceNew: true,
   120  							Optional: true,
   121  						},
   122  					},
   123  				},
   124  			},
   125  
   126  			"placement_constraints": {
   127  				Type:     schema.TypeSet,
   128  				Optional: true,
   129  				ForceNew: true,
   130  				MaxItems: 10,
   131  				Elem: &schema.Resource{
   132  					Schema: map[string]*schema.Schema{
   133  						"type": {
   134  							Type:     schema.TypeString,
   135  							ForceNew: true,
   136  							Required: true,
   137  						},
   138  						"expression": {
   139  							Type:     schema.TypeString,
   140  							ForceNew: true,
   141  							Optional: true,
   142  						},
   143  					},
   144  				},
   145  			},
   146  		},
   147  	}
   148  }
   149  
   150  func resourceAwsEcsServiceCreate(d *schema.ResourceData, meta interface{}) error {
   151  	conn := meta.(*AWSClient).ecsconn
   152  
   153  	input := ecs.CreateServiceInput{
   154  		ServiceName:    aws.String(d.Get("name").(string)),
   155  		TaskDefinition: aws.String(d.Get("task_definition").(string)),
   156  		DesiredCount:   aws.Int64(int64(d.Get("desired_count").(int))),
   157  		ClientToken:    aws.String(resource.UniqueId()),
   158  		DeploymentConfiguration: &ecs.DeploymentConfiguration{
   159  			MaximumPercent:        aws.Int64(int64(d.Get("deployment_maximum_percent").(int))),
   160  			MinimumHealthyPercent: aws.Int64(int64(d.Get("deployment_minimum_healthy_percent").(int))),
   161  		},
   162  	}
   163  
   164  	if v, ok := d.GetOk("cluster"); ok {
   165  		input.Cluster = aws.String(v.(string))
   166  	}
   167  
   168  	loadBalancers := expandEcsLoadBalancers(d.Get("load_balancer").(*schema.Set).List())
   169  	if len(loadBalancers) > 0 {
   170  		log.Printf("[DEBUG] Adding ECS load balancers: %s", loadBalancers)
   171  		input.LoadBalancers = loadBalancers
   172  	}
   173  	if v, ok := d.GetOk("iam_role"); ok {
   174  		input.Role = aws.String(v.(string))
   175  	}
   176  
   177  	strategies := d.Get("placement_strategy").(*schema.Set).List()
   178  	if len(strategies) > 0 {
   179  		var ps []*ecs.PlacementStrategy
   180  		for _, raw := range strategies {
   181  			p := raw.(map[string]interface{})
   182  			t := p["type"].(string)
   183  			f := p["field"].(string)
   184  			if err := validateAwsEcsPlacementStrategy(t, f); err != nil {
   185  				return err
   186  			}
   187  			ps = append(ps, &ecs.PlacementStrategy{
   188  				Type:  aws.String(p["type"].(string)),
   189  				Field: aws.String(p["field"].(string)),
   190  			})
   191  		}
   192  		input.PlacementStrategy = ps
   193  	}
   194  
   195  	constraints := d.Get("placement_constraints").(*schema.Set).List()
   196  	if len(constraints) > 0 {
   197  		var pc []*ecs.PlacementConstraint
   198  		for _, raw := range constraints {
   199  			p := raw.(map[string]interface{})
   200  			t := p["type"].(string)
   201  			e := p["expression"].(string)
   202  			if err := validateAwsEcsPlacementConstraint(t, e); err != nil {
   203  				return err
   204  			}
   205  			pc = append(pc, &ecs.PlacementConstraint{
   206  				Type:       aws.String(t),
   207  				Expression: aws.String(e),
   208  			})
   209  		}
   210  		input.PlacementConstraints = pc
   211  	}
   212  
   213  	log.Printf("[DEBUG] Creating ECS service: %s", input)
   214  
   215  	// Retry due to AWS IAM policy eventual consistency
   216  	// See https://github.com/hashicorp/terraform/issues/2869
   217  	var out *ecs.CreateServiceOutput
   218  	var err error
   219  	err = resource.Retry(2*time.Minute, func() *resource.RetryError {
   220  		out, err = conn.CreateService(&input)
   221  
   222  		if err != nil {
   223  			ec2err, ok := err.(awserr.Error)
   224  			if !ok {
   225  				return resource.NonRetryableError(err)
   226  			}
   227  			if ec2err.Code() == "InvalidParameterException" {
   228  				log.Printf("[DEBUG] Trying to create ECS service again: %q",
   229  					ec2err.Message())
   230  				return resource.RetryableError(err)
   231  			}
   232  
   233  			return resource.NonRetryableError(err)
   234  		}
   235  
   236  		return nil
   237  	})
   238  	if err != nil {
   239  		return err
   240  	}
   241  
   242  	service := *out.Service
   243  
   244  	log.Printf("[DEBUG] ECS service created: %s", *service.ServiceArn)
   245  	d.SetId(*service.ServiceArn)
   246  
   247  	return resourceAwsEcsServiceUpdate(d, meta)
   248  }
   249  
   250  func resourceAwsEcsServiceRead(d *schema.ResourceData, meta interface{}) error {
   251  	conn := meta.(*AWSClient).ecsconn
   252  
   253  	log.Printf("[DEBUG] Reading ECS service %s", d.Id())
   254  	input := ecs.DescribeServicesInput{
   255  		Services: []*string{aws.String(d.Id())},
   256  		Cluster:  aws.String(d.Get("cluster").(string)),
   257  	}
   258  
   259  	out, err := conn.DescribeServices(&input)
   260  	if err != nil {
   261  		return err
   262  	}
   263  
   264  	if len(out.Services) < 1 {
   265  		log.Printf("[DEBUG] Removing ECS service %s (%s) because it's gone", d.Get("name").(string), d.Id())
   266  		d.SetId("")
   267  		return nil
   268  	}
   269  
   270  	service := out.Services[0]
   271  
   272  	// Status==INACTIVE means deleted service
   273  	if *service.Status == "INACTIVE" {
   274  		log.Printf("[DEBUG] Removing ECS service %q because it's INACTIVE", *service.ServiceArn)
   275  		d.SetId("")
   276  		return nil
   277  	}
   278  
   279  	log.Printf("[DEBUG] Received ECS service %s", service)
   280  
   281  	d.SetId(*service.ServiceArn)
   282  	d.Set("name", service.ServiceName)
   283  
   284  	// Save task definition in the same format
   285  	if strings.HasPrefix(d.Get("task_definition").(string), "arn:"+meta.(*AWSClient).partition+":ecs:") {
   286  		d.Set("task_definition", service.TaskDefinition)
   287  	} else {
   288  		taskDefinition := buildFamilyAndRevisionFromARN(*service.TaskDefinition)
   289  		d.Set("task_definition", taskDefinition)
   290  	}
   291  
   292  	d.Set("desired_count", service.DesiredCount)
   293  
   294  	// Save cluster in the same format
   295  	if strings.HasPrefix(d.Get("cluster").(string), "arn:"+meta.(*AWSClient).partition+":ecs:") {
   296  		d.Set("cluster", service.ClusterArn)
   297  	} else {
   298  		clusterARN := getNameFromARN(*service.ClusterArn)
   299  		d.Set("cluster", clusterARN)
   300  	}
   301  
   302  	// Save IAM role in the same format
   303  	if service.RoleArn != nil {
   304  		if strings.HasPrefix(d.Get("iam_role").(string), "arn:"+meta.(*AWSClient).partition+":iam:") {
   305  			d.Set("iam_role", service.RoleArn)
   306  		} else {
   307  			roleARN := getNameFromARN(*service.RoleArn)
   308  			d.Set("iam_role", roleARN)
   309  		}
   310  	}
   311  
   312  	if service.DeploymentConfiguration != nil {
   313  		d.Set("deployment_maximum_percent", service.DeploymentConfiguration.MaximumPercent)
   314  		d.Set("deployment_minimum_healthy_percent", service.DeploymentConfiguration.MinimumHealthyPercent)
   315  	}
   316  
   317  	if service.LoadBalancers != nil {
   318  		d.Set("load_balancers", flattenEcsLoadBalancers(service.LoadBalancers))
   319  	}
   320  
   321  	if err := d.Set("placement_strategy", flattenPlacementStrategy(service.PlacementStrategy)); err != nil {
   322  		log.Printf("[ERR] Error setting placement_strategy for (%s): %s", d.Id(), err)
   323  	}
   324  	if err := d.Set("placement_constraints", flattenServicePlacementConstraints(service.PlacementConstraints)); err != nil {
   325  		log.Printf("[ERR] Error setting placement_constraints for (%s): %s", d.Id(), err)
   326  	}
   327  
   328  	return nil
   329  }
   330  
   331  func flattenServicePlacementConstraints(pcs []*ecs.PlacementConstraint) []map[string]interface{} {
   332  	if len(pcs) == 0 {
   333  		return nil
   334  	}
   335  	results := make([]map[string]interface{}, 0)
   336  	for _, pc := range pcs {
   337  		c := make(map[string]interface{})
   338  		c["type"] = *pc.Type
   339  		c["expression"] = *pc.Expression
   340  		results = append(results, c)
   341  	}
   342  	return results
   343  }
   344  
   345  func flattenPlacementStrategy(pss []*ecs.PlacementStrategy) []map[string]interface{} {
   346  	if len(pss) == 0 {
   347  		return nil
   348  	}
   349  	results := make([]map[string]interface{}, 0)
   350  	for _, ps := range pss {
   351  		c := make(map[string]interface{})
   352  		c["type"] = *ps.Type
   353  		c["field"] = *ps.Field
   354  		results = append(results, c)
   355  	}
   356  	return results
   357  }
   358  
   359  func resourceAwsEcsServiceUpdate(d *schema.ResourceData, meta interface{}) error {
   360  	conn := meta.(*AWSClient).ecsconn
   361  
   362  	log.Printf("[DEBUG] Updating ECS service %s", d.Id())
   363  	input := ecs.UpdateServiceInput{
   364  		Service: aws.String(d.Id()),
   365  		Cluster: aws.String(d.Get("cluster").(string)),
   366  	}
   367  
   368  	if d.HasChange("desired_count") {
   369  		_, n := d.GetChange("desired_count")
   370  		input.DesiredCount = aws.Int64(int64(n.(int)))
   371  	}
   372  	if d.HasChange("task_definition") {
   373  		_, n := d.GetChange("task_definition")
   374  		input.TaskDefinition = aws.String(n.(string))
   375  	}
   376  
   377  	if d.HasChange("deployment_maximum_percent") || d.HasChange("deployment_minimum_healthy_percent") {
   378  		input.DeploymentConfiguration = &ecs.DeploymentConfiguration{
   379  			MaximumPercent:        aws.Int64(int64(d.Get("deployment_maximum_percent").(int))),
   380  			MinimumHealthyPercent: aws.Int64(int64(d.Get("deployment_minimum_healthy_percent").(int))),
   381  		}
   382  	}
   383  
   384  	out, err := conn.UpdateService(&input)
   385  	if err != nil {
   386  		return err
   387  	}
   388  	service := out.Service
   389  	log.Printf("[DEBUG] Updated ECS service %s", service)
   390  
   391  	return resourceAwsEcsServiceRead(d, meta)
   392  }
   393  
   394  func resourceAwsEcsServiceDelete(d *schema.ResourceData, meta interface{}) error {
   395  	conn := meta.(*AWSClient).ecsconn
   396  
   397  	// Check if it's not already gone
   398  	resp, err := conn.DescribeServices(&ecs.DescribeServicesInput{
   399  		Services: []*string{aws.String(d.Id())},
   400  		Cluster:  aws.String(d.Get("cluster").(string)),
   401  	})
   402  	if err != nil {
   403  		return err
   404  	}
   405  
   406  	if len(resp.Services) == 0 {
   407  		log.Printf("[DEBUG] ECS Service %q is already gone", d.Id())
   408  		return nil
   409  	}
   410  
   411  	log.Printf("[DEBUG] ECS service %s is currently %s", d.Id(), *resp.Services[0].Status)
   412  
   413  	if *resp.Services[0].Status == "INACTIVE" {
   414  		return nil
   415  	}
   416  
   417  	// Drain the ECS service
   418  	if *resp.Services[0].Status != "DRAINING" {
   419  		log.Printf("[DEBUG] Draining ECS service %s", d.Id())
   420  		_, err = conn.UpdateService(&ecs.UpdateServiceInput{
   421  			Service:      aws.String(d.Id()),
   422  			Cluster:      aws.String(d.Get("cluster").(string)),
   423  			DesiredCount: aws.Int64(int64(0)),
   424  		})
   425  		if err != nil {
   426  			return err
   427  		}
   428  	}
   429  
   430  	// Wait until the ECS service is drained
   431  	err = resource.Retry(5*time.Minute, func() *resource.RetryError {
   432  		input := ecs.DeleteServiceInput{
   433  			Service: aws.String(d.Id()),
   434  			Cluster: aws.String(d.Get("cluster").(string)),
   435  		}
   436  
   437  		log.Printf("[DEBUG] Trying to delete ECS service %s", input)
   438  		_, err := conn.DeleteService(&input)
   439  		if err == nil {
   440  			return nil
   441  		}
   442  
   443  		ec2err, ok := err.(awserr.Error)
   444  		if !ok {
   445  			return resource.NonRetryableError(err)
   446  		}
   447  		if ec2err.Code() == "InvalidParameterException" {
   448  			// Prevent "The service cannot be stopped while deployments are active."
   449  			log.Printf("[DEBUG] Trying to delete ECS service again: %q",
   450  				ec2err.Message())
   451  			return resource.RetryableError(err)
   452  		}
   453  
   454  		return resource.NonRetryableError(err)
   455  
   456  	})
   457  	if err != nil {
   458  		return err
   459  	}
   460  
   461  	// Wait until it's deleted
   462  	wait := resource.StateChangeConf{
   463  		Pending:    []string{"DRAINING"},
   464  		Target:     []string{"INACTIVE"},
   465  		Timeout:    10 * time.Minute,
   466  		MinTimeout: 1 * time.Second,
   467  		Refresh: func() (interface{}, string, error) {
   468  			log.Printf("[DEBUG] Checking if ECS service %s is INACTIVE", d.Id())
   469  			resp, err := conn.DescribeServices(&ecs.DescribeServicesInput{
   470  				Services: []*string{aws.String(d.Id())},
   471  				Cluster:  aws.String(d.Get("cluster").(string)),
   472  			})
   473  			if err != nil {
   474  				return resp, "FAILED", err
   475  			}
   476  
   477  			log.Printf("[DEBUG] ECS service (%s) is currently %q", d.Id(), *resp.Services[0].Status)
   478  			return resp, *resp.Services[0].Status, nil
   479  		},
   480  	}
   481  
   482  	_, err = wait.WaitForState()
   483  	if err != nil {
   484  		return err
   485  	}
   486  
   487  	log.Printf("[DEBUG] ECS service %s deleted.", d.Id())
   488  	return nil
   489  }
   490  
   491  func resourceAwsEcsLoadBalancerHash(v interface{}) int {
   492  	var buf bytes.Buffer
   493  	m := v.(map[string]interface{})
   494  	buf.WriteString(fmt.Sprintf("%s-", m["elb_name"].(string)))
   495  	buf.WriteString(fmt.Sprintf("%s-", m["container_name"].(string)))
   496  	buf.WriteString(fmt.Sprintf("%d-", m["container_port"].(int)))
   497  
   498  	return hashcode.String(buf.String())
   499  }
   500  
   501  func buildFamilyAndRevisionFromARN(arn string) string {
   502  	return strings.Split(arn, "/")[1]
   503  }
   504  
   505  // Expects the following ARNs:
   506  // arn:aws:iam::0123456789:role/EcsService
   507  // arn:aws:ecs:us-west-2:0123456789:cluster/radek-cluster
   508  func getNameFromARN(arn string) string {
   509  	return strings.Split(arn, "/")[1]
   510  }
   511  
   512  func parseTaskDefinition(taskDefinition string) (string, string, error) {
   513  	matches := taskDefinitionRE.FindAllStringSubmatch(taskDefinition, 2)
   514  
   515  	if len(matches) == 0 || len(matches[0]) != 3 {
   516  		return "", "", fmt.Errorf(
   517  			"Invalid task definition format, family:rev or ARN expected (%#v)",
   518  			taskDefinition)
   519  	}
   520  
   521  	return matches[0][1], matches[0][2], nil
   522  }