Kubernetes health checks in C# ASP.NET apps aren’t just about knowing if your app is running, they’re about knowing if it’s ready to serve traffic.
Let’s see it in action. Imagine a simple ASP.NET Core app that needs to connect to a database. We’ll add a readiness check that fails if the database connection isn’t established.
// Program.cs
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System.Net.NetworkInformation; // For Ping
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy()) // Basic self-check
.AddUrlCheck("google", "https://www.google.com") // External dependency check
.AddCheck<DatabaseHealthCheck>("database"); // Custom health check
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseAuthorization();
app.MapControllers();
app.MapHealthChecks("/health"); // Expose health checks at /health
app.Run();
// Custom Health Check for Database
public class DatabaseHealthCheck : IHealthCheck
{
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
// Simulate checking database connectivity.
// In a real app, this would involve trying to connect to your DB.
bool isDatabaseAvailable = CheckDatabaseConnection(); // Replace with actual DB check
if (isDatabaseAvailable)
{
return Task.FromResult(HealthCheckResult.Healthy("Database is available."));
}
else
{
return Task.FromResult(new HealthCheckResult(context.Registration.FailureStatus, "Database is unavailable."));
}
}
private bool CheckDatabaseConnection()
{
// Dummy implementation: Replace with actual database connection logic.
// For example, try to open a connection to your database.
// If it fails, return false.
try
{
// Example: using (var connection = new SqlConnection("your_connection_string")) { connection.Open(); }
// For demonstration, let's just return true randomly or based on a simple condition.
return true; // Simulate success
}
catch
{
return false; // Simulate failure
}
}
}
In Kubernetes, you’ll typically configure two types of probes:
- Liveness Probe: This tells Kubernetes if your application is running. If the liveness probe fails, Kubernetes will restart your container.
- Readiness Probe: This tells Kubernetes if your application is ready to serve traffic. If the readiness probe fails, Kubernetes will stop sending traffic to your container until it starts succeeding again.
This distinction is crucial. An app can be running (liveness passes) but not ready to serve traffic (readiness fails) if it’s still initializing, connecting to external services, or performing a long-running startup task.
Here’s how you’d configure these in a Kubernetes deployment manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-aspnet-app
spec:
replicas: 3
selector:
matchLabels:
app: my-aspnet-app
template:
metadata:
labels:
app: my-aspnet-app
spec:
containers:
- name: my-aspnet-app
image: your-docker-image:latest
ports:
- containerPort: 80
livenessProbe:
httpGet:
path: /health/live # Kubernetes will hit this endpoint
port: 80
initialDelaySeconds: 15 # Give the app time to start
periodSeconds: 20 # Check every 20 seconds
readinessProbe:
httpGet:
path: /health/ready # Kubernetes will hit this endpoint
port: 80
initialDelaySeconds: 30 # Give the app more time for initial setup
periodSeconds: 10 # Check readiness more frequently
By default, ASP.NET Core’s health checks middleware exposes a /health endpoint. However, Kubernetes needs separate endpoints for liveness and readiness to differentiate. You can achieve this by configuring multiple health check registrations and mapping them to distinct endpoints.
To have separate /health/live and /health/ready endpoints, you’d modify your Program.cs:
// ... (previous using statements and builder setup)
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(), tags: new[] { "live", "ready" }) // A basic check for both
.AddCheck<DatabaseHealthCheck>("database", tags: new[] { "ready" }); // Database check only for readiness
var app = builder.Build();
// ... (middleware setup)
// Map specific health check endpoints
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = (registration) => registration.Tags.Contains("live")
});
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = (registration) => registration.Tags.Contains("ready")
});
app.Run();
In this setup:
/health/livewill only run checks tagged with "live"./health/readywill run checks tagged with "ready".
The initialDelaySeconds in the Kubernetes probes is vital. It prevents Kubernetes from immediately marking a newly started pod as unhealthy before your application has had a chance to initialize and become ready. Adjust these values based on your application’s startup time.
The most surprising thing about configuring these probes is how often people get them wrong by using the same endpoint and configuration for both liveness and readiness. This completely defeats the purpose of readiness probes, which is to gracefully take a pod out of service rotation during startup or temporary unavailability without causing a restart.
When a readiness probe fails, Kubernetes doesn’t restart the pod. Instead, it removes the pod’s IP address from the service’s load balancing pool. This means no new traffic will be directed to it. Once the readiness probe starts succeeding again, Kubernetes will add the pod back into the pool. This seamless process ensures that users never experience errors due to an app that’s running but not ready.
The next concept to master is implementing more sophisticated health checks, like those that verify connectivity and state for critical external dependencies beyond simple URL pings.