github.com/minamijoyo/terraform@v0.7.8-0.20161029001309-18b3736ba44b/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": &schema.Schema{
    30  				Type:     schema.TypeString,
    31  				Required: true,
    32  				ForceNew: true,
    33  			},
    34  
    35  			"cluster": &schema.Schema{
    36  				Type:     schema.TypeString,
    37  				Optional: true,
    38  				Computed: true,
    39  				ForceNew: true,
    40  			},
    41  
    42  			"task_definition": &schema.Schema{
    43  				Type:     schema.TypeString,
    44  				Required: true,
    45  			},
    46  
    47  			"desired_count": &schema.Schema{
    48  				Type:     schema.TypeInt,
    49  				Optional: true,
    50  			},
    51  
    52  			"iam_role": &schema.Schema{
    53  				Type:     schema.TypeString,
    54  				ForceNew: true,
    55  				Optional: true,
    56  			},
    57  
    58  			"deployment_maximum_percent": &schema.Schema{
    59  				Type:     schema.TypeInt,
    60  				Optional: true,
    61  				Default:  200,
    62  			},
    63  
    64  			"deployment_minimum_healthy_percent": &schema.Schema{
    65  				Type:     schema.TypeInt,
    66  				Optional: true,
    67  				Default:  100,
    68  			},
    69  
    70  			"load_balancer": &schema.Schema{
    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": &schema.Schema{
    78  							Type:     schema.TypeString,
    79  							Optional: true,
    80  							ForceNew: true,
    81  						},
    82  
    83  						"target_group_arn": &schema.Schema{
    84  							Type:     schema.TypeString,
    85  							Optional: true,
    86  							ForceNew: true,
    87  						},
    88  
    89  						"container_name": &schema.Schema{
    90  							Type:     schema.TypeString,
    91  							Required: true,
    92  							ForceNew: true,
    93  						},
    94  
    95  						"container_port": &schema.Schema{
    96  							Type:     schema.TypeInt,
    97  							Required: true,
    98  							ForceNew: true,
    99  						},
   100  					},
   101  				},
   102  				Set: resourceAwsEcsLoadBalancerHash,
   103  			},
   104  		},
   105  	}
   106  }
   107  
   108  func resourceAwsEcsServiceCreate(d *schema.ResourceData, meta interface{}) error {
   109  	conn := meta.(*AWSClient).ecsconn
   110  
   111  	input := ecs.CreateServiceInput{
   112  		ServiceName:    aws.String(d.Get("name").(string)),
   113  		TaskDefinition: aws.String(d.Get("task_definition").(string)),
   114  		DesiredCount:   aws.Int64(int64(d.Get("desired_count").(int))),
   115  		ClientToken:    aws.String(resource.UniqueId()),
   116  		DeploymentConfiguration: &ecs.DeploymentConfiguration{
   117  			MaximumPercent:        aws.Int64(int64(d.Get("deployment_maximum_percent").(int))),
   118  			MinimumHealthyPercent: aws.Int64(int64(d.Get("deployment_minimum_healthy_percent").(int))),
   119  		},
   120  	}
   121  
   122  	if v, ok := d.GetOk("cluster"); ok {
   123  		input.Cluster = aws.String(v.(string))
   124  	}
   125  
   126  	loadBalancers := expandEcsLoadBalancers(d.Get("load_balancer").(*schema.Set).List())
   127  	if len(loadBalancers) > 0 {
   128  		log.Printf("[DEBUG] Adding ECS load balancers: %s", loadBalancers)
   129  		input.LoadBalancers = loadBalancers
   130  	}
   131  	if v, ok := d.GetOk("iam_role"); ok {
   132  		input.Role = aws.String(v.(string))
   133  	}
   134  
   135  	log.Printf("[DEBUG] Creating ECS service: %s", input)
   136  
   137  	// Retry due to AWS IAM policy eventual consistency
   138  	// See https://github.com/hashicorp/terraform/issues/2869
   139  	var out *ecs.CreateServiceOutput
   140  	var err error
   141  	err = resource.Retry(2*time.Minute, func() *resource.RetryError {
   142  		out, err = conn.CreateService(&input)
   143  
   144  		if err != nil {
   145  			ec2err, ok := err.(awserr.Error)
   146  			if !ok {
   147  				return resource.NonRetryableError(err)
   148  			}
   149  			if ec2err.Code() == "InvalidParameterException" {
   150  				log.Printf("[DEBUG] Trying to create ECS service again: %q",
   151  					ec2err.Message())
   152  				return resource.RetryableError(err)
   153  			}
   154  
   155  			return resource.NonRetryableError(err)
   156  		}
   157  
   158  		return nil
   159  	})
   160  	if err != nil {
   161  		return err
   162  	}
   163  
   164  	service := *out.Service
   165  
   166  	log.Printf("[DEBUG] ECS service created: %s", *service.ServiceArn)
   167  	d.SetId(*service.ServiceArn)
   168  
   169  	return resourceAwsEcsServiceUpdate(d, meta)
   170  }
   171  
   172  func resourceAwsEcsServiceRead(d *schema.ResourceData, meta interface{}) error {
   173  	conn := meta.(*AWSClient).ecsconn
   174  
   175  	log.Printf("[DEBUG] Reading ECS service %s", d.Id())
   176  	input := ecs.DescribeServicesInput{
   177  		Services: []*string{aws.String(d.Id())},
   178  		Cluster:  aws.String(d.Get("cluster").(string)),
   179  	}
   180  
   181  	out, err := conn.DescribeServices(&input)
   182  	if err != nil {
   183  		return err
   184  	}
   185  
   186  	if len(out.Services) < 1 {
   187  		log.Printf("[DEBUG] Removing ECS service %s (%s) because it's gone", d.Get("name").(string), d.Id())
   188  		d.SetId("")
   189  		return nil
   190  	}
   191  
   192  	service := out.Services[0]
   193  
   194  	// Status==INACTIVE means deleted service
   195  	if *service.Status == "INACTIVE" {
   196  		log.Printf("[DEBUG] Removing ECS service %q because it's INACTIVE", *service.ServiceArn)
   197  		d.SetId("")
   198  		return nil
   199  	}
   200  
   201  	log.Printf("[DEBUG] Received ECS service %s", service)
   202  
   203  	d.SetId(*service.ServiceArn)
   204  	d.Set("name", service.ServiceName)
   205  
   206  	// Save task definition in the same format
   207  	if strings.HasPrefix(d.Get("task_definition").(string), "arn:aws:ecs:") {
   208  		d.Set("task_definition", service.TaskDefinition)
   209  	} else {
   210  		taskDefinition := buildFamilyAndRevisionFromARN(*service.TaskDefinition)
   211  		d.Set("task_definition", taskDefinition)
   212  	}
   213  
   214  	d.Set("desired_count", service.DesiredCount)
   215  
   216  	// Save cluster in the same format
   217  	if strings.HasPrefix(d.Get("cluster").(string), "arn:aws:ecs:") {
   218  		d.Set("cluster", service.ClusterArn)
   219  	} else {
   220  		clusterARN := getNameFromARN(*service.ClusterArn)
   221  		d.Set("cluster", clusterARN)
   222  	}
   223  
   224  	// Save IAM role in the same format
   225  	if service.RoleArn != nil {
   226  		if strings.HasPrefix(d.Get("iam_role").(string), "arn:aws:iam:") {
   227  			d.Set("iam_role", service.RoleArn)
   228  		} else {
   229  			roleARN := getNameFromARN(*service.RoleArn)
   230  			d.Set("iam_role", roleARN)
   231  		}
   232  	}
   233  
   234  	if service.DeploymentConfiguration != nil {
   235  		d.Set("deployment_maximum_percent", service.DeploymentConfiguration.MaximumPercent)
   236  		d.Set("deployment_minimum_healthy_percent", service.DeploymentConfiguration.MinimumHealthyPercent)
   237  	}
   238  
   239  	if service.LoadBalancers != nil {
   240  		d.Set("load_balancers", flattenEcsLoadBalancers(service.LoadBalancers))
   241  	}
   242  
   243  	return nil
   244  }
   245  
   246  func resourceAwsEcsServiceUpdate(d *schema.ResourceData, meta interface{}) error {
   247  	conn := meta.(*AWSClient).ecsconn
   248  
   249  	log.Printf("[DEBUG] Updating ECS service %s", d.Id())
   250  	input := ecs.UpdateServiceInput{
   251  		Service: aws.String(d.Id()),
   252  		Cluster: aws.String(d.Get("cluster").(string)),
   253  	}
   254  
   255  	if d.HasChange("desired_count") {
   256  		_, n := d.GetChange("desired_count")
   257  		input.DesiredCount = aws.Int64(int64(n.(int)))
   258  	}
   259  	if d.HasChange("task_definition") {
   260  		_, n := d.GetChange("task_definition")
   261  		input.TaskDefinition = aws.String(n.(string))
   262  	}
   263  
   264  	if d.HasChange("deployment_maximum_percent") || d.HasChange("deployment_minimum_healthy_percent") {
   265  		input.DeploymentConfiguration = &ecs.DeploymentConfiguration{
   266  			MaximumPercent:        aws.Int64(int64(d.Get("deployment_maximum_percent").(int))),
   267  			MinimumHealthyPercent: aws.Int64(int64(d.Get("deployment_minimum_healthy_percent").(int))),
   268  		}
   269  	}
   270  
   271  	out, err := conn.UpdateService(&input)
   272  	if err != nil {
   273  		return err
   274  	}
   275  	service := out.Service
   276  	log.Printf("[DEBUG] Updated ECS service %s", service)
   277  
   278  	return resourceAwsEcsServiceRead(d, meta)
   279  }
   280  
   281  func resourceAwsEcsServiceDelete(d *schema.ResourceData, meta interface{}) error {
   282  	conn := meta.(*AWSClient).ecsconn
   283  
   284  	// Check if it's not already gone
   285  	resp, err := conn.DescribeServices(&ecs.DescribeServicesInput{
   286  		Services: []*string{aws.String(d.Id())},
   287  		Cluster:  aws.String(d.Get("cluster").(string)),
   288  	})
   289  	if err != nil {
   290  		return err
   291  	}
   292  
   293  	if len(resp.Services) == 0 {
   294  		log.Printf("[DEBUG] ECS Service %q is already gone", d.Id())
   295  		return nil
   296  	}
   297  
   298  	log.Printf("[DEBUG] ECS service %s is currently %s", d.Id(), *resp.Services[0].Status)
   299  
   300  	if *resp.Services[0].Status == "INACTIVE" {
   301  		return nil
   302  	}
   303  
   304  	// Drain the ECS service
   305  	if *resp.Services[0].Status != "DRAINING" {
   306  		log.Printf("[DEBUG] Draining ECS service %s", d.Id())
   307  		_, err = conn.UpdateService(&ecs.UpdateServiceInput{
   308  			Service:      aws.String(d.Id()),
   309  			Cluster:      aws.String(d.Get("cluster").(string)),
   310  			DesiredCount: aws.Int64(int64(0)),
   311  		})
   312  		if err != nil {
   313  			return err
   314  		}
   315  	}
   316  
   317  	// Wait until the ECS service is drained
   318  	err = resource.Retry(5*time.Minute, func() *resource.RetryError {
   319  		input := ecs.DeleteServiceInput{
   320  			Service: aws.String(d.Id()),
   321  			Cluster: aws.String(d.Get("cluster").(string)),
   322  		}
   323  
   324  		log.Printf("[DEBUG] Trying to delete ECS service %s", input)
   325  		_, err := conn.DeleteService(&input)
   326  		if err == nil {
   327  			return nil
   328  		}
   329  
   330  		ec2err, ok := err.(awserr.Error)
   331  		if !ok {
   332  			return resource.NonRetryableError(err)
   333  		}
   334  		if ec2err.Code() == "InvalidParameterException" {
   335  			// Prevent "The service cannot be stopped while deployments are active."
   336  			log.Printf("[DEBUG] Trying to delete ECS service again: %q",
   337  				ec2err.Message())
   338  			return resource.RetryableError(err)
   339  		}
   340  
   341  		return resource.NonRetryableError(err)
   342  
   343  	})
   344  	if err != nil {
   345  		return err
   346  	}
   347  
   348  	// Wait until it's deleted
   349  	wait := resource.StateChangeConf{
   350  		Pending:    []string{"DRAINING"},
   351  		Target:     []string{"INACTIVE"},
   352  		Timeout:    10 * time.Minute,
   353  		MinTimeout: 1 * time.Second,
   354  		Refresh: func() (interface{}, string, error) {
   355  			log.Printf("[DEBUG] Checking if ECS service %s is INACTIVE", d.Id())
   356  			resp, err := conn.DescribeServices(&ecs.DescribeServicesInput{
   357  				Services: []*string{aws.String(d.Id())},
   358  				Cluster:  aws.String(d.Get("cluster").(string)),
   359  			})
   360  			if err != nil {
   361  				return resp, "FAILED", err
   362  			}
   363  
   364  			log.Printf("[DEBUG] ECS service (%s) is currently %q", d.Id(), *resp.Services[0].Status)
   365  			return resp, *resp.Services[0].Status, nil
   366  		},
   367  	}
   368  
   369  	_, err = wait.WaitForState()
   370  	if err != nil {
   371  		return err
   372  	}
   373  
   374  	log.Printf("[DEBUG] ECS service %s deleted.", d.Id())
   375  	return nil
   376  }
   377  
   378  func resourceAwsEcsLoadBalancerHash(v interface{}) int {
   379  	var buf bytes.Buffer
   380  	m := v.(map[string]interface{})
   381  	buf.WriteString(fmt.Sprintf("%s-", m["elb_name"].(string)))
   382  	buf.WriteString(fmt.Sprintf("%s-", m["container_name"].(string)))
   383  	buf.WriteString(fmt.Sprintf("%d-", m["container_port"].(int)))
   384  
   385  	return hashcode.String(buf.String())
   386  }
   387  
   388  func buildFamilyAndRevisionFromARN(arn string) string {
   389  	return strings.Split(arn, "/")[1]
   390  }
   391  
   392  // Expects the following ARNs:
   393  // arn:aws:iam::0123456789:role/EcsService
   394  // arn:aws:ecs:us-west-2:0123456789:cluster/radek-cluster
   395  func getNameFromARN(arn string) string {
   396  	return strings.Split(arn, "/")[1]
   397  }
   398  
   399  func parseTaskDefinition(taskDefinition string) (string, string, error) {
   400  	matches := taskDefinitionRE.FindAllStringSubmatch(taskDefinition, 2)
   401  
   402  	if len(matches) == 0 || len(matches[0]) != 3 {
   403  		return "", "", fmt.Errorf(
   404  			"Invalid task definition format, family:rev or ARN expected (%#v)",
   405  			taskDefinition)
   406  	}
   407  
   408  	return matches[0][1], matches[0][2], nil
   409  }