github.com/leeprovoost/terraform@v0.6.10-0.20160119085442-96f3f76118e7/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: "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 }