github.com/koding/terraform@v0.6.4-0.20170608090606-5d7e0339779d/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 & ECS eventual consistency 226 var out *ecs.CreateServiceOutput 227 var err error 228 err = resource.Retry(2*time.Minute, func() *resource.RetryError { 229 out, err = conn.CreateService(&input) 230 231 if err != nil { 232 awsErr, ok := err.(awserr.Error) 233 if !ok { 234 return resource.NonRetryableError(err) 235 } 236 if awsErr.Code() == "InvalidParameterException" { 237 log.Printf("[DEBUG] Trying to create ECS service again: %q", 238 awsErr.Message()) 239 return resource.RetryableError(err) 240 } 241 if awsErr.Code() == "ClusterNotFoundException" { 242 log.Printf("[DEBUG] Trying to create ECS service again: %q", 243 awsErr.Message()) 244 return resource.RetryableError(err) 245 } 246 247 return resource.NonRetryableError(err) 248 } 249 250 return nil 251 }) 252 if err != nil { 253 return fmt.Errorf("%s %q", err, d.Get("name").(string)) 254 } 255 256 service := *out.Service 257 258 log.Printf("[DEBUG] ECS service created: %s", *service.ServiceArn) 259 d.SetId(*service.ServiceArn) 260 261 return resourceAwsEcsServiceUpdate(d, meta) 262 } 263 264 func resourceAwsEcsServiceRead(d *schema.ResourceData, meta interface{}) error { 265 conn := meta.(*AWSClient).ecsconn 266 267 log.Printf("[DEBUG] Reading ECS service %s", d.Id()) 268 input := ecs.DescribeServicesInput{ 269 Services: []*string{aws.String(d.Id())}, 270 Cluster: aws.String(d.Get("cluster").(string)), 271 } 272 273 out, err := conn.DescribeServices(&input) 274 if err != nil { 275 return err 276 } 277 278 if len(out.Services) < 1 { 279 log.Printf("[DEBUG] Removing ECS service %s (%s) because it's gone", d.Get("name").(string), d.Id()) 280 d.SetId("") 281 return nil 282 } 283 284 service := out.Services[0] 285 286 // Status==INACTIVE means deleted service 287 if *service.Status == "INACTIVE" { 288 log.Printf("[DEBUG] Removing ECS service %q because it's INACTIVE", *service.ServiceArn) 289 d.SetId("") 290 return nil 291 } 292 293 log.Printf("[DEBUG] Received ECS service %s", service) 294 295 d.SetId(*service.ServiceArn) 296 d.Set("name", service.ServiceName) 297 298 // Save task definition in the same format 299 if strings.HasPrefix(d.Get("task_definition").(string), "arn:"+meta.(*AWSClient).partition+":ecs:") { 300 d.Set("task_definition", service.TaskDefinition) 301 } else { 302 taskDefinition := buildFamilyAndRevisionFromARN(*service.TaskDefinition) 303 d.Set("task_definition", taskDefinition) 304 } 305 306 d.Set("desired_count", service.DesiredCount) 307 308 // Save cluster in the same format 309 if strings.HasPrefix(d.Get("cluster").(string), "arn:"+meta.(*AWSClient).partition+":ecs:") { 310 d.Set("cluster", service.ClusterArn) 311 } else { 312 clusterARN := getNameFromARN(*service.ClusterArn) 313 d.Set("cluster", clusterARN) 314 } 315 316 // Save IAM role in the same format 317 if service.RoleArn != nil { 318 if strings.HasPrefix(d.Get("iam_role").(string), "arn:"+meta.(*AWSClient).partition+":iam:") { 319 d.Set("iam_role", service.RoleArn) 320 } else { 321 roleARN := getNameFromARN(*service.RoleArn) 322 d.Set("iam_role", roleARN) 323 } 324 } 325 326 if service.DeploymentConfiguration != nil { 327 d.Set("deployment_maximum_percent", service.DeploymentConfiguration.MaximumPercent) 328 d.Set("deployment_minimum_healthy_percent", service.DeploymentConfiguration.MinimumHealthyPercent) 329 } 330 331 if service.LoadBalancers != nil { 332 d.Set("load_balancers", flattenEcsLoadBalancers(service.LoadBalancers)) 333 } 334 335 if err := d.Set("placement_strategy", flattenPlacementStrategy(service.PlacementStrategy)); err != nil { 336 log.Printf("[ERR] Error setting placement_strategy for (%s): %s", d.Id(), err) 337 } 338 if err := d.Set("placement_constraints", flattenServicePlacementConstraints(service.PlacementConstraints)); err != nil { 339 log.Printf("[ERR] Error setting placement_constraints for (%s): %s", d.Id(), err) 340 } 341 342 return nil 343 } 344 345 func flattenServicePlacementConstraints(pcs []*ecs.PlacementConstraint) []map[string]interface{} { 346 if len(pcs) == 0 { 347 return nil 348 } 349 results := make([]map[string]interface{}, 0) 350 for _, pc := range pcs { 351 c := make(map[string]interface{}) 352 c["type"] = *pc.Type 353 if pc.Expression != nil { 354 c["expression"] = *pc.Expression 355 } 356 357 results = append(results, c) 358 } 359 return results 360 } 361 362 func flattenPlacementStrategy(pss []*ecs.PlacementStrategy) []map[string]interface{} { 363 if len(pss) == 0 { 364 return nil 365 } 366 results := make([]map[string]interface{}, 0) 367 for _, ps := range pss { 368 c := make(map[string]interface{}) 369 c["type"] = *ps.Type 370 c["field"] = *ps.Field 371 372 // for some fields the API requires lowercase for creation but will return uppercase on query 373 if *ps.Field == "MEMORY" || *ps.Field == "CPU" { 374 c["field"] = strings.ToLower(*ps.Field) 375 } 376 377 results = append(results, c) 378 } 379 return results 380 } 381 382 func resourceAwsEcsServiceUpdate(d *schema.ResourceData, meta interface{}) error { 383 conn := meta.(*AWSClient).ecsconn 384 385 log.Printf("[DEBUG] Updating ECS service %s", d.Id()) 386 input := ecs.UpdateServiceInput{ 387 Service: aws.String(d.Id()), 388 Cluster: aws.String(d.Get("cluster").(string)), 389 } 390 391 if d.HasChange("desired_count") { 392 _, n := d.GetChange("desired_count") 393 input.DesiredCount = aws.Int64(int64(n.(int))) 394 } 395 if d.HasChange("task_definition") { 396 _, n := d.GetChange("task_definition") 397 input.TaskDefinition = aws.String(n.(string)) 398 } 399 400 if d.HasChange("deployment_maximum_percent") || d.HasChange("deployment_minimum_healthy_percent") { 401 input.DeploymentConfiguration = &ecs.DeploymentConfiguration{ 402 MaximumPercent: aws.Int64(int64(d.Get("deployment_maximum_percent").(int))), 403 MinimumHealthyPercent: aws.Int64(int64(d.Get("deployment_minimum_healthy_percent").(int))), 404 } 405 } 406 407 // Retry due to IAM & ECS eventual consistency 408 err := resource.Retry(2*time.Minute, func() *resource.RetryError { 409 out, err := conn.UpdateService(&input) 410 if err != nil { 411 awsErr, ok := err.(awserr.Error) 412 if ok && awsErr.Code() == "InvalidParameterException" { 413 log.Printf("[DEBUG] Trying to update ECS service again: %#v", err) 414 return resource.RetryableError(err) 415 } 416 if ok && awsErr.Code() == "ServiceNotFoundException" { 417 log.Printf("[DEBUG] Trying to update ECS service again: %#v", err) 418 return resource.RetryableError(err) 419 } 420 421 return resource.NonRetryableError(err) 422 } 423 424 log.Printf("[DEBUG] Updated ECS service %s", out.Service) 425 return nil 426 }) 427 if err != nil { 428 return err 429 } 430 431 return resourceAwsEcsServiceRead(d, meta) 432 } 433 434 func resourceAwsEcsServiceDelete(d *schema.ResourceData, meta interface{}) error { 435 conn := meta.(*AWSClient).ecsconn 436 437 // Check if it's not already gone 438 resp, err := conn.DescribeServices(&ecs.DescribeServicesInput{ 439 Services: []*string{aws.String(d.Id())}, 440 Cluster: aws.String(d.Get("cluster").(string)), 441 }) 442 if err != nil { 443 return err 444 } 445 446 if len(resp.Services) == 0 { 447 log.Printf("[DEBUG] ECS Service %q is already gone", d.Id()) 448 return nil 449 } 450 451 log.Printf("[DEBUG] ECS service %s is currently %s", d.Id(), *resp.Services[0].Status) 452 453 if *resp.Services[0].Status == "INACTIVE" { 454 return nil 455 } 456 457 // Drain the ECS service 458 if *resp.Services[0].Status != "DRAINING" { 459 log.Printf("[DEBUG] Draining ECS service %s", d.Id()) 460 _, err = conn.UpdateService(&ecs.UpdateServiceInput{ 461 Service: aws.String(d.Id()), 462 Cluster: aws.String(d.Get("cluster").(string)), 463 DesiredCount: aws.Int64(int64(0)), 464 }) 465 if err != nil { 466 return err 467 } 468 } 469 470 // Wait until the ECS service is drained 471 err = resource.Retry(5*time.Minute, func() *resource.RetryError { 472 input := ecs.DeleteServiceInput{ 473 Service: aws.String(d.Id()), 474 Cluster: aws.String(d.Get("cluster").(string)), 475 } 476 477 log.Printf("[DEBUG] Trying to delete ECS service %s", input) 478 _, err := conn.DeleteService(&input) 479 if err == nil { 480 return nil 481 } 482 483 ec2err, ok := err.(awserr.Error) 484 if !ok { 485 return resource.NonRetryableError(err) 486 } 487 if ec2err.Code() == "InvalidParameterException" { 488 // Prevent "The service cannot be stopped while deployments are active." 489 log.Printf("[DEBUG] Trying to delete ECS service again: %q", 490 ec2err.Message()) 491 return resource.RetryableError(err) 492 } 493 494 return resource.NonRetryableError(err) 495 496 }) 497 if err != nil { 498 return err 499 } 500 501 // Wait until it's deleted 502 wait := resource.StateChangeConf{ 503 Pending: []string{"ACTIVE", "DRAINING"}, 504 Target: []string{"INACTIVE"}, 505 Timeout: 10 * time.Minute, 506 MinTimeout: 1 * time.Second, 507 Refresh: func() (interface{}, string, error) { 508 log.Printf("[DEBUG] Checking if ECS service %s is INACTIVE", d.Id()) 509 resp, err := conn.DescribeServices(&ecs.DescribeServicesInput{ 510 Services: []*string{aws.String(d.Id())}, 511 Cluster: aws.String(d.Get("cluster").(string)), 512 }) 513 if err != nil { 514 return resp, "FAILED", err 515 } 516 517 log.Printf("[DEBUG] ECS service (%s) is currently %q", d.Id(), *resp.Services[0].Status) 518 return resp, *resp.Services[0].Status, nil 519 }, 520 } 521 522 _, err = wait.WaitForState() 523 if err != nil { 524 return err 525 } 526 527 log.Printf("[DEBUG] ECS service %s deleted.", d.Id()) 528 return nil 529 } 530 531 func resourceAwsEcsLoadBalancerHash(v interface{}) int { 532 var buf bytes.Buffer 533 m := v.(map[string]interface{}) 534 535 buf.WriteString(fmt.Sprintf("%s-", m["elb_name"].(string))) 536 buf.WriteString(fmt.Sprintf("%s-", m["container_name"].(string))) 537 buf.WriteString(fmt.Sprintf("%d-", m["container_port"].(int))) 538 539 if s := m["target_group_arn"].(string); s != "" { 540 buf.WriteString(fmt.Sprintf("%s-", s)) 541 } 542 543 return hashcode.String(buf.String()) 544 } 545 546 func buildFamilyAndRevisionFromARN(arn string) string { 547 return strings.Split(arn, "/")[1] 548 } 549 550 // Expects the following ARNs: 551 // arn:aws:iam::0123456789:role/EcsService 552 // arn:aws:ecs:us-west-2:0123456789:cluster/radek-cluster 553 func getNameFromARN(arn string) string { 554 return strings.Split(arn, "/")[1] 555 } 556 557 func parseTaskDefinition(taskDefinition string) (string, string, error) { 558 matches := taskDefinitionRE.FindAllStringSubmatch(taskDefinition, 2) 559 560 if len(matches) == 0 || len(matches[0]) != 3 { 561 return "", "", fmt.Errorf( 562 "Invalid task definition format, family:rev or ARN expected (%#v)", 563 taskDefinition) 564 } 565 566 return matches[0][1], matches[0][2], nil 567 }