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

Add DeploymentAlarms to ECS Service #28521

Merged
merged 13 commits into from
Jan 4, 2023
3 changes: 3 additions & 0 deletions .changelog/28521.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/aws_ecs_service: Add `alarms` argument
```
172 changes: 122 additions & 50 deletions internal/service/ecs/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,31 @@ func ResourceService() *schema.Resource {
},

Schema: map[string]*schema.Schema{
"alarms": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
DiffSuppressFunc: verify.SuppressMissingOptionalConfigurationBlock,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"alarm_names": {
Type: schema.TypeSet,
Required: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"enable": {
Type: schema.TypeBool,
Required: true,
},
"rollback": {
Type: schema.TypeBool,
Required: true,
},
},
},
},
"capacity_provider_strategy": {
Type: schema.TypeSet,
Optional: true,
Expand Down Expand Up @@ -447,49 +472,49 @@ func resourceServiceCreate(d *schema.ResourceData, meta interface{}) error {
defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig
tags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{})))

deploymentController := expandDeploymentController(d.Get("deployment_controller").([]interface{}))
deploymentMinimumHealthyPercent := d.Get("deployment_minimum_healthy_percent").(int)
schedulingStrategy := d.Get("scheduling_strategy").(string)
deploymentController := expandDeploymentController(d.Get("deployment_controller").([]interface{}))

input := ecs.CreateServiceInput{
ClientToken: aws.String(resource.UniqueId()),
DeploymentController: deploymentController,
SchedulingStrategy: aws.String(schedulingStrategy),
ServiceName: aws.String(d.Get("name").(string)),
EnableECSManagedTags: aws.Bool(d.Get("enable_ecs_managed_tags").(bool)),
EnableExecuteCommand: aws.Bool(d.Get("enable_execute_command").(bool)),
CapacityProviderStrategy: expandCapacityProviderStrategy(d.Get("capacity_provider_strategy").(*schema.Set)),
ClientToken: aws.String(resource.UniqueId()),
DeploymentConfiguration: &ecs.DeploymentConfiguration{},
DeploymentController: deploymentController,
EnableECSManagedTags: aws.Bool(d.Get("enable_ecs_managed_tags").(bool)),
EnableExecuteCommand: aws.Bool(d.Get("enable_execute_command").(bool)),
NetworkConfiguration: expandNetworkConfiguration(d.Get("network_configuration").([]interface{})),
SchedulingStrategy: aws.String(schedulingStrategy),
ServiceName: aws.String(d.Get("name").(string)),
}

if v, ok := d.GetOk("task_definition"); ok {
input.TaskDefinition = aws.String(v.(string))
if v, ok := d.GetOk("alarms"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil {
input.DeploymentConfiguration.Alarms = expandAlarms(v.([]interface{})[0].(map[string]interface{}))
}

if v, ok := d.GetOk("cluster"); ok {
input.Cluster = aws.String(v.(string))
}

if schedulingStrategy == ecs.SchedulingStrategyDaemon && deploymentMinimumHealthyPercent != 100 {
input.DeploymentConfiguration = &ecs.DeploymentConfiguration{
MinimumHealthyPercent: aws.Int64(int64(deploymentMinimumHealthyPercent)),
}
input.DeploymentConfiguration.MinimumHealthyPercent = aws.Int64(int64(deploymentMinimumHealthyPercent))
} else if schedulingStrategy == ecs.SchedulingStrategyReplica {
input.DeploymentConfiguration = &ecs.DeploymentConfiguration{
MaximumPercent: aws.Int64(int64(d.Get("deployment_maximum_percent").(int))),
MinimumHealthyPercent: aws.Int64(int64(deploymentMinimumHealthyPercent)),
}

input.DeploymentConfiguration.MaximumPercent = aws.Int64(int64(d.Get("deployment_maximum_percent").(int)))
input.DeploymentConfiguration.MinimumHealthyPercent = aws.Int64(int64(deploymentMinimumHealthyPercent))
input.DesiredCount = aws.Int64(int64(d.Get("desired_count").(int)))
}

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

if v, ok := d.GetOk("cluster"); ok {
input.Cluster = aws.String(v.(string))
}

if v, ok := d.GetOk("health_check_grace_period_seconds"); ok {
input.HealthCheckGracePeriodSeconds = aws.Int64(int64(v.(int)))
}

if v, ok := d.GetOk("iam_role"); ok {
input.Role = aws.String(v.(string))
}

if v, ok := d.GetOk("launch_type"); ok {
input.LaunchType = aws.String(v.(string))
// When creating a service that uses the EXTERNAL deployment controller,
Expand All @@ -501,26 +526,10 @@ func resourceServiceCreate(d *schema.ResourceData, meta interface{}) error {
}
}

if v, ok := d.GetOk("propagate_tags"); ok {
input.PropagateTags = aws.String(v.(string))
}

if v, ok := d.GetOk("platform_version"); ok {
input.PlatformVersion = aws.String(v.(string))
}

input.CapacityProviderStrategy = expandCapacityProviderStrategy(d.Get("capacity_provider_strategy").(*schema.Set))

loadBalancers := expandLoadBalancers(d.Get("load_balancer").(*schema.Set).List())
if len(loadBalancers) > 0 {
log.Printf("[DEBUG] Adding ECS load balancers: %s", loadBalancers)
input.LoadBalancers = loadBalancers
}
if v, ok := d.GetOk("iam_role"); ok {
input.Role = aws.String(v.(string))
}

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

if v, ok := d.GetOk("ordered_placement_strategy"); ok {
ps, err := expandPlacementStrategy(v.([]interface{}))
Expand All @@ -542,6 +551,14 @@ func resourceServiceCreate(d *schema.ResourceData, meta interface{}) error {
input.PlacementConstraints = pc
}

if v, ok := d.GetOk("platform_version"); ok {
input.PlatformVersion = aws.String(v.(string))
}

if v, ok := d.GetOk("propagate_tags"); ok {
input.PropagateTags = aws.String(v.(string))
}

if v, ok := d.GetOk("service_connect_configuration"); ok && len(v.([]interface{})) > 0 {
input.ServiceConnectConfiguration = expandServiceConnectConfiguration(v.([]interface{}))
}
Expand Down Expand Up @@ -569,12 +586,14 @@ func resourceServiceCreate(d *schema.ResourceData, meta interface{}) error {
input.ServiceRegistries = srs
}

if v, ok := d.GetOk("task_definition"); ok {
input.TaskDefinition = aws.String(v.(string))
}

if len(tags) > 0 {
input.Tags = Tags(tags.IgnoreAWS()) // tags field doesn't exist in all partitions
}

log.Printf("[DEBUG] Creating ECS Service: %s", input)

output, err := serviceCreateWithRetry(conn, input)

// Some partitions (i.e., ISO) may not support tag-on-create
Expand All @@ -586,14 +605,9 @@ func resourceServiceCreate(d *schema.ResourceData, meta interface{}) error {
}

if err != nil {
return fmt.Errorf("error creating ECS service (%s): %w", d.Get("name").(string), err)
}

if output == nil || output.Service == nil {
return fmt.Errorf("error creating ECS service: empty response")
return fmt.Errorf("creating ECS Service (%s): %w", d.Get("name").(string), err)
}

log.Printf("[DEBUG] ECS service created: %s", aws.StringValue(output.Service.ServiceArn))
d.SetId(aws.StringValue(output.Service.ServiceArn))

cluster := d.Get("cluster").(string)
Expand Down Expand Up @@ -707,6 +721,14 @@ func resourceServiceRead(d *schema.ResourceData, meta interface{}) error {
d.Set("deployment_maximum_percent", service.DeploymentConfiguration.MaximumPercent)
d.Set("deployment_minimum_healthy_percent", service.DeploymentConfiguration.MinimumHealthyPercent)

if service.DeploymentConfiguration.Alarms != nil {
if err := d.Set("alarms", []interface{}{flattenAlarms(service.DeploymentConfiguration.Alarms)}); err != nil {
return fmt.Errorf("setting alarms: %w", err)
}
} else {
d.Set("alarms", nil)
}

if service.DeploymentConfiguration.DeploymentCircuitBreaker != nil {
if err := d.Set("deployment_circuit_breaker", []interface{}{flattenDeploymentCircuitBreaker(service.DeploymentConfiguration.DeploymentCircuitBreaker)}); err != nil {
return fmt.Errorf("error setting deployment_circuit_break: %w", err)
Expand Down Expand Up @@ -774,13 +796,14 @@ func resourceServiceUpdate(d *schema.ResourceData, meta interface{}) error {

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

if schedulingStrategy == ecs.SchedulingStrategyDaemon {
switch schedulingStrategy {
case 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 {
case ecs.SchedulingStrategyReplica:
bschaatsbergen marked this conversation as resolved.
Show resolved Hide resolved
if d.HasChange("desired_count") {
input.DesiredCount = aws.Int64(int64(d.Get("desired_count").(int)))
}
Expand Down Expand Up @@ -838,6 +861,12 @@ func resourceServiceUpdate(d *schema.ResourceData, meta interface{}) error {
}
}

if d.HasChange("alarms") {
if v, ok := d.GetOk("alarms"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil {
input.DeploymentConfiguration.Alarms = expandAlarms(v.([]interface{})[0].(map[string]interface{}))
}
}

if d.HasChange("platform_version") {
input.PlatformVersion = aws.String(d.Get("platform_version").(string))
}
Expand Down Expand Up @@ -884,7 +913,6 @@ func resourceServiceUpdate(d *schema.ResourceData, meta interface{}) error {
input.ServiceRegistries = expandServiceRegistries(d.Get("service_registries").([]interface{}))
}

log.Printf("[DEBUG] Updating ECS Service (%s): %s", d.Id(), input)
// Retry due to IAM eventual consistency
err := resource.Retry(propagationTimeout+serviceUpdateTimeout, func() *resource.RetryError {
_, err := conn.UpdateService(input)
Expand Down Expand Up @@ -1082,6 +1110,50 @@ func capacityProviderStrategyForceNew(d *schema.ResourceDiff) error {
return nil
}

func expandAlarms(tfMap map[string]interface{}) *ecs.DeploymentAlarms {
if tfMap == nil {
return nil
}

apiObject := &ecs.DeploymentAlarms{}

if v, ok := tfMap["enable"].(bool); ok {
apiObject.Enable = aws.Bool(v)
}

if v, ok := tfMap["enable"].(bool); ok {
apiObject.Rollback = aws.Bool(v)
}

if v, ok := tfMap["alarm_names"].(*schema.Set); ok && v.Len() > 0 {
apiObject.AlarmNames = flex.ExpandStringSet(v)
}

return apiObject
}

func flattenAlarms(apiObject *ecs.DeploymentAlarms) map[string]interface{} {
if apiObject == nil {
return nil
}

tfMap := map[string]interface{}{}

if v := apiObject.AlarmNames; v != nil {
tfMap["alarm_names"] = aws.StringValueSlice(v)
}

if v := apiObject.Enable; v != nil {
tfMap["enable"] = aws.BoolValue(v)
}

if v := apiObject.Rollback; v != nil {
tfMap["rollback"] = aws.BoolValue(v)
}

return tfMap
}

func expandDeploymentController(l []interface{}) *ecs.DeploymentController {
if len(l) == 0 || l[0] == nil {
return nil
Expand Down
75 changes: 75 additions & 0 deletions internal/service/ecs/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func TestAccECSService_basic(t *testing.T) {
Config: testAccServiceConfig_basic(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckServiceExists(resourceName, &service),
resource.TestCheckResourceAttr(resourceName, "alarms.#", "0"),
resource.TestCheckResourceAttr(resourceName, "service_registries.#", "0"),
resource.TestCheckResourceAttr(resourceName, "scheduling_strategy", "REPLICA"),
),
Expand All @@ -45,6 +46,7 @@ func TestAccECSService_basic(t *testing.T) {
Config: testAccServiceConfig_modified(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckServiceExists(resourceName, &service),
resource.TestCheckResourceAttr(resourceName, "alarms.#", "0"),
resource.TestCheckResourceAttr(resourceName, "service_registries.#", "0"),
resource.TestCheckResourceAttr(resourceName, "scheduling_strategy", "REPLICA"),
),
Expand Down Expand Up @@ -486,6 +488,28 @@ func TestAccECSService_DeploymentControllerType_external(t *testing.T) {
})
}

func TestAccECSService_Alarms(t *testing.T) {
var service 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_alarms(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckServiceExists(resourceName, &service),
resource.TestCheckResourceAttr(resourceName, "alarms.#", "1"),
),
},
},
})
}

func TestAccECSService_DeploymentValues_basic(t *testing.T) {
var service ecs.Service
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
Expand Down Expand Up @@ -2559,6 +2583,57 @@ resource "aws_ecs_service" "test" {
`, rName)
}

func testAccServiceConfig_alarms(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" {
name = %[1]q
cluster = aws_ecs_cluster.default.id
task_definition = aws_ecs_task_definition.test.arn
desired_count = 1

alarms {
enable = true
rollback = true
alarm_names = [
aws_cloudwatch_metric_alarm.test.alarm_name
]
}
}

resource "aws_cloudwatch_metric_alarm" "test" {
alarm_name = %[1]q
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = "2"
metric_name = "CPUReservation"
namespace = "AWS/ECS"
period = "120"
statistic = "Average"
threshold = "80"
insufficient_data_actions = []
}
`, rName)
}

func testAccServiceConfig_deploymentValues(rName string) string {
return fmt.Sprintf(`
resource "aws_ecs_cluster" "default" {
Expand Down
Loading