github.com/andresvia/terraform@v0.6.15-0.20160412045437-d51c75946785/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": &schema.Schema{ 30 Type: schema.TypeString, 31 Required: true, 32 ForceNew: true, 33 }, 34 35 "cluster": &schema.Schema{ 36 Type: schema.TypeString, 37 Optional: true, 38 Computed: true, 39 ForceNew: true, 40 }, 41 42 "task_definition": &schema.Schema{ 43 Type: schema.TypeString, 44 Required: true, 45 }, 46 47 "desired_count": &schema.Schema{ 48 Type: schema.TypeInt, 49 Optional: true, 50 }, 51 52 "iam_role": &schema.Schema{ 53 Type: schema.TypeString, 54 ForceNew: true, 55 Optional: true, 56 }, 57 58 "deployment_maximum_percent": &schema.Schema{ 59 Type: schema.TypeInt, 60 Optional: true, 61 Default: 200, 62 }, 63 64 "deployment_minimum_healthy_percent": &schema.Schema{ 65 Type: schema.TypeInt, 66 Optional: true, 67 Default: 100, 68 }, 69 70 "load_balancer": &schema.Schema{ 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": &schema.Schema{ 78 Type: schema.TypeString, 79 Required: true, 80 ForceNew: true, 81 }, 82 83 "container_name": &schema.Schema{ 84 Type: schema.TypeString, 85 Required: true, 86 ForceNew: true, 87 }, 88 89 "container_port": &schema.Schema{ 90 Type: schema.TypeInt, 91 Required: true, 92 ForceNew: true, 93 }, 94 }, 95 }, 96 Set: resourceAwsEcsLoadBalancerHash, 97 }, 98 }, 99 } 100 } 101 102 func resourceAwsEcsServiceCreate(d *schema.ResourceData, meta interface{}) error { 103 conn := meta.(*AWSClient).ecsconn 104 105 input := ecs.CreateServiceInput{ 106 ServiceName: aws.String(d.Get("name").(string)), 107 TaskDefinition: aws.String(d.Get("task_definition").(string)), 108 DesiredCount: aws.Int64(int64(d.Get("desired_count").(int))), 109 ClientToken: aws.String(resource.UniqueId()), 110 DeploymentConfiguration: &ecs.DeploymentConfiguration{ 111 MaximumPercent: aws.Int64(int64(d.Get("deployment_maximum_percent").(int))), 112 MinimumHealthyPercent: aws.Int64(int64(d.Get("deployment_minimum_healthy_percent").(int))), 113 }, 114 } 115 116 if v, ok := d.GetOk("cluster"); ok { 117 input.Cluster = aws.String(v.(string)) 118 } 119 120 loadBalancers := expandEcsLoadBalancers(d.Get("load_balancer").(*schema.Set).List()) 121 if len(loadBalancers) > 0 { 122 log.Printf("[DEBUG] Adding ECS load balancers: %s", loadBalancers) 123 input.LoadBalancers = loadBalancers 124 } 125 if v, ok := d.GetOk("iam_role"); ok { 126 input.Role = aws.String(v.(string)) 127 } 128 129 log.Printf("[DEBUG] Creating ECS service: %s", input) 130 131 // Retry due to AWS IAM policy eventual consistency 132 // See https://github.com/hashicorp/terraform/issues/2869 133 var out *ecs.CreateServiceOutput 134 var err error 135 err = resource.Retry(2*time.Minute, func() *resource.RetryError { 136 out, err = conn.CreateService(&input) 137 138 if err != nil { 139 ec2err, ok := err.(awserr.Error) 140 if !ok { 141 return resource.NonRetryableError(err) 142 } 143 if ec2err.Code() == "InvalidParameterException" { 144 log.Printf("[DEBUG] Trying to create ECS service again: %q", 145 ec2err.Message()) 146 return resource.RetryableError(err) 147 } 148 149 return resource.NonRetryableError(err) 150 } 151 152 return nil 153 }) 154 if err != nil { 155 return err 156 } 157 158 service := *out.Service 159 160 log.Printf("[DEBUG] ECS service created: %s", *service.ServiceArn) 161 d.SetId(*service.ServiceArn) 162 163 return resourceAwsEcsServiceUpdate(d, meta) 164 } 165 166 func resourceAwsEcsServiceRead(d *schema.ResourceData, meta interface{}) error { 167 conn := meta.(*AWSClient).ecsconn 168 169 log.Printf("[DEBUG] Reading ECS service %s", d.Id()) 170 input := ecs.DescribeServicesInput{ 171 Services: []*string{aws.String(d.Id())}, 172 Cluster: aws.String(d.Get("cluster").(string)), 173 } 174 175 out, err := conn.DescribeServices(&input) 176 if err != nil { 177 return err 178 } 179 180 if len(out.Services) < 1 { 181 log.Printf("[DEBUG] Removing ECS service %s (%s) because it's gone", d.Get("name").(string), d.Id()) 182 d.SetId("") 183 return nil 184 } 185 186 service := out.Services[0] 187 188 // Status==INACTIVE means deleted service 189 if *service.Status == "INACTIVE" { 190 log.Printf("[DEBUG] Removing ECS service %q because it's INACTIVE", *service.ServiceArn) 191 d.SetId("") 192 return nil 193 } 194 195 log.Printf("[DEBUG] Received ECS service %s", service) 196 197 d.SetId(*service.ServiceArn) 198 d.Set("name", *service.ServiceName) 199 200 // Save task definition in the same format 201 if strings.HasPrefix(d.Get("task_definition").(string), "arn:aws:ecs:") { 202 d.Set("task_definition", *service.TaskDefinition) 203 } else { 204 taskDefinition := buildFamilyAndRevisionFromARN(*service.TaskDefinition) 205 d.Set("task_definition", taskDefinition) 206 } 207 208 d.Set("desired_count", *service.DesiredCount) 209 210 // Save cluster in the same format 211 if strings.HasPrefix(d.Get("cluster").(string), "arn:aws:ecs:") { 212 d.Set("cluster", *service.ClusterArn) 213 } else { 214 clusterARN := getNameFromARN(*service.ClusterArn) 215 d.Set("cluster", clusterARN) 216 } 217 218 // Save IAM role in the same format 219 if service.RoleArn != nil { 220 if strings.HasPrefix(d.Get("iam_role").(string), "arn:aws:iam:") { 221 d.Set("iam_role", *service.RoleArn) 222 } else { 223 roleARN := getNameFromARN(*service.RoleArn) 224 d.Set("iam_role", roleARN) 225 } 226 } 227 228 if service.DeploymentConfiguration != nil { 229 d.Set("deployment_maximum_percent", *service.DeploymentConfiguration.MaximumPercent) 230 d.Set("deployment_minimum_healthy_percent", *service.DeploymentConfiguration.MinimumHealthyPercent) 231 } 232 233 if service.LoadBalancers != nil { 234 d.Set("load_balancers", flattenEcsLoadBalancers(service.LoadBalancers)) 235 } 236 237 return nil 238 } 239 240 func resourceAwsEcsServiceUpdate(d *schema.ResourceData, meta interface{}) error { 241 conn := meta.(*AWSClient).ecsconn 242 243 log.Printf("[DEBUG] Updating ECS service %s", d.Id()) 244 input := ecs.UpdateServiceInput{ 245 Service: aws.String(d.Id()), 246 Cluster: aws.String(d.Get("cluster").(string)), 247 } 248 249 if d.HasChange("desired_count") { 250 _, n := d.GetChange("desired_count") 251 input.DesiredCount = aws.Int64(int64(n.(int))) 252 } 253 if d.HasChange("task_definition") { 254 _, n := d.GetChange("task_definition") 255 input.TaskDefinition = aws.String(n.(string)) 256 } 257 258 if d.HasChange("deployment_maximum_percent") || d.HasChange("deployment_minimum_healthy_percent") { 259 input.DeploymentConfiguration = &ecs.DeploymentConfiguration{ 260 MaximumPercent: aws.Int64(int64(d.Get("deployment_maximum_percent").(int))), 261 MinimumHealthyPercent: aws.Int64(int64(d.Get("deployment_minimum_healthy_percent").(int))), 262 } 263 } 264 265 out, err := conn.UpdateService(&input) 266 if err != nil { 267 return err 268 } 269 service := out.Service 270 log.Printf("[DEBUG] Updated ECS service %s", service) 271 272 return resourceAwsEcsServiceRead(d, meta) 273 } 274 275 func resourceAwsEcsServiceDelete(d *schema.ResourceData, meta interface{}) error { 276 conn := meta.(*AWSClient).ecsconn 277 278 // Check if it's not already gone 279 resp, err := conn.DescribeServices(&ecs.DescribeServicesInput{ 280 Services: []*string{aws.String(d.Id())}, 281 Cluster: aws.String(d.Get("cluster").(string)), 282 }) 283 if err != nil { 284 return err 285 } 286 287 if len(resp.Services) == 0 { 288 log.Printf("[DEBUG] ECS Service %q is already gone", d.Id()) 289 return nil 290 } 291 292 log.Printf("[DEBUG] ECS service %s is currently %s", d.Id(), *resp.Services[0].Status) 293 294 if *resp.Services[0].Status == "INACTIVE" { 295 return nil 296 } 297 298 // Drain the ECS service 299 if *resp.Services[0].Status != "DRAINING" { 300 log.Printf("[DEBUG] Draining ECS service %s", d.Id()) 301 _, err = conn.UpdateService(&ecs.UpdateServiceInput{ 302 Service: aws.String(d.Id()), 303 Cluster: aws.String(d.Get("cluster").(string)), 304 DesiredCount: aws.Int64(int64(0)), 305 }) 306 if err != nil { 307 return err 308 } 309 } 310 311 // Wait until the ECS service is drained 312 err = resource.Retry(5*time.Minute, func() *resource.RetryError { 313 input := ecs.DeleteServiceInput{ 314 Service: aws.String(d.Id()), 315 Cluster: aws.String(d.Get("cluster").(string)), 316 } 317 318 log.Printf("[DEBUG] Trying to delete ECS service %s", input) 319 _, err := conn.DeleteService(&input) 320 if err == nil { 321 return nil 322 } 323 324 ec2err, ok := err.(awserr.Error) 325 if !ok { 326 return resource.NonRetryableError(err) 327 } 328 if ec2err.Code() == "InvalidParameterException" { 329 // Prevent "The service cannot be stopped while deployments are active." 330 log.Printf("[DEBUG] Trying to delete ECS service again: %q", 331 ec2err.Message()) 332 return resource.RetryableError(err) 333 } 334 335 return resource.NonRetryableError(err) 336 337 }) 338 if err != nil { 339 return err 340 } 341 342 // Wait until it's deleted 343 wait := resource.StateChangeConf{ 344 Pending: []string{"DRAINING"}, 345 Target: []string{"INACTIVE"}, 346 Timeout: 5 * time.Minute, 347 MinTimeout: 1 * time.Second, 348 Refresh: func() (interface{}, string, error) { 349 log.Printf("[DEBUG] Checking if ECS service %s is INACTIVE", d.Id()) 350 resp, err := conn.DescribeServices(&ecs.DescribeServicesInput{ 351 Services: []*string{aws.String(d.Id())}, 352 Cluster: aws.String(d.Get("cluster").(string)), 353 }) 354 if err != nil { 355 return resp, "FAILED", err 356 } 357 358 log.Printf("[DEBUG] ECS service (%s) is currently %q", d.Id(), *resp.Services[0].Status) 359 return resp, *resp.Services[0].Status, nil 360 }, 361 } 362 363 _, err = wait.WaitForState() 364 if err != nil { 365 return err 366 } 367 368 log.Printf("[DEBUG] ECS service %s deleted.", d.Id()) 369 return nil 370 } 371 372 func resourceAwsEcsLoadBalancerHash(v interface{}) int { 373 var buf bytes.Buffer 374 m := v.(map[string]interface{}) 375 buf.WriteString(fmt.Sprintf("%s-", m["elb_name"].(string))) 376 buf.WriteString(fmt.Sprintf("%s-", m["container_name"].(string))) 377 buf.WriteString(fmt.Sprintf("%d-", m["container_port"].(int))) 378 379 return hashcode.String(buf.String()) 380 } 381 382 func buildFamilyAndRevisionFromARN(arn string) string { 383 return strings.Split(arn, "/")[1] 384 } 385 386 // Expects the following ARNs: 387 // arn:aws:iam::0123456789:role/EcsService 388 // arn:aws:ecs:us-west-2:0123456789:cluster/radek-cluster 389 func getNameFromARN(arn string) string { 390 return strings.Split(arn, "/")[1] 391 } 392 393 func parseTaskDefinition(taskDefinition string) (string, string, error) { 394 matches := taskDefinitionRE.FindAllStringSubmatch(taskDefinition, 2) 395 396 if len(matches) == 0 || len(matches[0]) != 3 { 397 return "", "", fmt.Errorf( 398 "Invalid task definition format, family:rev or ARN expected (%#v)", 399 taskDefinition) 400 } 401 402 return matches[0][1], matches[0][2], nil 403 }