Consulを用いたカスタムサービス登録実装の最適化パターン

設定モデルの再設計

サービスディスカバリーの設定構造を再構築します。以下は再設計された構成オブジェクトです:

namespace ServiceDiscovery.Configuration
{
    public class DiscoveryConfig
    {
        public ClusterEndpoint Cluster { get; set; }
        public ServiceRegistration Service { get; set; }
    }
}
namespace ServiceDiscovery.Configuration
{
    public class ServiceRegistration
    {
        public string ServiceId { get; set; }
        public string Host { get; set; }
        public string ServiceName { get; set; }
        public string[] MetadataTags { get; set; }
        public int ServicePort { get; set; }
        public HealthCheckConfig HealthProbe { get; set; }
    }

    public class HealthCheckConfig
    {
        public string ProbeEndpoint { get; set; }
        public int IntervalSeconds { get; set; }
    }
}
namespace ServiceDiscovery.Configuration
{
    public class ClusterEndpoint
    {
        public string ApiUrl { get; set; }
        public string Region { get; set; }
    }
}

サービス登録の拡張メソッド実装

DIコンテナへの登録処理をカプセル化します:

using Consul;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using ServiceDiscovery.Configuration;

namespace ServiceDiscovery.Extensions
{
    public static class ServiceDiscoveryExtensions
    {
        public static IServiceCollection AddServiceDiscovery(
            this IServiceCollection services, 
            IConfiguration config)
        {
            services.Configure<DiscoveryConfig>(
                config.GetSection("DiscoveryConfig"));
                
            services.AddScoped<ConsulClient>(sp =>
            {
                var configSnapshot = sp.GetRequiredService<
                    IOptionsSnapshot<DiscoveryConfig>>();
                var endpoint = configSnapshot.Value.Cluster;
                
                return new ConsulClient(cfg =>
                {
                    cfg.Address = new Uri(endpoint.ApiUrl);
                    cfg.Datacenter = endpoint.Region;
                });
            });
            
            services.AddScoped<IServiceRegistry, ServiceRegistry>();
            return services;
        }

        public static async Task<IApplicationBuilder> 
            RegisterWithDiscovery(this IApplicationBuilder app)
        {
            using var scope = app.ApplicationServices.CreateScope();
            var config = scope.ServiceProvider
                .GetRequiredService<IOptionsSnapshot<DiscoveryConfig>>()
                .Value;

            var client = new ConsulClient(cfg =>
            {
                cfg.Address = new Uri(config.Cluster.ApiUrl);
                cfg.Datacenter = config.Cluster.Region;
            });

            var registration = new AgentServiceRegistration
            {
                ID = $"{config.Service.ServiceName}-{Guid.NewGuid():N}",
                Address = config.Service.Host,
                Service = config.Service.ServiceName,
                Tags = config.Service.MetadataTags,
                Port = config.Service.ServicePort,
                Check = new AgentServiceCheck
                {
                    HTTP = config.Service.HealthProbe.ProbeEndpoint,
                    Interval = TimeSpan.FromSeconds(
                        config.Service.HealthProbe.IntervalSeconds)
                }
            };

            await client.Agent.ServiceRegister(registration);
            return app;
        }
    }
}

サービス操作インターフェースの実装

サービス管理のコアロジックをインターフェースで抽象化:

namespace ServiceDiscovery
{
    public interface IServiceRegistry : IDisposable
    {
        Task<ServiceEndpoint[]> GetAllServicesAsync();
        Task DeregisterAllServicesAsync();
        Task<int> DeregisterServiceAsync(string serviceId);
        Task<int> RefreshClusterStateAsync();
        Task<int> RegisterServiceAsync(ServiceRegistration config);
    }
}
using Consul;
using ServiceDiscovery.Configuration;

namespace ServiceDiscovery
{
    public class ServiceRegistry : IServiceRegistry
    {
        private readonly ConsulClient _discoveryClient;

        public ServiceRegistry(ConsulClient discoveryClient)
        {
            _discoveryClient = discoveryClient;
        }

        public async Task<ServiceEndpoint[]> GetAllServicesAsync()
        {
            var result = await _discoveryClient.Agent.Services();
            return result.Response.Values.Select(s => new ServiceEndpoint
            {
                Host = s.Address,
                ServiceId = s.ID,
                Port = s.Port,
                Name = s.Service,
                Tags = s.Tags
            }).ToArray();
        }

        public async Task DeregisterAllServicesAsync()
        {
            var services = (await _discoveryClient.Agent.Services())
                .Response.Values;
                
            foreach (var service in services)
            {
                await _discoveryClient.Agent.ServiceDeregister(service.ID);
            }
        }

        public async Task<int> DeregisterServiceAsync(string serviceId)
        {
            var result = await _discoveryClient.Agent
                .ServiceDeregister(serviceId);
            return (int)result.StatusCode;
        }

        public async Task<int> RefreshClusterStateAsync()
        {
            var nodeName = await _discoveryClient.Agent.NodeName();
            var result = await _discoveryClient.Agent.Reload(nodeName);
            return (int)result.StatusCode;
        }

        public async Task<int> RegisterServiceAsync(ServiceRegistration config)
        {
            var registration = new AgentServiceRegistration
            {
                ID = config.ServiceId,
                Address = config.Host,
                Service = config.ServiceName,
                Tags = config.MetadataTags,
                Port = config.ServicePort,
                Check = new AgentServiceCheck
                {
                    HTTP = config.HealthProbe.ProbeEndpoint,
                    Interval = TimeSpan.FromSeconds(
                        config.HealthProbe.IntervalSeconds)
                }
            };

            var result = await _discoveryClient.Agent
                .ServiceRegister(registration);
            return (int)result.StatusCode;
        }

        public void Dispose() => _discoveryClient?.Dispose();
    }
}

アプリケーション設定と登録フロー

appsettings.jsonの構成例:

{
  "DiscoveryConfig": {
    "Cluster": {
      "ApiUrl": "http://consul-server:8500",
      "Region": "tokyo-dc"
    },
    "Service": {
      "Host": "api-service",
      "ServiceName": "payment-service",
      "MetadataTags": ["v2", "secure"],
      "ServicePort": 5001,
      "HealthProbe": {
        "ProbeEndpoint": "http://api-service:5001/health",
        "IntervalSeconds": 10
      }
    }
  }
}

Startup処理の実装:

var app = WebApplication.CreateBuilder(args);

// サービスディスカバリーの登録
app.Services.AddServiceDiscovery(app.Configuration);

var app = builder.Build();

// ヘルスチェックエンドポイント
app.Map("/health", context => 
{
    context.Response.StatusCode = 200;
    return context.Response.WriteAsync("Healthy");
});

// サービス登録の実行
await app.RegisterWithDiscovery();

app.MapControllers();
app.Run();

タグ: Consul ASP.NET Core サービスディスカバリー マイクロサービス

6月13日 20:50 投稿