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