"""
Cilium ClusterMesh 환경에서 Node → Prometheus / OpenSearch 접근 확인 스크립트
실행:
python scripts/check_clustermesh_connectivity.py
python scripts/check_clustermesh_connectivity.py --prometheus-url http://10.96.x.x:9090
python scripts/check_clustermesh_connectivity.py --verbose
"""
import argparse
import json
import socket
import sys
import time
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
GREEN = "\033[92m"
YELLOW = "\033[93m"
RED = "\033[91m"
CYAN = "\033[96m"
RESET = "\033[0m"
BOLD = "\033[1m"
def ok(msg): print(f" {GREEN}✓{RESET} {msg}")
def warn(msg): print(f" {YELLOW}⚠{RESET} {msg}")
def fail(msg): print(f" {RED}✗{RESET} {msg}")
def info(msg): print(f" {CYAN}→{RESET} {msg}")
@dataclass
class CheckResult:
name: str
status: str
latency_ms: float = 0.0
detail: str = ""
sub_checks: list = field(default_factory=list)
results: list[CheckResult] = []
def check_dns(hostname: str) -> Optional[str]:
"""DNS 해석 시도"""
try:
ip = socket.gethostbyname(hostname)
return ip
except socket.gaierror:
return None
def check_tcp(host: str, port: int, timeout: int = 5) -> tuple[bool, float]:
"""TCP 연결 + latency 측정"""
start = time.time()
try:
sock = socket.create_connection((host, port), timeout=timeout)
sock.close()
return True, round((time.time() - start) * 1000, 2)
except (socket.timeout, ConnectionRefusedError, OSError):
return False, 0.0
def http_get(url: str, timeout: int = 10, auth=None,
verify: bool = False, headers: dict = None) -> tuple[bool, int, float, str]:
"""HTTP GET + latency 측정"""
start = time.time()
try:
r = requests.get(
url, timeout=timeout, auth=auth,
verify=verify, headers=headers or {},
allow_redirects=True,
)
latency = round((time.time() - start) * 1000, 2)
return True, r.status_code, latency, r.text[:500]
except requests.exceptions.ConnectionError as e:
return False, 0, 0.0, f"ConnectionError: {e}"
except requests.exceptions.Timeout:
return False, 0, 0.0, "Timeout"
except Exception as e:
return False, 0, 0.0, str(e)
def check_prometheus(base_url: str, verbose: bool = False):
print(f"\n{BOLD}{'─'*55}{RESET}")
print(f"{BOLD}🔥 Prometheus 연결 확인{RESET}")
print(f" URL: {base_url}")
print(f"{'─'*55}")
host = base_url.split("//")[-1].split(":")[0]
port_str = base_url.split(":")[-1].rstrip("/").split("/")[0]
port = int(port_str) if port_str.isdigit() else 9090
sub = []
print("\n [1] DNS 해석")
ip = check_dns(host)
if ip:
ok(f"DNS 해석 성공: {host} → {ip}")
sub.append(("DNS", "OK", ip))
if "svc.cluster.local" in host:
info("ClusterMesh FQDN 형식 감지 — 클러스터 내 DNS 사용 중")
elif "svc" in host:
info("서비스 도메인 형식 감지")
else:
fail(f"DNS 해석 실패: {host}")
warn("ClusterMesh FQDN이라면 Node의 /etc/resolv.conf 에 cluster DNS 서버가 없을 수 있음")
info(f"대안: ClusterIP 직접 사용 또는 /etc/hosts 에 {host} 추가")
sub.append(("DNS", "FAIL", "해석 불가"))
print("\n [2] TCP 연결 (포트 {port})")
tcp_ok, tcp_ms = check_tcp(host, port)
if tcp_ok:
ok(f"TCP 연결 성공 (latency: {tcp_ms}ms)")
sub.append(("TCP", "OK", f"{tcp_ms}ms"))
if tcp_ms > 100:
warn(f"latency {tcp_ms}ms — 100ms 초과, 클러스터 간 네트워크 지연 의심")
else:
fail(f"TCP 연결 실패 ({host}:{port})")
info("확인 사항:")
info(" cilium clustermesh status → 클러스터 연결 상태")
info(" kubectl get svc -A | grep prometheus → 서비스 존재 여부")
info(" NetworkPolicy가 Node → Pod 접근을 차단하는지 확인")
sub.append(("TCP", "FAIL", "연결 불가"))
results.append(CheckResult("Prometheus", "FAIL", 0, "TCP 연결 불가", sub))
return
print("\n [3] HTTP Health Check")
ok_flag, code, ms, body = http_get(f"{base_url}/-/healthy")
if ok_flag and code == 200:
ok(f"/-/healthy → {code} ({ms}ms)")
sub.append(("Health", "OK", f"{ms}ms"))
else:
fail(f"/-/healthy → code={code}, {ms}ms")
sub.append(("Health", "FAIL", body[:100]))
print("\n [4] HTTP Ready Check")
ok_flag, code, ms, body = http_get(f"{base_url}/-/ready")
if ok_flag and code == 200:
ok(f"/-/ready → {code} ({ms}ms)")
sub.append(("Ready", "OK", f"{ms}ms"))
else:
warn(f"/-/ready → code={code} (아직 초기화 중일 수 있음)")
sub.append(("Ready", "WARN", str(code)))
print("\n [5] PromQL 쿼리 테스트")
test_queries = {
"up 메트릭": "up",
"scrape 대상 수": "count(up)",
}
for label, promql in test_queries.items():
ok_flag, code, ms, body = http_get(
f"{base_url}/api/v1/query",
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
ok_flag, code, ms, body = http_get(
f"{base_url}/api/v1/query?query={requests.utils.quote(promql)}"
)
if ok_flag and code == 200:
try:
data = json.loads(body.split("\n")[0] if "\n" in body else body)
result_len = len(data.get("data", {}).get("result", []))
ok(f"{label}: {result_len}개 시리즈 ({ms}ms)")
sub.append((f"PromQL:{label}", "OK", f"{result_len} series"))
if verbose:
info(f" 응답 미리보기: {body[:200]}")
except Exception:
ok(f"{label}: 응답 수신 ({ms}ms)")
else:
warn(f"{label}: 응답 오류 code={code}")
print("\n [6] ClusterMesh 관련 메트릭")
ok_flag, code, ms, body = http_get(
f"{base_url}/api/v1/query?query=cilium_clustermesh_remote_clusters"
)
if ok_flag and code == 200:
try:
data = json.loads(body)
result = data.get("data", {}).get("result", [])
if result:
ok(f"ClusterMesh 메트릭 존재: {len(result)}개 시리즈")
for r in result[:3]:
info(f" {r.get('metric')} = {r.get('value', ['', ''])[1]}")
else:
info("ClusterMesh 메트릭 없음 (Cilium metrics가 수집 안될 수 있음)")
except Exception:
pass
else:
info("ClusterMesh 메트릭 조회 불가 (선택적 확인)")
overall = "OK" if all(s[1] == "OK" for s in sub[:4]) else "WARN"
results.append(CheckResult("Prometheus", overall, tcp_ms, base_url, sub))
def check_opensearch(
host: str, port: int,
user: str, password: str,
use_ssl: bool = False,
index_pattern: str = "logs-*",
verbose: bool = False,
):
scheme = "https" if use_ssl else "http"
base_url = f"{scheme}://{host}:{port}"
auth = (user, password) if user else None
print(f"\n{BOLD}{'─'*55}{RESET}")
print(f"{BOLD}🔍 OpenSearch 연결 확인{RESET}")
print(f" URL: {base_url}")
print(f" Auth: {'yes' if auth else 'no'} / SSL: {use_ssl}")
print(f"{'─'*55}")
sub = []
print("\n [1] DNS 해석")
ip = check_dns(host)
if ip:
ok(f"DNS 해석 성공: {host} → {ip}")
sub.append(("DNS", "OK", ip))
else:
fail(f"DNS 해석 실패: {host}")
info(f"ClusterMesh FQDN이면 Node DNS에 cluster DNS 설정 필요")
sub.append(("DNS", "FAIL", "해석 불가"))
print(f"\n [2] TCP 연결 (포트 {port})")
tcp_ok, tcp_ms = check_tcp(host, port)
if tcp_ok:
ok(f"TCP 연결 성공 ({tcp_ms}ms)")
sub.append(("TCP", "OK", f"{tcp_ms}ms"))
else:
fail(f"TCP 연결 실패 ({host}:{port})")
info("확인 사항:")
info(" kubectl get svc -A | grep opensearch")
info(" cilium clustermesh status")
sub.append(("TCP", "FAIL", "연결 불가"))
results.append(CheckResult("OpenSearch", "FAIL", 0, "TCP 연결 불가", sub))
return
print("\n [3] Cluster Health")
ok_flag, code, ms, body = http_get(
f"{base_url}/_cluster/health",
auth=auth, verify=False,
)
if ok_flag and code == 200:
try:
data = json.loads(body)
status = data.get("status", "unknown")
nodes = data.get("number_of_nodes", 0)
shards = data.get("active_shards", 0)
color = {"green": GREEN, "yellow": YELLOW, "red": RED}.get(status, RESET)
ok(f"Cluster status: {color}{status.upper()}{RESET} | nodes: {nodes} | active_shards: {shards} ({ms}ms)")
sub.append(("ClusterHealth", status.upper(), f"nodes={nodes}"))
if status == "red":
warn("Cluster 상태 RED — 일부 primary shard 미할당 상태")
elif status == "yellow":
info("Cluster 상태 YELLOW — replica shard 미할당 (단일 노드면 정상)")
except Exception:
ok(f"응답 수신 code={code} ({ms}ms)")
else:
fail(f"/_cluster/health → code={code}")
if code == 401:
fail("인증 실패 — OPENSEARCH_USER/PASSWORD 확인")
elif code == 0:
fail(f"연결 오류: {body}")
sub.append(("ClusterHealth", "FAIL", str(code)))
print("\n [4] Node 정보")
ok_flag, code, ms, body = http_get(
f"{base_url}/_nodes/stats/os,jvm",
auth=auth, verify=False,
)
if ok_flag and code == 200:
try:
data = json.loads(body)
nodes = data.get("nodes", {})
for nid, ninfo in list(nodes.items())[:2]:
name = ninfo.get("name", nid[:8])
heap_used = ninfo.get("jvm", {}).get("mem", {}).get("heap_used_percent", "?")
cpu = ninfo.get("os", {}).get("cpu", {}).get("percent", "?")
ok(f"Node '{name}': heap={heap_used}% cpu={cpu}%")
if isinstance(heap_used, int) and heap_used > 85:
warn(f" heap 사용률 {heap_used}% 높음 — GC 압박 가능")
except Exception:
ok(f"Node stats 수신 ({ms}ms)")
else:
warn(f"Node stats 조회 실패 code={code}")
print(f"\n [5] 인덱스 확인 ({index_pattern})")
ok_flag, code, ms, body = http_get(
f"{base_url}/_cat/indices/{index_pattern}?h=index,health,docs.count,store.size&s=docs.count:desc&format=json",
auth=auth, verify=False,
)
if ok_flag and code == 200:
try:
indices = json.loads(body)
if indices:
ok(f"{len(indices)}개 인덱스 발견")
for idx in indices[:5]:
h = idx.get("health", "?")
color = {"green": GREEN, "yellow": YELLOW, "red": RED}.get(h, RESET)
print(f" {color}{h:6}{RESET} | {idx.get('index', ''):<40} | docs: {idx.get('docs.count', 0):>10} | size: {idx.get('store.size', '?')}")
if verbose and len(indices) > 5:
info(f" ... 외 {len(indices)-5}개")
sub.append(("Indices", "OK", f"{len(indices)}개"))
else:
warn(f"'{index_pattern}' 패턴에 매칭되는 인덱스 없음")
sub.append(("Indices", "WARN", "0개"))
except Exception as e:
warn(f"인덱스 목록 파싱 실패: {e}")
else:
warn(f"인덱스 조회 실패 code={code}")
print(f"\n [6] 최근 로그 쿼리 테스트")
query = {
"query": {"range": {"@timestamp": {"gte": "now-30m", "lte": "now"}}},
"size": 1,
"_source": ["@timestamp", "message", "level"],
}
try:
start = time.time()
r = requests.post(
f"{base_url}/{index_pattern}/_search",
json=query,
auth=auth,
verify=False,
timeout=10,
)
ms = round((time.time() - start) * 1000, 2)
if r.status_code == 200:
data = r.json()
total = data.get("hits", {}).get("total", {}).get("value", 0)
took = data.get("took", 0)
ok(f"최근 30분 로그: {total}건 (ES took: {took}ms, 응답: {ms}ms)")
hits = data.get("hits", {}).get("hits", [])
if hits and verbose:
src = hits[0].get("_source", {})
info(f" 최신 로그: {src.get('@timestamp')} [{src.get('level', '')}] {str(src.get('message', ''))[:100]}")
sub.append(("LogQuery", "OK", f"total={total}"))
else:
warn(f"로그 쿼리 응답 code={r.status_code}")
sub.append(("LogQuery", "WARN", str(r.status_code)))
except Exception as e:
fail(f"로그 쿼리 실패: {e}")
sub.append(("LogQuery", "FAIL", str(e)[:80]))
overall = "OK" if tcp_ok and all(
s[1] in ("OK", "WARN") for s in sub
) else "FAIL"
results.append(CheckResult("OpenSearch", overall, tcp_ms, base_url, sub))
def check_clustermesh_status():
import subprocess
print(f"\n{BOLD}{'─'*55}{RESET}")
print(f"{BOLD}🌐 Cilium ClusterMesh 상태{RESET}")
print(f"{'─'*55}")
commands = {
"ClusterMesh 상태": ["cilium", "clustermesh", "status"],
"Cilium 전체 상태": ["cilium", "status", "--brief"],
"Remote cluster 엔드포인트": ["kubectl", "get", "ciliumnodes", "-A", "--no-headers"],
}
for label, cmd in commands.items():
print(f"\n [{label}]")
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=10
)
if result.returncode == 0:
for line in result.stdout.strip().split("\n")[:10]:
if line.strip():
ok(line.strip())
else:
warn(f"명령 실패: {result.stderr.strip()[:100]}")
except FileNotFoundError:
info(f"'{cmd[0]}' 명령 없음 (Node에 미설치)")
except subprocess.TimeoutExpired:
warn("타임아웃 (10s)")
except Exception as e:
warn(str(e))
def print_summary():
print(f"\n{BOLD}{'='*55}{RESET}")
print(f"{BOLD}📋 최종 결과 요약{RESET}")
print(f"{BOLD}{'='*55}{RESET}")
print(f" 시각: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
all_ok = True
for r in results:
color = {
"OK": GREEN, "WARN": YELLOW, "FAIL": RED
}.get(r.status, RESET)
icon = {"OK": "✓", "WARN": "⚠", "FAIL": "✗"}.get(r.status, "?")
lat = f" ({r.latency_ms}ms)" if r.latency_ms else ""
print(f" {color}{icon} {r.name:<15}{RESET} {color}{r.status}{RESET}{lat}")
print(f" └ {r.detail}")
for name, status, detail in r.sub_checks:
c = {
"OK": GREEN, "WARN": YELLOW,
"FAIL": RED
}.get(status, RESET)
print(f" {'✓' if status=='OK' else '⚠' if status=='WARN' else '✗'} {name}: {c}{status}{RESET} — {detail}")
if r.status == "FAIL":
all_ok = False
print(f"\n{'─'*55}")
if all_ok:
print(f" {GREEN}{BOLD}✅ 모든 서비스 접근 가능 — config.py에 반영하세요{RESET}")
else:
print(f" {RED}{BOLD}❌ 일부 서비스 접근 불가 — 위 항목 확인 필요{RESET}")
print(f"\n {YELLOW}config.py 권장 설정 (접근 가능한 서비스만):{RESET}")
for r in results:
if r.status in ("OK", "WARN"):
print(f" {r.name}: {r.detail}")
print(f"{'='*55}\n")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="ClusterMesh 환경 Prometheus/OpenSearch 연결 확인"
)
parser.add_argument("--prometheus-url", default="http://prometheus:9090")
parser.add_argument("--opensearch-host", default="opensearch")
parser.add_argument("--opensearch-port", type=int, default=9200)
parser.add_argument("--opensearch-user", default="admin")
parser.add_argument("--opensearch-pass", default="admin")
parser.add_argument("--opensearch-ssl", action="store_true")
parser.add_argument("--index-pattern", default="logs-*")
parser.add_argument("--skip-clustermesh", action="store_true",
help="cilium/kubectl 명령 스킵")
parser.add_argument("--verbose", "-v", action="store_true")
args = parser.parse_args()
print(f"\n{BOLD}ClusterMesh 서비스 접근 진단{RESET}")
print(f"{'='*55}")
if not args.skip_clustermesh:
check_clustermesh_status()
check_prometheus(args.prometheus_url, args.verbose)
check_opensearch(
host=args.opensearch_host,
port=args.opensearch_port,
user=args.opensearch_user,
password=args.opensearch_pass,
use_ssl=args.opensearch_ssl,
index_pattern=args.index_pattern,
verbose=args.verbose,
)
print_summary()