Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

r/aws_ecs_service: update propagate_tags enable_ecs_managed_tags load_balancer and service_registries without destroy & recreate #23600

Merged
merged 6 commits into from
Mar 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/23600.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/aws_ecs_service: `enable_ecs_managed_tags`, `load_balancer`, `propagate_tags` and `service_registries` can now be updated in-place
```
242 changes: 111 additions & 131 deletions internal/service/ecs/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,11 @@ func ResourceService() *schema.Resource {
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"type": {
Type: schema.TypeString,
ForceNew: true,
Optional: true,
Default: ecs.DeploymentControllerTypeEcs,
ValidateFunc: validation.StringInSlice([]string{
ecs.DeploymentControllerTypeCodeDeploy,
ecs.DeploymentControllerTypeEcs,
ecs.DeploymentControllerTypeExternal,
}, false),
Type: schema.TypeString,
ForceNew: true,
Optional: true,
Default: ecs.DeploymentControllerTypeEcs,
ValidateFunc: validation.StringInSlice(ecs.DeploymentControllerType_Values(), false),
},
},
},
Expand Down Expand Up @@ -153,7 +149,6 @@ func ResourceService() *schema.Resource {
Type: schema.TypeBool,
Optional: true,
Default: false,
ForceNew: true,
},
"enable_execute_command": {
Type: schema.TypeBool,
Expand Down Expand Up @@ -185,7 +180,6 @@ func ResourceService() *schema.Resource {
"load_balancer": {
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"elb_name": {
Expand Down Expand Up @@ -255,13 +249,9 @@ func ResourceService() *schema.Resource {
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"type": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice([]string{
ecs.PlacementStrategyTypeBinpack,
ecs.PlacementStrategyTypeRandom,
ecs.PlacementStrategyTypeSpread,
}, false),
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice(ecs.PlacementStrategyType_Values(), false),
},
"field": {
Type: schema.TypeString,
Expand Down Expand Up @@ -291,12 +281,9 @@ func ResourceService() *schema.Resource {
Optional: true,
},
"type": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice([]string{
ecs.PlacementConstraintTypeDistinctInstance,
ecs.PlacementConstraintTypeMemberOf,
}, false),
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice(ecs.PlacementConstraintType_Values(), false),
},
},
},
Expand All @@ -309,33 +296,24 @@ func ResourceService() *schema.Resource {
"propagate_tags": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
if old == "NONE" && new == "" {
return true
}
return false
},
ValidateFunc: validation.StringInSlice([]string{
ecs.PropagateTagsService,
ecs.PropagateTagsTaskDefinition,
"",
}, false),
ValidateFunc: validation.StringInSlice(ecs.PropagateTags_Values(), false),
},
"scheduling_strategy": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Default: ecs.SchedulingStrategyReplica,
ValidateFunc: validation.StringInSlice([]string{
ecs.SchedulingStrategyDaemon,
ecs.SchedulingStrategyReplica,
}, false),
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Default: ecs.SchedulingStrategyReplica,
ValidateFunc: validation.StringInSlice(ecs.SchedulingStrategy_Values(), false),
},
"service_registries": {
Type: schema.TypeList,
Optional: true,
ForceNew: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
Expand Down Expand Up @@ -1000,122 +978,124 @@ func flattenServiceRegistries(srs []*ecs.ServiceRegistry) []map[string]interface

func resourceServiceUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*conns.AWSClient).ECSConn
updateService := false

input := ecs.UpdateServiceInput{
Cluster: aws.String(d.Get("cluster").(string)),
ForceNewDeployment: aws.Bool(d.Get("force_new_deployment").(bool)),
Service: aws.String(d.Id()),
}
if d.HasChangesExcept("tags", "tags_all") {
input := &ecs.UpdateServiceInput{
Cluster: aws.String(d.Get("cluster").(string)),
ForceNewDeployment: aws.Bool(d.Get("force_new_deployment").(bool)),
Service: aws.String(d.Id()),
}

schedulingStrategy := d.Get("scheduling_strategy").(string)
schedulingStrategy := d.Get("scheduling_strategy").(string)

if schedulingStrategy == ecs.SchedulingStrategyDaemon {
if d.HasChange("deployment_minimum_healthy_percent") {
updateService = true
input.DeploymentConfiguration = &ecs.DeploymentConfiguration{
MinimumHealthyPercent: aws.Int64(int64(d.Get("deployment_minimum_healthy_percent").(int))),
if schedulingStrategy == ecs.SchedulingStrategyDaemon {
if d.HasChange("deployment_minimum_healthy_percent") {
input.DeploymentConfiguration = &ecs.DeploymentConfiguration{
MinimumHealthyPercent: aws.Int64(int64(d.Get("deployment_minimum_healthy_percent").(int))),
}
}
} else if schedulingStrategy == ecs.SchedulingStrategyReplica {
if d.HasChange("desired_count") {
input.DesiredCount = aws.Int64(int64(d.Get("desired_count").(int)))
}
}
} else if schedulingStrategy == ecs.SchedulingStrategyReplica {
if d.HasChange("desired_count") {
updateService = true
input.DesiredCount = aws.Int64(int64(d.Get("desired_count").(int)))
}

if d.HasChanges("deployment_maximum_percent", "deployment_minimum_healthy_percent") {
updateService = true
input.DeploymentConfiguration = &ecs.DeploymentConfiguration{
MaximumPercent: aws.Int64(int64(d.Get("deployment_maximum_percent").(int))),
MinimumHealthyPercent: aws.Int64(int64(d.Get("deployment_minimum_healthy_percent").(int))),
if d.HasChanges("deployment_maximum_percent", "deployment_minimum_healthy_percent") {
input.DeploymentConfiguration = &ecs.DeploymentConfiguration{
MaximumPercent: aws.Int64(int64(d.Get("deployment_maximum_percent").(int))),
MinimumHealthyPercent: aws.Int64(int64(d.Get("deployment_minimum_healthy_percent").(int))),
}
}
}
}

if d.HasChange("deployment_circuit_breaker") {
updateService = true
if d.HasChange("deployment_circuit_breaker") {
if input.DeploymentConfiguration == nil {
input.DeploymentConfiguration = &ecs.DeploymentConfiguration{}
}

// To remove an existing deployment circuit breaker, specify an empty object.
input.DeploymentConfiguration.DeploymentCircuitBreaker = &ecs.DeploymentCircuitBreaker{}

if input.DeploymentConfiguration == nil {
input.DeploymentConfiguration = &ecs.DeploymentConfiguration{}
if v, ok := d.GetOk("deployment_circuit_breaker"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil {
input.DeploymentConfiguration.DeploymentCircuitBreaker = expandDeploymentCircuitBreaker(v.([]interface{})[0].(map[string]interface{}))
}
}

// To remove an existing deployment circuit breaker, specify an empty object.
input.DeploymentConfiguration.DeploymentCircuitBreaker = &ecs.DeploymentCircuitBreaker{}
if d.HasChange("ordered_placement_strategy") {
// Reference: https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_UpdateService.html#ECS-UpdateService-request-placementStrategy
// To remove an existing placement strategy, specify an empty object.
input.PlacementStrategy = []*ecs.PlacementStrategy{}

if v, ok := d.GetOk("ordered_placement_strategy"); ok && len(v.([]interface{})) > 0 {
ps, err := expandPlacementStrategy(v.([]interface{}))

if v, ok := d.GetOk("deployment_circuit_breaker"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil {
input.DeploymentConfiguration.DeploymentCircuitBreaker = expandDeploymentCircuitBreaker(v.([]interface{})[0].(map[string]interface{}))
if err != nil {
return err
}

input.PlacementStrategy = ps
}
}
}

if d.HasChange("ordered_placement_strategy") {
updateService = true
// Reference: https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_UpdateService.html#ECS-UpdateService-request-placementStrategy
// To remove an existing placement strategy, specify an empty object.
input.PlacementStrategy = []*ecs.PlacementStrategy{}
if d.HasChange("placement_constraints") {
// Reference: https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_UpdateService.html#ECS-UpdateService-request-placementConstraints
// To remove all existing placement constraints, specify an empty array.
input.PlacementConstraints = []*ecs.PlacementConstraint{}

if v, ok := d.GetOk("ordered_placement_strategy"); ok && len(v.([]interface{})) > 0 {
ps, err := expandPlacementStrategy(v.([]interface{}))
if v, ok := d.Get("placement_constraints").(*schema.Set); ok && v.Len() > 0 {
pc, err := expandPlacementConstraints(v.List())

if err != nil {
return err
}
if err != nil {
return err
}

input.PlacementStrategy = ps
input.PlacementConstraints = pc
}
}
}

if d.HasChange("placement_constraints") {
updateService = true
// Reference: https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_UpdateService.html#ECS-UpdateService-request-placementConstraints
// To remove all existing placement constraints, specify an empty array.
input.PlacementConstraints = []*ecs.PlacementConstraint{}
if d.HasChange("platform_version") {
input.PlatformVersion = aws.String(d.Get("platform_version").(string))
}

if v, ok := d.Get("placement_constraints").(*schema.Set); ok && v.Len() > 0 {
pc, err := expandPlacementConstraints(v.List())
if d.HasChange("health_check_grace_period_seconds") {
input.HealthCheckGracePeriodSeconds = aws.Int64(int64(d.Get("health_check_grace_period_seconds").(int)))
}

if err != nil {
return err
}
if d.HasChange("task_definition") {
input.TaskDefinition = aws.String(d.Get("task_definition").(string))
}

input.PlacementConstraints = pc
if d.HasChange("network_configuration") {
input.NetworkConfiguration = expandNetworkConfiguration(d.Get("network_configuration").([]interface{}))
}
}

if d.HasChange("platform_version") {
updateService = true
input.PlatformVersion = aws.String(d.Get("platform_version").(string))
}
if d.HasChange("capacity_provider_strategy") {
input.CapacityProviderStrategy = expandCapacityProviderStrategy(d.Get("capacity_provider_strategy").(*schema.Set))
}

if d.HasChange("health_check_grace_period_seconds") {
updateService = true
input.HealthCheckGracePeriodSeconds = aws.Int64(int64(d.Get("health_check_grace_period_seconds").(int)))
}
if d.HasChange("enable_execute_command") {
input.EnableExecuteCommand = aws.Bool(d.Get("enable_execute_command").(bool))
}

if d.HasChange("task_definition") {
updateService = true
input.TaskDefinition = aws.String(d.Get("task_definition").(string))
}
if d.HasChange("enable_ecs_managed_tags") {
input.EnableECSManagedTags = aws.Bool(d.Get("enable_ecs_managed_tags").(bool))
}

if d.HasChange("network_configuration") {
updateService = true
input.NetworkConfiguration = expandNetworkConfiguration(d.Get("network_configuration").([]interface{}))
}
if d.HasChange("load_balancer") {
input.LoadBalancers = expandLoadBalancers(d.Get("load_balancer").([]interface{}))
}

if d.HasChange("capacity_provider_strategy") {
updateService = true
input.CapacityProviderStrategy = expandCapacityProviderStrategy(d.Get("capacity_provider_strategy").(*schema.Set))
}
if d.HasChange("propagate_tags") {
input.PropagateTags = aws.String(d.Get("propagate_tags").(string))
}

if d.HasChange("enable_execute_command") {
updateService = true
input.EnableExecuteCommand = aws.Bool(d.Get("enable_execute_command").(bool))
}
if d.HasChange("service_registries") {
input.ServiceRegistries = expandServiceRegistries(d.Get("service_registries").([]interface{}))
}

if updateService {
log.Printf("[DEBUG] Updating ECS Service (%s): %s", d.Id(), input)
// Retry due to IAM eventual consistency
err := resource.Retry(tfiam.PropagationTimeout+serviceUpdateTimeout, func() *resource.RetryError {
_, err := conn.UpdateService(&input)
_, err := conn.UpdateService(input)

if err != nil {
if tfawserr.ErrMessageContains(err, ecs.ErrCodeInvalidParameterException, "verify that the ECS service role being passed has the proper permissions") {
Expand All @@ -1132,22 +1112,22 @@ func resourceServiceUpdate(d *schema.ResourceData, meta interface{}) error {
})

if tfresource.TimedOut(err) {
_, err = conn.UpdateService(&input)
_, err = conn.UpdateService(input)
}

if err != nil {
return fmt.Errorf("error updating ECS Service (%s): %w", d.Id(), err)
}
}

if d.Get("wait_for_steady_state").(bool) {
cluster := ""
if v, ok := d.GetOk("cluster"); ok {
cluster = v.(string)
}
if d.Get("wait_for_steady_state").(bool) {
cluster := ""
if v, ok := d.GetOk("cluster"); ok {
cluster = v.(string)
}

if err := waitServiceStable(conn, d.Id(), cluster); err != nil {
return fmt.Errorf("error waiting for ECS service (%s) to become ready: %w", d.Id(), err)
if err := waitServiceStable(conn, d.Id(), cluster); err != nil {
return fmt.Errorf("error waiting for ECS service (%s) to become ready: %w", d.Id(), err)
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions internal/service/ecs/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1233,10 +1233,10 @@ func TestAccECSService_Tags_propagate(t *testing.T) {
),
},
{
Config: testAccServiceManagedTagsConfig(rName),
Config: testAccServicePropagateTagsConfig(rName, "NONE"),
Check: resource.ComposeTestCheckFunc(
testAccCheckServiceExists(resourceName, &third),
resource.TestCheckResourceAttr(resourceName, "propagate_tags", "NONE"),
resource.TestCheckResourceAttr(resourceName, "propagate_tags", ecs.PropagateTagsNone),
),
},
},
Expand Down