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