Dashboard Infrastructure : Visualiser ses services Nomad/Consul en temps réel

devops
nomad
consul
react
infrastructure
monitoring
Author

Sylvain Pham

Published

January 16, 2026

Dashboard React pour visualiser en temps réel l’état de santé de 45+ services déployés sur Nomad, via l’API Consul. Filtrage par catégorie, recherche, et statut live.

Dashboard Infrastructure

Contexte

Migration d’une plateforme data de Docker Compose vers Nomad/Consul. Besoin d’une vue d’ensemble des services déployés sans passer par les UI natives de Nomad et Consul.

Stack technique

Composant Technologie
Frontend React 19 + TypeScript + Vite
Styling Tailwind CSS
Backend API Consul (health checks)
Orchestration HashiCorp Nomad
Discovery HashiCorp Consul

Architecture

┌─────────────────┐     ┌─────────────────┐
│  React Dashboard │────▶│   Consul API    │
│   (Vite + TS)   │     │  /v1/health/*   │
└─────────────────┘     └────────┬────────┘
                                 │
                        ┌────────▼────────┐
                        │     Nomad       │
                        │  (25+ jobs)     │
                        └─────────────────┘

Le dashboard interroge Consul toutes les 10 secondes pour récupérer l’état des health checks. Chaque service Nomad s’enregistre automatiquement dans Consul avec ses checks TCP/HTTP.

Catalogue de services

Le dashboard catégorise 45 services en 8 catégories :

export const SERVICE_CATALOG: ServiceInfo[] = [
  // Databases
  { id: 'postgres', name: 'PostgreSQL', category: 'Databases', port: 5432 },
  { id: 'mongodb', name: 'MongoDB', category: 'Databases', port: 27017 },
  { id: 'neo4j', name: 'Neo4j', category: 'Databases', port: 7474, hasUI: true },
  { id: 'influxdb', name: 'InfluxDB', category: 'Databases', port: 8086, hasUI: true },
  { id: 'redis', name: 'Redis', category: 'Databases', port: 6379 },

  // Streaming
  { id: 'kafka', name: 'Kafka', category: 'Streaming', port: 9092 },
  { id: 'zookeeper', name: 'Zookeeper', category: 'Streaming', port: 2181 },

  // APIs
  { id: 'fastapi', name: 'FastAPI', category: 'APIs', port: 8000, hasUI: true },
  { id: 'graphql', name: 'GraphQL', category: 'APIs', port: 4000, hasUI: true },

  // Orchestration
  { id: 'airflow-webserver', name: 'Airflow', category: 'Orchestration', port: 8080, hasUI: true },

  // Admin Tools
  { id: 'pgadmin', name: 'pgAdmin', category: 'Admin Tools', port: 5050, hasUI: true },
  { id: 'mongo-express', name: 'Mongo Express', category: 'Admin Tools', port: 8081, hasUI: true },
  { id: 'kafka-ui', name: 'Kafka UI', category: 'Admin Tools', port: 9080, hasUI: true },
  { id: 'grafana', name: 'Grafana', category: 'Admin Tools', port: 3000, hasUI: true },

  // Infrastructure
  { id: 'nomad', name: 'Nomad', category: 'Infrastructure', port: 4646, hasUI: true },
  { id: 'consul', name: 'Consul', category: 'Infrastructure', port: 8500, hasUI: true },
  { id: 'traefik', name: 'Traefik', category: 'Infrastructure', port: 8080, hasUI: true },

  // Security
  { id: 'keycloak', name: 'Keycloak', category: 'Security & IAM', port: 8180, hasUI: true },
  { id: 'vault', name: 'Vault', category: 'Security & IAM', port: 8200, hasUI: true },
];

Hook de health check

import { useState, useEffect } from 'react';

interface HealthCheck {
  ServiceID: string;
  Status: string;
  Node: string;
}

export function useConsulHealth(refreshInterval = 10000) {
  const [healthData, setHealthData] = useState<Record<string, boolean>>({});
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchHealth = async () => {
      try {
        const response = await fetch('/api/consul/v1/health/state/passing');
        const checks: HealthCheck[] = await response.json();

        const healthMap: Record<string, boolean> = {};
        checks.forEach(check => {
          healthMap[check.ServiceID] = check.Status === 'passing';
        });

        setHealthData(healthMap);
      } catch (error) {
        console.error('Failed to fetch health data:', error);
      } finally {
        setLoading(false);
      }
    };

    fetchHealth();
    const interval = setInterval(fetchHealth, refreshInterval);
    return () => clearInterval(interval);
  }, [refreshInterval]);

  return { healthData, loading };
}

Composant ServiceCard

interface ServiceCardProps {
  service: ServiceInfo;
  isOnline: boolean;
  host: string;
}

export function ServiceCard({ service, isOnline, host }: ServiceCardProps) {
  const statusColor = isOnline ? 'bg-green-500' : 'bg-red-500';
  const url = service.hasUI ? `http://${host}:${service.port}` : null;

  return (
    <div className="bg-white rounded-lg shadow p-4 hover:shadow-md transition">
      <div className="flex items-center justify-between">
        <div className="flex items-center gap-3">
          <ServiceIcon category={service.category} />
          <div>
            <h3 className="font-semibold">{service.name}</h3>
            <p className="text-sm text-gray-500">:{service.port}</p>
          </div>
        </div>
        <span className={`w-3 h-3 rounded-full ${statusColor}`} />
      </div>
      {url && (
        <a href={url} target="_blank" className="text-blue-600 text-sm mt-2 block">
          Ouvrir →
        </a>
      )}
    </div>
  );
}

Filtrage et recherche

Le dashboard permet de :

  • Filtrer par catégorie : Databases, APIs, Streaming, etc.
  • Rechercher par nom, ID, catégorie ou port
  • Voir les stats : nombre de services online/offline par catégorie

Déploiement

Le dashboard est lui-même déployé sur Nomad :

job "infra-dashboard" {
  type = "service"

  group "dashboard" {
    network {
      port "http" {
        static = 3001
      }
    }

    task "dashboard" {
      driver = "docker"

      config {
        image = "registry:5000/infra-dashboard:latest"
        ports = ["http"]
      }

      resources {
        cpu    = 100
        memory = 128
      }

      service {
        name = "infra-dashboard"
        port = "http"

        check {
          type     = "http"
          path     = "/"
          interval = "10s"
          timeout  = "2s"
        }
      }
    }
  }
}

Résultat

Le dashboard affiche en temps réel :

  • 45 services répartis en 8 catégories
  • Statut online/offline avec indicateur visuel
  • Liens directs vers les UI (Grafana, pgAdmin, Consul, etc.)
  • Compteurs par catégorie et global

Nomad UI

L’UI native de Nomad reste accessible pour le détail des jobs et allocations :

Liste des jobs Nomad

Détail d’un job complexe comme Airflow (5 task groups) :

Job Airflow dans Nomad

Consul API · Nomad · React 19