Skip to content

Commit

Permalink
Merge pull request #28521 from bschaatsbergen/ecs-deployment-alarms
Browse files Browse the repository at this point in the history
Add DeploymentAlarms to ECS Service
  • Loading branch information
ewbankkit authored Jan 4, 2023
2 parents 037b561 + 01f7b6b commit ec2b82f
Show file tree
Hide file tree
Showing 4 changed files with 226 additions and 50 deletions.
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:
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

0 comments on commit ec2b82f

Please sign in to comment.