github.com/bradfeehan/terraform@v0.7.0-rc3.0.20170529055808-34b45c5ad841/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 awsErr, ok := err.(awserr.Error) 234 if !ok { 235 return resource.NonRetryableError(err) 236 } 237 if awsErr.Code() == "InvalidParameterException" { 238 log.Printf("[DEBUG] Trying to create ECS service again: %q", 239 awsErr.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 // Retry due to AWS IAM policy eventual consistency 404 // See https://github.com/hashicorp/terraform/issues/4375 405 err := resource.Retry(2*time.Minute, func() *resource.RetryError { 406 out, err := conn.UpdateService(&input) 407 if err != nil { 408 awsErr, ok := err.(awserr.Error) 409 if ok && awsErr.Code() == "InvalidParameterException" { 410 log.Printf("[DEBUG] Trying to update ECS service again: %#v", err) 411 return resource.RetryableError(err) 412 } 413 414 return resource.NonRetryableError(err) 415 } 416 417 log.Printf("[DEBUG] Updated ECS service %s", out.Service) 418 return nil 419 }) 420 if err != nil { 421 return err 422 } 423 424 return resourceAwsEcsServiceRead(d, meta) 425 } 426 427 func resourceAwsEcsServiceDelete(d *schema.ResourceData, meta interface{}) error { 428 conn := meta.(*AWSClient).ecsconn 429 430 // Check if it's not already gone 431 resp, err := conn.DescribeServices(&ecs.DescribeServicesInput{ 432 Services: []*string{aws.String(d.Id())}, 433 Cluster: aws.String(d.Get("cluster").(string)), 434 }) 435 if err != nil { 436 return err 437 } 438 439 if len(resp.Services) == 0 { 440 log.Printf("[DEBUG] ECS Service %q is already gone", d.Id()) 441 return nil 442 } 443 444 log.Printf("[DEBUG] ECS service %s is currently %s", d.Id(), *resp.Services[0].Status) 445 446 if *resp.Services[0].Status == "INACTIVE" { 447 return nil 448 } 449 450 // Drain the ECS service 451 if *resp.Services[0].Status != "DRAINING" { 452 log.Printf("[DEBUG] Draining ECS service %s", d.Id()) 453 _, err = conn.UpdateService(&ecs.UpdateServiceInput{ 454 Service: aws.String(d.Id()), 455 Cluster: aws.String(d.Get("cluster").(string)), 456 DesiredCount: aws.Int64(int64(0)), 457 }) 458 if err != nil { 459 return err 460 } 461 } 462 463 // Wait until the ECS service is drained 464 err = resource.Retry(5*time.Minute, func() *resource.RetryError { 465 input := ecs.DeleteServiceInput{ 466 Service: aws.String(d.Id()), 467 Cluster: aws.String(d.Get("cluster").(string)), 468 } 469 470 log.Printf("[DEBUG] Trying to delete ECS service %s", input) 471 _, err := conn.DeleteService(&input) 472 if err == nil { 473 return nil 474 } 475 476 ec2err, ok := err.(awserr.Error) 477 if !ok { 478 return resource.NonRetryableError(err) 479 } 480 if ec2err.Code() == "InvalidParameterException" { 481 // Prevent "The service cannot be stopped while deployments are active." 482 log.Printf("[DEBUG] Trying to delete ECS service again: %q", 483 ec2err.Message()) 484 return resource.RetryableError(err) 485 } 486 487 return resource.NonRetryableError(err) 488 489 }) 490 if err != nil { 491 return err 492 } 493 494 // Wait until it's deleted 495 wait := resource.StateChangeConf{ 496 Pending: []string{"ACTIVE", "DRAINING"}, 497 Target: []string{"INACTIVE"}, 498 Timeout: 10 * time.Minute, 499 MinTimeout: 1 * time.Second, 500 Refresh: func() (interface{}, string, error) { 501 log.Printf("[DEBUG] Checking if ECS service %s is INACTIVE", d.Id()) 502 resp, err := conn.DescribeServices(&ecs.DescribeServicesInput{ 503 Services: []*string{aws.String(d.Id())}, 504 Cluster: aws.String(d.Get("cluster").(string)), 505 }) 506 if err != nil { 507 return resp, "FAILED", err 508 } 509 510 log.Printf("[DEBUG] ECS service (%s) is currently %q", d.Id(), *resp.Services[0].Status) 511 return resp, *resp.Services[0].Status, nil 512 }, 513 } 514 515 _, err = wait.WaitForState() 516 if err != nil { 517 return err 518 } 519 520 log.Printf("[DEBUG] ECS service %s deleted.", d.Id()) 521 return nil 522 } 523 524 func resourceAwsEcsLoadBalancerHash(v interface{}) int { 525 var buf bytes.Buffer 526 m := v.(map[string]interface{}) 527 528 buf.WriteString(fmt.Sprintf("%s-", m["elb_name"].(string))) 529 buf.WriteString(fmt.Sprintf("%s-", m["container_name"].(string))) 530 buf.WriteString(fmt.Sprintf("%d-", m["container_port"].(int))) 531 532 if s := m["target_group_arn"].(string); s != "" { 533 buf.WriteString(fmt.Sprintf("%s-", s)) 534 } 535 536 return hashcode.String(buf.String()) 537 } 538 539 func buildFamilyAndRevisionFromARN(arn string) string { 540 return strings.Split(arn, "/")[1] 541 } 542 543 // Expects the following ARNs: 544 // arn:aws:iam::0123456789:role/EcsService 545 // arn:aws:ecs:us-west-2:0123456789:cluster/radek-cluster 546 func getNameFromARN(arn string) string { 547 return strings.Split(arn, "/")[1] 548 } 549 550 func parseTaskDefinition(taskDefinition string) (string, string, error) { 551 matches := taskDefinitionRE.FindAllStringSubmatch(taskDefinition, 2) 552 553 if len(matches) == 0 || len(matches[0]) != 3 { 554 return "", "", fmt.Errorf( 555 "Invalid task definition format, family:rev or ARN expected (%#v)", 556 taskDefinition) 557 } 558 559 return matches[0][1], matches[0][2], nil 560 }