github.com/econnell/terraform@v0.5.4-0.20150722160631-78eb236786a4/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/awsutil" 13 "github.com/aws/aws-sdk-go/service/ecs" 14 "github.com/aws/aws-sdk-go/service/iam" 15 "github.com/hashicorp/terraform/helper/hashcode" 16 "github.com/hashicorp/terraform/helper/resource" 17 "github.com/hashicorp/terraform/helper/schema" 18 ) 19 20 var taskDefinitionRE = regexp.MustCompile("^([a-zA-Z0-9_-]+):([0-9]+)$") 21 22 func resourceAwsEcsService() *schema.Resource { 23 return &schema.Resource{ 24 Create: resourceAwsEcsServiceCreate, 25 Read: resourceAwsEcsServiceRead, 26 Update: resourceAwsEcsServiceUpdate, 27 Delete: resourceAwsEcsServiceDelete, 28 29 Schema: map[string]*schema.Schema{ 30 "name": &schema.Schema{ 31 Type: schema.TypeString, 32 Required: true, 33 ForceNew: true, 34 }, 35 36 "cluster": &schema.Schema{ 37 Type: schema.TypeString, 38 Optional: true, 39 Computed: true, 40 ForceNew: true, 41 }, 42 43 "task_definition": &schema.Schema{ 44 Type: schema.TypeString, 45 Required: true, 46 }, 47 48 "desired_count": &schema.Schema{ 49 Type: schema.TypeInt, 50 Optional: true, 51 }, 52 53 "iam_role": &schema.Schema{ 54 Type: schema.TypeString, 55 Optional: true, 56 }, 57 58 "load_balancer": &schema.Schema{ 59 Type: schema.TypeSet, 60 Optional: true, 61 Elem: &schema.Resource{ 62 Schema: map[string]*schema.Schema{ 63 "elb_name": &schema.Schema{ 64 Type: schema.TypeString, 65 Required: true, 66 }, 67 68 "container_name": &schema.Schema{ 69 Type: schema.TypeString, 70 Required: true, 71 }, 72 73 "container_port": &schema.Schema{ 74 Type: schema.TypeInt, 75 Required: true, 76 }, 77 }, 78 }, 79 Set: resourceAwsEcsLoadBalancerHash, 80 }, 81 }, 82 } 83 } 84 85 func resourceAwsEcsServiceCreate(d *schema.ResourceData, meta interface{}) error { 86 conn := meta.(*AWSClient).ecsconn 87 88 input := ecs.CreateServiceInput{ 89 ServiceName: aws.String(d.Get("name").(string)), 90 TaskDefinition: aws.String(d.Get("task_definition").(string)), 91 DesiredCount: aws.Long(int64(d.Get("desired_count").(int))), 92 ClientToken: aws.String(resource.UniqueId()), 93 } 94 95 if v, ok := d.GetOk("cluster"); ok { 96 input.Cluster = aws.String(v.(string)) 97 } 98 99 loadBalancers := expandEcsLoadBalancers(d.Get("load_balancer").(*schema.Set).List()) 100 if len(loadBalancers) > 0 { 101 log.Printf("[DEBUG] Adding ECS load balancers: %s", awsutil.StringValue(loadBalancers)) 102 input.LoadBalancers = loadBalancers 103 } 104 if v, ok := d.GetOk("iam_role"); ok { 105 input.Role = aws.String(v.(string)) 106 } 107 108 log.Printf("[DEBUG] Creating ECS service: %s", awsutil.StringValue(input)) 109 out, err := conn.CreateService(&input) 110 if err != nil { 111 return err 112 } 113 114 service := *out.Service 115 116 log.Printf("[DEBUG] ECS service created: %s", *service.ServiceARN) 117 d.SetId(*service.ServiceARN) 118 d.Set("cluster", *service.ClusterARN) 119 120 return resourceAwsEcsServiceUpdate(d, meta) 121 } 122 123 func resourceAwsEcsServiceRead(d *schema.ResourceData, meta interface{}) error { 124 conn := meta.(*AWSClient).ecsconn 125 126 log.Printf("[DEBUG] Reading ECS service %s", d.Id()) 127 input := ecs.DescribeServicesInput{ 128 Services: []*string{aws.String(d.Id())}, 129 Cluster: aws.String(d.Get("cluster").(string)), 130 } 131 132 out, err := conn.DescribeServices(&input) 133 if err != nil { 134 return err 135 } 136 137 if len(out.Services) < 1 { 138 return nil 139 } 140 141 service := out.Services[0] 142 log.Printf("[DEBUG] Received ECS service %s", awsutil.StringValue(service)) 143 144 d.SetId(*service.ServiceARN) 145 d.Set("name", *service.ServiceName) 146 147 // Save task definition in the same format 148 if strings.HasPrefix(d.Get("task_definition").(string), "arn:aws:ecs:") { 149 d.Set("task_definition", *service.TaskDefinition) 150 } else { 151 taskDefinition := buildFamilyAndRevisionFromARN(*service.TaskDefinition) 152 d.Set("task_definition", taskDefinition) 153 } 154 155 d.Set("desired_count", *service.DesiredCount) 156 d.Set("cluster", *service.ClusterARN) 157 158 if service.RoleARN != nil { 159 d.Set("iam_role", *service.RoleARN) 160 } 161 162 if service.LoadBalancers != nil { 163 d.Set("load_balancers", flattenEcsLoadBalancers(service.LoadBalancers)) 164 } 165 166 return nil 167 } 168 169 func resourceAwsEcsServiceUpdate(d *schema.ResourceData, meta interface{}) error { 170 conn := meta.(*AWSClient).ecsconn 171 172 log.Printf("[DEBUG] Updating ECS service %s", d.Id()) 173 input := ecs.UpdateServiceInput{ 174 Service: aws.String(d.Id()), 175 Cluster: aws.String(d.Get("cluster").(string)), 176 } 177 178 if d.HasChange("desired_count") { 179 _, n := d.GetChange("desired_count") 180 input.DesiredCount = aws.Long(int64(n.(int))) 181 } 182 if d.HasChange("task_definition") { 183 _, n := d.GetChange("task_definition") 184 input.TaskDefinition = aws.String(n.(string)) 185 } 186 187 out, err := conn.UpdateService(&input) 188 if err != nil { 189 return err 190 } 191 service := out.Service 192 log.Printf("[DEBUG] Updated ECS service %s", awsutil.StringValue(service)) 193 194 return resourceAwsEcsServiceRead(d, meta) 195 } 196 197 func resourceAwsEcsServiceDelete(d *schema.ResourceData, meta interface{}) error { 198 conn := meta.(*AWSClient).ecsconn 199 200 // Check if it's not already gone 201 resp, err := conn.DescribeServices(&ecs.DescribeServicesInput{ 202 Services: []*string{aws.String(d.Id())}, 203 Cluster: aws.String(d.Get("cluster").(string)), 204 }) 205 if err != nil { 206 return err 207 } 208 log.Printf("[DEBUG] ECS service %s is currently %s", d.Id(), *resp.Services[0].Status) 209 210 if *resp.Services[0].Status == "INACTIVE" { 211 return nil 212 } 213 214 // Drain the ECS service 215 if *resp.Services[0].Status != "DRAINING" { 216 log.Printf("[DEBUG] Draining ECS service %s", d.Id()) 217 _, err = conn.UpdateService(&ecs.UpdateServiceInput{ 218 Service: aws.String(d.Id()), 219 Cluster: aws.String(d.Get("cluster").(string)), 220 DesiredCount: aws.Long(int64(0)), 221 }) 222 if err != nil { 223 return err 224 } 225 } 226 227 input := ecs.DeleteServiceInput{ 228 Service: aws.String(d.Id()), 229 Cluster: aws.String(d.Get("cluster").(string)), 230 } 231 232 log.Printf("[DEBUG] Deleting ECS service %s", awsutil.StringValue(input)) 233 out, err := conn.DeleteService(&input) 234 if err != nil { 235 return err 236 } 237 238 // Wait until it's deleted 239 wait := resource.StateChangeConf{ 240 Pending: []string{"DRAINING"}, 241 Target: "INACTIVE", 242 Timeout: 5 * time.Minute, 243 MinTimeout: 1 * time.Second, 244 Refresh: func() (interface{}, string, error) { 245 log.Printf("[DEBUG] Checking if ECS service %s is INACTIVE", d.Id()) 246 resp, err := conn.DescribeServices(&ecs.DescribeServicesInput{ 247 Services: []*string{aws.String(d.Id())}, 248 Cluster: aws.String(d.Get("cluster").(string)), 249 }) 250 if err != nil { 251 return resp, "FAILED", err 252 } 253 254 return resp, *resp.Services[0].Status, nil 255 }, 256 } 257 258 _, err = wait.WaitForState() 259 if err != nil { 260 return err 261 } 262 263 log.Printf("[DEBUG] ECS service %s deleted.", *out.Service.ServiceARN) 264 return nil 265 } 266 267 func resourceAwsEcsLoadBalancerHash(v interface{}) int { 268 var buf bytes.Buffer 269 m := v.(map[string]interface{}) 270 buf.WriteString(fmt.Sprintf("%s-", m["elb_name"].(string))) 271 buf.WriteString(fmt.Sprintf("%s-", m["container_name"].(string))) 272 buf.WriteString(fmt.Sprintf("%d-", m["container_port"].(int))) 273 274 return hashcode.String(buf.String()) 275 } 276 277 func buildFamilyAndRevisionFromARN(arn string) string { 278 return strings.Split(arn, "/")[1] 279 } 280 281 func buildTaskDefinitionARN(taskDefinition string, meta interface{}) (string, error) { 282 // If it's already an ARN, just return it 283 if strings.HasPrefix(taskDefinition, "arn:aws:ecs:") { 284 return taskDefinition, nil 285 } 286 287 // Parse out family & revision 288 family, revision, err := parseTaskDefinition(taskDefinition) 289 if err != nil { 290 return "", err 291 } 292 293 iamconn := meta.(*AWSClient).iamconn 294 region := meta.(*AWSClient).region 295 296 // An zero value GetUserInput{} defers to the currently logged in user 297 resp, err := iamconn.GetUser(&iam.GetUserInput{}) 298 if err != nil { 299 return "", fmt.Errorf("GetUser ERROR: %#v", err) 300 } 301 302 // arn:aws:iam::0123456789:user/username 303 userARN := *resp.User.ARN 304 accountID := strings.Split(userARN, ":")[4] 305 306 // arn:aws:ecs:us-west-2:01234567890:task-definition/mongodb:3 307 arn := fmt.Sprintf("arn:aws:ecs:%s:%s:task-definition/%s:%s", 308 region, accountID, family, revision) 309 log.Printf("[DEBUG] Built task definition ARN: %s", arn) 310 return arn, nil 311 } 312 313 func parseTaskDefinition(taskDefinition string) (string, string, error) { 314 matches := taskDefinitionRE.FindAllStringSubmatch(taskDefinition, 2) 315 316 if len(matches) == 0 || len(matches[0]) != 3 { 317 return "", "", fmt.Errorf( 318 "Invalid task definition format, family:rev or ARN expected (%#v)", 319 taskDefinition) 320 } 321 322 return matches[0][1], matches[0][2], nil 323 }