// Copyright 2015-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"). You may // not use this file except in compliance with the License. A copy of the // License is located at // // http://aws.amazon.com/apache2.0/ // // or in the "license" file accompanying this file. This file is distributed // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either // express or implied. See the License for the specific language governing // permissions and limitations under the License. package service import ( "fmt" "strconv" "strings" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/compose/context" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/compose/entity" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/compose/entity/types" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/servicediscovery" ecsclient "github.com/aws/amazon-ecs-cli/ecs-cli/modules/clients/aws/ecs" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/clients/aws/route53" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/clients/aws/tagging" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/commands/flags" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/config" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/utils" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/utils/cache" composeutils "github.com/aws/amazon-ecs-cli/ecs-cli/modules/utils/compose" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ecs" taggingSDK "github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi" "github.com/docker/libcompose/project" "github.com/pkg/errors" log "github.com/sirupsen/logrus" ) // Service type is placeholder for a single task definition and its cache // and it performs operations on ECS Service level type Service struct { taskDef *ecs.TaskDefinition cache cache.Cache ecsContext *context.ECSContext deploymentConfig *ecs.DeploymentConfiguration loadBalancers []*ecs.LoadBalancer role string healthCheckGP *int64 serviceRegistries []*ecs.ServiceRegistry tags []*ecs.Tag } const ( ecsActiveResourceCode = "ACTIVE" ecsMissingResourceCode = "MISSING" ) // make servicediscovery.Create easily mockable in tests var servicediscoveryCreate servicediscovery.CreateFunc = servicediscovery.Create // make servicediscovery.Update easily mockable in tests var servicediscoveryUpdate servicediscovery.UpdateFunc = servicediscovery.Update // make servicediscovery.Delete easily mockable in tests var servicediscoveryDelete servicediscovery.DeleteFunc = servicediscovery.Delete // make servicediscovery.Delete easily mockable in tests var waitUntilSDSDeletable route53.WaitUntilSDSDeletableFunc = route53.WaitUntilSDSDeletable // NewService creates an instance of a Service and also sets up a cache for task definition func NewService(ecsContext *context.ECSContext) entity.ProjectEntity { return &Service{ cache: entity.SetupTaskDefinitionCache(), ecsContext: ecsContext, } } // LoadContext reads the ECS context set in NewService and loads DeploymentConfiguration and LoadBalancer // TODO: refactor to memoize s.Context().CLIContext, since that's the only // thing that LoadContext seems to care about? (even in getInt64FromCLIContext) func (s *Service) LoadContext() error { maxPercent, err := getInt64FromCLIContext(s.Context(), flags.DeploymentMaxPercentFlag) if err != nil { return err } minHealthyPercent, err := getInt64FromCLIContext(s.Context(), flags.DeploymentMinHealthyPercentFlag) if err != nil { return err } s.deploymentConfig = &ecs.DeploymentConfiguration{ MaximumPercent: maxPercent, MinimumHealthyPercent: minHealthyPercent, } // Load Balancer targetGroups := s.Context().CLIContext.StringSlice(flags.TargetGroupsFlag) role := s.Context().CLIContext.String(flags.RoleFlag) targetGroupArn := s.Context().CLIContext.String(flags.TargetGroupArnFlag) loadBalancerName := s.Context().CLIContext.String(flags.LoadBalancerNameFlag) containerName := s.Context().CLIContext.String(flags.ContainerNameFlag) containerPort, err := getInt64FromCLIContext(s.Context(), flags.ContainerPortFlag) if err != nil { return err } // Health Check Grace Period healthCheckGP, err := getInt64FromCLIContext(s.Context(), flags.HealthCheckGracePeriodFlag) if err != nil { return err } s.healthCheckGP = healthCheckGP // Validates LoadBalancerName and TargetGroupArn cannot exist at the same time. // Other validation is taken care of by the API call. This currently // includes errors on absence of container name and port if target // group or ELB name is specified or if the load balancing resources // specified do not exist. // TODO: Add validation on targetGroupArn or loadBalancerName being // present if containerName or containerPort are specified if (targetGroupArn != "" || loadBalancerName != "" || containerName != "" || containerPort != nil) && len(targetGroups) != 0 { return errors.Errorf("[--%s] cannot be used with [--%s], [--%s], [--%s] or [--%s]", flags.TargetGroupsFlag, flags.LoadBalancerNameFlag, flags.TargetGroupArnFlag, flags.ContainerNameFlag, flags.ContainerPortFlag) } else if targetGroupArn != "" || loadBalancerName != "" || containerName != "" || containerPort != nil { if targetGroupArn != "" && loadBalancerName != "" { return errors.Errorf("[--%s] and [--%s] flags cannot both be specified", flags.LoadBalancerNameFlag, flags.TargetGroupArnFlag) } if targetGroupArn == "" && loadBalancerName == "" { return errors.Errorf("Must specify either [--%s] or [--%s] flag for your service", flags.LoadBalancerNameFlag, flags.TargetGroupArnFlag) } if containerName == "" { return errors.Errorf("[--%s] is required if [--%s] or [--%s] is specified", flags.ContainerNameFlag, flags.LoadBalancerNameFlag, flags.TargetGroupArnFlag) } if containerPort == nil { return errors.Errorf("[--%s] is required if [--%s] or [--%s] is specified", flags.ContainerPortFlag, flags.LoadBalancerNameFlag, flags.TargetGroupArnFlag) } loadBalancer := &ecs.LoadBalancer{ ContainerName: aws.String(containerName), ContainerPort: containerPort, } if targetGroupArn != "" { loadBalancer.TargetGroupArn = aws.String(targetGroupArn) } if loadBalancerName != "" { loadBalancer.LoadBalancerName = aws.String(loadBalancerName) } s.loadBalancers = []*ecs.LoadBalancer{loadBalancer} } else if len(targetGroups) != 0 { loadBalancers, err := utils.ParseLoadBalancers(targetGroups) if err != nil { return err } s.loadBalancers = append(s.loadBalancers, loadBalancers...) } s.role = role return nil } // getInt64FromCLIContext reads the flag from the cli context and typecasts into *int64 func getInt64FromCLIContext(context *context.ECSContext, flag string) (*int64, error) { value := context.CLIContext.String(flag) if value == "" { return nil, nil } intValue, err := strconv.ParseInt(value, 10, 64) if err != nil { return nil, fmt.Errorf("Please pass integer value for the flag %s", flag) } return aws.Int64(intValue), nil } // SetTaskDefinition sets the ecs task definition to the current instance of Service func (s *Service) SetTaskDefinition(taskDefinition *ecs.TaskDefinition) { s.taskDef = taskDefinition } // Context returs the context of this project func (s *Service) Context() *context.ECSContext { return s.ecsContext } // TaskDefinition returns the task definition object that was created by // transforming the Service Configs to ECS acceptable format func (s *Service) TaskDefinition() *ecs.TaskDefinition { return s.taskDef } // TaskDefinitionCache returns the cache that should be used when checking for // previous task definition func (s *Service) TaskDefinitionCache() cache.Cache { return s.cache } // DeploymentConfig returns the configuration that control how many tasks run during the // deployment and the ordering of stopping and starting tasks func (s *Service) DeploymentConfig() *ecs.DeploymentConfiguration { return s.deploymentConfig } // ----------- Commands' implementations -------- // Create creates a task definition in ECS for the containers in the compose file // and persists it in a cache locally. It always checks the cache before creating func (s *Service) Create() error { _, err := entity.GetOrCreateTaskDefinition(s) if err != nil { return err } err = entity.OptionallyCreateLogs(s) if err != nil { return err } return s.createService(0) } // Start starts the containers if they weren't already running. Internally, start calls // ECS.DescribeService to find out if the service is Active and if the count is 0, // it updates the service with desired count as 1 else its a no-op // TODO: Instead of always setting count=1, if the containers were Stopped before, // Start should fetch the previously set desired-count from the cache and start x count of containers func (s *Service) Start() error { err := entity.OptionallyCreateLogs(s) if err != nil { return err } return s.startService() } // Up creates the task definition and service and starts the containers if necessary. // It does so by calling DescribeService to see if it exists, then calls Create() and Start(). // Otherwise, if the compose or ecs-params files have changed, it will update // the existing service with the new task definition by calling UpdateService // with the new task definition and service parameters. func (s *Service) Up() error { // describe service to get the task definition and count running ecsService, err := s.describeService() var missingServiceErr bool if err != nil { if strings.Contains(err.Error(), ecsMissingResourceCode) { missingServiceErr = true } else { return err } } // get the current snapshot of compose yml // and update this instance with the latest task definition newTaskDefinition, err := entity.GetOrCreateTaskDefinition(s) if err != nil { return err } err = entity.OptionallyCreateLogs(s) if err != nil { return err } // if ECS service was not created before, or is inactive, create and start the ECS Service if missingServiceErr || aws.StringValue(ecsService.Status) != ecsActiveResourceCode { // uses the latest task definition to create the service return s.createService(1) } // Update Existing Service if err = s.updateService(ecsService, newTaskDefinition); err != nil { return err } // Update Service Discovery if s.Context().CLIContext.Bool(flags.UpdateServiceDiscoveryFlag) { return servicediscoveryUpdate(aws.StringValue(newTaskDefinition.NetworkMode), entity.GetServiceName(s), s.Context()) } // add new tags, if provided if tagVal := s.Context().CLIContext.String(flags.ResourceTagsFlag); tagVal != "" { err = s.updateTags(ecsService, tagVal) if err != nil { return err } log.Info("Added new tags to service") } return nil } // TODO: Refactor this if we use the stand alone tagging client in more places in the future var newTaggingClient = tagging.NewTaggingClient func (s *Service) updateTags(ecsService *ecs.Service, tagVal string) error { taggingClient := newTaggingClient(s.Context().CommandConfig) tags, err := utils.GetTagsMap(tagVal) if err != nil { return err } input := &taggingSDK.TagResourcesInput{ ResourceARNList: []*string{ ecsService.ServiceArn, }, Tags: tags, } output, err := taggingClient.TagResources(input) if err != nil { return err } for resource, info := range output.FailedResourcesMap { return fmt.Errorf("Failed to tag Service %s; error=%s", resource, *info.ErrorMessage) } return nil } func (s *Service) buildUpdateServiceInput(count *int64, serviceName, taskDefinition string) (*ecs.UpdateServiceInput, error) { cluster := s.Context().CommandConfig.Cluster deploymentConfig := s.DeploymentConfig() forceDeployment := s.Context().CLIContext.Bool(flags.ForceDeploymentFlag) networkConfig, err := composeutils.ConvertToECSNetworkConfiguration(s.ecsContext.ECSParams) if err != nil { return nil, err } input := &ecs.UpdateServiceInput{ DesiredCount: count, Service: aws.String(serviceName), Cluster: aws.String(cluster), DeploymentConfiguration: deploymentConfig, ForceNewDeployment: &forceDeployment, } if s.healthCheckGP != nil { input.HealthCheckGracePeriodSeconds = aws.Int64(*s.healthCheckGP) } if networkConfig != nil { input.NetworkConfiguration = networkConfig } if taskDefinition != "" { input.TaskDefinition = aws.String(taskDefinition) } return input, nil } func (s *Service) updateService(ecsService *ecs.Service, newTaskDefinition *ecs.TaskDefinition) error { if s.Context().CLIContext.Bool(flags.EnableServiceDiscoveryFlag) { log.Warningln("Service Discovery can not be enabled on an existing ECS Service. Skipping this flag...") } schedulingStrategy := strings.ToUpper(s.Context().CLIContext.String(flags.SchedulingStrategyFlag)) if schedulingStrategy != "" && schedulingStrategy != aws.StringValue(ecsService.SchedulingStrategy) { return fmt.Errorf("Scheduling Strategy cannot be updated on an existing ECS Service") } ecsServiceName := aws.StringValue(ecsService.ServiceName) if s.loadBalancers != nil { log.WithFields(log.Fields{ "serviceName": ecsServiceName, }).Warn("You cannot update the load balancer configuration on an existing service.") } oldCount := aws.Int64Value(ecsService.DesiredCount) newCount := int64(1) count := &newCount if oldCount != 0 { count = &oldCount // get the current non-zero count } // if both the task definitions are the same, call update with the new count oldTaskDefinitionId := entity.GetIdFromArn(ecsService.TaskDefinition) newTaskDefinitionId := entity.GetIdFromArn(newTaskDefinition.TaskDefinitionArn) if aws.StringValue(ecsService.SchedulingStrategy) == ecs.SchedulingStrategyDaemon { count = nil } if oldTaskDefinitionId == newTaskDefinitionId { return s.updateServiceCount(count) } // if the task definitions were different, updateService with new task definition // this creates a deployment in ECS and slowly takes down the containers with old ones and starts new ones updateServiceInput, err := s.buildUpdateServiceInput(count, ecsServiceName, newTaskDefinitionId) if err != nil { return err } err = s.Context().ECSClient.UpdateService(updateServiceInput) if err != nil { return err } message := "Updated the ECS service with a new task definition. " + "Old containers will be stopped automatically, and replaced with new ones" s.logUpdateService(updateServiceInput, message) return waitForServiceTasks(s, ecsServiceName) } // Info returns a formatted list of containers (running and stopped) started by this service func (s *Service) Info(filterProjectTasks bool, desiredStatus string) (project.InfoSet, error) { // filterProjectTasks is not honored for services, because ECS Services have their // own custom Group field, overriding that with startedBy=project will result in no tasks // We should instead filter by ServiceName=service return entity.Info(s, false, desiredStatus) } // Scale the service desired count to be the specified count func (s *Service) Scale(count int) error { return s.updateServiceCount(aws.Int64(int64(count))) } // Stop stops all the containers in the service by calling ECS.UpdateService(count=0) // TODO, Store the current desiredCount in a cache, so that number of tasks(group of containers) can be started again func (s *Service) Stop() error { return s.updateServiceCount(aws.Int64(0)) } // Down stops any running containers(tasks) by calling Stop() and deletes an active ECS Service // NoOp if the service is inactive func (s *Service) Down() error { // describe the service ecsService, err := s.describeService() if err != nil { return err } ecsServiceName := aws.StringValue(ecsService.ServiceName) // if already deleted, NoOp if aws.StringValue(ecsService.Status) != ecsActiveResourceCode { log.WithFields(log.Fields{ "serviceName": ecsServiceName, }).Info("ECS Service is already deleted") return nil } // DeleteService will ignore desiredCount being non-zero by making use // of the force flag if err = s.Context().ECSClient.DeleteService(ecsServiceName); err != nil { return err } if err = waitForServiceTasks(s, ecsServiceName); err != nil { return err } // delete Service Discovery resources if they exist if len(ecsService.ServiceRegistries) > 0 { log.Info("Trying to delete any Service Discovery Resources that were created by the ECS CLI...") registryArn := aws.StringValue(ecsService.ServiceRegistries[0].RegistryArn) if err = s.deleteServiceDiscoveryResources(registryArn, ecsServiceName); err != nil { // SD deletion errors are logged but aren't fatal. log.Errorf("Problem deleting Service Discovery resources: %v", err) } } return nil } func (s *Service) deleteServiceDiscoveryResources(registryArn, ecsServiceName string) error { sdsID := getSDSIDFromArn(registryArn) if err := waitUntilSDSDeletable(sdsID, s.Context().CommandConfig); err != nil { return err } return servicediscoveryDelete(ecsServiceName, s.Context()) } // Run expects to issue a command override and start containers. But that doesnt apply to the context of ECS Services func (s *Service) Run(commandOverrides map[string][]string) error { return composeutils.ErrUnsupported } // EntityType returns service as the type func (s *Service) EntityType() types.Type { return types.Service } func (s *Service) GetTags() ([]*ecs.Tag, error) { if s.tags == nil { tags := make([]*ecs.Tag, 0) if tagVal := s.Context().CLIContext.String(flags.ResourceTagsFlag); tagVal != "" { var err error tags, err = utils.ParseTags(tagVal, tags) if err != nil { return nil, err } } s.tags = tags } return s.tags, nil } // ----------- Commands' helper functions -------- func (s *Service) buildCreateServiceInput(serviceName, taskDefName string, desiredCount int) (*ecs.CreateServiceInput, error) { launchType := s.Context().CommandConfig.LaunchType cluster := s.Context().CommandConfig.Cluster ecsParams := s.ecsContext.ECSParams schedulingStrategy := strings.ToUpper(s.Context().CLIContext.String(flags.SchedulingStrategyFlag)) networkConfig, err := composeutils.ConvertToECSNetworkConfiguration(ecsParams) if err != nil { return nil, err } placementConstraints, err := composeutils.ConvertToECSPlacementConstraints(ecsParams) if err != nil { return nil, err } placementStrategy, err := composeutils.ConvertToECSPlacementStrategy(ecsParams) if err != nil { return nil, err } // NOTE: this validation is not useful if called after GetOrCreateTaskDefinition() if err = entity.ValidateFargateParams(s.Context().ECSParams, launchType); err != nil { return nil, err } if s.healthCheckGP != nil && s.loadBalancers == nil { return nil, fmt.Errorf("--%v is only valid for services configured to use load balancers", flags.HealthCheckGracePeriodFlag) } createServiceInput := &ecs.CreateServiceInput{ DesiredCount: aws.Int64(int64(desiredCount)), // Required unless DAEMON schedulingStrategy ServiceName: aws.String(serviceName), // Required TaskDefinition: aws.String(taskDefName), // Required Cluster: aws.String(cluster), DeploymentConfiguration: s.deploymentConfig, LoadBalancers: s.loadBalancers, Role: aws.String(s.role), } // TODO: revert to "LATEST" when latest refers to 1.4.0 if launchType == config.LaunchTypeFargate && ecsParams != nil && len(ecsParams.TaskDefinition.EFSVolumes) > 0 { log.Warnf("Detected an EFS Volume in task definition %s", taskDefName) log.Warnf("Using Fargate platform version %s, which includes changes to the networking flows for VPC endpoint customers.", config.PlatformVersion140) log.Warn("Learn more: https://aws.amazon.com/blogs/containers/aws-fargate-launches-platform-version-1-4/") createServiceInput.PlatformVersion = aws.String(config.PlatformVersion140) } if schedulingStrategy != "" { createServiceInput.SchedulingStrategy = aws.String(schedulingStrategy) if schedulingStrategy == ecs.SchedulingStrategyDaemon { createServiceInput.DesiredCount = nil } } if s.healthCheckGP != nil { createServiceInput.HealthCheckGracePeriodSeconds = aws.Int64(*s.healthCheckGP) } if len(s.serviceRegistries) > 0 { createServiceInput.ServiceRegistries = s.serviceRegistries } if networkConfig != nil { createServiceInput.NetworkConfiguration = networkConfig } if placementConstraints != nil { createServiceInput.PlacementConstraints = placementConstraints } if placementStrategy != nil { createServiceInput.PlacementStrategy = placementStrategy } if launchType != "" { createServiceInput.LaunchType = aws.String(launchType) } if err = createServiceInput.Validate(); err != nil { return nil, err } tags, err := s.GetTags() if err != nil { return nil, err } if len(tags) > 0 { createServiceInput.Tags = tags } arnEnabled, err := isTaskLongARNEnabled(s.Context().ECSClient) if err != nil { return nil, err } if arnEnabled { // Even if the customer didn't create the service with tags, we enable propogation // So that later, if the customer updates to a new task def that has tags, // those tags will be propogated to tasks createServiceInput.PropagateTags = aws.String(ecs.PropagateTagsTaskDefinition) } if !s.Context().CLIContext.Bool(flags.DisableECSManagedTagsFlag) { if arnEnabled { log.Info("Auto-enabling ECS Managed Tags") createServiceInput.EnableECSManagedTags = aws.Bool(true) } } return createServiceInput, nil } func isTaskLongARNEnabled(client ecsclient.ECSClient) (bool, error) { output, err := client.ListAccountSettings(&ecs.ListAccountSettingsInput{ EffectiveSettings: aws.Bool(true), Name: aws.String(ecs.SettingNameTaskLongArnFormat), }) if err != nil { return false, err } // This should never evaluate to true, unless there is a problem with API // This if block ensures that the CLI does not panic in that case if len(output.Settings) < 1 { return false, fmt.Errorf("Received unexpected response from ECS Settings API: %s", output) } return aws.StringValue(output.Settings[0].Value) == "enabled", nil } func (s *Service) logCreateService(serviceName, taskDefName string) { fields := log.Fields{ "service": serviceName, "taskDefinition": taskDefName, } if s.deploymentConfig != nil && s.deploymentConfig.MaximumPercent != nil { fields["deployment-max-percent"] = aws.Int64Value(s.deploymentConfig.MaximumPercent) } if s.deploymentConfig != nil && s.deploymentConfig.MinimumHealthyPercent != nil { fields["deployment-min-healthy-percent"] = aws.Int64Value(s.deploymentConfig.MinimumHealthyPercent) } if s.healthCheckGP != nil { fields["health-check-grace-period"] = *s.healthCheckGP } log.WithFields(fields).Info("Created an ECS service") } // createService calls the underlying ECS.CreateService func (s *Service) createService(desiredCount int) error { serviceName := entity.GetServiceName(s) taskDefName := entity.GetIdFromArn(s.TaskDefinition().TaskDefinitionArn) cliContext := s.Context().CLIContext if cliContext.Bool(flags.EnableServiceDiscoveryFlag) { networkMode := aws.StringValue(s.TaskDefinition().NetworkMode) serviceRegistry, err := servicediscoveryCreate(networkMode, serviceName, s.Context()) if err != nil { return err } s.serviceRegistries = []*ecs.ServiceRegistry{ serviceRegistry, } } // Create request input createServiceInput, err := s.buildCreateServiceInput(serviceName, taskDefName, desiredCount) if err != nil { return err } defer s.logCreateService(serviceName, taskDefName) // Call ECS Client err = s.Context().ECSClient.CreateService(createServiceInput) if err != nil { return err } err = waitForServiceDescribable(s) if err != nil { return err } return waitForServiceTasks(s, serviceName) } // describeService calls underlying ECS.DescribeService and expects the service to be present, // returns error otherwise func (s *Service) describeService() (*ecs.Service, error) { serviceName := entity.GetServiceName(s) output, err := s.Context().ECSClient.DescribeService(serviceName) if err != nil { return nil, err } if len(output.Failures) > 0 { reason := aws.StringValue(output.Failures[0].Reason) return nil, fmt.Errorf("Got an error describing service '%s' : '%s'", serviceName, reason) } else if len(output.Services) == 0 { return nil, fmt.Errorf("Got an empty list of services while describing the service '%s'", serviceName) } return output.Services[0], nil } // startService checks if the service has a zero desired count and updates the count to 1 (of each container) func (s *Service) startService() error { ecsService, err := s.describeService() if err != nil { // Describe API returns the failures for resources in the response (instead of returning an error) // Read the custom error returned from describeService to see if the resource was missing if strings.Contains(err.Error(), ecsMissingResourceCode) { return fmt.Errorf("Please use '%s' command to create the service '%s' first", flags.CreateServiceCommandName, entity.GetServiceName(s)) } return err } serviceName := aws.StringValue(ecsService.ServiceName) desiredCount := aws.Int64Value(ecsService.DesiredCount) forceDeployment := s.Context().CLIContext.Bool(flags.ForceDeploymentFlag) schedulingStrategy := aws.StringValue(ecsService.SchedulingStrategy) if desiredCount != 0 || schedulingStrategy == ecs.SchedulingStrategyDaemon { if forceDeployment { log.WithFields(log.Fields{ "serviceName": serviceName, "desiredCount": desiredCount, "schedulingStrategy": schedulingStrategy, "force-deployment": strconv.FormatBool(forceDeployment), }).Info("Forcing new deployment of running ECS Service") count := aws.Int64(desiredCount) if schedulingStrategy == ecs.SchedulingStrategyDaemon { count = nil } return s.updateServiceCount(count) } //NoOp log.WithFields(log.Fields{ "serviceName": serviceName, "desiredCount": desiredCount, "schedulingStrategy": schedulingStrategy, }).Info("ECS Service is already running") return waitForServiceTasks(s, serviceName) } return s.updateServiceCount(aws.Int64(1)) } // updateServiceCount calls the underlying ECS.UpdateService with the specified count // NOTE: If network configuration has changed in ECS Params, this will also be updated func (s *Service) updateServiceCount(count *int64) error { serviceName := entity.GetServiceName(s) updateServiceInput, err := s.buildUpdateServiceInput(count, serviceName, "") if err != nil { return err } if err = s.Context().ECSClient.UpdateService(updateServiceInput); err != nil { return err } s.logUpdateService(updateServiceInput, "Updated ECS service successfully") return waitForServiceTasks(s, serviceName) } func (s *Service) logUpdateService(input *ecs.UpdateServiceInput, message string) { fields := log.Fields{ "service": aws.StringValue(input.Service), "desiredCount": aws.Int64Value(input.DesiredCount), } if s.deploymentConfig != nil && s.deploymentConfig.MaximumPercent != nil { fields["deployment-max-percent"] = aws.Int64Value(s.deploymentConfig.MaximumPercent) } if s.deploymentConfig != nil && s.deploymentConfig.MinimumHealthyPercent != nil { fields["deployment-min-healthy-percent"] = aws.Int64Value(s.deploymentConfig.MinimumHealthyPercent) } if s.healthCheckGP != nil { fields["health-check-grace-period"] = *s.healthCheckGP } if input.ForceNewDeployment != nil { fields["force-deployment"] = aws.BoolValue(input.ForceNewDeployment) } log.WithFields(fields).Info(message) } func getSDSIDFromArn(sdsARN string) string { return strings.Split(sdsARN, "/")[1] }