github.com/hazelops/ize@v1.1.12-0.20230915191306-97d7c0e48f11/internal/manager/ecs/native.go (about) 1 package ecs 2 3 import ( 4 "fmt" 5 "io" 6 "strconv" 7 "strings" 8 "time" 9 10 "github.com/aws/aws-sdk-go/service/ecs/ecsiface" 11 12 "github.com/aws/aws-sdk-go/aws" 13 "github.com/aws/aws-sdk-go/service/cloudwatchlogs" 14 "github.com/aws/aws-sdk-go/service/ecs" 15 "github.com/aws/aws-sdk-go/service/elbv2" 16 "github.com/pterm/pterm" 17 ) 18 19 func (e *Manager) deployLocal(w io.Writer) error { 20 pterm.SetDefaultOutput(w) 21 22 svc := e.Project.AWSClient.ECSClient 23 24 name := fmt.Sprintf("%s-%s", e.Project.Env, e.App.Name) 25 26 dso, err := svc.DescribeServices(&ecs.DescribeServicesInput{ 27 Cluster: &e.App.Cluster, 28 Services: []*string{&name}, 29 }) 30 if err != nil { 31 return err 32 } 33 34 if len(dso.Services) == 0 { 35 return fmt.Errorf("app %s not found not found in %s cluster", name, e.App.Cluster) 36 } 37 38 dtdo, err := svc.DescribeTaskDefinition(&ecs.DescribeTaskDefinitionInput{ 39 TaskDefinition: dso.Services[0].TaskDefinition, 40 }) 41 if err != nil { 42 return err 43 } 44 45 definitions, err := svc.ListTaskDefinitions(&ecs.ListTaskDefinitionsInput{ 46 FamilyPrefix: &name, 47 Sort: aws.String(ecs.SortOrderDesc), 48 }) 49 if err != nil { 50 return err 51 } 52 53 var oldTaskDef ecs.TaskDefinition 54 var newTaskDef ecs.TaskDefinition 55 56 if len(definitions.TaskDefinitionArns) != 0 && *dtdo.TaskDefinition.TaskDefinitionArn != *definitions.TaskDefinitionArns[0] { 57 definition, err := svc.DescribeTaskDefinition(&ecs.DescribeTaskDefinitionInput{ 58 TaskDefinition: definitions.TaskDefinitionArns[0], 59 }) 60 if err != nil { 61 return err 62 } 63 64 oldTaskDef = *definition.TaskDefinition 65 } else { 66 oldTaskDef = *dtdo.TaskDefinition 67 } 68 69 pterm.Printfln("Deploying based on task definition: %s:%d", *oldTaskDef.Family, *oldTaskDef.Revision) 70 71 var image string 72 73 for i := 0; i < len(oldTaskDef.ContainerDefinitions); i++ { 74 container := oldTaskDef.ContainerDefinitions[i] 75 76 // We are changing the image/tag only for the app-specific container (not sidecars) 77 if *container.Name == e.App.Name { 78 if len(e.Project.Tag) != 0 && len(e.App.Image) == 0 { 79 name := strings.Split(*container.Image, ":")[0] 80 image = fmt.Sprintf("%s:%s", name, e.Project.Tag) 81 } else { 82 image = e.App.Image 83 } 84 85 pterm.Printfln(`Changed image of container "%s" to : "%s" (was: "%s")`, *container.Name, image, *container.Image) 86 container.Image = &image 87 } 88 } 89 90 pterm.Println("Creating new task definition revision") 91 92 rtdo, err := svc.RegisterTaskDefinition(&ecs.RegisterTaskDefinitionInput{ 93 ContainerDefinitions: oldTaskDef.ContainerDefinitions, 94 Family: oldTaskDef.Family, 95 Volumes: oldTaskDef.Volumes, 96 TaskRoleArn: oldTaskDef.TaskRoleArn, 97 ExecutionRoleArn: oldTaskDef.ExecutionRoleArn, 98 RuntimePlatform: oldTaskDef.RuntimePlatform, 99 RequiresCompatibilities: oldTaskDef.RequiresCompatibilities, 100 NetworkMode: oldTaskDef.NetworkMode, 101 Cpu: oldTaskDef.Cpu, 102 Memory: oldTaskDef.Memory, 103 }) 104 if err != nil { 105 return err 106 } 107 108 newTaskDef = *rtdo.TaskDefinition 109 110 pterm.Printfln("Successfully created revision: %s:%d", *rtdo.TaskDefinition.Family, *rtdo.TaskDefinition.Revision) 111 112 if err = e.updateTaskDefinition(&newTaskDef, &oldTaskDef, name, "Deploying new task definition"); err != nil { 113 err := e.getLastContainerLogs(fmt.Sprintf("%s-%s", e.Project.Env, e.App.Name)) 114 if err != nil { 115 pterm.Println("Failed to get logs:", err) 116 } 117 118 sr, err := getStoppedReason(e.App.Cluster, name, svc) 119 if err != nil { 120 return err 121 } 122 123 pterm.Printfln("Container %s couldn't start: %s", name, sr) 124 125 pterm.Printfln("Rolling back to old task definition: %s:%d", *oldTaskDef.Family, *oldTaskDef.Revision) 126 e.App.Timeout = 600 127 if err = e.updateTaskDefinition(&oldTaskDef, &newTaskDef, name, "Deploying previous task definition"); err != nil { 128 return fmt.Errorf("unable to rollback to old task definition: %w", err) 129 } 130 131 pterm.Println("Rollback successful") 132 133 return fmt.Errorf("deployment failed, but service has been rolled back to previous task definition: %s", *oldTaskDef.Family) 134 } 135 136 return nil 137 } 138 139 func (e *Manager) redeployLocal(w io.Writer) error { 140 pterm.SetDefaultOutput(w) 141 142 svc := e.Project.AWSClient.ECSClient 143 144 name := fmt.Sprintf("%s-%s", e.Project.Env, e.App.Name) 145 146 dso, err := getService(name, e.App.Cluster, svc) 147 if err != nil { 148 return err 149 } 150 151 var td *ecs.TaskDefinition 152 153 switch e.App.TaskDefinitionRevision { 154 case "latest": 155 tds, err := svc.ListTaskDefinitions(&ecs.ListTaskDefinitionsInput{ 156 FamilyPrefix: aws.String(name), 157 Sort: aws.String("DESC"), 158 }) 159 if err != nil { 160 return fmt.Errorf("unable to list task definitions: %w", err) 161 } 162 163 dtdo, err := svc.DescribeTaskDefinition(&ecs.DescribeTaskDefinitionInput{ 164 TaskDefinition: tds.TaskDefinitionArns[0], 165 }) 166 if err != nil { 167 return fmt.Errorf("unable to describe task definition: %w", err) 168 } 169 170 td = dtdo.TaskDefinition 171 case "current": 172 dtdo, err := svc.DescribeTaskDefinition(&ecs.DescribeTaskDefinitionInput{ 173 TaskDefinition: dso.Services[0].TaskDefinition, 174 }) 175 if err != nil { 176 return fmt.Errorf("unable to describe task definition: %w", err) 177 } 178 179 td = dtdo.TaskDefinition 180 default: 181 r, err := strconv.Atoi(e.App.TaskDefinitionRevision) 182 if err == nil && r > 0 { 183 arn := fmt.Sprintf("%s:%s", name, e.App.TaskDefinitionRevision) 184 185 dtdo, err := svc.DescribeTaskDefinition(&ecs.DescribeTaskDefinitionInput{ 186 TaskDefinition: &arn, 187 }) 188 if err != nil { 189 return fmt.Errorf("unable to describe task definition: %w", err) 190 } 191 192 td = dtdo.TaskDefinition 193 } else { 194 return fmt.Errorf("invalid task definition revision: %s", e.App.TaskDefinitionRevision) 195 } 196 } 197 198 if err = e.updateTaskDefinition(td, nil, name, "Redeploying new task definition"); err != nil { 199 pterm.Println(err) 200 err := e.getLastContainerLogs(fmt.Sprintf("%s-%s", e.Project.Env, e.App.Name)) 201 if err != nil { 202 pterm.Println("Failed to get logs:", err) 203 } 204 return fmt.Errorf("redeployment failed") 205 } 206 207 return nil 208 } 209 210 func getService(name string, cluster string, svc ecsiface.ECSAPI) (*ecs.DescribeServicesOutput, error) { 211 dso, err := svc.DescribeServices(&ecs.DescribeServicesInput{ 212 Cluster: &cluster, 213 Services: []*string{&name}, 214 }) 215 if err != nil { 216 return nil, err 217 } 218 219 if len(dso.Services) == 0 { 220 return nil, fmt.Errorf("app %s not found", name) 221 } 222 return dso, nil 223 } 224 225 func (e *Manager) updateTaskDefinition(newTD *ecs.TaskDefinition, oldTD *ecs.TaskDefinition, serviceName string, title string) error { 226 pterm.Println("Updating service") 227 228 svc := e.Project.AWSClient.ECSClient 229 230 uso, err := svc.UpdateService(&ecs.UpdateServiceInput{ 231 Service: aws.String(serviceName), 232 Cluster: aws.String(e.App.Cluster), 233 TaskDefinition: aws.String(*newTD.TaskDefinitionArn), 234 ForceNewDeployment: aws.Bool(true), 235 }) 236 if err != nil { 237 return fmt.Errorf("unable to update service: %w", err) 238 } 239 240 var dtgo *elbv2.DescribeTargetGroupsOutput 241 if e.App.Unsafe { 242 elb := e.Project.AWSClient.ELBV2Client 243 dtgo, err = elb.DescribeTargetGroups(&elbv2.DescribeTargetGroupsInput{ 244 TargetGroupArns: aws.StringSlice([]string{*uso.Service.LoadBalancers[0].TargetGroupArn}), 245 }) 246 if err != nil { 247 return fmt.Errorf("can't describe target groups: %w", err) 248 } 249 250 _, err = e.Project.AWSClient.ELBV2Client.ModifyTargetGroup(&elbv2.ModifyTargetGroupInput{ 251 HealthyThresholdCount: aws.Int64(2), 252 HealthCheckIntervalSeconds: aws.Int64(5), 253 HealthCheckTimeoutSeconds: aws.Int64(2), 254 UnhealthyThresholdCount: aws.Int64(2), 255 TargetGroupArn: uso.Service.LoadBalancers[0].TargetGroupArn, 256 }) 257 if err != nil { 258 return fmt.Errorf("unable to modify target group: %w", err) 259 } 260 } 261 262 pterm.Printfln("Successfully changed task definition to: %s:%d", *newTD.Family, *newTD.Revision) 263 pterm.Println(title) 264 265 waitingTimeout := time.Now().Add(time.Duration(e.App.Timeout) * time.Second) 266 waiting := true 267 268 for waiting && time.Now().Before(waitingTimeout) { 269 d, err := isDeployed(svc, serviceName, e.App.Cluster) 270 if err != nil { 271 return err 272 } 273 274 waiting = !d 275 276 if waiting { 277 time.Sleep(time.Second * 5) 278 } 279 } 280 281 if waiting && time.Now().After(waitingTimeout) { 282 pterm.Println("Deployment failed due to timeout") 283 return fmt.Errorf("deployment failed due to timeout") 284 } 285 286 if e.App.Unsafe { 287 _, err = e.Project.AWSClient.ELBV2Client.ModifyTargetGroup(&elbv2.ModifyTargetGroupInput{ 288 HealthyThresholdCount: dtgo.TargetGroups[0].HealthyThresholdCount, 289 HealthCheckIntervalSeconds: dtgo.TargetGroups[0].HealthCheckIntervalSeconds, 290 HealthCheckTimeoutSeconds: dtgo.TargetGroups[0].HealthCheckTimeoutSeconds, 291 UnhealthyThresholdCount: dtgo.TargetGroups[0].UnhealthyThresholdCount, 292 TargetGroupArn: uso.Service.LoadBalancers[0].TargetGroupArn, 293 }) 294 if err != nil { 295 return fmt.Errorf("unable to modify target group: %w", err) 296 } 297 } 298 299 if oldTD != nil { 300 if err = deregisterTaskDefinition(svc, oldTD); err != nil { 301 return err 302 } 303 } 304 305 return nil 306 } 307 308 func isDeployed(svc ecsiface.ECSAPI, name string, cluster string) (bool, error) { 309 dso, err := svc.DescribeServices(&ecs.DescribeServicesInput{ 310 Cluster: &cluster, 311 Services: []*string{&name}, 312 }) 313 if err != nil { 314 return false, err 315 } 316 317 if len(dso.Services) == 0 { 318 return false, nil 319 } 320 321 if len(dso.Services[0].Deployments) != 1 { 322 return false, nil 323 } 324 325 runningTasks, err := svc.ListTasks(&ecs.ListTasksInput{ 326 Cluster: &cluster, 327 ServiceName: &name, 328 }) 329 if err != nil { 330 return false, err 331 } 332 333 if len(runningTasks.TaskArns) == 0 { 334 return *dso.Services[0].DesiredCount == 0, nil 335 } 336 337 runningCount, err := getRunningTaskCount(cluster, runningTasks.TaskArns, *dso.Services[0].TaskDefinition, svc) 338 if err != nil { 339 return false, err 340 } 341 342 return runningCount == *dso.Services[0].DesiredCount, nil 343 } 344 345 func getRunningTaskCount(cluster string, tasks []*string, serviceArn string, svc ecsiface.ECSAPI) (int64, error) { 346 count := 0 347 348 dto, err := svc.DescribeTasks(&ecs.DescribeTasksInput{ 349 Cluster: &cluster, 350 Tasks: tasks, 351 }) 352 if err != nil { 353 return 0, err 354 } 355 356 for _, t := range dto.Tasks { 357 if *t.TaskDefinitionArn == serviceArn && *t.LastStatus == "RUNNING" { 358 count++ 359 } 360 } 361 362 return int64(count), nil 363 } 364 365 func getStoppedReason(cluster string, name string, svc ecsiface.ECSAPI) (string, error) { 366 stopped := ecs.DesiredStatusStopped 367 368 runningTasks, err := svc.ListTasks(&ecs.ListTasksInput{ 369 Cluster: &cluster, 370 ServiceName: &name, 371 DesiredStatus: &stopped, 372 }) 373 if err != nil { 374 return "", err 375 } 376 377 dto, err := svc.DescribeTasks(&ecs.DescribeTasksInput{ 378 Cluster: &cluster, 379 Tasks: runningTasks.TaskArns, 380 }) 381 if err != nil { 382 return "", err 383 } 384 385 if dto.Tasks[0].StoppedReason == nil { 386 return "", nil 387 } 388 389 return *dto.Tasks[0].StoppedReason, nil 390 } 391 392 func deregisterTaskDefinition(svc ecsiface.ECSAPI, td *ecs.TaskDefinition) error { 393 pterm.Println("Deregister task definition revision") 394 395 _, err := svc.DeregisterTaskDefinition(&ecs.DeregisterTaskDefinitionInput{ 396 TaskDefinition: td.TaskDefinitionArn, 397 }) 398 if err != nil { 399 return err 400 } 401 402 pterm.Printfln("Successfully deregistered revision: %s:%d", *td.Family, *td.Revision) 403 404 return nil 405 } 406 407 func (e *Manager) getLastContainerLogs(logGroup string) error { 408 cwl := e.Project.AWSClient.CloudWatchLogsClient 409 out, err := cwl.DescribeLogStreams(&cloudwatchlogs.DescribeLogStreamsInput{ 410 LogGroupName: &logGroup, 411 Limit: aws.Int64(1), 412 Descending: aws.Bool(true), 413 OrderBy: aws.String("LastEventTime"), 414 }) 415 if err != nil { 416 return err 417 } 418 419 if len(out.LogStreams) == 0 { 420 return nil 421 } 422 423 pterm.Println("Container logs:") 424 425 for _, stream := range out.LogStreams { 426 out, err := cwl.GetLogEvents(&cloudwatchlogs.GetLogEventsInput{ 427 LogGroupName: &logGroup, 428 LogStreamName: stream.LogStreamName, 429 }) 430 if err != nil { 431 return err 432 } 433 434 for _, event := range out.Events { 435 pterm.Println("| " + *event.Message) 436 } 437 } 438 439 pterm.Println() 440 441 return nil 442 }