From harness-claude
Implements service registration, dynamic discovery, and health checks for microservices using Kubernetes DNS or Consul, ideal for dynamic deployments with changing IPs and horizontal scaling.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Implement service registration and dynamic discovery with health checks in microservices.
Manages Consul service discovery operations, providing step-by-step guidance, production-ready configurations, and best practices for DevOps workflows.
Designs and implements microservices architectures covering service boundaries, communication patterns, API gateways, service mesh, service discovery, and distributed system patterns. Use for building microservices or distributed systems.
Provides patterns for designing, building, and operating microservices: service decomposition, inter-service communication, resilience, data consistency, observability in distributed systems. Useful when microservices mentioned.
Share bugs, ideas, or general feedback.
Implement service registration and dynamic discovery with health checks in microservices.
Kubernetes DNS-based discovery (most common — no code needed):
# Kubernetes handles discovery automatically via DNS
# Service A calls Service B using the DNS name: http://order-service:8080
# Kubernetes resolves this to the correct pod IPs
apiVersion: v1
kind: Service
metadata:
name: order-service
namespace: production
spec:
selector:
app: order-service
ports:
- port: 8080
targetPort: 8080
type: ClusterIP # internal only
---
# In your app code — just use the DNS name
const ORDER_SERVICE_URL = process.env.ORDER_SERVICE_URL ?? 'http://order-service:8080';
Consul-based service registration (for non-Kubernetes environments):
import Consul from 'consul';
const consul = new Consul({ host: process.env.CONSUL_HOST ?? 'consul' });
async function registerService(): Promise<void> {
const serviceId = `order-service-${process.env.POD_NAME ?? process.env.HOSTNAME}`;
await consul.agent.service.register({
id: serviceId,
name: 'order-service',
port: parseInt(process.env.PORT ?? '8080'),
address: process.env.POD_IP ?? '0.0.0.0',
tags: ['v1', 'production'],
check: {
http: `http://${process.env.POD_IP}:${process.env.PORT}/health`,
interval: '10s',
timeout: '5s',
deregistercriticalserviceafter: '30s',
},
});
console.log(`Registered service: ${serviceId}`);
// Deregister gracefully on shutdown
const cleanup = async () => {
await consul.agent.service.deregister(serviceId);
process.exit(0);
};
process.on('SIGTERM', cleanup);
process.on('SIGINT', cleanup);
}
// Client-side discovery — resolve service address from Consul
async function resolveService(name: string): Promise<string> {
const services = await consul.health.service({
service: name,
passing: true, // only healthy instances
});
if (services.length === 0) throw new Error(`No healthy instances of ${name}`);
// Simple round-robin
const instance = services[Math.floor(Math.random() * services.length)];
return `http://${instance.Service.Address}:${instance.Service.Port}`;
}
// Usage
const orderServiceUrl = await resolveService('order-service');
const order = await fetch(`${orderServiceUrl}/orders/${orderId}`);
Service discovery with caching (avoid per-request lookup):
class ServiceRegistry {
private cache = new Map<string, { url: string; expiresAt: number }>();
private readonly ttlMs = 30_000; // cache for 30 seconds
async resolve(serviceName: string): Promise<string> {
const cached = this.cache.get(serviceName);
if (cached && Date.now() < cached.expiresAt) return cached.url;
const url = await this.lookup(serviceName);
this.cache.set(serviceName, { url, expiresAt: Date.now() + this.ttlMs });
return url;
}
private async lookup(name: string): Promise<string> {
// Could be Consul, etcd, or DNS
return resolveService(name);
}
invalidate(serviceName: string): void {
this.cache.delete(serviceName);
}
}
const registry = new ServiceRegistry();
// Use in HTTP client
async function callOrderService(path: string): Promise<Response> {
const baseUrl = await registry.resolve('order-service');
const response = await fetch(`${baseUrl}${path}`);
if (response.status === 503) {
// Service may have moved — invalidate cache and retry once
registry.invalidate('order-service');
const freshUrl = await registry.resolve('order-service');
return fetch(`${freshUrl}${path}`);
}
return response;
}
Client-side vs. server-side discovery:
| Client-Side | Server-Side | |
|---|---|---|
| Who resolves | Each service | Load balancer / gateway |
| Example | Consul + client library | AWS ALB, Kubernetes Service |
| Complexity | Higher (each service implements) | Lower (transparent) |
| Flexibility | More control | Less control |
In Kubernetes, prefer server-side discovery — Services and DNS handle it for you.
Health checks are required: Discovery without health checks routes traffic to dead instances. Register health check endpoints and ensure Consul/k8s removes unhealthy instances quickly (see microservices-health-check skill).
Anti-patterns:
Environment variable pattern for configuration:
// Use environment variables for service URLs in all environments
// Development: http://localhost:8081
// Staging/Production: http://order-service (Kubernetes DNS)
const ORDER_SERVICE_URL = process.env.ORDER_SERVICE_URL;
if (!ORDER_SERVICE_URL) throw new Error('ORDER_SERVICE_URL is required');
microservices.io/patterns/service-registry.html