kube-state-metrics (NodePort 30808) und ServiceAccount prometheus-infra (nur nodes/metrics) im Cluster; Prometheus auf infra liest kubelet/cAdvisor per TLS gegen die k3s-CA. Ziele per file_sd aus kubectl get nodes, cAdvisor auf die nötigen Reihen begrenzt. Dashboard "k3s – Pods" (Vorgabe nex), Alerts K3s* nur mit notify. Playbook monitoring-k3s.yml. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
abf4a75063
commit
4ac251715e
@@ -0,0 +1,74 @@
|
||||
# Pod-Monitoring (Task 6.5): kube-state-metrics und Lesezugang für Prometheus in k3s, danach der Infra-Stack
|
||||
# (Prometheus-Jobs kubelet/cadvisor/kube-state-metrics, file_sd-Ziele, Dashboard „k3s – Pods“). Läuft auf ops:
|
||||
# ansible-playbook playbooks/monitoring-k3s.yml
|
||||
# Token und CA landen auf ops unter /root/.config/knaster (0600), die Rolle infra-stack kopiert sie nach infra.
|
||||
- name: k3s – kube-state-metrics und Zugang für Prometheus
|
||||
hosts: localhost
|
||||
connection: local
|
||||
gather_facts: false
|
||||
vars:
|
||||
manifests: "{{ playbook_dir }}/../../../../deploy/k8s/monitoring"
|
||||
token_file: /root/.config/knaster/k3s-prometheus.token
|
||||
ca_file: /root/.config/knaster/k3s-ca.crt
|
||||
tasks:
|
||||
- name: Manifeste anwenden
|
||||
ansible.builtin.command: kubectl apply -f {{ manifests }}/{{ item }}
|
||||
loop: [prometheus-access.yaml, kube-state-metrics.yaml]
|
||||
register: apply
|
||||
changed_when: "'configured' in apply.stdout or 'created' in apply.stdout"
|
||||
|
||||
- name: Warten, bis kube-state-metrics bereit ist
|
||||
ansible.builtin.command: kubectl -n kube-system rollout status deployment/kube-state-metrics --timeout=180s
|
||||
changed_when: false
|
||||
|
||||
- name: Token des ServiceAccounts prometheus-infra lesen (Secret wird vom Controller befüllt)
|
||||
ansible.builtin.shell: >
|
||||
set -o pipefail;
|
||||
kubectl -n kube-system get secret prometheus-infra-token -o go-template='{{ "{{" }}.data.token{{ "}}" }}' | base64 -d
|
||||
args: {executable: /bin/bash}
|
||||
register: token
|
||||
until: token.stdout | length > 0
|
||||
retries: 10
|
||||
delay: 2
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Token als Datei auf ops ablegen (600, nie ausgeben)
|
||||
ansible.builtin.copy:
|
||||
content: "{{ token.stdout }}"
|
||||
dest: "{{ token_file }}"
|
||||
mode: "0600"
|
||||
no_log: true
|
||||
|
||||
# Die kubelet-Zertifikate sind von k3s-server-ca signiert und tragen die Knoten-IP (19.10. geprüft).
|
||||
- name: CA des Clusters lesen
|
||||
ansible.builtin.shell: >
|
||||
set -o pipefail;
|
||||
kubectl -n kube-system get secret prometheus-infra-token -o go-template='{{ "{{" }}index .data "ca.crt"{{ "}}" }}' | base64 -d
|
||||
args: {executable: /bin/bash}
|
||||
register: ca
|
||||
changed_when: false
|
||||
|
||||
- name: CA als Datei auf ops ablegen
|
||||
ansible.builtin.copy:
|
||||
content: "{{ ca.stdout }}\n"
|
||||
dest: "{{ ca_file }}"
|
||||
mode: "0600"
|
||||
|
||||
- name: Zugang prüfen (cAdvisor eines Knotens mit Token und CA)
|
||||
ansible.builtin.shell: >
|
||||
set -o pipefail;
|
||||
ip=$(kubectl get node k3s -o jsonpath='{.status.addresses[?(@.type=="InternalIP")].address}');
|
||||
curl -fsS -m 10 --cacert {{ ca_file }} -H "Authorization: Bearer $(cat {{ token_file }})"
|
||||
https://$ip:10250/metrics/cadvisor | grep -c '^container_memory_working_set_bytes'
|
||||
args: {executable: /bin/bash}
|
||||
register: probe
|
||||
changed_when: false
|
||||
|
||||
- name: Ergebnis
|
||||
ansible.builtin.debug:
|
||||
msg: "cAdvisor auf k3s erreichbar, {{ probe.stdout }} Reihen container_memory_working_set_bytes"
|
||||
|
||||
- name: Infra-LXC – Prometheus, Ziele und Dashboard
|
||||
hosts: infra
|
||||
roles: [infra-stack]
|
||||
@@ -24,6 +24,10 @@ infra_secrets: [POSTGRES_PASSWORD, KNASTER_API_TOKEN, GRAFANA_ADMIN_PASSWORD, GR
|
||||
# Lese-Token monitoring@pve!prom (PVEAuditor) auf ops
|
||||
infra_pve_exporter_env: /root/.config/knaster/pve-exporter.env
|
||||
|
||||
# Lesezugang zum kubelet für Prometheus (Task 6.5), auf ops von playbooks/monitoring-k3s.yml abgelegt
|
||||
infra_k3s_token: /root/.config/knaster/k3s-prometheus.token
|
||||
infra_k3s_ca: /root/.config/knaster/k3s-ca.crt
|
||||
|
||||
# file_sd: auf ops aus `tofu output -json gpu_vms` erzeugt (nicht im Repo)
|
||||
infra_tofu_dir: "{{ playbook_dir }}/../../tofu"
|
||||
infra_targets_dir: "{{ playbook_dir }}/../monitoring-targets.generated"
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
- {path: "{{ infra_repo_dir }}", mode: "0755"}
|
||||
- {path: "{{ infra_state_dir }}", mode: "0700"}
|
||||
- {path: "{{ infra_state_dir }}/secrets", mode: "0700"}
|
||||
- {path: "{{ infra_state_dir }}/secrets/k3s", mode: "0755"}
|
||||
|
||||
# Nur die Teile, die Build und Konfiguration brauchen. Nie .git, .venv, tofu-State oder .env-Dateien.
|
||||
- name: Repo-Auszug von ops abgleichen
|
||||
@@ -87,6 +88,30 @@
|
||||
mode: "0600"
|
||||
no_log: true
|
||||
|
||||
# Task 6.5: Token des ServiceAccounts prometheus-infra und CA des Clusters (von playbooks/monitoring-k3s.yml
|
||||
# auf ops abgelegt). Lesbar für nobody (Prometheus im Container). Fehlen sie, bleiben die k3s-Jobs down.
|
||||
- name: k3s-Zugang für Prometheus vorhanden? (auf ops)
|
||||
ansible.builtin.stat:
|
||||
path: "{{ item }}"
|
||||
loop: ["{{ infra_k3s_token }}", "{{ infra_k3s_ca }}"]
|
||||
delegate_to: localhost
|
||||
register: k3s_access
|
||||
|
||||
- name: k3s-Zugang für Prometheus kopieren
|
||||
ansible.builtin.copy:
|
||||
src: "{{ item.src }}"
|
||||
dest: "{{ infra_state_dir }}/secrets/k3s/{{ item.dest }}"
|
||||
owner: "65534"
|
||||
group: "65534"
|
||||
mode: "0400"
|
||||
loop:
|
||||
- {src: "{{ infra_k3s_token }}", dest: token}
|
||||
- {src: "{{ infra_k3s_ca }}", dest: ca.crt}
|
||||
loop_control:
|
||||
label: "{{ item.dest }}"
|
||||
when: k3s_access.results | map(attribute='stat.exists') | min
|
||||
no_log: true
|
||||
|
||||
- name: file_sd-Ziele
|
||||
ansible.builtin.include_tasks: targets.yml
|
||||
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
# file_sd-Ziele der GPU-VMs (node, gpu, llamacpp). Auch einzeln aufrufbar: playbooks/monitoring-targets.yml
|
||||
# (nach refresh-vm-access.sh, wenn sich DHCP-Adressen geändert haben). Prometheus liest die Dateien selbst neu.
|
||||
# k3s (Task 6.5): kubelet und kube-state-metrics aus `kubectl get nodes` auf ops. Ist der Cluster nicht
|
||||
# erreichbar, bleiben diese Listen leer und der Rest läuft weiter.
|
||||
- name: k3s-Knoten lesen (auf ops)
|
||||
ansible.builtin.shell: mkdir -p {{ infra_targets_dir }} && kubectl get nodes -o json > {{ infra_targets_dir }}/k3s-nodes.json
|
||||
args:
|
||||
executable: /bin/bash
|
||||
delegate_to: localhost
|
||||
run_once: true
|
||||
register: k3s_nodes
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
|
||||
- name: file_sd-Ziele aus tofu output erzeugen (auf ops)
|
||||
ansible.builtin.shell: |
|
||||
set -euo pipefail
|
||||
tofu output -json gpu_vms | python3 {{ infra_repo_src }}/platform/proxmox/scripts/monitoring-targets.py \
|
||||
--registry {{ infra_repo_src }}/sites/{{ knaster_site }}/registry.yaml --out {{ infra_targets_dir }} \
|
||||
--llamacpp '{{ dict(_llamacpp_hosts | zip(_llamacpp_hosts | map('extract', hostvars, 'llamacpp_profiles'))) | to_json }}'
|
||||
--llamacpp '{{ dict(_llamacpp_hosts | zip(_llamacpp_hosts | map('extract', hostvars, 'llamacpp_profiles'))) | to_json }}' \
|
||||
{{ ('--k3s-nodes ' ~ infra_targets_dir ~ '/k3s-nodes.json') if k3s_nodes.rc == 0 else '' }}
|
||||
args:
|
||||
chdir: "{{ infra_tofu_dir }}"
|
||||
executable: /bin/bash
|
||||
@@ -27,4 +40,4 @@
|
||||
src: "{{ infra_targets_dir }}/{{ item }}.json"
|
||||
dest: "{{ infra_state_dir }}/file_sd/{{ item }}.json"
|
||||
mode: "0644"
|
||||
loop: [node, gpu, llamacpp]
|
||||
loop: [node, gpu, llamacpp, kubelet, kube-state-metrics]
|
||||
|
||||
@@ -18,6 +18,11 @@ Registry (die kennt je Slot nur eine VM, die zweite Embedding-Instanz fehlt dort
|
||||
`--llamacpp '{"gpu-pve44": ["embed"], ...}'`. Die Slots eines Dienstes kommen aus der Registry
|
||||
(engine llamacpp, gleicher Container). Labels zusätzlich docker_host (= VM) und container, damit der
|
||||
Healer die tatsächliche VM trifft. Diese Slots zählen auch für node/gpu der VM.
|
||||
|
||||
k3s (Task 6.5): Mit `--k3s-nodes DATEI` (Ausgabe von `kubectl get nodes -o json`) entstehen zusätzlich
|
||||
DIR/kubelet.json (jeder Knoten, Port 10250, für die Jobs kubelet und cadvisor) und
|
||||
DIR/kube-state-metrics.json (ein Ziel: NodePort auf der Dienst-VM k3s, sonst auf dem ersten bereiten Knoten).
|
||||
Ohne die Option bleiben beide Listen leer.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -35,6 +40,9 @@ LLAMACPP_PROFILES = {
|
||||
"embed": [("llamacpp-embed", 8081), ("llamacpp-rerank", 8082)],
|
||||
"chat": [("llamacpp-chat", 8080)],
|
||||
}
|
||||
KUBELET_PORT = 10250
|
||||
KSM_NODEPORT = 30808 # deploy/k8s/monitoring/kube-state-metrics.yaml
|
||||
K3S_SERVER = "k3s" # Dienst-VM mit der Steuerung (HA), bevorzugtes Ziel für kube-state-metrics
|
||||
|
||||
|
||||
def slots_by_vmid(registry: dict) -> dict[str, list[str]]:
|
||||
@@ -89,11 +97,41 @@ def build_targets(gpu_vms: dict, registry: dict,
|
||||
return result
|
||||
|
||||
|
||||
def _internal_ip(node: dict) -> str | None:
|
||||
return next((a["address"] for a in node.get("status", {}).get("addresses", [])
|
||||
if a.get("type") == "InternalIP"), None)
|
||||
|
||||
|
||||
def _ready(node: dict) -> bool:
|
||||
return any(c.get("type") == "Ready" and c.get("status") == "True"
|
||||
for c in node.get("status", {}).get("conditions", []))
|
||||
|
||||
|
||||
def build_k3s_targets(nodes: dict) -> dict[str, list[dict]]:
|
||||
"""Ziele aus `kubectl get nodes -o json`. instance ist der Knotenname (bleibt bei IP-Wechsel gleich)."""
|
||||
kubelet: list[dict] = []
|
||||
ready: list[tuple[str, str]] = []
|
||||
for node in sorted(nodes.get("items", []), key=lambda n: n["metadata"]["name"]):
|
||||
name, ip = node["metadata"]["name"], _internal_ip(node)
|
||||
if not ip:
|
||||
print(f"Warnung: k3s-Knoten {name} ohne InternalIP, übersprungen", file=sys.stderr)
|
||||
continue
|
||||
kubelet.append({"targets": [f"{ip}:{KUBELET_PORT}"], "labels": {"instance": name, "node": name}})
|
||||
if _ready(node):
|
||||
ready.append((name, ip))
|
||||
ksm: list[dict] = []
|
||||
if ready:
|
||||
_, ip = next((r for r in ready if r[0] == K3S_SERVER), ready[0])
|
||||
ksm.append({"targets": [f"{ip}:{KSM_NODEPORT}"], "labels": {"instance": "kube-state-metrics"}})
|
||||
return {"kubelet": kubelet, "kube-state-metrics": ksm}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
ap.add_argument("--registry", required=True, type=Path)
|
||||
ap.add_argument("--out", required=True, type=Path)
|
||||
ap.add_argument("--llamacpp", default="{}", help='JSON {"<vm>": ["embed"|"chat", ...]} aus dem Inventar')
|
||||
ap.add_argument("--k3s-nodes", type=Path, help="Ausgabe von `kubectl get nodes -o json` (Task 6.5)")
|
||||
args = ap.parse_args()
|
||||
|
||||
gpu_vms = json.load(sys.stdin)
|
||||
@@ -103,6 +141,8 @@ def main() -> int:
|
||||
if unknown:
|
||||
ap.error(f"unbekannte llama.cpp-Profile: {sorted(unknown)}")
|
||||
targets = build_targets(gpu_vms, registry, llamacpp)
|
||||
k3s_nodes = json.loads(args.k3s_nodes.read_text(encoding="utf-8")) if args.k3s_nodes else {}
|
||||
targets.update(build_k3s_targets(k3s_nodes))
|
||||
args.out.mkdir(parents=True, exist_ok=True)
|
||||
for job, entries in targets.items():
|
||||
path = args.out / f"{job}.json"
|
||||
|
||||
Reference in New Issue
Block a user