ASP.NET Core でのサービス発見と負荷分散の実装

Consul はサービスガバナンスとサービス発見を実現するツールです。複数のサービスが存在する場合、サービスのアドレスを一元的に管理し、アプリケーションがまとまった場所からサービスにアクセスできるようにします。

ASP.NET Core と Ocelot 組み合わせでは、以下の負荷分散方法が可能です。

LeastConnection:現在アクティブな接続数が最も少ないサーバーにリクエストを送信 RoundRobin:サーバーを順番に選択 NoLoadBalancer:最初のサーバーのみを使用

Consul クライアントの基本的な設定方法を示します。

Consul クライアントの設定例 使用する名前空間:Consul クライアントの設定を管理する名前空間

using Consul;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;

namespace ServiceDiscovery.Configuration
{
    public static class ServiceDiscoverySetup
    {
        public static async Task<IApplicationBuilder> ConfigureConsulAsync(this IApplicationBuilder app, IConfiguration config)
        {
            var consulConfig = new ConsulClientConfiguration();
            config.GetSection("ServiceDiscovery").Bind(consulConfig);
            return await RegisterServiceAsync(app, consulConfig);
        }

        private static async Task<IApplicationBuilder> RegisterServiceAsync(IApplicationBuilder app, ConsulClientConfiguration config)
        {
            using var client = new ConsulClient(cfg =>
            {
                cfg.Address = new Uri(config.ConsulServer.Address);
                cfg.Datacenter = config.ConsulServer.Datacenter;
            });

            await client.Agent.ServiceRegister(new AgentServiceRegistration
            {
                ID = Guid.NewGuid().ToString(),
                Name = config.ServiceRegistration.Name,
                Address = config.ServiceRegistration.Address,
                Port = config.ServiceRegistration.Port,
                Tags = new[] { config.ServiceRegistration.Tag },
                Check = new AgentServiceCheck
                {
                    DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(config.ServiceRegistration.DeregisterAfter),
                    Interval = TimeSpan.FromSeconds(config.ServiceRegistration.CheckInterval),
                    HTTP = config.ServiceRegistration.HealthCheckUrl,
                    Timeout = TimeSpan.FromSeconds(config.ServiceRegistration.Timeout)
                }
            });

            return app;
        }
    }

    public class ConsulClientConfiguration
    {
        public ConsulServer ConsulServer { get; set; }
        public ServiceRegistration ServiceRegistration { get; set; }
    }

    public class ConsulServer
    {
        public string Address { get; set; }
        public string Datacenter { get; set; }
    }

    public class ServiceRegistration
    {
        public string Name { get; set; }
        public string Address { get; set; }
        public int Port { get; set; }
        public string Tag { get; set; }
        public string HealthCheckUrl { get; set; }
        public int CheckInterval { get; set; }
        public int Timeout { get; set; }
        public int DeregisterAfter { get; set; }
    }

サービスの登録設定例を示します。

{
  "ServiceDiscovery": {
    "ConsulServer": {
      "Address": "http://localhost:8500",
      "Datacenter": "dc1"
    },
    "ServiceRegistration": {
      "Name": "MainSvc",
      "Address": "localhost",
      "Port": 5000,
      "Tag": "primary",
      "HealthCheckUrl": "http://localhost:5000/health",
      "CheckInterval": 10,
      "Timeout": 5,
      "DeregisterAfter": 20
    }
  }
}

Consul を使用したサービス発見のための Ocelot 設定例を示します。

{
  "Routes": [
    {
      "UpstreamPathTemplate": "/api/auth/{*everything}",
      "UpstreamHttpMethod": [ "GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS" ],
      "DownstreamPathTemplate": "/api/auth/{*everything}",
      "UseServiceDiscovery": true,
      "ServiceName": "AuthService",
      "LoadBalancerOptions": {
        "Type": "RoundRobin"
      }
    }
  ],
  "GlobalConfiguration": {
    "ServiceDiscoveryProvider": {
      "Scheme": "http",
      "Host": "localhost",
      "Port": 8500,
      "Type": "Consul"
    }
  }
}

サービス発見を有効にするときは、以下の点に注意します。

サービスの名前(ServiceName)を正しく指定 Consul サーバーのホスト名とポートを正しく設定 負荷分散の種類(RoundRobin または LeastConnection)を選択

ASP.NET Core アプリケーションへの適用例を示します。

using ServiceDiscovery.Configuration;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.Filters;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
    options.OperationFilter<AddResponseHeadersFilter>();
    options.OperationFilter<AppendAuthorizeToSummaryOperationFilter>();
    options.OperationFilter<SecurityRequirementsOperationFilter>();

    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Description = "JWT 認証用トークンを入力",
        Name = "Authorization",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.ApiKey,
        BearerFormat = "JWT",
        Scheme = "Bearer"
    });

    options.AddSecurityRequirement(new OpenApiSecurityRequirement {
        {
            new OpenApiSecurityScheme{
                Reference = new OpenApiReference{
                    Type = ReferenceType.SecurityScheme,
                    Id = "Bearer"
                }
            }, new string[] { }
        }
    });

    string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "JwtMainSvc.xml");
    options.IncludeXmlComments(path, true);
});

builder.Services.Configure<ConsulClientConfiguration>(builder.Configuration.GetSection("ServiceDiscovery"));

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    var audienceConfig = builder.Configuration.GetSection("Audience");
    var symmetricKey = audienceConfig["Secret"];
    var keyBytes = Encoding.ASCII.GetBytes(symmetricKey);
    var signingKey = new SymmetricSecurityKey(keyBytes);

    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = signingKey,
        ValidateIssuer = true,
        ValidIssuer = audienceConfig["Issuer"],
        ValidateAudience = true,
        ValidAudience = audienceConfig["Audience"],
        ValidateLifetime = true,
        ClockSkew = TimeSpan.Zero,
        RequireExpirationTime = true,
    };
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI(options =>
    {
        options.SwaggerEndpoint("/swagger/v1/swagger.json", "v1");
        options.RoutePrefix = "";
    });
}

app.UseCustomConsulAsync(app.Configuration).Wait();

app.Map("/health", config =>
{
    config.Run(async context =>
    {
        context.Response.StatusCode = 200;
        await Task.CompletedTask;
    });
});

app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

タグ: Ocelot Consul JWT ASP.NET Core サービス発見

6月6日 20:33 投稿