Configure Load Balancer using Eureka and ocelot with asp.net core and docker
Introduction
A load balancer is a component that distributes incoming network traffic across multiple servers to optimize resource utilization, maximize throughput, minimize response time, and avoid overloading any single server.
Eureka is a service discovery tool provided by Netflix OSS, it can be used to solve load balancing by registering all instances of a service with Eureka and having the load balancer forward traffic to those instances. The Eureka server maintains a registry of all available instances of a service, and clients of the service query the registry to obtain a list of available service instances. The load balancer can then use this information to distribute incoming requests to the available service instances.
In this article, I will show you how to configure load balancer using Eureka and ocelot in API gateway.
Fig – Load balancing architecture (Collected from internet)
Here, I am skipping service discovery. If you want to know more about service discovery, please read the article - Service Discovery with Ocelot and ASP.net core
Implementation
Let’s configure Load Balancer using Eureka, Ocelot
Tools and Technologies Used
- Visual Studio 2022
- .NET 6.0
- ASP.NET Core Web API
- Visual C#
- Eureka
- Ocelot
- Docker
Step 1: Create solution and projects
- Create a solution name SDDemoDocker.sln
- Add four Web Api Projects name BFF.Web, LocationA.API, LocationB.API, LocationC.API
- Here BFF.Web is an api gateway
Step 2: Install nuget packages
- Install following nuget packages in BFF.Web project
PM> Install-Package MMLib.SwaggerForOcelot
PM> Install-Package Ocelot
PM> Install-Package Ocelot.Provider.Eureka
PM> Install-Package Ocelot.Provider.Kubernetes
PM> Install-Package Ocelot.Provider.Polly
PM> Install-Package Steeltoe.Discovery.Eureka
- Install following nuget packages in LocationA.API project
PM> Install-Package Steeltoe.Discovery.ClientCore
PM> Install-Package Steeltoe.Discovery.Eureka
PM> Install-Package Microsoft.VisualStudio.Azure.Containers.Tools.Targets
- Install following nuget packages in LocationB.API project
PM> Install-Package Steeltoe.Discovery.ClientCore
PM> Install-Package Steeltoe.Discovery.Eureka
PM> Install-Package Microsoft.VisualStudio.Azure.Containers.Tools.Targets
- Install following nuget packages in LocationC.API project
PM> Install-Package Steeltoe.Discovery.ClientCore
PM> Install-Package Steeltoe.Discovery.Eureka
PM> Install-Package Microsoft.VisualStudio.Azure.Containers.Tools.Targets
Step 3: Organize LocationA.API projects
- Register Eureka and modify program.cs as follows.
Program.cs
using Steeltoe.Discovery.Client;
using Steeltoe.Discovery.Eureka;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Add or register service discovery to your application
builder.Services.AddServiceDiscovery(o => o.UseEureka());
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
//app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
- Create a controller name DistrictController in Controllers folder and modify get method as follows.
DistrictController.cs
using Microsoft.AspNetCore.Mvc;
// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
namespace LocationA.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class DistrictController : ControllerBase
{
// GET: api/<DistrictController>
[HttpGet("GetAll")]
public IEnumerable<string> Get()
{
return new string[] { "Serivce:LocationA.API->", "Dhaka", "Chittagong", "Chandpur", "Barisal", "Noakhali" };
}
}
}
Modify appsettings.json to connect to the service registry
appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Spring": {
"Application": {
"Name": "SERVICE.LOCATIONAPI"
}
},
"Eureka": {
"Client": {
"ServiceUrl": "http://eureka-server:8761/eureka/",
"ValidateCertificates": false,
"ShouldRegisterWithEureka": true
},
// No docke instance added
"Instance": {
//"port": 9001,
"NonSecurePort": 80,
"HostName": "locationa.api",
"InstanceId": "LocationA.API,Port:80",
"StatusPageUrlPath": "/swagger/index.html"
//"HealthCheckUrlPath": "/api/values/healthcheck"
}
}
}
- Add docker file in the project
Dockerfile
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["LocationA.API/LocationA.API.csproj", "Location.API/"]
RUN dotnet restore "LocationA.API/LocationA.API.csproj"
COPY . .
WORKDIR "/src/LocationA.API"
RUN dotnet build "LocationA.API.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "LocationA.API.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "LocationA.API.dll"]
Step 4: Organize LocationB.API project
- Register Eureka and modify program.cs as follows.
Program.cs
using Steeltoe.Discovery.Client;
using Steeltoe.Discovery.Eureka;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Add or register service discovery to your application
builder.Services.AddServiceDiscovery(o => o.UseEureka());
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
//app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
- Create a controller name DistrictController in Controllers folder and modify get method as follows.
DistrictController.cs
using Microsoft.AspNetCore.Mvc;
namespace LocationB.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class DistrictController : ControllerBase
{
// GET: api/<DistrictController>
[HttpGet("GetAll")]
public IEnumerable<string> Get()
{
return new string[] { "Serivce:LocationB.API->", "Kumilla", "Bogura", "Natore", "Kurigram", "Natore" };
}
}
}
- Modify appsettings.json to connect to the service registry.
appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Spring": {
"Application": {
"Name": "SERVICE.LOCATIONAPI"
}
},
"Eureka": {
"Client": {
"ServiceUrl": "http://eureka-server:8761/eureka/",
"ValidateCertificates": false,
"ShouldRegisterWithEureka": true
},
// No docke instance added
"Instance": {
//"port": 9001,
"NonSecurePort": 80,
"HostName": "locationb.api",
"InstanceId": "LocationB.API,Port:80",
"StatusPageUrlPath": "/swagger/index.html"
}
}
}
- Add Dockerfile in the project
Dockerfile
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["LocationB.API/LocationB.API.csproj", "LocationB.API/"]
RUN dotnet restore "LocationB.API/LocationB.API.csproj"
COPY . .
WORKDIR "/src/LocationB.API"
RUN dotnet build "LocationB.API.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "LocationB.API.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "LocationB.API.dll"]
Step 5: Organize LocationC.API
- Register Eureka and modify program.cs as follows.
Program.cs
using Steeltoe.Discovery.Client;
using Steeltoe.Discovery.Eureka;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Add or register service discovery to your application
builder.Services.AddServiceDiscovery(o => o.UseEureka());
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
//app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
- Create a controller name DistrictController in Controllers folder and modify get method as follows.
DistrictController.cs
using Microsoft.AspNetCore.Mvc;
namespace LocationC.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class DistrictController : ControllerBase
{
// GET: api/<DistrictController>
[HttpGet("GetAll")]
public IEnumerable<string> Get()
{
return new string[] {"Serivce:LocationC.API->","Kustia", "Norail", "Kurigram", "Netrokona"};
}
}
}
- Modify appsettings.json to connect to the service registry.
appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Spring": {
"Application": {
"Name": "SERVICE.LOCATIONAPI"
}
},
"Eureka": {
"Client": {
"ServiceUrl": "http://eureka-server:8761/eureka/",
"ValidateCertificates": false,
"ShouldRegisterWithEureka": true
},
// No docke instance added
"Instance": {
//"port": 9001,
"NonSecurePort": 80,
"HostName": "locationc.api",
"InstanceId": "LocationC.API,Port:80",
"StatusPageUrlPath": "/swagger/index.html"
}
}
}
- Add Dockerfile in the project
Dockerfile
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["LocationC.API/LocationC.API.csproj", "LocationC.API/"]
RUN dotnet restore "LocationC.API/LocationC.API.csproj"
COPY . .
WORKDIR "/src/LocationC.API"
RUN dotnet build "LocationC.API.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "LocationC.API.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "LocationC.API.dll"]
Step 6: Organize BFF.Web
In this stage, we are going to configure a gateway to communicate with other services using ocelot.
- Create two folder name - Routes.dev and Routes.Prod in root directory and add the following files ocelot.global.json, ocelot.location.api.json, ocelot.SwaggerEndPoints.json in Routes.dev and Routes.Prod folder.
*Now modify the json files as follows.
ocelot.location.api.json
{
"Routes": [
{
"DownstreamPathTemplate": "/{everything}",
"DownstreamScheme": "http",
"SwaggerKey": "location",
"UseServiceDiscovery": true,
"ServiceName": "SERVICE.LOCATIONAPI",
"UpstreamPathTemplate": "/location/{everything}",
"UpstreamHttpMethod": [
"GET",
"POST",
"PUT",
"DELETE"
],
/*
LeastConnection - tracks which services are dealing with requests and sends new requests to service with least existing requests. The algorythm state is not distributed across a cluster of Ocelot’s.
RoundRobin - loops through available services and sends requests. The algorythm state is not distributed across a cluster of Ocelot’s.
NoLoadBalancer - takes the first available service from config or service discovery.
CookieStickySessions - uses a cookie to stick all requests to a specific server
*/
"LoadBalancerOptions": {
"Type": "RoundRobin"
}
}
]
}
ocelot.product.api.json
{
"Routes": [
{
"DownstreamPathTemplate": "/{everything}",
"DownstreamScheme": "http",
"SwaggerKey": "product",
"UseServiceDiscovery": true,
"ServiceName": "SERVICE.PRODUCTAPI",
"UpstreamPathTemplate": "/product/{everything}",
"UpstreamHttpMethod": [
"GET",
"POST",
"PUT",
"DELETE"
],
/*
LeastConnection - tracks which services are dealing with requests and sends new requests to service with least existing requests. The algorythm state is not distributed across a cluster of Ocelot’s.
RoundRobin - loops through available services and sends requests. The algorythm state is not distributed across a cluster of Ocelot’s.
NoLoadBalancer - takes the first available service from config or service discovery.
CookieStickySessions - uses a cookie to stick all requests to a specific server
*/
"LoadBalancerOptions": {
"Type": "LeastConnection"
}
}
]
}
ocelot.global.json
{
"GlobalConfiguration": {
"RequestIdKey": "OcRequestId",
"DownstreamScheme": "http",
"UseServiceDiscovery": true,
"ServiceDiscoveryProvider": {
"Host": "bff.web",
"Port": 80,
//Type can be Consul, Eureka
"Type": "Eureka"
}
}
}
ocelot.SwaggerEndPoints.json
{
"SwaggerEndPoints": [
{
"Key": "bff.web",
"TransformByOcelotConfig": false,
"Config": [
{
"Name": "BFF.Web",
"Version": "1.0",
"Url": "http://bff.web:80/swagger/v1/swagger.json"
}
]
},
{
"Key": "location",
"TransformByOcelotConfig": true,
"Config": [
{
"Name": "Location.API",
"Version": "1.0",
"Url": "http://bff.web:80/location/swagger/v1/swagger.json"
}
]
}
]
}
- Add AlterUpstream class in Config folder
AlterUpstream.cs
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BFF.Web.Config
{
public class AlterUpstream
{
public static string AlterUpstreamSwaggerJson(HttpContext context, string swaggerJson)
{
var swagger = JObject.Parse(swaggerJson);
// ... alter upstream json
return swagger.ToString(Formatting.Indented);
}
}
}
- Modify Program.cs file as follows
Program.cs
using BFF.Web.Config;
using MMLib.SwaggerForOcelot.DependencyInjection;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using Ocelot.Provider.Eureka;
using Ocelot.Provider.Polly;
using Steeltoe.Discovery.Client;
using Steeltoe.Discovery.Eureka;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var routes = "";
#if DEBUG
routes = "Routes.dev";
#else
routes = "Routes.prod";
#endif
;
builder.Configuration.AddOcelotWithSwaggerSupport(options =>
{
options.Folder = routes;
});
// Configure ocelot
builder.Services.AddOcelot(builder.Configuration).AddEureka().AddPolly();
//builder.Services.AddOcelot(builder.Configuration).AddPolly();
builder.Services.AddSwaggerForOcelot(builder.Configuration);
// Add or register service discovery to your application
builder.Services.AddServiceDiscovery(o => o.UseEureka());
builder.Host.ConfigureAppConfiguration((hostingContext, config) =>
{
config.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.local.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", optional: true)
.AddOcelot(routes, builder.Environment)
.AddEnvironmentVariables();
});
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
//app.UseSwaggerUI();
}
//app.UseHttpsRedirection();
//app.UseOcelot();
app.UseAuthorization();
app.UseSwaggerForOcelotUI(options =>
{
options.PathToSwaggerGenerator = "/swagger/docs";
options.ReConfigureUpstreamSwaggerJson = AlterUpstream.AlterUpstreamSwaggerJson;
}).UseOcelot().Wait();
app.MapControllers();
app.Run();
- Now modify appsettings.json for BFF.Web
Appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"Spring": {
"Application": {
"Name": "SERVICE.BFF.Web"
}
},
"Eureka": {
"Client": {
"ServiceUrl": "http://eureka-server:8761/eureka/",
"ValidateCertificates": false,
"ShouldRegisterWithEureka": true
},
"Instance": {
"NonSecurePort": 80,
"HostName": "bff.web",
"InstanceId": "BFF.Web",
"StatusPageUrlPath": "/swagger/index.html"
//"HealthCheckUrlPath": "/api/values/healthcheck"
//"StatusPageUrlPath": "/api/values/status"
},
"AllowedHosts": "*"
}
}
Step 7: Configure docker-compose
- Add docker-compose file in the project and modify as follows
docker-compose.yml
version: '3.4'
services:
eureka-server:
container_name: eureka-server
image: steeltoeoss/eurekaserver:latest
restart: on-failure
hostname: eureka-server
networks:
- backend_network
#ports:
# - "8761:8761"
locationa.api:
container_name: locationa.api
image: ${DOCKER_REGISTRY-}locationaapi
restart: on-failure
hostname: locationa.api
build:
context: .
dockerfile: Locationa.API/Dockerfile
networks:
- backend_network
locationb.api:
container_name: locationb.api
image: ${DOCKER_REGISTRY-}locationbapi
restart: on-failure
hostname: locationb.api
build:
context: .
dockerfile: LocationB.API/Dockerfile
networks:
- backend_network
locationc.api:
container_name: locationc.api
image: ${DOCKER_REGISTRY-}locationcapi
restart: on-failure
hostname: locationc.api
build:
context: .
dockerfile: LocationC.API/Dockerfile
networks:
- backend_network
bff.web:
container_name: bff.web
image: ${DOCKER_REGISTRY-}bffweb
restart: on-failure
hostname: bff.web
build:
context: .
dockerfile: BFF.Web/Dockerfile
networks:
- backend_network
networks:
backend_network:
docker-compose.override.yml
version: '3.4'
services:
eureka-server:
environment:
- EUREKA_SERVER_ENABLE_SELF_PRESERVATION=false
ports:
- 8761:8761
locationa.api:
environment:
- ASPNETCORE_ENVIRONMENT=Development
#- ASPNETCORE_URLS=https://+:443;http://+:80
- ASPNETCORE_URLS=http://+:80
- EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://eureka-server:8761/eureka
ports:
- "9001:80"
#- "4001:443"
volumes:
- ${APPDATA}/Microsoft/UserSecrets:/root/.microsoft/usersecrets:ro
- ${APPDATA}/ASP.NET/Https:/root/.aspnet/https:ro
depends_on:
- eureka-server
locationb.api:
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=http://+:80
- EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://eureka-server:8761/eureka
ports:
- "9002:80"
volumes:
- ${APPDATA}/Microsoft/UserSecrets:/root/.microsoft/usersecrets:ro
- ${APPDATA}/ASP.NET/Https:/root/.aspnet/https:ro
depends_on:
- eureka-server
locationc.api:
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=http://+:80
- EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://eureka-server:8761/eureka
ports:
- "9003:80"
depends_on:
- eureka-server
bff.web:
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=http://+:80
- EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://eureka-server:8761/eureka
ports:
- "8001:80"
volumes:
- ${APPDATA}/Microsoft/UserSecrets:/root/.microsoft/usersecrets:ro
- ${APPDATA}/ASP.NET/Https:/root/.aspnet/https:ro
depends_on:
- eureka-server
- locationa.api
- locationb.api
- locationc.api
- Now open your browser and visit http://localhost:8761/ to see Eureka server
Step 8: Now run the applications
- Set docker-compose as start up project
- Now run the application
- Now browse location service using BFF. You will get different data from different service as round robin basis
- You will see Eureka server as follows.