terraform-aws-ecs: one module for clusters, services and capacity
Terraform module to create AWS ECS resources 🇺🇦
At a glance
- What is it?
- The terraform-aws-ecs module creates ECS clusters and services with Fargate, Fargate Spot or EC2 capacity, container definitions with FireLens sidecars, Service Connect, load balancers and security group rules. Its most useful section is the known limitations block, which explains why Terraform can undo a task definition your pipeline deployed.
- Who is it for?
- Adopt terraform-aws-ecs if you want a reviewed, versioned implementation of the ECS resources rather than a folder of aws_ecs resources, and if you will use the module's ignore_task_definition_changes handling because your delivery pipeline deploys task definitions outside Terraform.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 28 days ago.
- What is it written in?
- Mainly HCL, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the module creates and for whom
The module is a Terraform implementation of AWS Elastic Container Service resources, published in the Terraform registry as terraform-aws-modules/ecs/aws and written in HCL. The available features are stated compactly: an ECS cluster with Fargate or EC2 Auto Scaling capacity providers, an ECS service with task definition, task set and container definition support, and either separate sub-modules or an integrated module covering cluster and service. That list is the whole scope, and it matters for the audience question. This is not a container platform, it is the AWS service expressed as Terraform resources, and the people who benefit are the platform engineers who would otherwise write the same cluster, service and task definition resources by hand for every environment. The repository layout confirms the shape of the project. There is a main.tf, variables.tf, outputs.tf and versions.tf at the top level, a modules directory for the sub-modules, a wrappers directory, a docs directory with the design document, an examples directory, and the usual release plumbing such as a .releaserc.json and a pre-commit configuration. The licence is Apache-2.0, and the release history shows a steady line of minor versions, with v7.5.1 on 2026-08-08, v7.6.0 on 2026-08-09 and v7.6.1 on 2026-09-18, and the last push on 2026-09-25.
One integrated call, or the sub-modules separately
The module can be consumed in two ways, and the README states the choice plainly. Resources can be created through individual sub-modules, or through a single module that creates both the cluster and the service resources. The integrated form is the one the main usage example shows, with the cluster attributes and the service map passed in the same call, and the docs point at the design document for the details. The sub-module route exists because the two halves are often owned by different teams and deployed on different schedules, which is the usual reason a cluster outlives the services that run on it. The second reason is granularity of state: if a team only needs a cluster, or only needs a service in a cluster that exists, the separate sub-modules let them express that without taking a dependency on resources they do not manage. The example directory in the repository follows the same split in practice, with a complete example for the integrated form, and separate examples for a container definition, an EC2 Auto Scaling capacity provider, an express service, a Fargate capacity provider, and a cluster with the ECS managed instances capacity provider. Those directories are the fastest way to see which shape fits your case.
Capacity providers and the weight and base strategy
The cluster configuration in the usage example shows how capacity is expressed, and it is where the operational decisions live. The module takes a list of cluster capacity providers, and the example enables FARGATE and FARGATE_SPOT, and a default capacity provider strategy that assigns a weight and an optional base to each entry.
module "ecs" {
source = "terraform-aws-modules/ecs/aws"
cluster_name = "ecs-integrated"
cluster_capacity_providers = ["FARGATE", "FARGATE_SPOT"]
default_capacity_provider_strategy = {
FARGATE = {
weight = 50
base = 20
}
FARGATE_SPOT = {
weight = 50
}
}The shape of that block is the shape of the ECS API, which is a good sign for a module in this position, because the inputs read the way the underlying service does. A base of 20 with a weight of 50 means twenty tasks are placed on Fargate before the weighted split applies to the rest, and a provider with no base only receives its share. If you also want the cluster to run shell commands into containers, the cluster configuration carries an execute command configuration with a logging mode of OVERRIDE and a CloudWatch log group name, which in the example points at an EC2 style log group path. The module does not decide that for you, and the detail that the logging override has to name a log group you have already created is the kind of thing that is easier to get wrong than the rest.
Container definitions with a FireLens sidecar for logs
A service in this module carries its container definitions as a map keyed by container name, and the example shows two containers in one task, a log shipper and the application. The shipper is a fluent-bit container with its own cpu and memory allocation, marked essential, using an image from a private ECR repository and a firelensConfiguration with a type of fluentbit. The application container, ecs-sample, has a port mapping for container port 80 over tcp, and it is wired to depend on the shipper starting first through a dependsOn block naming the container and a condition of START. The log routing is where the module earns its keep, because the application container sets enable_cloudwatch_logging to false and instead supplies a logConfiguration with a logDriver of awsfirelens, whose options name Firehose as the delivery target, a region, a delivery stream, and a log driver buffer limit. The result is that CloudWatch logging is bypassed for that container and the FireLens sidecar forwards instead. The example also turns off the read only root filesystem, with a comment explaining that the sample image needs to write to the root filesystem, and sets memoryReservation values alongside the hard memory limits, which is the ECS pattern for reserving capacity and giving it back under pressure.
Service Connect, load balancers and explicit security group rules
The remaining service inputs in the example are the pieces that make a service reachable and keep it private. The service connect configuration gives the service a namespace and a service block with a client alias mapping a port and a dns name, plus a port name and a discovery name, which is how containers find each other by name inside the cluster rather than by IP. The load balancer block maps the service to a target group by ARN and names the container and container port that traffic should land on, so the target group in the example is a literal ARN in eu-west-1. The networking inputs are explicit rather than implicit. Subnet identifiers are passed as a list, the ingress rules are a map keyed by a label, in the example alb_3000, each with a description, a from_port, an ip_protocol of tcp and a referenced security group id, and the egress rules are a single entry with ip_protocol of -1 and cidr_ipv4 of 0.0.0.0/0. The module does not paper over that egress rule, and it should not, because a rule set that only has ingress is a common cause of a service that starts but cannot reach anything, and having it written out in the configuration is the point. Tagging is likewise explicit at the module level, with Environment and Project in the example.
The known limitation that bites pipeline users
The most valuable part of the README is the block marked as known limitations, and it opens by saying these are limitations of Terraform and OpenTofu rather than of the module. The root cause is that Terraform requires lifecycle arguments to be literal values inside the resource block, and the README links the open issues: hashicorp/terraform#18367, open since 2018, and #22544 since 2019, plus opentofu/opentofu#1329 for OpenTofu. The symptom that matters is stated in a heading of its own: Terraform reverts the task definition your pipeline just deployed. The native remedies are two. Set ignore_task_definition_changes to true, which means an existing service moves to a new state address and therefore needs a moved block, or let the pipeline update the Terraform configuration instead of calling the ECS API. In other words, the module does not paper over a conflict between two writers of the same resource, it names it and gives you a choice, and the choice is yours to make before you deploy. The README also documents a commercial alternative in the same block, the compliance.tf service, which serves the module with the rule applied at download time, with the note that the source line changes while inputs and outputs do not, and that pinning stays available by adding a version argument to the URL. The disclosure at the end is worth reading too, since the known limitations text and that service are written by the module's maintainer.
Pinning, upgrading and the shape of the release line
The mechanics of consuming and upgrading the module are simple and worth stating precisely. The source argument names the module and the registry, and a version can be pinned by adding an ampersand and the release number to the URL, which matters because a module that changes the resource graph can force a state migration on the next apply. The examples directory doubles as the upgrade guide, since each of the six examples is a complete, runnable configuration for a different shape, so you can diff your setup against the one that matches yours after a major bump. The known limitations section also tells you where the full workarounds live, with the exact diff each rule makes documented for the module, and the project keeps a CHANGELOG.md and a design document in docs, so the reasoning behind a change is usually recorded. The wrapper around compliance.tf is worth understanding as a pattern rather than an endorsement: it rewrites the source line only, leaves inputs and outputs alone, and can be combined with a version pin, which is a general technique for applying a policy to a shared module without forking it. If you take one operational habit from this repository, take the one about reading the limitations block before every major version bump, because the task definition behaviour is the part that breaks pipelines and it is not obvious from the variable names.
Editorial conclusion
Adopt terraform-aws-ecs if you want a reviewed, versioned implementation of the ECS resources rather than a folder of aws_ecs resources, and if you will use the module's ignore_task_definition_changes handling because your delivery pipeline deploys task definitions outside Terraform. Do not adopt it if your pipeline is the only writer of task definitions and you are not prepared to set that flag, because the module will otherwise revert them, and do not adopt it for a project that needs the underlying ECS resources shaped in a way the inputs do not allow, since the module owns the resource graph. Before you upgrade, read the known limitations block in the README, check the version you pin in the source argument, and decide whether the moved block migration described there is acceptable for your existing state.
Frequently asked questions
What does the terraform-aws-ecs module create?
It creates AWS ECS resources, with an ECS cluster using Fargate or EC2 Auto Scaling capacity providers and an ECS service with task definition, task set and container definition support. You can use it as a single integrated module for cluster and service, or through separate sub-modules.
How do I stop Terraform from reverting a task definition my pipeline deployed?
Set ignore_task_definition_changes to true, which moves the existing service to a new state address and means you also need a moved block, or change the pipeline so it updates the Terraform configuration instead of calling the ECS API. The README notes this comes from Terraform requiring lifecycle arguments to be literal values inside the resource block.
How do I configure Fargate and Fargate Spot in the module?
Pass the provider names in cluster_capacity_providers, for example FARGATE and FARGATE_SPOT, and set default_capacity_provider_strategy with a weight and an optional base for each provider. The weight controls the share of tasks placed on that provider and the base sets how many are placed on it before the weighted split applies.
How does the module handle logging with a FireLens sidecar?
The example in the README runs a fluent-bit container as an essential sidecar with a firelensConfiguration of type fluentbit, sets enable_cloudwatch_logging to false on the application container, and gives that container a logConfiguration with a logDriver of awsfirelens whose options name Firehose, a region, a delivery stream and a buffer limit.
How do I pin a specific version of the module?
Add the version argument to the source URL with an ampersand and the release number, for example an ampersand followed by the version. The README also describes a compliance.tf wrapper that serves the module with the known limitations rule applied at download time, where the source line changes but inputs and outputs stay the same.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/terraform-aws-modules-terraform-aws-ecs)Was this useful?