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