diff --git a/pkg/app/piped/executor/ecs/deploy.go b/pkg/app/piped/executor/ecs/deploy.go index 3fc22bf779..2cd3bebf5f 100644 --- a/pkg/app/piped/executor/ecs/deploy.go +++ b/pkg/app/piped/executor/ecs/deploy.go @@ -16,6 +16,7 @@ package ecs import ( "context" + "strconv" "github.com/aws/aws-sdk-go-v2/service/ecs/types" @@ -109,7 +110,12 @@ func (e *deployExecutor) ensureSync(ctx context.Context) model.StageStatus { } recreate := e.appCfg.QuickSync.Recreate - if !sync(ctx, &e.Input, e.platformProviderName, e.platformProviderCfg, recreate, taskDefinition, servicedefinition, primary) { + forceNewDeployment := e.appCfg.QuickSync.ForceNewDeployment + + // Store force new deployment flag to metadata store. + e.Input.MetadataStore.Shared().Put(ctx, forceNewDeploymentKey, strconv.FormatBool(forceNewDeployment)) + + if !sync(ctx, &e.Input, e.platformProviderName, e.platformProviderCfg, recreate, forceNewDeployment, taskDefinition, servicedefinition, primary) { return model.StageStatus_STAGE_FAILURE } diff --git a/pkg/app/piped/executor/ecs/ecs.go b/pkg/app/piped/executor/ecs/ecs.go index 1fad9172c1..6e408fee1a 100644 --- a/pkg/app/piped/executor/ecs/ecs.go +++ b/pkg/app/piped/executor/ecs/ecs.go @@ -41,6 +41,8 @@ const ( canaryScaleMetadataKey = "canary-scale" currentListenersKey = "current-listeners" canaryTargetGroupArnKey = "canary-target-group-arn" + // Force new deployment flag metadata key. + forceNewDeploymentKey = "force-new-deployment" ) type registerer interface { @@ -147,7 +149,7 @@ func applyTaskDefinition(ctx context.Context, cli provider.Client, taskDefinitio return td, nil } -func applyServiceDefinition(ctx context.Context, cli provider.Client, serviceDefinition types.Service) (*types.Service, error) { +func applyServiceDefinition(ctx context.Context, cli provider.Client, serviceDefinition types.Service, forceNewDeployment bool) (*types.Service, error) { found, err := cli.ServiceExists(ctx, *serviceDefinition.ClusterArn, *serviceDefinition.ServiceName) if err != nil { return nil, fmt.Errorf("unable to validate service name %s: %w", *serviceDefinition.ServiceName, err) @@ -155,7 +157,7 @@ func applyServiceDefinition(ctx context.Context, cli provider.Client, serviceDef var service *types.Service if found { - service, err = cli.UpdateService(ctx, serviceDefinition) + service, err = cli.UpdateService(ctx, serviceDefinition, forceNewDeployment) if err != nil { return nil, fmt.Errorf("failed to update ECS service %s: %w", *serviceDefinition.ServiceName, err) } @@ -291,7 +293,7 @@ func createPrimaryTaskSet(ctx context.Context, client provider.Client, service t return nil } -func sync(ctx context.Context, in *executor.Input, platformProviderName string, platformProviderCfg *config.PlatformProviderECSConfig, recreate bool, taskDefinition types.TaskDefinition, serviceDefinition types.Service, targetGroup *types.LoadBalancer) bool { +func sync(ctx context.Context, in *executor.Input, platformProviderName string, platformProviderCfg *config.PlatformProviderECSConfig, recreate bool, forceNewDeployment bool, taskDefinition types.TaskDefinition, serviceDefinition types.Service, targetGroup *types.LoadBalancer) bool { client, err := provider.DefaultRegistry().Client(platformProviderName, platformProviderCfg, in.Logger) if err != nil { in.LogPersister.Errorf("Unable to create ECS client for the provider %s: %v", platformProviderName, err) @@ -306,7 +308,7 @@ func sync(ctx context.Context, in *executor.Input, platformProviderName string, } in.LogPersister.Infof("Start applying the ECS service definition") - service, err := applyServiceDefinition(ctx, client, serviceDefinition) + service, err := applyServiceDefinition(ctx, client, serviceDefinition, forceNewDeployment) if err != nil { in.LogPersister.Errorf("Failed to apply service %s: %v", *serviceDefinition.ServiceName, err) return false @@ -330,7 +332,7 @@ func sync(ctx context.Context, in *executor.Input, platformProviderName string, // Scale up the service tasks count back to its desired. in.LogPersister.Infof("Scale up ECS desired tasks count back to %d", cnt) service.DesiredCount = cnt - if _, err = client.UpdateService(ctx, *service); err != nil { + if _, err = client.UpdateService(ctx, *service, forceNewDeployment); err != nil { in.LogPersister.Errorf("Failed to turning back service tasks: %v", err) return false } @@ -367,7 +369,11 @@ func rollout(ctx context.Context, in *executor.Input, platformProviderName strin } in.LogPersister.Infof("Start applying the ECS service definition") - service, err := applyServiceDefinition(ctx, client, serviceDefinition) + + // forceNewDeployment is false since this configuration only available for QuickSync strategy. + forceNewDeployment := false + + service, err := applyServiceDefinition(ctx, client, serviceDefinition, forceNewDeployment) if err != nil { in.LogPersister.Errorf("Failed to apply service %s: %v", *serviceDefinition.ServiceName, err) return false diff --git a/pkg/app/piped/executor/ecs/rollback.go b/pkg/app/piped/executor/ecs/rollback.go index 893eab20c9..9dd347a321 100644 --- a/pkg/app/piped/executor/ecs/rollback.go +++ b/pkg/app/piped/executor/ecs/rollback.go @@ -111,8 +111,15 @@ func rollback(ctx context.Context, in *executor.Input, platformProviderName stri return false } + // Retrieve force new deployment flag from metadata store. + forceNewDeployment := false + val, ok := in.MetadataStore.Shared().Get(forceNewDeploymentKey) + if ok && val == "true" { + forceNewDeployment = true + } + // Rollback ECS service configuration to previous state including commit-hash of the tag. - service, err := applyServiceDefinition(ctx, client, serviceDefinition) + service, err := applyServiceDefinition(ctx, client, serviceDefinition, forceNewDeployment) if err != nil { in.LogPersister.Errorf("Unable to rollback ECS service %s configuration to previous stage: %v", *serviceDefinition.ServiceName, err) return false diff --git a/pkg/app/piped/platformprovider/ecs/client.go b/pkg/app/piped/platformprovider/ecs/client.go index c961355dc8..febc76e3d5 100644 --- a/pkg/app/piped/platformprovider/ecs/client.go +++ b/pkg/app/piped/platformprovider/ecs/client.go @@ -144,13 +144,14 @@ func (c *client) PruneServiceTasks(ctx context.Context, service types.Service) e return nil } -func (c *client) UpdateService(ctx context.Context, service types.Service) (*types.Service, error) { +func (c *client) UpdateService(ctx context.Context, service types.Service, forceNewDeployment bool) (*types.Service, error) { if service.LaunchType != "" && service.CapacityProviderStrategy != nil { return nil, fmt.Errorf("failed to update ECS service %s: launch type and capacity provider strategy cannot be specified together", *service.ServiceName) } input := &ecs.UpdateServiceInput{ Cluster: service.ClusterArn, Service: service.ServiceName, + ForceNewDeployment: forceNewDeployment, EnableExecuteCommand: aws.Bool(service.EnableExecuteCommand), PlacementStrategy: service.PlacementStrategy, // TODO: Support update other properties of service. diff --git a/pkg/app/piped/platformprovider/ecs/ecs.go b/pkg/app/piped/platformprovider/ecs/ecs.go index f787de2802..ae5810d045 100644 --- a/pkg/app/piped/platformprovider/ecs/ecs.go +++ b/pkg/app/piped/platformprovider/ecs/ecs.go @@ -45,7 +45,7 @@ type ECS interface { ListClusters(ctx context.Context) ([]string, error) ServiceExists(ctx context.Context, clusterName string, servicesName string) (bool, error) CreateService(ctx context.Context, service types.Service) (*types.Service, error) - UpdateService(ctx context.Context, service types.Service) (*types.Service, error) + UpdateService(ctx context.Context, service types.Service, forceNewDeployment bool) (*types.Service, error) PruneServiceTasks(ctx context.Context, service types.Service) error WaitServiceStable(ctx context.Context, service types.Service) error GetServices(ctx context.Context, clusterName string) ([]*types.Service, error) diff --git a/pkg/config/application_ecs.go b/pkg/config/application_ecs.go index 469183913b..b9f14c3cce 100644 --- a/pkg/config/application_ecs.go +++ b/pkg/config/application_ecs.go @@ -109,6 +109,11 @@ type ECSSyncStageOptions struct { // If this is set, the application may be unavailable for a short of time during the deployment. // Default is false. Recreate bool `json:"recreate"` + // Whether to force a new deployment or not. + // If this is set, the new service deployment will be forced to start even if the service is already in a desired state. + // This is useful when you want to update the service configuration. + // Default is false. + ForceNewDeployment bool `json:"forceNewDeployment"` } // ECSCanaryRolloutStageOptions contains all configurable values for a ECS_CANARY_ROLLOUT stage.