Configure Load Balancer using Eureka and ocelot with asp.net core and docker

9 minute read

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.

Source code