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

[#137] Add AutoScaling feature to ECS cluster #187

Merged
merged 14 commits into from
Apr 28, 2023
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
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Team
# @hoangmirs is the Team Lead and the others are team members
* @hoangmirs @andyduong1920 @bterone @byhbt @longnd @malparty @Nihisil @nvminhtue @rosle
* @hoangmirs @andyduong1920 @bterone @byhbt @longnd @malparty @Nihisil @nvminhtue @rosle @liamstevens111

# Engineering Leads
CODEOWNERS @nimblehq/engineering-leads
127 changes: 110 additions & 17 deletions skeleton/aws/modules/ecs/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,20 @@ locals {

container_definitions = templatefile("${path.module}/service.json.tftpl", local.container_vars)

ecs_task_execution_ssm_policy = {
ecs_task_execution_assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
}
]
})

ecs_task_execution_ssm_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Expand All @@ -41,35 +54,59 @@ locals {
Resource = var.secrets_arns
}
]
}
})

# Required IAM permissions from
# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/service-auto-scaling.html#auto-scaling-IAM
ecs_service_scaling_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Action = [
"application-autoscaling:*",
"ecs:DescribeServices",
"ecs:UpdateService",
"cloudwatch:DescribeAlarms",
"cloudwatch:PutMetricAlarm",
"cloudwatch:DeleteAlarms",
"cloudwatch:DescribeAlarmHistory",
"cloudwatch:DescribeAlarmsForMetric",
"cloudwatch:GetMetricStatistics",
"cloudwatch:ListMetrics",
"cloudwatch:DisableAlarmActions",
"cloudwatch:EnableAlarmActions",
"iam:CreateServiceLinkedRole",
"sns:CreateTopic",
"sns:Subscribe",
"sns:Get*",
"sns:List*"
],
Resource = "*"
}
]
})
}

# Current task definition on AWS including deployments outside terraform (e.g. CI deployments)
data "aws_ecs_task_definition" "task" {
task_definition = aws_ecs_task_definition.main.family
}

data "aws_iam_policy_document" "ecs_task_execution_role" {
version = "2012-10-17"
statement {
sid = ""
effect = "Allow"
actions = ["sts:AssumeRole"]

principals {
type = "Service"
identifiers = ["ecs-tasks.amazonaws.com"]
}
}
resource "aws_iam_policy" "ecs_task_execution_ssm" {
name = "${var.namespace}-ECSTaskExecutionAccessSSMPolicy"
policy = local.ecs_task_execution_ssm_policy
}

resource "aws_iam_policy" "ecs_task_execution_ssm" {
policy = jsonencode(local.ecs_task_execution_ssm_policy)
# tfsec:ignore:aws-iam-no-policy-wildcards
resource "aws_iam_policy" "ecs_task_excution_service_scaling" {
name = "${var.namespace}-ECSAutoScalingPolicy"
policy = local.ecs_service_scaling_policy
}

resource "aws_iam_role" "ecs_task_execution_role" {
name = "${var.namespace}-ecs-execution-role"
assume_role_policy = data.aws_iam_policy_document.ecs_task_execution_role.json
assume_role_policy = local.ecs_task_execution_assume_role_policy
}

resource "aws_iam_role_policy_attachment" "ecs_task_execution_role" {
Expand All @@ -82,6 +119,11 @@ resource "aws_iam_role_policy_attachment" "ecs_task_execution_ssm_policy" {
policy_arn = aws_iam_policy.ecs_task_execution_ssm.arn
}

resource "aws_iam_role_policy_attachment" "ecs_task_excution_service_scaling_policy" {
role = aws_iam_role.ecs_task_execution_role.name
policy_arn = aws_iam_policy.ecs_task_excution_service_scaling.arn
}

resource "aws_ecs_cluster" "main" {
name = "${var.namespace}-ecs-cluster"
}
Expand Down Expand Up @@ -120,4 +162,55 @@ resource "aws_ecs_service" "main" {
container_name = var.namespace
container_port = var.app_port
}

# Allow external changes without Terraform plan to the desired_count as it can be changed by Autoscaling
lifecycle {
ignore_changes = [desired_count]
}
}

resource "aws_appautoscaling_target" "main" {
max_capacity = var.max_instance_count
min_capacity = var.min_instance_count
resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.main.name}"
scalable_dimension = "ecs:service:DesiredCount"
service_namespace = "ecs"
}

resource "aws_appautoscaling_policy" "memory_policy" {
name = "${var.namespace}-appautoscaling-memory-policy"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.main.resource_id
scalable_dimension = aws_appautoscaling_target.main.scalable_dimension
service_namespace = aws_appautoscaling_target.main.service_namespace

target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageMemoryUtilization"
}

scale_in_cooldown = var.scale_in_cooldown_period
scale_out_cooldown = var.scale_out_cooldown_period

target_value = var.autoscaling_target_memory_percentage
}
}

resource "aws_appautoscaling_policy" "cpu_policy" {
name = "${var.namespace}-appautoscaling-cpu-policy"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.main.resource_id
scalable_dimension = aws_appautoscaling_target.main.scalable_dimension
service_namespace = aws_appautoscaling_target.main.service_namespace

target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}

scale_in_cooldown = var.scale_in_cooldown_period
scale_out_cooldown = var.scale_out_cooldown_period

target_value = var.autoscaling_target_cpu_percentage
}
}
30 changes: 30 additions & 0 deletions skeleton/aws/modules/ecs/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,33 @@ variable "secrets_arns" {
description = "The ARNs of the SSM Parameter Store parameters"
type = list(string)
}

variable "min_instance_count" {
description = "Autoscaling minimum instance count"
type = number
}

variable "max_instance_count" {
description = "Autoscaling maximum instance count"
type = number
}

variable "autoscaling_target_cpu_percentage" {
description = "Autoscaling target CPU percentage"
type = number
}

variable "autoscaling_target_memory_percentage" {
description = "Autoscaling target memory percentage"
type = number
}

variable "scale_in_cooldown_period" {
description = "The minimum time (in seconds) between two scaling-in activities"
default = 300
}

variable "scale_out_cooldown_period" {
description = "The minimum time (in seconds) between two scaling-out activities"
default = 300
}
2 changes: 1 addition & 1 deletion skeleton/aws/modules/ssm/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ locals {
secret_arns = zipmap(local.secret_names, local.parameter_store_arns)

# Create the formatted secrets for ECS task definition
secrets_variables = [for secret_key, secret_arn in local.secrets_name_arn_map :
secrets_variables = [for secret_key, secret_arn in local.secret_arns :
hoangmirs marked this conversation as resolved.
Show resolved Hide resolved
tomap({ "name" = upper(secret_key), "valueFrom" = secret_arn })
]
}
2 changes: 1 addition & 1 deletion src/commands/generate/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ describe('Generator command', () => {

it('displays the success message', () => {
expect(stdoutSpy).toHaveBeenCalledWith(
'The infrastructure has been generated!\n'
"The infrastructure code was generated at 'aws-advanced-test'\n"
);
});
});
Expand Down
4 changes: 3 additions & 1 deletion src/commands/generate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ export default class Generator extends Command {

await this.postProcess(generalOptions);

this.log('The infrastructure has been generated!');
this.log(
`The infrastructure code was generated at '${generalOptions.projectName}'`
);
} catch (error) {
remove('/', generalOptions.projectName);
console.error(error);
Expand Down
2 changes: 1 addition & 1 deletion src/templates/addons/versionControl/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const versionControlChoices = [
{
type: 'list',
name: 'versionControl',
message: 'Enable version control for this project',
message: 'Which version control hosting would you like to use?',
choices: [
{
value: 'github',
Expand Down
28 changes: 20 additions & 8 deletions src/templates/aws/addons/ecs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const ecsVariablesContent = dedent`
type = string
}

variable "ecs" {
variable "ecs_config" {
description = "ECS input variables"
type = object({
task_cpu = number
Expand All @@ -29,6 +29,12 @@ const ecsVariablesContent = dedent`
task_container_memory = number
deployment_maximum_percent = number
deployment_minimum_healthy_percent = number

# Auto-scaling
min_instance_count = number
max_instance_count = number
autoscaling_target_cpu_percentage = number
autoscaling_target_memory_percentage = number
})
}

Expand Down Expand Up @@ -75,13 +81,19 @@ const ecsModuleContent = dedent`
ecr_tag = var.ecr_tag
security_groups = module.security_group.ecs_security_group_ids
alb_target_group_arn = module.alb.alb_target_group_arn
aws_cloudwatch_log_group_name = module.log.aws_cloudwatch_log_group_name
desired_count = var.ecs.task_desired_count
cpu = var.ecs.task_cpu
memory = var.ecs.task_memory
deployment_maximum_percent = var.ecs.deployment_maximum_percent
deployment_minimum_healthy_percent = var.ecs.deployment_minimum_healthy_percent
container_memory = var.ecs.task_container_memory
aws_cloudwatch_log_group_name = module.cloudwatch.aws_cloudwatch_log_group_name
hoangmirs marked this conversation as resolved.
Show resolved Hide resolved
desired_count = var.ecs_config.task_desired_count
cpu = var.ecs_config.task_cpu
memory = var.ecs_config.task_memory
deployment_maximum_percent = var.ecs_config.deployment_maximum_percent
deployment_minimum_healthy_percent = var.ecs_config.deployment_minimum_healthy_percent
container_memory = var.ecs_config.task_container_memory

# Auto-scaling
min_instance_count = var.ecs_config.min_instance_count
max_instance_count = var.ecs_config.max_instance_count
autoscaling_target_cpu_percentage = var.ecs_config.autoscaling_target_cpu_percentage
autoscaling_target_memory_percentage = var.ecs_config.autoscaling_target_memory_percentage

environment_variables = var.environment_variables
secrets_variables = module.ssm.secrets_variables
Expand Down