AWS ECS Fargate is a serverless container provider that lets you run containers in the cloud without managing the underlying servers. They're often used as easy-to-manage and cost-effective methods of hosting web APIs. Fargate as it is works perfectly for hosting, say, your frontend on one container and your API on another. But as the complexity of an application grows you might find that you need to split up your code into multiple services. Container orchestration is perfect for this as it lets your services scale up and down separately depending on their requirements, and keeps code confined into smaller, more manageable pieces. I won't go into the details of microservice architecture in this post but using ECS is a common method of developing a microservice architecture.

If you're developing a microservice architecture on ECS, you might reach the point where you need to allow one container to speak to another in the same cluster. This sounds simple but is fairly complex in practice. For one, each service can scale up and down independently, so if one container sends a request to another, how does that container know the actual IP of the container to send the request to? There may be multiple IPs available, and the sender would need to figure out which IP to send it to. The process of a container figuring out how to send request to another is called service discovery. There are many, many ways to handle service discovery that go beyond the scope of this post. I'll be discussing a method that's relevant to ECS and the AWS ecosystem: AWS Cloud Map.

AWS Cloud Map

Cloud Map is AWS' service discovery offering, that allows you to register IPs that the service discovery service can then provide that information to other services who need to know. Cloud Map offers three namespace types:

  1. Http Namespaces
  2. Public DNS Namespaces
  3. Private DNS Namespaces

Http Namespaces

HTTP Namespaces allow your services to use the DiscoverInstances API call to retrieve a list of IPs of your services. Your service can then send a request to any of the IPs however you feel is most appropriate.

Private DNS Namespaces

Private DNS namespaces create Route53 records for each registered service instance. DNS queries can then be made within your VPC to discover services. You can choose between a WEIGHTED routing policy, in which Route53 responds with the IP of a random instance, or MULTIVALUE in which the DNS query will respond with the IPs of up to 8 healthy instances. You can also still access the DiscoverInstances API call when you're using private namespaces.

Public DNS Namespaces

Public DNS namespaces are similar to the private namespace, except it will obviously use a public DNS to respond to instance requests. Cloud Map also doesn't support making API calls the get registered instances when using public namespaces.

In this post, I'll be going over using Private DNS Namespaces and using Route53's weighted routing policy to respond to DNS queries.

Setting up the namespace

Let's say we have two services running in ECS, where service A needs to be able to request some data from service B. We want service A to be able to find out where it should be sending the request to, and the system should be able to automatically register new instances as the containers scale up and down. We'll be using a Cloud Map Private DNS Namespace to do this. To demonstrate, I'll be using some CDK code.

We'll start by setting up our Fargate cluster

import * as servicediscovery from '@aws-cdk/aws-servicediscovery';
import * as ecs from "@aws-cdk/aws-ecs";

const taskDefinition = new ecs.FargateTaskDefinition(this, "MyTaskDefinition", {
  cpu: 512,
  memoryLimitMiB: 2048
});

taskDefinition.addContainer("FargateContainer", {
  image: "<your container definition>",
  logging: ecs.LogDriver.awsLogs({ streamPrefix: "myService" }),
  portMappings: [
    { containerPort: 80 } // important. Default is no port mapping
  ]
});

const ecsService = new ecs.FargateService(this, 'MyService', {
   cluster,
   taskDefinition
});

  const cloudMapNamespace = new servicediscovery.PrivateDnsNamespace(this, `ServiceDiscoveryNamespace`, {
    name: 'mydomain.com', // The domain your want to use in the DNS lookup
    vpc: yourVpc
  });

const cloudMapService = new servicediscovery.Service(this, `ServiceDiscovery`, {
  namespace: cloudMapNamespace,
  dnsRecordType: servicediscovery.DnsRecordType.A,
  dnsTtl: cdk.Duration.seconds(300),
  name: 'service-name', // will be used as a subdomain of the domain set in the namespace
  routingPolicy: servicediscovery.RoutingPolicy.WEIGHTED,
  loadBalancer: true // Important! If you choose WEIGHTED but don't set this, the routing policy will default to MULTIVALUE instead
 })

ecsService.associateCloudMapService({
  service: cloudMapService
 })

Let's go through this step by step:

  1. Create a task definition, and add a container to it.
  2. Create a Fargate service and set the task definition
  3. Create the Cloud Map namespace
  4. Create the CloudMap service and set the record type to A, and the routing policy to WEIGHTED. Note: it's important that you set the loadBalancer property to true if you want to use weighted routing.
  5. Associate the ECS service with the Cloud Map service.

Once this is deployed, go to Route53 and have a look for the hosted zone created by Cloud Map. You should see that it has 1 A record with a routing policy set to weighted and the value as the private IP of your container. If your containers are set up and working correctly, you should be able to launch another one, and send a request to <service-name>.<namespace-domain> to reach your service container.