github.com/koding/terraform@v0.6.4-0.20170608090606-5d7e0339779d/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  							DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
   122  								if strings.ToLower(old) == strings.ToLower(new) {
   123  									return true
   124  								}
   125  								return false
   126  							},
   127  						},
   128  					},
   129  				},
   130  			},
   131  
   132  			"placement_constraints": {
   133  				Type:     schema.TypeSet,
   134  				Optional: true,
   135  				ForceNew: true,
   136  				MaxItems: 10,
   137  				Elem: &schema.Resource{
   138  					Schema: map[string]*schema.Schema{
   139  						"type": {
   140  							Type:     schema.TypeString,
   141  							ForceNew: true,
   142  							Required: true,
   143  						},
   144  						"expression": {
   145  							Type:     schema.TypeString,
   146  							ForceNew: true,
   147  							Optional: true,
   148  						},
   149  					},
   150  				},
   151  			},
   152  		},
   153  	}
   154  }
   155  
   156  func resourceAwsEcsServiceCreate(d *schema.ResourceData, meta interface{}) error {
   157  	conn := meta.(*AWSClient).ecsconn
   158  
   159  	input := ecs.CreateServiceInput{
   160  		ServiceName:    aws.String(d.Get("name").(string)),
   161  		TaskDefinition: aws.String(d.Get("task_definition").(string)),
   162  		DesiredCount:   aws.Int64(int64(d.Get("desired_count").(int))),
   163  		ClientToken:    aws.String(resource.UniqueId()),
   164  		DeploymentConfiguration: &ecs.DeploymentConfiguration{
   165  			MaximumPercent:        aws.Int64(int64(d.Get("deployment_maximum_percent").(int))),
   166  			MinimumHealthyPercent: aws.Int64(int64(d.Get("deployment_minimum_healthy_percent").(int))),
   167  		},
   168  	}
   169  
   170  	if v, ok := d.GetOk("cluster"); ok {
   171  		input.Cluster = aws.String(v.(string))
   172  	}
   173  
   174  	loadBalancers := expandEcsLoadBalancers(d.Get("load_balancer").(*schema.Set).List())
   175  	if len(loadBalancers) > 0 {
   176  		log.Printf("[DEBUG] Adding ECS load balancers: %s", loadBalancers)
   177  		input.LoadBalancers = loadBalancers
   178  	}
   179  	if v, ok := d.GetOk("iam_role"); ok {
   180  		input.Role = aws.String(v.(string))
   181  	}
   182  
   183  	strategies := d.Get("placement_strategy").(*schema.Set).List()
   184  	if len(strategies) > 0 {
   185  		var ps []*ecs.PlacementStrategy
   186  		for _, raw := range strategies {
   187  			p := raw.(map[string]interface{})
   188  			t := p["type"].(string)
   189  			f := p["field"].(string)
   190  			if err := validateAwsEcsPlacementStrategy(t, f); err != nil {
   191  				return err
   192  			}
   193  			ps = append(ps, &ecs.PlacementStrategy{
   194  				Type:  aws.String(p["type"].(string)),
   195  				Field: aws.String(p["field"].(string)),
   196  			})
   197  		}
   198  		input.PlacementStrategy = ps
   199  	}
   200  
   201  	constraints := d.Get("placement_constraints").(*schema.Set).List()
   202  	if len(constraints) > 0 {
   203  		var pc []*ecs.PlacementConstraint
   204  		for _, raw := range constraints {
   205  			p := raw.(map[string]interface{})
   206  			t := p["type"].(string)
   207  			e := p["expression"].(string)
   208  			if err := validateAwsEcsPlacementConstraint(t, e); err != nil {
   209  				return err
   210  			}
   211  			constraint := &ecs.PlacementConstraint{
   212  				Type: aws.String(t),
   213  			}
   214  			if e != "" {
   215  				constraint.Expression = aws.String(e)
   216  			}
   217  
   218  			pc = append(pc, constraint)
   219  		}
   220  		input.PlacementConstraints = pc
   221  	}
   222  
   223  	log.Printf("[DEBUG] Creating ECS service: %s", input)
   224  
   225  	// Retry due to AWS IAM & ECS eventual consistency
   226  	var out *ecs.CreateServiceOutput
   227  	var err error
   228  	err = resource.Retry(2*time.Minute, func() *resource.RetryError {
   229  		out, err = conn.CreateService(&input)
   230  
   231  		if err != nil {
   232  			awsErr, ok := err.(awserr.Error)
   233  			if !ok {
   234  				return resource.NonRetryableError(err)
   235  			}
   236  			if awsErr.Code() == "InvalidParameterException" {
   237  				log.Printf("[DEBUG] Trying to create ECS service again: %q",
   238  					awsErr.Message())
   239  				return resource.RetryableError(err)
   240  			}
   241  			if awsErr.Code() == "ClusterNotFoundException" {
   242  				log.Printf("[DEBUG] Trying to create ECS service again: %q",
   243  					awsErr.Message())
   244  				return resource.RetryableError(err)
   245  			}
   246  
   247  			return resource.NonRetryableError(err)
   248  		}
   249  
   250  		return nil
   251  	})
   252  	if err != nil {
   253  		return fmt.Errorf("%s %q", err, d.Get("name").(string))
   254  	}
   255  
   256  	service := *out.Service
   257  
   258  	log.Printf("[DEBUG] ECS service created: %s", *service.ServiceArn)
   259  	d.SetId(*service.ServiceArn)
   260  
   261  	return resourceAwsEcsServiceUpdate(d, meta)
   262  }
   263  
   264  func resourceAwsEcsServiceRead(d *schema.ResourceData, meta interface{}) error {
   265  	conn := meta.(*AWSClient).ecsconn
   266  
   267  	log.Printf("[DEBUG] Reading ECS service %s", d.Id())
   268  	input := ecs.DescribeServicesInput{
   269  		Services: []*string{aws.String(d.Id())},
   270  		Cluster:  aws.String(d.Get("cluster").(string)),
   271  	}
   272  
   273  	out, err := conn.DescribeServices(&input)
   274  	if err != nil {
   275  		return err
   276  	}
   277  
   278  	if len(out.Services) < 1 {
   279  		log.Printf("[DEBUG] Removing ECS service %s (%s) because it's gone", d.Get("name").(string), d.Id())
   280  		d.SetId("")
   281  		return nil
   282  	}
   283  
   284  	service := out.Services[0]
   285  
   286  	// Status==INACTIVE means deleted service
   287  	if *service.Status == "INACTIVE" {
   288  		log.Printf("[DEBUG] Removing ECS service %q because it's INACTIVE", *service.ServiceArn)
   289  		d.SetId("")
   290  		return nil
   291  	}
   292  
   293  	log.Printf("[DEBUG] Received ECS service %s", service)
   294  
   295  	d.SetId(*service.ServiceArn)
   296  	d.Set("name", service.ServiceName)
   297  
   298  	// Save task definition in the same format
   299  	if strings.HasPrefix(d.Get("task_definition").(string), "arn:"+meta.(*AWSClient).partition+":ecs:") {
   300  		d.Set("task_definition", service.TaskDefinition)
   301  	} else {
   302  		taskDefinition := buildFamilyAndRevisionFromARN(*service.TaskDefinition)
   303  		d.Set("task_definition", taskDefinition)
   304  	}
   305  
   306  	d.Set("desired_count", service.DesiredCount)
   307  
   308  	// Save cluster in the same format
   309  	if strings.HasPrefix(d.Get("cluster").(string), "arn:"+meta.(*AWSClient).partition+":ecs:") {
   310  		d.Set("cluster", service.ClusterArn)
   311  	} else {
   312  		clusterARN := getNameFromARN(*service.ClusterArn)
   313  		d.Set("cluster", clusterARN)
   314  	}
   315  
   316  	// Save IAM role in the same format
   317  	if service.RoleArn != nil {
   318  		if strings.HasPrefix(d.Get("iam_role").(string), "arn:"+meta.(*AWSClient).partition+":iam:") {
   319  			d.Set("iam_role", service.RoleArn)
   320  		} else {
   321  			roleARN := getNameFromARN(*service.RoleArn)
   322  			d.Set("iam_role", roleARN)
   323  		}
   324  	}
   325  
   326  	if service.DeploymentConfiguration != nil {
   327  		d.Set("deployment_maximum_percent", service.DeploymentConfiguration.MaximumPercent)
   328  		d.Set("deployment_minimum_healthy_percent", service.DeploymentConfiguration.MinimumHealthyPercent)
   329  	}
   330  
   331  	if service.LoadBalancers != nil {
   332  		d.Set("load_balancers", flattenEcsLoadBalancers(service.LoadBalancers))
   333  	}
   334  
   335  	if err := d.Set("placement_strategy", flattenPlacementStrategy(service.PlacementStrategy)); err != nil {
   336  		log.Printf("[ERR] Error setting placement_strategy for (%s): %s", d.Id(), err)
   337  	}
   338  	if err := d.Set("placement_constraints", flattenServicePlacementConstraints(service.PlacementConstraints)); err != nil {
   339  		log.Printf("[ERR] Error setting placement_constraints for (%s): %s", d.Id(), err)
   340  	}
   341  
   342  	return nil
   343  }
   344  
   345  func flattenServicePlacementConstraints(pcs []*ecs.PlacementConstraint) []map[string]interface{} {
   346  	if len(pcs) == 0 {
   347  		return nil
   348  	}
   349  	results := make([]map[string]interface{}, 0)
   350  	for _, pc := range pcs {
   351  		c := make(map[string]interface{})
   352  		c["type"] = *pc.Type
   353  		if pc.Expression != nil {
   354  			c["expression"] = *pc.Expression
   355  		}
   356  
   357  		results = append(results, c)
   358  	}
   359  	return results
   360  }
   361  
   362  func flattenPlacementStrategy(pss []*ecs.PlacementStrategy) []map[string]interface{} {
   363  	if len(pss) == 0 {
   364  		return nil
   365  	}
   366  	results := make([]map[string]interface{}, 0)
   367  	for _, ps := range pss {
   368  		c := make(map[string]interface{})
   369  		c["type"] = *ps.Type
   370  		c["field"] = *ps.Field
   371  
   372  		// for some fields the API requires lowercase for creation but will return uppercase on query
   373  		if *ps.Field == "MEMORY" || *ps.Field == "CPU" {
   374  			c["field"] = strings.ToLower(*ps.Field)
   375  		}
   376  
   377  		results = append(results, c)
   378  	}
   379  	return results
   380  }
   381  
   382  func resourceAwsEcsServiceUpdate(d *schema.ResourceData, meta interface{}) error {
   383  	conn := meta.(*AWSClient).ecsconn
   384  
   385  	log.Printf("[DEBUG] Updating ECS service %s", d.Id())
   386  	input := ecs.UpdateServiceInput{
   387  		Service: aws.String(d.Id()),
   388  		Cluster: aws.String(d.Get("cluster").(string)),
   389  	}
   390  
   391  	if d.HasChange("desired_count") {
   392  		_, n := d.GetChange("desired_count")
   393  		input.DesiredCount = aws.Int64(int64(n.(int)))
   394  	}
   395  	if d.HasChange("task_definition") {
   396  		_, n := d.GetChange("task_definition")
   397  		input.TaskDefinition = aws.String(n.(string))
   398  	}
   399  
   400  	if d.HasChange("deployment_maximum_percent") || d.HasChange("deployment_minimum_healthy_percent") {
   401  		input.DeploymentConfiguration = &ecs.DeploymentConfiguration{
   402  			MaximumPercent:        aws.Int64(int64(d.Get("deployment_maximum_percent").(int))),
   403  			MinimumHealthyPercent: aws.Int64(int64(d.Get("deployment_minimum_healthy_percent").(int))),
   404  		}
   405  	}
   406  
   407  	// Retry due to IAM & ECS eventual consistency
   408  	err := resource.Retry(2*time.Minute, func() *resource.RetryError {
   409  		out, err := conn.UpdateService(&input)
   410  		if err != nil {
   411  			awsErr, ok := err.(awserr.Error)
   412  			if ok && awsErr.Code() == "InvalidParameterException" {
   413  				log.Printf("[DEBUG] Trying to update ECS service again: %#v", err)
   414  				return resource.RetryableError(err)
   415  			}
   416  			if ok && awsErr.Code() == "ServiceNotFoundException" {
   417  				log.Printf("[DEBUG] Trying to update ECS service again: %#v", err)
   418  				return resource.RetryableError(err)
   419  			}
   420  
   421  			return resource.NonRetryableError(err)
   422  		}
   423  
   424  		log.Printf("[DEBUG] Updated ECS service %s", out.Service)
   425  		return nil
   426  	})
   427  	if err != nil {
   428  		return err
   429  	}
   430  
   431  	return resourceAwsEcsServiceRead(d, meta)
   432  }
   433  
   434  func resourceAwsEcsServiceDelete(d *schema.ResourceData, meta interface{}) error {
   435  	conn := meta.(*AWSClient).ecsconn
   436  
   437  	// Check if it's not already gone
   438  	resp, err := conn.DescribeServices(&ecs.DescribeServicesInput{
   439  		Services: []*string{aws.String(d.Id())},
   440  		Cluster:  aws.String(d.Get("cluster").(string)),
   441  	})
   442  	if err != nil {
   443  		return err
   444  	}
   445  
   446  	if len(resp.Services) == 0 {
   447  		log.Printf("[DEBUG] ECS Service %q is already gone", d.Id())
   448  		return nil
   449  	}
   450  
   451  	log.Printf("[DEBUG] ECS service %s is currently %s", d.Id(), *resp.Services[0].Status)
   452  
   453  	if *resp.Services[0].Status == "INACTIVE" {
   454  		return nil
   455  	}
   456  
   457  	// Drain the ECS service
   458  	if *resp.Services[0].Status != "DRAINING" {
   459  		log.Printf("[DEBUG] Draining ECS service %s", d.Id())
   460  		_, err = conn.UpdateService(&ecs.UpdateServiceInput{
   461  			Service:      aws.String(d.Id()),
   462  			Cluster:      aws.String(d.Get("cluster").(string)),
   463  			DesiredCount: aws.Int64(int64(0)),
   464  		})
   465  		if err != nil {
   466  			return err
   467  		}
   468  	}
   469  
   470  	// Wait until the ECS service is drained
   471  	err = resource.Retry(5*time.Minute, func() *resource.RetryError {
   472  		input := ecs.DeleteServiceInput{
   473  			Service: aws.String(d.Id()),
   474  			Cluster: aws.String(d.Get("cluster").(string)),
   475  		}
   476  
   477  		log.Printf("[DEBUG] Trying to delete ECS service %s", input)
   478  		_, err := conn.DeleteService(&input)
   479  		if err == nil {
   480  			return nil
   481  		}
   482  
   483  		ec2err, ok := err.(awserr.Error)
   484  		if !ok {
   485  			return resource.NonRetryableError(err)
   486  		}
   487  		if ec2err.Code() == "InvalidParameterException" {
   488  			// Prevent "The service cannot be stopped while deployments are active."
   489  			log.Printf("[DEBUG] Trying to delete ECS service again: %q",
   490  				ec2err.Message())
   491  			return resource.RetryableError(err)
   492  		}
   493  
   494  		return resource.NonRetryableError(err)
   495  
   496  	})
   497  	if err != nil {
   498  		return err
   499  	}
   500  
   501  	// Wait until it's deleted
   502  	wait := resource.StateChangeConf{
   503  		Pending:    []string{"ACTIVE", "DRAINING"},
   504  		Target:     []string{"INACTIVE"},
   505  		Timeout:    10 * time.Minute,
   506  		MinTimeout: 1 * time.Second,
   507  		Refresh: func() (interface{}, string, error) {
   508  			log.Printf("[DEBUG] Checking if ECS service %s is INACTIVE", d.Id())
   509  			resp, err := conn.DescribeServices(&ecs.DescribeServicesInput{
   510  				Services: []*string{aws.String(d.Id())},
   511  				Cluster:  aws.String(d.Get("cluster").(string)),
   512  			})
   513  			if err != nil {
   514  				return resp, "FAILED", err
   515  			}
   516  
   517  			log.Printf("[DEBUG] ECS service (%s) is currently %q", d.Id(), *resp.Services[0].Status)
   518  			return resp, *resp.Services[0].Status, nil
   519  		},
   520  	}
   521  
   522  	_, err = wait.WaitForState()
   523  	if err != nil {
   524  		return err
   525  	}
   526  
   527  	log.Printf("[DEBUG] ECS service %s deleted.", d.Id())
   528  	return nil
   529  }
   530  
   531  func resourceAwsEcsLoadBalancerHash(v interface{}) int {
   532  	var buf bytes.Buffer
   533  	m := v.(map[string]interface{})
   534  
   535  	buf.WriteString(fmt.Sprintf("%s-", m["elb_name"].(string)))
   536  	buf.WriteString(fmt.Sprintf("%s-", m["container_name"].(string)))
   537  	buf.WriteString(fmt.Sprintf("%d-", m["container_port"].(int)))
   538  
   539  	if s := m["target_group_arn"].(string); s != "" {
   540  		buf.WriteString(fmt.Sprintf("%s-", s))
   541  	}
   542  
   543  	return hashcode.String(buf.String())
   544  }
   545  
   546  func buildFamilyAndRevisionFromARN(arn string) string {
   547  	return strings.Split(arn, "/")[1]
   548  }
   549  
   550  // Expects the following ARNs:
   551  // arn:aws:iam::0123456789:role/EcsService
   552  // arn:aws:ecs:us-west-2:0123456789:cluster/radek-cluster
   553  func getNameFromARN(arn string) string {
   554  	return strings.Split(arn, "/")[1]
   555  }
   556  
   557  func parseTaskDefinition(taskDefinition string) (string, string, error) {
   558  	matches := taskDefinitionRE.FindAllStringSubmatch(taskDefinition, 2)
   559  
   560  	if len(matches) == 0 || len(matches[0]) != 3 {
   561  		return "", "", fmt.Errorf(
   562  			"Invalid task definition format, family:rev or ARN expected (%#v)",
   563  			taskDefinition)
   564  	}
   565  
   566  	return matches[0][1], matches[0][2], nil
   567  }