Includes scale in (decrease) and scale out (increase) in CloudFormation format.
https://github.com/nathanpeck/ecs-cloudformation/blob/master/service/service-ec2-public-lb.yml
Includes security groups for load balancer, ecs, etc.
ecs_execution_role: Optional, required if using container secrets. Task execution role that the Amazon ECS container agent and the Docker daemon can assume.
ecs_task_role: IAM role that allows the Amazon ECS container task to make calls to other AWS services.
https://hub.docker.com/r/testcontainers/helloworld
docker pull docker.io/testcontainers/helloworld
http://localhost:8080
http://localhost:8080/ping
http://localhost:8080/uuid
------------------
Push to AWS ECR
Tag image:
docker tag docker.io/testcontainers/helloworld 123456789012.dkr.ecr.eu-west-1.amazonaws.com/myapp_back:latest
Login:
aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.eu-west-1.amazonaws.com
Push:
docker push 123456789012.dkr.ecr.eu-west-1.amazonaws.com/myapp_back:latest
== Force new deployment
aws ecs update-service --cluster dev-myapp-ecs-cluster --service dev-myapp-ecs-service --task-definition dev-myapp_back --force-new-deployment
Note: Ensure the Load Balancing Target Groups health check succeeds (change it if needed)
Eg: dagger
resource "aws_ecs_service"
enable_execute_command = true #(Optional) Whether to enable Amazon ECS Exec for the tasks within the service
resource "aws_ecs_task_definition"
must have block runtime_platform:
runtime_platform { #This block is crucial for enabling executeCommand (OS & arch)
operating_system_family = "LINUX" # or "WINDOWS_SERVER_2019_CORE"
cpu_architecture = "X86_64" # or "ARM64"
}
The ECS task role needs SSM permissions for ECS Exec. Add this to the IAM ECS task role policy:
{
Sid = "ECSExec"
Effect = "Allow"
Action = [
"ssmmessages:CreateControlChannel",
"ssmmessages:CreateDataChannel",
"ssmmessages:OpenControlChannel",
"ssmmessages:OpenDataChannel"
]
Resource = "*"
}
AWS CLI Session Manager plugin
https://docs.aws.amazon.com/systems-manager/latest/userguide/install-plugin-debian-and-ubuntu.html
curl -o "session-manager-plugin.deb" "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb"
sudo dpkg -i session-manager-plugin.deb
(for uninstalling) sudo dpkg -r session-manager-plugin
Verify installation
session-manager-plugin
Interactive shell:
aws ecs execute-command \
--cluster myapp-dev-ecs-cluster \
--task 497e158aeb3243288a36b4fba3dfd2c2 \
--container myapp_back \
--command "/bin/sh" \
--interactive
Find out the public IP:
wget -qO- icanhazip.com
55.176.247.88
Test connectivity to host and port
nc -zv sso.myapp.org 443
modules/ecs/fargate.tf
resource "aws_ecs_task_definition" "task_definition" {
lifecycle {
create_before_destroy = true
}
...
#execution_role_arn (Optional, required if using container secrets) ARN of the task execution role that the Amazon ECS container agent and the Docker daemon can assume
execution_role_arn = var.ecs_execution_role_arn
#task_role_arn (Optional) ARN of IAM role that allows your Amazon ECS container task to make calls to other AWS services
task_role_arn = var.ecs_task_role_arn
requires_compatibilities = ["FARGATE"]
}
Eg: docrepo
modules/ecs-cluster/ecluster.tf
resource "aws_ecs_cluster" "cluster" {
name = var.cluster_name
setting {
name = "containerInsights" #CloudWatch Container Insights
value = "enabled" #{enhanced, enabled, disabled} Additional charges
}
}
resource "aws_ecs_cluster_capacity_providers" "cluster_capacity" {
cluster_name = aws_ecs_cluster.cluster.name
capacity_providers = var.cluster_capacity_providers
default_capacity_provider_strategy {
capacity_provider = var.cluster_default_capacity_provider
weight = 2 #AWS console requires >=1 (default value is 0)
}
}
modules/ecs-cluster/ecapture.tf
Note: It probably needs the cluster containerInsights not to be disabled
It creates an Amazon EventBridge rule that captures ECS events, a CloudWatch log group to store these events, and a resource policy to allow EventBridge to write to CloudWatch Logs.
# ------------------------------------------------------------------------------
# ECS Event Capture (EventBridge + CloudWatch Logs)
# ------------------------------------------------------------------------------
locals {
env = data.aws_default_tags.dt.tags.env
ci = data.aws_default_tags.dt.tags.ci
account_id = data.aws_caller_identity.current.account_id
region = data.aws_region.current.id
}
data "aws_caller_identity" "current" {}
data "aws_default_tags" "dt" {}
data "aws_region" "current" {}
# 1. Create the Log Group where events will be stored
resource "aws_cloudwatch_log_group" "ecs_events" {
#name Must be /aws/events/ecs/containerinsights/${var.cluster_name}/performance
name = "/aws/events/ecs/containerinsights/${var.name_ecs_cluster}/performance"
retention_in_days = var.log_retention_in_days
tags = {
ClusterName = var.name_ecs_cluster
EventBridge-AssociatedRuleName = aws_cloudwatch_event_rule.ecs_event_capture.name
}
}
# 2. Define the EventBridge Rule to capture ECS events
resource "aws_cloudwatch_event_rule" "ecs_event_capture" {
name = format("EventsToLogs-${local.ci}-evntbrdg--%s-${local.env}", "ecscapture") #Must prefix 'EventsToLogs'
description = "Captures ECS Task and Service events for cluster ${var.name_ecs_cluster}"
event_pattern = jsonencode({
source = ["aws.ecs"],
detail = {
clusterArn = [aws_ecs_cluster.cluster.arn]
}
})
}
# 3. Set the Log Group as the target for the Rule
resource "aws_cloudwatch_event_target" "ecs_events_target" {
rule = aws_cloudwatch_event_rule.ecs_event_capture.name
target_id = "SendToCloudWatch"
arn = aws_cloudwatch_log_group.ecs_events.arn
}
# 4. Allow EventBridge to write to CloudWatch Logs
# DOC. The LimitExceededException for a CloudWatch Logs Resource Policy
# means you've hit the hard limit of 10 resource policies per AWS Region.
resource "aws_cloudwatch_log_resource_policy" "eventbridge_to_cw" {
count = var.create_cw_log_resource_policy ? 1 : 0
policy_name = "EventBridgeCloudWatchLogs" # Don't change, to avoid LimitExceededException
policy_document = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "EventBridgeToCloudWatchLogs" # Don't change, to avoid LimitExceededException
Effect = "Allow"
Principal = { Service = "events.amazonaws.com" }
Action = [
"logs:CreateLogStream",
"logs:PutLogEvents"
]
Resource = "arn:aws:logs:${local.region}:${local.account_id}:log-group:/aws/events/ecs/containerinsights/*:*" # Don't change, to avoid LimitExceededException
Condition = {
StringEquals = {
"aws:SourceAccount" = local.account_id
}
ArnLike = {
"aws:SourceArn" = "arn:aws:events:${local.region}:${local.account_id}:rule/EventsToLogs*" # Don't change, to avoid LimitExceededException
}
}
}
]
})
}
Tested w/ Terraform AWS provider v5.x.x
main.tf
###Scale mem false to disable or true to enable
is_mem_scale = true #scale task by memory usage
mem_target_value = 40 #target value of scale task by memory usage (%)
mem_scale_in_cooldown = 300 #cool down time of scale in (decrease) task by memory usage
mem_scale_out_cooldown = 300 #cool down time of scale out (increase) task by memory usage
###Scale cpu false to disable or true to enable
is_cpu_scale = true #scale task by cpu usage
cpu_target_value = 40 #target value of scale task by cpu usage (%)
cpu_scale_in_cooldown = 300 #cool down time of scale in (decrease) task by cpu usage
cpu_scale_out_cooldown = 300 #cool down time of scale out (increase) task by cpu usage
###Time inactivity to desire
inactivity = true
ecs.tf
# -------------------------------------
# Auto Scaling Target
# -------------------------------------
resource "aws_appautoscaling_target" "main" {
count = var.inactivity || var.is_cpu_scale || var.is_mem_scale ? 1 : 0
depends_on = [aws_ecs_service.service]
max_capacity = var.task_max_count
min_capacity = var.task_desired_count
resource_id = "service/${var.ecs_cluster_name}/${var.ecs_service_name}"
scalable_dimension = "ecs:service:DesiredCount"
service_namespace = "ecs"
}
# ----------------------------------------
# Auto Scaling Policy (ECS desired tasks)
# ----------------------------------------
resource "aws_appautoscaling_policy" "cpu_scale_down" {
count = var.is_cpu_scale ? 1 : 0
depends_on = [aws_appautoscaling_target.main]
name = "scale/${local.env}/${local.ci}/down"
policy_type = "StepScaling" #"TargetTrackingScaling"
resource_id = aws_appautoscaling_target.main[0].resource_id
scalable_dimension = aws_appautoscaling_target.main[0].scalable_dimension
service_namespace = aws_appautoscaling_target.main[0].service_namespace
step_scaling_policy_configuration {
adjustment_type = "ChangeInCapacity"
step_adjustment {
metric_interval_upper_bound = 0
scaling_adjustment = -1
}
metric_aggregation_type = "Maximum"
cooldown = var.cpu_scale_in_cooldown
}
}
resource "aws_appautoscaling_policy" "cpu_scale_up" {
count = var.is_cpu_scale ? 1 : 0
depends_on = [aws_appautoscaling_target.main]
name = "scale/${local.env}/${local.ci}/up"
policy_type = "StepScaling" #"TargetTrackingScaling"
resource_id = aws_appautoscaling_target.main[0].resource_id
scalable_dimension = aws_appautoscaling_target.main[0].scalable_dimension
service_namespace = aws_appautoscaling_target.main[0].service_namespace
step_scaling_policy_configuration {
adjustment_type = "ChangeInCapacity"
step_adjustment {
metric_interval_lower_bound = 0
metric_interval_upper_bound = 15
scaling_adjustment = 1
}
step_adjustment {
metric_interval_lower_bound = 15
metric_interval_upper_bound = 25
scaling_adjustment = 2
}
step_adjustment {
metric_interval_lower_bound = 25
scaling_adjustment = 3
}
metric_aggregation_type = "Maximum"
cooldown = var.cpu_scale_out_cooldown
}
}
resource "aws_cloudwatch_metric_alarm" "low_cpu_usage_alarm" {
count = var.is_cpu_scale ? 1 : 0
alarm_name = "${local.env}-${local.ci}-low-cpu-fargate"
comparison_operator = "LessThanOrEqualToThreshold"
evaluation_periods = "4"
metric_name = "CPUUtilization"
namespace = "AWS/ECS"
period = "10" # In seconds. Valid values are 10, 30, or any multiple of 60
statistic = "Average"
threshold = var.cpu_target_value
dimensions = {
"ClusterName" = aws_ecs_cluster.jboss.name
"ServiceName" = aws_ecs_service.service.name
}
alarm_description = "Low CPU utilization for service ${local.env}-${local.ci}"
alarm_actions = [aws_appautoscaling_policy.cpu_scale_down[0].arn]
insufficient_data_actions = [] #(Optional)
}
resource "aws_cloudwatch_metric_alarm" "high_cpu_usage_alarm" {
count = var.is_cpu_scale ? 1 : 0
alarm_name = "${local.env}-${local.ci}-high-cpu-fargate"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = "4"
metric_name = "CPUUtilization"
namespace = "AWS/ECS"
period = "10" # In seconds. Valid values are 10, 30, or multiple of 60
statistic = "Maximum"
threshold = var.cpu_target_value
dimensions = {
"ClusterName" = aws_ecs_cluster.jboss.name
"ServiceName" = aws_ecs_service.service.name
}
alarm_description = "High CPU utilization for service ${local.env}-${local.ci}"
alarm_actions = [aws_appautoscaling_policy.cpu_scale_up[0].arn]
insufficient_data_actions = [] #(Optional)
}
# -------------------------------------
# Auto Scaling Policy (Memory)
# -------------------------------------
resource "aws_appautoscaling_policy" "mem" {
count = var.is_mem_scale ? 1 : 0
depends_on = [aws_appautoscaling_target.main]
name = "memory"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.main[0].resource_id
scalable_dimension = aws_appautoscaling_target.main[0].scalable_dimension
service_namespace = aws_appautoscaling_target.main[0].service_namespace
target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageMemoryUtilization"
}
target_value = var.mem_target_value
scale_in_cooldown = var.mem_scale_in_cooldown
scale_out_cooldown = var.mem_scale_out_cooldown
}
}
resource "aws_ecs_service" "service" {
//DOC. make sure to set depends_on to the related aws_iam_role_policy
depends_on = [aws_ecs_task_definition.task_definition]
lifecycle {
ignore_changes = [
desired_count,
task_definition,
]
}
name = var.name_ecs_service
cluster = aws_ecs_cluster.cluster.id
deployment_circuit_breaker {
enable = true
rollback = true
}
task_definition = "${aws_ecs_task_definition.task_definition.family}:${aws_ecs_task_definition.task_definition.revision}"
desired_count = var.task_desire_count
enable_execute_command = true #(Optional) Whether to enable Amazon ECS Exec for the tasks within the service
# launch_type = "FARGATE"
load_balancer { #Use multiple load_balancer blocks for multiple LB target groups
target_group_arn = var.lb_target_group_external_arn
container_name = var.container_name
container_port = 8080
}
load_balancer {
target_group_arn = var.lb_target_group_internal_arn
container_name = var.container_name
container_port = 8080
}
network_configuration {
security_groups = [aws_security_group.sg_ecs.id]
subnets = var.subnets_ids
}
capacity_provider_strategy {
capacity_provider = "FARGATE"
weight = "1"
base = "1"
}
capacity_provider_strategy {
capacity_provider = "FARGATE_SPOT"
weight = "2"
}
propagate_tags = "SERVICE" #(Optional) SERVICE, TASK_DEFINITION
tags = {
Name = var.name_ecs_service,
Schedule = var.schedule
}
}