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