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