How To Create a Dynamic AWS ECS Cluster With Terraform
By: Liam Hartley
Tags: Terraform, Software Engineering, AWS, Software Development, Cloud Computing

0. Networking

The definition of the actual ECS cluster does not have any networking requirements.

resource "aws_ecs_cluster" "cluster" {

name = "${var.environment}-cluster"

tags = var.tags

}

This is because the networking features are defined at the service level as these are the elastic components that host the tasks.

ECS overview

Our network requirements are specified in the service definition:

resource "aws_ecs_service" "fargate-microservices" {

for_each = var.create_microservices == true ? var.fargate_microservices : {}

name = each.value["name"]

cluster = aws_ecs_cluster.cluster.id

desired_count = each.value["desired_count"]

launch_type = each.value["launch_type"]

depends_on = [aws_ecs_cluster.cluster,

aws_ecs_task_definition.ecs_tasks]

task_definition = each.value["task_definition"]



network_configuration {

subnets = var.ecs_service_subnets

security_groups = [aws_security_group.ecs_security_groups[each.value["security_group_mapping"]].id]

}



lifecycle {

ignore_changes = [

task_definition

]

}

}

There’s a lot to unpack here, I’m going to start by focusing on the network configuration.

network_configuration {

subnets = var.ecs_service_subnets

security_groups = [aws_security_group.ecs_security_groups[each.value["security_group_mapping"]].id]

}

In this instance, the (private) subnets are inherited from a module which defines our VPC, when creating the dynamic cluster we simply reference our list of subnets in the VPC module in order to build the services into our desired VPC.

The security groups make use of the ‘for_each’ function in Terraform that underpins a lot of the mechanics of this module.

This functionality allows us to build out multiple resources that share the same arguments. The module simply requires for_each to be defined within the resource and then a ‘map’ variable to be passed into the specified argument.

for_each = var.create_microservices == true ? var.fargate_microservices : {}

In this case, we’ve specified that create_microservices must be true in order to create our services. Then the fargate_microservices variable is a map that contains all arguments required for our service (an example can be found in the ‘dynamic services’ chapter).

Now that we understand the for_each loop, here is the code within the module which allows you to define an arbitrary amount of security groups.

resource "aws_security_group" "ecs_security_groups" {

vpc_id = var.vpc_id



for_each = var.security_groups

name = "${var.environment}-${each.value["ingress_port"]}"



ingress {

from_port = each.value["ingress_port"]

to_port = each.value["ingress_port"]

protocol = each.value["ingress_protocol"]

cidr_blocks = each.value["ingress_cidr_blocks"]

}



egress {

from_port = each.value["egress_port"]

to_port = each.value["egress_port"]

protocol = each.value["egress_protocol"]

cidr_blocks = each.value["egress_cidr_blocks"]

}



tags = var.tags

}

This resource block will iterate over your ‘var.security_groups’ object which is defined outside of the module and will pick up a variable for each variable that is prefixed with ‘each.value’.

This is how you define a single security group outside of the module:

"ecs_security_groups": {

"prod-ecs-sg": {

"ingress_port": "redacted",

"ingress_protocol": "redacted",

"ingress_cidr_blocks": [

"redacted"

],

"egress_port": "redacted",

"egress_protocol": "redacted",

"egress_cidr_blocks": [

"redacted"

]

}

}

These are then mapped to their respective services using the ‘security_group_mapping’ variable inside the service, the variable matches the id of this security group (prod-ecs-sg) to the specified service. From here it is simple to add another security group by appending this map object.

Now we’ll revisit the service definition to see how the security_group_mapping variable is used in conjunction with the other dynamic variables.