Skip to content

Commit

Permalink
horizontal pod scaling, v1 (#154)
Browse files Browse the repository at this point in the history
* add an autoscaling module for app-level scaling + setup a cpu-based policy as a starting point

* switch to a target tracking policy type

* tweak our variable formatting to support using predefined or custom metrics optionally

* bump desired count / min capacity to 2 + add an ignore so TF doesn't undo the scaling
  • Loading branch information
redterror authored Apr 25, 2023
1 parent b5a2911 commit 458e415
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 2 deletions.
15 changes: 15 additions & 0 deletions tf/envs/staging/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 39 additions & 0 deletions tf/envs/staging/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,42 @@ module "app" {
module "env_defns" {
source = "../../modules/env_defns"
}

module "autoscaling" {
source = "../../modules/autoscale"

env = module.envconfig.env
ecs_cluster = module.envconfig.ecs_cluster
service_name = module.app.service_name

min_capacity = 2
max_capacity = 10

metrics = {
CPUAverage = {
target = 60
predefined_metric = [{
type = "ECSServiceAverageCPUUtilization"
}]
}
MemoryAverage = {
target = 60
predefined_metric = [{
type = "ECSServiceAverageMemoryUtilization"
}]
}
CPUSpike = {
target = 85
customized_metric = [{
metric_name = "CPUUtilization"
namespace = "AWS/ECS"
statistic = "Maximum"
unit = "Percent"
dimensions = {
"ClusterName" = module.envconfig.ecs_cluster
"ServiceName" = module.app.service_name
}
}]
}
}
}
9 changes: 7 additions & 2 deletions tf/modules/app/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ resource "aws_ecs_service" "api" {
name = "capp-api"
cluster = var.ecs_cluster
task_definition = aws_ecs_task_definition.api.arn
desired_count = 1
desired_count = 2
health_check_grace_period_seconds = 30
enable_ecs_managed_tags = true
propagate_tags = "SERVICE"
Expand Down Expand Up @@ -107,10 +107,15 @@ resource "aws_ecs_service" "api" {
# could explicitly set the strategy to be the default strategy, which would be acceptable.
lifecycle {
ignore_changes = [
capacity_provider_strategy
capacity_provider_strategy,
desired_count
]
}
}
output service_name {
description = "ECS service name"
value = aws_ecs_service.api.name
}

data "aws_region" "current" {}

Expand Down
88 changes: 88 additions & 0 deletions tf/modules/autoscale/autoscale.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Originally inspired by: https://github.com/techservicesillinois/terraform-aws-ecs-service/blob/main/autoscale.tf

data "aws_partition" "current" {}
data "aws_region" "current" {}
data "aws_caller_identity" "current" {}

resource "aws_iam_role" "ecs-autoscale-role" {
name = format("%s-%s-app-scaling", var.env, var.service_name)

assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "application-autoscaling.amazonaws.com"
},
"Effect": "Allow"
}
]
}
EOF
}

resource "aws_iam_role_policy_attachment" "ecs-autoscale" {
role = aws_iam_role.ecs-autoscale-role.id
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceAutoscaleRole"
}

resource "aws_appautoscaling_target" "default" {
max_capacity = var.max_capacity
min_capacity = var.min_capacity
resource_id = format("service/%s/%s", var.ecs_cluster, var.service_name)
role_arn = aws_iam_role.ecs-autoscale-role.arn
scalable_dimension = "ecs:service:DesiredCount"
service_namespace = "ecs"
}

# Target tracking policy
resource "aws_appautoscaling_policy" "default" {
for_each = var.metrics

name = format("ecs-target-%s-%s", var.service_name, lower(each.key))
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.default.resource_id
scalable_dimension = aws_appautoscaling_target.default.scalable_dimension
service_namespace = aws_appautoscaling_target.default.service_namespace

target_tracking_scaling_policy_configuration {
target_value = each.value.target
disable_scale_in = each.value.disable_scale_in
scale_in_cooldown = each.value.scale_in_cooldown
scale_out_cooldown = each.value.scale_out_cooldown

dynamic predefined_metric_specification {
for_each = each.value.predefined_metric
iterator = metric

content {
predefined_metric_type = metric.value.type
}
}

dynamic customized_metric_specification {
for_each = each.value.customized_metric
iterator = metric

content {
metric_name = metric.value.metric_name
namespace = metric.value.namespace
statistic = metric.value.statistic
unit = metric.value.unit

dynamic dimensions {
for_each = metric.value.dimensions
iterator = dim

content {
name = dim.key
value = dim.value
}
}
}
}
}
}

51 changes: 51 additions & 0 deletions tf/modules/autoscale/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
variable env {
type = string
description = "Environment name"
}

variable ecs_cluster {
type = string
}

variable service_name {
type = string
}

variable min_capacity {
type = number
default = 1
}

variable max_capacity {
type = number
default = 1
}

variable "metrics" {
description = "Autoscaling metrics configuration"
type = map(
object({
target = string
disable_scale_in = optional(bool, false)
scale_in_cooldown = optional(number, 120)
scale_out_cooldown = optional(number, 120)

predefined_metric = optional(list(object({
type = string
resource_label = optional(string, null)
})), [])
customized_metric = optional(list(object({
metric_name = string
namespace = string
statistic = optional(string, "Average")
unit = optional(string, null)
dimensions = optional(map(string), {})
})), [])
})
)
default = null
validation {
condition = var.metrics == null || try(length(var.metrics) > 0, true)
error_message = "The 'metrics' block must have one or more metrics"
}
}

0 comments on commit 458e415

Please sign in to comment.