Seamless gRPC Communication in AWS Fargate
AWS Cloud Map vs. NLB for Private Service Discovery and Load Balancing
Seamless gRPC Communication in AWS Fargate
AWS Cloud Map vs. NLB for Private Service Discovery and Load Balancing

In modern cloud-native architectures, you often need to enable smooth, secure, and scalable communication between microservices. When running your applications on AWS Fargate inside an ECS cluster within a VPC with restricted security groups, one common challenge is how to let a private gRPC client reach a private gRPC server. In this post, we’ll walk through two approaches for mapping your server’s address to an accessible network route: AWS Cloud Map and Network Load Balancer (NLB). We’ll also provide example Infrastructure as Code (IaC) using AWS CDK alongside guidance on how you might set these up using the AWS Console. We simulate the scenario with two Node.js apps on Fargate — one acting as the gRPC server and the other as the gRPC client.
The Challenge: Private gRPC Communication in a Restricted VPC
Imagine you have an ECS cluster running inside a VPC with a restricted security group. Both your gRPC server and client run on Fargate. Unlike public APIs exposed to the internet, your services live in a private network. The key challenge is to let the gRPC client find and connect to the gRPC server. You have to make the server address discoverable and routable within your secure VPC environment while also taking into account autoscaling.
Two common AWS approaches include:
- AWS Cloud Map Cloud Map is AWS’s service discovery tool. By registering your ECS service with Cloud Map, your client can resolve the server’s hostname to the correct IP address, even as tasks scale up or down. However, while Cloud Map is excellent for name resolution, it does not offer built-in load balancing. That means if your gRPC server service scales out, Cloud Map won’t distribute traffic evenly across the tasks — you’d need to implement load balancing in your client or through an application-level strategy.
- Network Load Balancer (NLB) NLB sits at the network layer and can automatically distribute traffic across your Fargate tasks as they autoscale. The downside is that you might need an extra mapping layer (the load balancer’s endpoint) between your client and server, and the configuration tends to be a bit more involved compared to simple name resolution.
In the following sections, we’ll cover both approaches with details and sample CDK code.
Using AWS Cloud Map for Service Discovery
How AWS Cloud Map Works in This Context
- Registration of Service: When your gRPC server Fargate service starts, it registers with AWS Cloud Map, associating a friendly DNS name (e.g., grpc-server.myapp.local) with its IP address.
- DNS-based Discovery: The gRPC client resolves that DNS name to discover and connect to the server.
- Static Mapping vs. Load Balancing: While this approach helps with service discovery, if your server service scales to multiple tasks, Cloud Map returns all the registered IPs without offering load balancing capabilities.
Setup Through AWS Console
Create a Private DNS Namespace:
- Go to the AWS Cloud Map Console.
- Create a new Private DNS Namespace (for example, myapp.local) associated with your VPC.
Register Your Service:
When defining your ECS service, enable service discovery:
- Specify the DNS namespace (e.g.,
myapp.local). - Provide a service name (e.g.,
grpc-server).
Configure Security Group:
Make sure your security group rules allow communication between the client and the server tasks.
For the gRPC Server:
- Create a security group allowing inbound traffic on the gRPC port (e.g.,
50051) from the client’s security group. - Allow outbound traffic to all for service discovery and responses.
For the gRPC Client:
- Create a security group allowing all outbound traffic (default) so it can reach the gRPC server.
- No inbound rules are required for the client unless it also serves traffic.
Configure the gRPC Client:
Update your client application to resolve the server DNS name (e.g., grpc-server.myapp.local) when connecting to the gRPC server.
Example AWS CDK Code: Cloud Map Integration
Below is a simplified example in TypeScript using AWS CDK that sets up an ECS cluster with two Fargate services — one for the gRPC server with Cloud Map registration and one for the gRPC client.
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as servicediscovery from 'aws-cdk-lib/aws-servicediscovery';
export class GrpcWithCloudMapStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Create a VPC
const vpc = new ec2.Vpc(this, 'GrpcVpc', {
maxAzs: 2, // Default is all AZs in the region
});
// Create an ECS cluster
const cluster = new ecs.Cluster(this, 'GrpcCluster', {
vpc,
});
// Create a Cloud Map Private DNS Namespace.
const namespace = new servicediscovery.PrivateDnsNamespace(this, 'Namespace', {
name: 'myapp.local',
vpc,
});
// Create security groups for the server and client.
const serverSg = new ec2.SecurityGroup(this, 'ServerSecurityGroup', { vpc });
const clientSg = new ec2.SecurityGroup(this, 'ClientSecurityGroup', { vpc });
// Allow inbound gRPC traffic (port 50051) from the client security group to the server.
serverSg.addIngressRule(clientSg, ec2.Port.tcp(50051), 'Allow gRPC traffic from client');
// Define a Fargate Task Definition for the gRPC server.
const serverTaskDef = new ecs.FargateTaskDefinition(this, 'GrpcServerTaskDef');
image: ecs.ContainerImage.fromAsset('./docker/server'), // gRPC server Dockerfile directory
memoryLimitMiB: 512,
logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'grpc-server' }),
portMappings: [{ containerPort: 50051 }],
});
// Create the Fargate Service for the gRPC server.
const serverService = new ecs.FargateService(this, 'GrpcServerService', {
cluster,
taskDefinition: serverTaskDef,
desiredCount: 2,
securityGroups: [serverSg], // Attach server security group
cloudMapOptions: {
// This registers the service in Cloud Map for service discovery.
name: 'grpc-server',
cloudMapNamespace: namespace,
},
});
// Define a Fargate Task Definition for the gRPC client.
const clientTaskDef = new ecs.FargateTaskDefinition(this, 'GrpcClientTaskDef');
clientTaskDef.addContainer('GrpcClientContainer', {
image: ecs.ContainerImage.fromAsset('./docker/client'), // gRPC client Dockerfile directory
memoryLimitMiB: 512,
logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'grpc-client' }),
portMappings: [{ containerPort: 50052 }],
});
// Create the Fargate Service for the gRPC client.
new ecs.FargateService(this, 'GrpcClientService', {
cluster,
taskDefinition: clientTaskDef,
desiredCount: 1,
securityGroups: [clientSg], // Attach client security group
// No CloudMap registration needed since this service consumes the server.
});
}
}
Note: In the example above, the gRPC server registers itself under the DNS name
*grpc-server.myapp.local*. The client application can perform a DNS lookup on that hostname. If you have multiple tasks running the gRPC server, Cloud Map will return multiple IP addresses (or a DNS record with multiple A records). You’ll need to implement a client-side balancing mechanism if required.
Using a Network Load Balancer (NLB) for Traffic Distribution
How the NLB Approach Works
- Load Balancing Built In: Unlike Cloud Map, an NLB automatically balances incoming traffic across all healthy gRPC server tasks. This ensures even distribution as tasks scale.
- Mapping the Address: The gRPC server registers with an NLB target group. The client sends requests to the NLB’s DNS name (e.g.,
nlb-grpc-server-1234567890abcdef.elb.amazonaws.com), and the NLB routes traffic to an available task. - Handling Auto Scaling: The integration with ECS and Fargate ensures that when new tasks are launched, they get added to the target group, while terminated tasks are removed — making the solution highly dynamic.
Setup Through AWS Console
Create a Network Load Balancer:
- Navigate to the EC2 Console → Load Balancers.
- Create an NLB and associate it with your VPC.
- Configure a listener for the gRPC port (e.g.,
50051) with a target group pointing to the ECS service (gRPC server).
Register ECS Service as Target:
- Define the ECS service (gRPC server) to register dynamically as targets in the NLB’s target group.
- Ensure the target type is set to IP for compatibility with AWS Fargate.
Configure Security Groups:
Ensure the security groups allow the necessary traffic from the client service to the NLB, as well as between the NLB and server tasks.
For the gRPC Server:
- Create a security group allowing inbound traffic from the NLB’s security group on the gRPC port (e.g.,
50051). - Allow outbound traffic to all for responses.
For the NLB:
- NLB itself doesn’t use a security group, but you’ll configure the client security group to allow traffic to the NLB.
For the gRPC Client:
- Create a security group allowing outbound traffic to the NLB on the gRPC port.
Configure the gRPC Client:
Update your client application to connect to the NLB’s DNS name.
Example AWS CDK Code: NLB Integration
Below is a simple example that creates an NLB and associates it with a gRPC server service running on Fargate.
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
export class GrpcWithNlbStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Create a VPC.
const vpc = new ec2.Vpc(this, 'Vpc', { maxAzs: 2 });
// Create an ECS Cluster.
const cluster = new ecs.Cluster(this, 'EcsCluster', { vpc });
// Create security groups.
const serverSg = new ec2.SecurityGroup(this, 'ServerSecurityGroup', { vpc });
const clientSg = new ec2.SecurityGroup(this, 'ClientSecurityGroup', { vpc });
// Allow the NLB to communicate with the server on the gRPC port (50051).
serverSg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(50051), 'Allow gRPC traffic from NLB');
// Allow the client to communicate with the NLB on the gRPC port.
clientSg.addEgressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(50051), 'Allow gRPC traffic to NLB');
// Define a Fargate Task Definition for the gRPC server.
const serverTaskDef = new ecs.FargateTaskDefinition(this, 'GrpcServerTaskDef');
serverTaskDef.addContainer('GrpcServerContainer', {
image: ecs.ContainerImage.fromAsset('./docker/server'), // gRPC server Dockerfile directory
memoryLimitMiB: 512,
logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'grpc-server' }),
portMappings: [{ containerPort: 50051 }],
});
// Create the Fargate Service for the gRPC server.
const serverService = new ecs.FargateService(this, 'GrpcServerService', {
cluster,
taskDefinition: serverTaskDef,
desiredCount: 2,
assignPublicIp: false,
});
// Create a Network Load Balancer.
const nlb = new elbv2.NetworkLoadBalancer(this, 'NLB', {
vpc,
internetFacing: false, // Private load balancer inside the VPC.
});
// Add a listener on the NLB (TCP port for gRPC, e.g., 50051).
const listener = nlb.addListener('GrpcListener', {
port: 50051,
protocol: elbv2.Protocol.TCP,
});
// Attach the ECS service to the listener's target group.
listener.addTargets('EcsTargets', {
port: 50051,
targets: [serverService.loadBalancerTarget({
containerName: 'GrpcServerContainer',
containerPort: 50051,
})],
healthCheck: {
port: '8080',
protocol: elbv2.Protocol.HTTP, // Health check via HTTP
path: '/health', // A simple HTTP health check endpoint
interval: cdk.Duration.seconds(30),
timeout: cdk.Duration.seconds(5), // Increase timeout for slow startups
healthyThresholdCount: 2,
unhealthyThresholdCount: 2,
},
});
// Enable autoscaling for gRPC Server
const serverScaling = serverService.autoScaleTaskCount({ minCapacity:2, maxCapacity: 3 });
serverScaling.scaleOnCpuUtilization('GrpcServerCpuScaling', {
targetUtilizationPercent: 50,
});
// Define a Fargate Task Definition for the gRPC client.
const clientTaskDef = new ecs.FargateTaskDefinition(this, 'GrpcClientTaskDef');
clientTaskDef.addContainer('GrpcClientContainer', {
image: ecs.ContainerImage.fromAsset('./docker/client'), // gRPC client Dockerfile directory
memoryLimitMiB: 512,
logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'grpc-client' }),
portMappings: [{ containerPort: 50051 }],
});
// Create the Fargate Service for the gRPC client.
new ecs.FargateService(this, 'GrpcClientService', {
cluster,
taskDefinition: clientTaskDef,
securityGroups: [clientSg],
desiredCount: 1,
// The client simply calls the NLB DNS name to connect to the server.
});
}
}
Key Points for the NLB Approach
- Automatic Traffic Distribution: The NLB automatically detects task scaling. New tasks are added to the target group, and the NLB will distribute incoming gRPC requests among them.
- Simpler Client Configuration: Instead of handling multiple IP addresses or implementing client-side load balancing, your client connects to the single endpoint provided by the NLB.
- Security Considerations: Since the NLB and tasks reside in your VPC, you can tightly control access via security groups.
Making the Choice: Cloud Map vs. NLB
[embed]Making the Choice: Cloud Map vs. NLB
When to use Cloud Map: If your system already implements or requires client-side balancing or if you want a simpler DNS-based lookup for service discovery without an external load balancer, Cloud Map is a good fit.
When to use NLB: If you prefer AWS to handle traffic distribution automatically — especially in high-scale environments — an NLB provides a robust and scalable solution for directing gRPC traffic among your server tasks.
Conclusion
Enabling effective, secure, and scalable gRPC communication between Fargate tasks inside a private ECS cluster requires careful consideration of service discovery and load balancing. Both AWS Cloud Map and Network Load Balancer offer viable pathways:
- AWS Cloud Map is perfect for simple DNS-based service discovery and is easy to integrate with ECS; however, it leaves load balancing up to the client or additional layers.
- NLB natively handles autoscaling and traffic distribution, making it a strong option when you need an out-of-the-box solution for balancing gRPC requests among many server instances.
Through the examples provided above using AWS CDK, you can quickly prototype and deploy either approach. Additionally, configuring these options via the AWS Console is straightforward and allows you to tailor the solution to your specific networking and security needs.
If you’d like to see a working demo and test it yourself, feel free to explore my simple Proof of Concept (POC) available in this repository: https://github.com/mahdiridho/ecs-multiple-fargate-grpc.
Happy coding and let your gRPC microservices talk seamlessly in your AWS environment!
메타데이터
- post_id
- b79397110f3e
- slug
- seamless-grpc-communication-in-aws-fargate-b79397110f3e
- url
- https://medium.com/@whomahdi/seamless-grpc-communication-in-aws-fargate-b79397110f3e
- canonical_url
- https://medium.com/@whomahdi/seamless-grpc-communication-in-aws-fargate-b79397110f3e
- author_url
- https://medium.com/@whomahdi
- status
- ok
- fetched_at
- 2026-08-17 10:22:15