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