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  }