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