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: add triggers attribute to force update in-place #25840

Merged
merged 6 commits into from
Nov 17, 2022
Merged
Show file tree
Hide file tree
Changes from 5 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/25840.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/aws_ecs_service: Add `triggers` argument to enable in-place updates (redeployments) on each apply, when used with `force_new_deployment = true`
```
36 changes: 36 additions & 0 deletions internal/service/ecs/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,14 @@ func ResourceService() *schema.Resource {
Type: schema.TypeString,
Optional: true,
},
// modeled after null_resource & aws_api_gateway_deployment
// only for _updates in-place_ rather than replacements
"triggers": {
Type: schema.TypeMap,
Optional: true,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"wait_for_steady_state": {
Type: schema.TypeBool,
Optional: true,
Expand All @@ -350,10 +358,36 @@ func ResourceService() *schema.Resource {
CustomizeDiff: customdiff.Sequence(
verify.SetTagsDiff,
capacityProviderStrategyCustomizeDiff,
triggersCustomizeDiff,
),
}
}

func triggersCustomizeDiff(_ context.Context, d *schema.ResourceDiff, meta interface{}) error {
// clears diff to avoid extraneous diffs but lets it pass for triggering update
fnd := false
if v, ok := d.GetOk("force_new_deployment"); ok {
fnd = v.(bool)
}

if d.HasChange("triggers") && !fnd {
d.Clear("triggers")

return nil
}

if d.HasChange("triggers") && fnd {
o, n := d.GetChange("triggers")
if len(o.(map[string]interface{})) > 0 && len(n.(map[string]interface{})) == 0 {
d.Clear("triggers")
}

return nil
}

return nil
}

func capacityProviderStrategyCustomizeDiff(_ context.Context, d *schema.ResourceDiff, meta interface{}) error {
// to be backward compatible, should ForceNew almost always (previous behavior), unless:
// force_new_deployment is true and
Expand Down Expand Up @@ -642,6 +676,8 @@ func resourceServiceRead(d *schema.ResourceData, meta interface{}) error {
d.Set("platform_version", service.PlatformVersion)
d.Set("enable_execute_command", service.EnableExecuteCommand)

d.Set("triggers", d.Get("triggers"))

// Save cluster in the same format
if strings.HasPrefix(d.Get("cluster").(string), "arn:"+meta.(*conns.AWSClient).Partition+":ecs:") {
d.Set("cluster", service.ClusterArn)
Expand Down
77 changes: 77 additions & 0 deletions internal/service/ecs/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,42 @@ func TestAccECSService_forceNewDeployment(t *testing.T) {
})
}

func TestAccECSService_forceNewDeploymentTriggers(t *testing.T) {
var service1, service2 ecs.Service
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
resourceName := "aws_ecs_service.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t) },
ErrorCheck: acctest.ErrorCheck(t, ecs.EndpointsID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckServiceDestroy,
Steps: []resource.TestStep{
{
Config: testAccServiceConfig_forceNewDeployment(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckServiceExists(resourceName, &service1),
resource.TestCheckResourceAttr(resourceName, "ordered_placement_strategy.#", "1"),
resource.TestCheckResourceAttr(resourceName, "ordered_placement_strategy.0.type", "binpack"),
resource.TestCheckResourceAttr(resourceName, "ordered_placement_strategy.0.field", "memory"),
),
},
{
Config: testAccServiceConfig_forceNewDeploymentTriggers(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckServiceExists(resourceName, &service2),
testAccCheckServiceNotRecreated(&service1, &service2),
resource.TestCheckResourceAttr(resourceName, "force_new_deployment", "true"),
resource.TestCheckResourceAttrSet(resourceName, "triggers.update"),
resource.TestCheckResourceAttr(resourceName, "ordered_placement_strategy.#", "1"),
resource.TestCheckResourceAttr(resourceName, "ordered_placement_strategy.0.type", "binpack"),
resource.TestCheckResourceAttr(resourceName, "ordered_placement_strategy.0.field", "memory"),
),
},
},
})
}

func TestAccECSService_PlacementStrategy_basic(t *testing.T) {
var service1, service2, service3, service4 ecs.Service
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
Expand Down Expand Up @@ -1941,6 +1977,47 @@ resource "aws_ecs_service" "test" {
`, rName)
}

func testAccServiceConfig_forceNewDeploymentTriggers(rName string) string {
return fmt.Sprintf(`
resource "aws_ecs_cluster" "default" {
name = %[1]q
}

resource "aws_ecs_task_definition" "test" {
family = %[1]q

container_definitions = <<DEFINITION
[
{
"cpu": 128,
"essential": true,
"image": "mongo:latest",
"memory": 128,
"name": "mongodb"
}
]
DEFINITION
}

resource "aws_ecs_service" "test" {
cluster = aws_ecs_cluster.default.id
desired_count = 1
force_new_deployment = true
name = %[1]q
task_definition = aws_ecs_task_definition.test.arn

ordered_placement_strategy {
type = "binpack"
field = "memory"
}

triggers = {
update = timestamp()
}
}
`, rName)
}

func testAccServiceConfig_placementStrategy(rName string) string {
return fmt.Sprintf(`
resource "aws_ecs_cluster" "default" {
Expand Down
17 changes: 17 additions & 0 deletions website/docs/r/ecs_service.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,22 @@ resource "aws_ecs_service" "example" {
}
```

### Redeploy Service On Every Apply

The key used with `triggers` is arbitrary.

```terraform
resource "aws_ecs_service" "example" {
# ... other configurations ...

force_new_deployment = true

triggers = {
redeployment = timestamp()
}
}
```

## Argument Reference

The following arguments are required:
Expand Down Expand Up @@ -116,6 +132,7 @@ The following arguments are optional:
* `service_registries` - (Optional) Service discovery registries for the service. The maximum number of `service_registries` blocks is `1`. See below.
* `tags` - (Optional) Key-value map of resource tags. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level.
* `task_definition` - (Optional) Family and revision (`family:revision`) or full ARN of the task definition that you want to run in your service. Required unless using the `EXTERNAL` deployment controller. If a revision is not specified, the latest `ACTIVE` revision is used.
* `triggers` - (Optional) Map of arbitrary keys and values that, when changed, will trigger an in-place update (redeployment). Useful with `timestamp()`. See example above.
* `wait_for_steady_state` - (Optional) If `true`, Terraform will wait for the service to reach a steady state (like [`aws ecs wait services-stable`](https://docs.aws.amazon.com/cli/latest/reference/ecs/wait/services-stable.html)) before continuing. Default `false`.

### capacity_provider_strategy
Expand Down