Skip to content

Microservice communication in AWS ECS without a Load Balancer, using DNS Service Discovery

One day I dreamed about a simple AWS architecture. A few FastAPI microservices, a simple SQLite database (RDS was too expensive for me at this learning stage), and no ALB (Application Load Balancer) cost either.

This time I wanted to experiment with AWS ECS. I needed fast communication between services — reliable and scalable. The standard solution is an Application Load Balancer between microservices.

BUT. I wanted to build this without a Load Balancer, using AWS Cloud Map and DNS-based Service Discovery.


1. The problem: communication without a Load Balancer

How does it usually look?

In a typical AWS ECS project, microservices talk to each other through a Load Balancer. For example, if user-service needs to talk to auth-service, it usually looks like this:

  1. Create an Application Load Balancer (ALB).
  2. Register auth-service as a target group.
  3. Use the ALB address to communicate, for example:
http://auth-alb.mycompany.internal/login

Problems with a Load Balancer

  • Cost — every ALB is an extra, fixed cost. With more services, the cost grows fast. It costs money even when traffic is small, and for a simple POC it was just unnecessary. I almost gave up on this small project, but I didn't — I pushed through instead.
  • Complexity — you have to manage routing, target groups, ports, and health checks.
  • Latency — requests go through the ALB layer, which adds an unnecessary hop, especially inside the same VPC.

What if... no Load Balancer at all?

In simple and medium microservice architectures, you can skip the Load Balancer completely. AWS ECS supports DNS Service Discovery, which lets services talk to each other directly by DNS name — simple, cheap, and fast.


2. The solution: DNS-based Service Discovery with AWS Cloud Map

What is Cloud Map?

AWS Cloud Map is a service that lets you create service names that point dynamically to running ECS instances (or other resources). ECS automatically registers running containers in Cloud Map.

Thanks to this, another microservice can simply ask:

stock-service.trading.platform

...and Cloud Map returns the correct IP address of the container.


3. Configuration in Terraform

The code below creates an entry in Cloud Map with A and SRV records and a MULTIVALUE routing policy.

resource "aws_service_discovery_service" "service_discovery" {
  name = var.service_name

  dns_config {
    namespace_id = var.namespace_id

    dns_records {
      type = "A"
      ttl  = 60
    }

    dns_records {
      type = "SRV"
      ttl  = 60
    }

    routing_policy = "MULTIVALUE"
  }
}

This DNS record and routing part was the hardest for me to understand.

  • A — a classic DNS record with the container's IP address
  • SRV — includes IP, port, weight, and protocol
  • MULTIVALUE — DNS can return more than one IP

What does MULTIVALUE do? When another microservice asks DNS for stock-service.trading-platform.local, Cloud Map (with routing policy = MULTIVALUE) returns all active IPs (and ports, if you use SRV) that ECS registered for that service. This means the client gets, say, 3 IP records, and it can pick any one of them — or try them one by one if the first one fails.

Success!

Or is it?

So first we create the DNS namespace:

module "dns_namespace" {
  source        = "./modules/core/dns_namespace"
  namespace_name = "trading-platform.local"
  vpc_id       = var.vpc_id  # Set this variable in root variables
}

Then we create service discovery entries:

module "stock_data_service_discovery" {
  source        = "./modules/core/service_discovery"
  service_name  = "stock-service"
  namespace_id  = module.dns_namespace.namespace_id
}

module "signal_processing_service_discovery" {
  source        = "./modules/core/service_discovery"
  service_name  = "signal-processing-service"
  namespace_id  = module.dns_namespace.namespace_id
}

Create a new ECS service:

resource "aws_ecs_service" "signal_processing_service" {
  name            = "signal-processing-service"
  cluster         = var.ecs_cluster_id
  task_definition = aws_ecs_task_definition.task_definition.arn
  desired_count   = 1
  launch_type     = "FARGATE"
  service_registries {
    registry_arn = aws_service_discovery_service.signal_processing_service_discovery.arn
    port         = "80"

  }
  network_configuration {
    subnets          = var.subnets
    security_groups  = var.security_groups
  }
  depends_on = [aws_service_discovery_service.signal_processing_service_discovery]
}

Here it was important to connect our Service Discovery:

service_registries {
  registry_arn = aws_service_discovery_service.signal_processing_service_discovery.arn
  port         = "80"
}

This registers our service in AWS Cloud Map — and that's our DNS-based service discovery!

And everything together as a service:

module "stock_data_service_ecs" {
  source                      = "./modules/services/stock_data_service"
  subnets                     = var.subnets
  security_groups             = var.security_groups
  ecs_cluster_id              = aws_ecs_cluster.ecs_cluster.id
  efs_file_system_id          = module.efs.file_system_id
  ecs_task_execution_role_arn = module.iam.ecs_task_execution_role_arn
  ecr_repository_url          = module.stock_data_ecr.repository_url
  namespace_id                = module.dns_namespace.id
}

module "signal_processing_service_ecs" {
  source                      = "./modules/services/signal_processing_service"
  subnets                     = var.subnets
  security_groups             = var.security_groups
  ecs_cluster_id              = aws_ecs_cluster.ecs_cluster.id
  efs_file_system_id          = module.efs.file_system_id
  ecs_task_execution_role_arn = module.iam.ecs_task_execution_role_arn
  ecr_repository_url          = module.signal_processing_ecr.repository_url
  namespace_id                = module.dns_namespace.id
}

4. In practice: how do microservices find each other?

Example service: stock-service.trading.platform, reachable from signal-service. So we have one service that handles CRUD operations for stock quotes — stock-service — and a second service, signal-service, that wants to fetch that data and run some computation on it.

curl http://stock-service.trading.platform/api/v1/data/TICKER_1

When we have 3 task replicas running on ECS, DNS answers like this:

$ dig stock-service.trading-platform.local

;; ANSWER SECTION:
stock-service.trading.local.  60  IN  A  10.0.1.101
stock-service.trading.local.  60  IN  A  10.0.1.102
stock-service.trading.local.  60  IN  A  10.0.1.103

5. Conclusions and what I learned

Sometimes it's good to have a small budget for learning :) With AWS Cloud Map and DNS Service Discovery, I managed to build light, flexible, and cheap microservice communication without a classic Load Balancer. Great for test environments and architectures where simplicity and low cost matter!

6. About me

I'm a beginner enthusiast of cloud computing, distributed system architecture, and simple, elegant (?) solutions. I build my own microservice-based platform and share what I learn along the way!!

Thanks!