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