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