Add platform infrastructure configuration

This commit is contained in:
donghyeon-ka
2026-08-28 17:35:41 +09:00
parent fa76531e5b
commit 16c337bcc9
302 changed files with 83259 additions and 1 deletions
+18
View File
@@ -0,0 +1,18 @@
# Observability core
This root composes the internal observability workloads deployed after the
namespace, CRD, and Local PV prerequisites:
- kube-prometheus-stack without Grafana or its bundled node-exporter;
- single-binary Loki and Tempo using isolated AIStor buckets;
- Alloy for Pod logs and internal OTLP/gRPC traces;
- a standalone node-exporter;
- the exact core NetworkPolicy matrix, platform rules, scrape targets, verified
dashboards, and source-only Alertmanager Slack routing.
It intentionally does not create public ingress, Grafana, OIDC, or credential
Secrets. The Slack routing source references
`alertmanager-slack-webhook/url`; while that Secret is absent, do not apply the
AlertmanagerConfig, its kube-prometheus-stack reference, its public HTTPS
NetworkPolicy, or alert rules. Namespace, CRD, and Local PV roots remain outside
this aggregate because their protected apply order is different.
@@ -0,0 +1,24 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: observability-allow-alertmanager-public-https
namespace: observability
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: alertmanager
app.kubernetes.io/instance: observability-core-kube-pr-alertmanager
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 10.0.0.0/8
- 100.64.0.0/10
- 172.16.0.0/12
- 192.168.0.0/16
ports:
- protocol: TCP
port: 443
@@ -0,0 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: observability
resources:
- platform-alertmanager.yaml
- alertmanager-public-https-egress.yaml
@@ -0,0 +1,80 @@
apiVersion: monitoring.coreos.com/v1alpha1
kind: AlertmanagerConfig
metadata:
name: platform-alertmanager
namespace: observability
labels:
observability.hyeonworks.com/instance: home
spec:
route:
receiver: platform-slack
groupBy:
- cluster
- namespace
- alertname
- severity
groupWait: 30s
groupInterval: 5m
repeatInterval: 4h
routes:
- receiver: platform-null
matchers:
- name: alertname
matchType: "="
value: InfoInhibitor
inhibitRules:
- sourceMatch:
- name: alertname
matchType: "="
value: InfoInhibitor
targetMatch:
- name: severity
matchType: "="
value: info
equal:
- namespace
receivers:
- name: platform-null
- name: platform-slack
slackConfigs:
- apiURL:
name: alertmanager-slack-webhook
key: url
sendResolved: true
linkNames: false
mrkdwnIn:
- text
- fields
fallback: >-
{{ if eq .Status "firing" }}FIRING{{ else }}RESOLVED{{ end }}: {{ if .CommonLabels.severity }}{{ .CommonLabels.severity | toUpper }}{{ else }}UNKNOWN{{ end }} · {{ .CommonLabels.alertname }}
title: >-
{{ if eq .Status "firing" }}[FIRING:{{ .Alerts.Firing | len }}]{{ else }}[RESOLVED]{{ end }} {{ if .CommonLabels.severity }}{{ .CommonLabels.severity | toUpper }}{{ else }}UNKNOWN{{ end }} · {{ .CommonLabels.alertname }}
titleLink: https://grafana.learn.hyeonworks.com/
color: >-
{{ if eq .Status "resolved" }}good{{ else if or (eq .CommonLabels.severity "emergency") (eq .CommonLabels.severity "critical") }}danger{{ else if eq .CommonLabels.severity "warning" }}warning{{ else }}#439FE0{{ end }}
fields:
- title: Status
value: '{{ .Status | toUpper }}'
short: true
- title: Severity
value: '{{ if .CommonLabels.severity }}{{ .CommonLabels.severity | toUpper }}{{ else }}UNKNOWN{{ end }}'
short: true
- title: Location
value: '{{ if .CommonLabels.cluster }}{{ .CommonLabels.cluster }}{{ else }}unknown-cluster{{ end }} / {{ if .CommonLabels.namespace }}{{ .CommonLabels.namespace }}{{ else }}cluster-scoped{{ end }}'
short: true
- title: Alert count
value: '{{ len .Alerts }}'
short: true
text: |-
{{ range .Alerts }}
*Alert status:* {{ .Status | toUpper }}
*Target:* {{ $target := .Labels.Remove $.GroupLabels.Names }}{{ if $target }}{{ range $target.SortedPairs }}{{ .Name }}={{ .Value }} {{ end }}{{ else }}unknown{{ end }}
*Summary:* {{ with .Annotations.summary }}{{ . }}{{ else }}No summary provided{{ end }}
*Details:* {{ with .Annotations.description }}{{ . }}{{ else }}No description provided{{ end }}
*Started:* {{ .StartsAt.Format "2006-01-02T15:04:05Z07:00" }}
{{ if eq .Status "resolved" }}*Ended:* {{ .EndsAt.Format "2006-01-02T15:04:05Z07:00" }}{{ end }}
{{ with .Annotations.runbook_url }}*Runbook:* <{{ . }}|대응 절차 열기>{{ end }}
{{ end }}
<https://grafana.learn.hyeonworks.com/|Grafana> · <https://grafana.learn.hyeonworks.com/explore|Explore>
footer: hyeonworks observability · Alertmanager
+12
View File
@@ -0,0 +1,12 @@
# Alloy
Grafana Alloy runs as a single-node DaemonSet in `observability-agent`.
It tails only `/var/log/pods`, writes logs to the in-cluster Loki service, and
accepts OTLP/gRPC traces on port `4317` for the in-cluster Tempo service.
The deployment deliberately disables host namespaces, automatic service
account token mounts, CRD creation, the config reloader, public exposure, and
persistent OpenTelemetry queues. The 2 GiB PVC stores only Alloy file
positions and local state. Alloy keeps primary UID/GID `473:473`; supplemental
group `0` permits read-only traversal of k3s' `root:root 0750` Pod log tree
without changing host permissions or running the container as root.
+214
View File
@@ -0,0 +1,214 @@
discovery.kubernetes "pods" {
role = "pod"
selectors {
role = "pod"
field = "spec.nodeName=" + sys.env("HOSTNAME")
}
}
discovery.relabel "pod_logs" {
targets = discovery.kubernetes.pods.targets
rule {
source_labels = ["__meta_kubernetes_namespace"]
target_label = "namespace"
}
rule {
source_labels = ["__meta_kubernetes_pod_name"]
target_label = "pod"
}
rule {
source_labels = ["__meta_kubernetes_pod_container_name"]
target_label = "container"
}
rule {
source_labels = ["__meta_kubernetes_pod_label_app_kubernetes_io_name"]
regex = "(.+)"
target_label = "app"
}
rule {
source_labels = [
"__meta_kubernetes_pod_label_app_kubernetes_io_name",
"__meta_kubernetes_pod_label_app",
]
separator = ";"
regex = "^;(.+)$"
replacement = "$1"
target_label = "app"
}
rule {
source_labels = [
"__meta_kubernetes_pod_uid",
"__meta_kubernetes_pod_container_name",
]
separator = "/"
replacement = "/var/log/pods/*$1/*.log"
target_label = "__path__"
}
rule {
source_labels = ["__meta_kubernetes_pod_container_id"]
regex = `^(\S+):\/\/.+$`
replacement = "$1"
target_label = "container_runtime"
}
}
local.file_match "pod_logs" {
path_targets = discovery.relabel.pod_logs.output
}
loki.source.file "pod_logs" {
targets = local.file_match.pod_logs.targets
forward_to = [loki.process.pod_logs.receiver]
}
loki.process "pod_logs" {
stage.match {
selector = `{container_runtime=~"containerd|cri-o"}`
stage.cri {}
}
stage.match {
selector = `{container_runtime="docker"}`
stage.docker {}
}
stage.replace {
expression = `(?i)"(?:authorization|bearer|jwt|password|passwd|client[_-]?secret)"\s*:\s*"([^"\\]*(?:\\.[^"\\]*)*)"`
replace = "[REDACTED]"
}
stage.replace {
expression = `(?i)(?:authorization\s*[:=]\s*)(?:bearer\s+)?([A-Za-z0-9._~+/=-]+)`
replace = "[REDACTED]"
}
stage.replace {
expression = `(?i)\bbearer\s+([A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+){0,2})`
replace = "[REDACTED]"
}
stage.replace {
expression = `(?i)(?:password|passwd|client[_-]?secret)\s*[:=]\s*([^\s"'{},]+)`
replace = "[REDACTED]"
}
stage.json {
expressions = {
level = "level",
}
}
stage.labels {
values = {
level = "",
}
}
stage.match {
selector = `{level!~"(?i)^(trace|debug|info|warn|warning|error|fatal|panic)$"}`
stage.label_drop {
values = ["level"]
}
}
stage.static_labels {
values = {
cluster = "home",
}
}
stage.label_keep {
values = ["cluster", "namespace", "app", "pod", "container", "level"]
}
forward_to = [loki.write.local.receiver]
}
loki.write "local" {
endpoint {
url = "http://loki.observability.svc.cluster.local:3100/loki/api/v1/push"
batch_size = "256KiB"
batch_wait = "1s"
remote_timeout = "10s"
min_backoff_period = "500ms"
max_backoff_period = "30s"
max_backoff_retries = 10
}
}
otelcol.receiver.otlp "traces" {
grpc {
endpoint = "0.0.0.0:4317"
}
output {
traces = [otelcol.processor.k8sattributes.traces.input]
}
}
otelcol.processor.k8sattributes "traces" {
auth_type = "serviceAccount"
extract {
deployment_name_from_replicaset = false
metadata = [
"k8s.namespace.name",
"k8s.deployment.name",
"k8s.node.name",
"k8s.pod.name",
"k8s.pod.uid",
"k8s.pod.start_time",
"k8s.container.name",
]
}
pod_association {
source {
from = "resource_attribute"
name = "k8s.pod.uid"
}
}
pod_association {
source {
from = "resource_attribute"
name = "k8s.pod.ip"
}
}
pod_association {
source {
from = "connection"
}
}
output {
traces = [otelcol.processor.batch.traces.input]
}
}
otelcol.processor.batch "traces" {
timeout = "1s"
send_batch_size = 512
send_batch_max_size = 1024
output {
traces = [otelcol.exporter.otlp.tempo.input]
}
}
otelcol.exporter.otlp "tempo" {
client {
endpoint = "tempo.observability.svc.cluster.local:4317"
tls {
insecure = true
}
}
retry_on_failure {
enabled = true
initial_interval = "1s"
max_interval = "15s"
max_elapsed_time = "5m"
}
sending_queue {
enabled = true
queue_size = 256
num_consumers = 2
sizer = "requests"
block_on_overflow = true
}
}
@@ -0,0 +1,51 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: observability-agent
resources:
- pvc.yaml
configMapGenerator:
- name: alloy-config
files:
- config.alloy=config.alloy
helmGlobals:
chartHome: .helm/charts
helmCharts:
- name: alloy
repo: https://grafana.github.io/helm-charts
version: 1.11.0
releaseName: alloy
namespace: observability-agent
valuesFile: values/home.yaml
includeCRDs: false
skipTests: true
images:
- name: docker.io/grafana/alloy
newName: docker.io/grafana/alloy
digest: sha256:eb21f4c0858edffcdd1b385910ddeef26f692fc2c282f61baa724fc09d274a17
patches:
- target:
group: apps
version: v1
kind: DaemonSet
name: alloy
patch: |-
- op: add
path: /spec/template/spec/automountServiceAccountToken
value: false
- op: add
path: /spec/template/spec/hostNetwork
value: false
- op: add
path: /spec/template/spec/hostPID
value: false
- op: add
path: /spec/template/spec/hostIPC
value: false
+17
View File
@@ -0,0 +1,17 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: alloy-storage
namespace: observability-agent
labels:
app.kubernetes.io/name: alloy
observability.hyeonworks.com/instance: home
spec:
accessModes:
- ReadWriteOnce
storageClassName: ssd-local-observability-alloy-retain
volumeName: observability-alloy-local-pv
resources:
requests:
storage: 2Gi
@@ -0,0 +1,155 @@
crds:
create: false
global:
podSecurityContext:
runAsNonRoot: true
runAsUser: 473
runAsGroup: 473
fsGroup: 473
fsGroupChangePolicy: OnRootMismatch
# k3s keeps /var/log/pods root:root 0750. Retain Alloy's non-root primary
# identity while granting read-only traversal of that single hostPath.
supplementalGroups:
- 0
seccompProfile:
type: RuntimeDefault
alloy:
configMap:
create: false
name: alloy-config
key: config.alloy
clustering:
enabled: false
stabilityLevel: generally-available
storagePath: /var/lib/alloy
enableHttpServerPort: true
listenAddr: 0.0.0.0
listenPort: 12345
enableReporting: false
extraPorts:
- name: otlp-grpc
port: 4317
targetPort: 4317
protocol: TCP
mounts:
varlog: false
dockercontainers: false
extra:
- name: pod-logs
mountPath: /var/log/pods
readOnly: true
- name: alloy-storage
mountPath: /var/lib/alloy
- name: tmp
mountPath: /tmp
- name: kubernetes-api-access
mountPath: /var/run/secrets/kubernetes.io/serviceaccount
readOnly: true
securityContext:
runAsNonRoot: true
runAsUser: 473
runAsGroup: 473
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
resources:
requests:
cpu: 200m
memory: 384Mi
limits:
cpu: "1"
memory: 768Mi
image:
registry: docker.io
repository: grafana/alloy
tag: v1.18.0
digest: sha256:eb21f4c0858edffcdd1b385910ddeef26f692fc2c282f61baa724fc09d274a17
pullPolicy: IfNotPresent
rbac:
create: true
namespaces: []
rules:
- apiGroups:
- ""
resources:
- pods
- namespaces
verbs:
- get
- list
- watch
clusterRules:
- apiGroups:
- apps
resources:
- replicasets
verbs:
- get
- list
- watch
serviceAccount:
create: true
name: alloy
automountServiceAccountToken: false
configReloader:
enabled: false
controller:
type: daemonset
hostNetwork: false
hostPID: false
volumes:
extra:
- name: pod-logs
hostPath:
path: /var/log/pods
type: Directory
- name: alloy-storage
persistentVolumeClaim:
claimName: alloy-storage
- name: tmp
emptyDir: {}
- name: kubernetes-api-access
projected:
defaultMode: 420
sources:
- serviceAccountToken:
expirationSeconds: 3600
path: token
- configMap:
name: kube-root-ca.crt
items:
- key: ca.crt
path: ca.crt
- downwardAPI:
items:
- path: namespace
fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
networkPolicy:
enabled: false
service:
enabled: true
type: ClusterIP
serviceMonitor:
enabled: true
interval: 30s
additionalLabels:
observability.hyeonworks.com/instance: home
ingress:
enabled: false
@@ -0,0 +1,22 @@
# Blackbox exporter
This component deploys the pinned Prometheus blackbox exporter as an internal
`ClusterIP` service in the `observability` namespace. The `/probe` handler is
reachable only from the Prometheus Pod; it has no Ingress, NodePort, host port,
or service-account token.
Only three fixed `Probe` groups are declared:
- `platform-public-edge`: Gitea health and Keycloak discovery through Host Nginx
- `platform-private-edge`: Grafana, pgAdmin, and AIStor Console must return 403
- `platform-private-internal`: the same private services must return 200 through
their ClusterIP Services
The internal Grafana and pgAdmin targets use the blackbox exporter's `hostname`
parameter to set their required HTTP Host header. Target URLs and modules are
declarative; arbitrary callers are not granted access to port 9115.
The private-edge source boundary was proven separately and recorded as a
root-only `platform-blackbox-source-v1` evidence file. Applying this component
must remain gated on that evidence matching the active deny-only Nginx config.
@@ -0,0 +1,45 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: observability
resources:
- probes-public.yaml
- probes-private-edge.yaml
- probes-private-internal.yaml
- network-policy.yaml
helmGlobals:
chartHome: .helm/charts
helmCharts:
- name: prometheus-blackbox-exporter
repo: https://prometheus-community.github.io/helm-charts
version: 11.16.0
releaseName: blackbox-exporter
namespace: observability
valuesFile: values/home.yaml
includeCRDs: false
skipTests: true
patches:
- target:
version: v1
kind: ServiceAccount
name: blackbox-exporter
patch: |-
- op: add
path: /automountServiceAccountToken
value: false
- target:
group: apps
version: v1
kind: Deployment
name: blackbox-exporter
patch: |-
- op: add
path: /spec/template/spec/hostPID
value: false
- op: add
path: /spec/template/spec/hostIPC
value: false
@@ -0,0 +1,88 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: observability-allow-prometheus-to-blackbox
namespace: observability
labels:
app.kubernetes.io/name: prometheus-blackbox-exporter
app.kubernetes.io/part-of: platform-observability
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: prometheus-blackbox-exporter
app.kubernetes.io/instance: blackbox-exporter
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: prometheus
app.kubernetes.io/instance: observability-core-kube-pr-prometheus
ports:
- protocol: TCP
port: 9115
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: observability-allow-blackbox-egress
namespace: observability
labels:
app.kubernetes.io/name: prometheus-blackbox-exporter
app.kubernetes.io/part-of: platform-observability
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: prometheus-blackbox-exporter
app.kubernetes.io/instance: blackbox-exporter
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to:
- ipBlock:
cidr: 192.168.0.107/32
ports:
- protocol: TCP
port: 443
- to:
- podSelector:
matchLabels:
app.kubernetes.io/name: grafana
app.kubernetes.io/instance: grafana
ports:
- protocol: TCP
port: 3000
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: platform-admin
podSelector:
matchLabels:
app.kubernetes.io/name: pgadmin4
app.kubernetes.io/instance: pgadmin
ports:
- protocol: TCP
port: 5050
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: object-storage
podSelector:
matchLabels:
aistor.min.io/objectStore: minio-aistor
ports:
- protocol: TCP
port: 9090
@@ -0,0 +1,28 @@
apiVersion: monitoring.coreos.com/v1
kind: Probe
metadata:
name: platform-private-edge
namespace: observability
labels:
app.kubernetes.io/name: prometheus-blackbox-exporter
app.kubernetes.io/instance: blackbox-exporter
app.kubernetes.io/part-of: platform-observability
observability.hyeonworks.com/instance: home
spec:
jobName: blackbox-private-edge
interval: 30s
scrapeTimeout: 15s
module: http_private_edge_403
prober:
url: blackbox-exporter.observability.svc.cluster.local:9115
scheme: http
path: /probe
proxyFromEnvironment: false
targets:
staticConfig:
labels:
observability.hyeonworks.com/probe-group: private-edge
static:
- https://grafana.learn.hyeonworks.com/
- https://storage-admin.learn.hyeonworks.com/
- https://db-admin.learn.hyeonworks.com/
@@ -0,0 +1,44 @@
apiVersion: monitoring.coreos.com/v1
kind: Probe
metadata:
name: platform-private-internal
namespace: observability
labels:
app.kubernetes.io/name: prometheus-blackbox-exporter
app.kubernetes.io/instance: blackbox-exporter
app.kubernetes.io/part-of: platform-observability
observability.hyeonworks.com/instance: home
spec:
jobName: blackbox-private-internal
interval: 30s
scrapeTimeout: 15s
module: http_private_internal_200
prober:
url: blackbox-exporter.observability.svc.cluster.local:9115
scheme: http
path: /probe
proxyFromEnvironment: false
targets:
staticConfig:
labels:
observability.hyeonworks.com/probe-group: private-internal
static:
- http://grafana.observability.svc.cluster.local/api/health
- http://pgadmin.platform-admin.svc.cluster.local/misc/ping
- http://minio-aistor-console.object-storage.svc.cluster.local:9090/
relabelingConfigs:
- sourceLabels:
- __param_target
regex: http://grafana[.]observability[.]svc[.]cluster[.]local/api/health
targetLabel: __param_hostname
replacement: grafana.learn.hyeonworks.com
- sourceLabels:
- __param_target
regex: http://pgadmin[.]platform-admin[.]svc[.]cluster[.]local/misc/ping
targetLabel: __param_hostname
replacement: db-admin.learn.hyeonworks.com
- sourceLabels:
- __param_target
regex: http://minio-aistor-console[.]object-storage[.]svc[.]cluster[.]local:9090/
targetLabel: __param_hostname
replacement: storage-admin.learn.hyeonworks.com
@@ -0,0 +1,27 @@
apiVersion: monitoring.coreos.com/v1
kind: Probe
metadata:
name: platform-public-edge
namespace: observability
labels:
app.kubernetes.io/name: prometheus-blackbox-exporter
app.kubernetes.io/instance: blackbox-exporter
app.kubernetes.io/part-of: platform-observability
observability.hyeonworks.com/instance: home
spec:
jobName: blackbox-public-edge
interval: 30s
scrapeTimeout: 15s
module: http_2xx
prober:
url: blackbox-exporter.observability.svc.cluster.local:9115
scheme: http
path: /probe
proxyFromEnvironment: false
targets:
staticConfig:
labels:
observability.hyeonworks.com/probe-group: public-edge
static:
- https://git.learn.hyeonworks.com/api/healthz
- https://id.learn.hyeonworks.com/realms/hyeonworks/.well-known/openid-configuration
@@ -0,0 +1,128 @@
fullnameOverride: blackbox-exporter
commonLabels:
observability.hyeonworks.com/instance: home
kind: Deployment
replicas: 1
revisionHistoryLimit: 3
strategy:
type: Recreate
rollingUpdate: null
automountServiceAccountToken: false
hostNetwork: false
hostPort: 0
image:
registry: quay.io
repository: prometheus/blackbox-exporter
tag: v0.28.0
digest: sha256:43027b43fb785b7c5adc53bd3b5dbc1a258270a2e8aff24f477b45c4e38dac68
pullPolicy: IfNotPresent
podSecurityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
seccompProfile:
type: RuntimeDefault
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
resources:
requests:
cpu: 25m
memory: 32Mi
limits:
cpu: 200m
memory: 128Mi
serviceAccount:
create: true
name: blackbox-exporter
service:
type: ClusterIP
port: 9115
ingress:
enabled: false
route:
main:
enabled: false
networkPolicy:
enabled: false
configReloader:
enabled: false
serviceMonitor:
enabled: false
selfMonitor:
enabled: true
port: http
interval: 30s
scrapeTimeout: 10s
config:
modules:
http_2xx:
prober: http
timeout: 10s
http:
method: GET
valid_status_codes:
- 200
valid_http_versions:
- HTTP/1.1
- HTTP/2.0
follow_redirects: true
enable_http2: true
preferred_ip_protocol: ip4
ip_protocol_fallback: false
tls_config:
insecure_skip_verify: false
http_private_edge_403:
prober: http
timeout: 10s
http:
method: GET
valid_status_codes:
- 403
valid_http_versions:
- HTTP/1.1
- HTTP/2.0
follow_redirects: true
enable_http2: true
preferred_ip_protocol: ip4
ip_protocol_fallback: false
tls_config:
insecure_skip_verify: false
http_private_internal_200:
prober: http
timeout: 10s
http:
method: GET
valid_status_codes:
- 200
valid_http_versions:
- HTTP/1.1
- HTTP/2.0
follow_redirects: true
enable_http2: true
preferred_ip_protocol: ip4
ip_protocol_fallback: false
@@ -0,0 +1,208 @@
{
"annotations": {
"list": []
},
"editable": false,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"id": null,
"links": [],
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"max": 1,
"min": 0,
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "min by (job, instance) (probe_success{})",
"legendFormat": "{{job}} / {{instance}}",
"range": true,
"refId": "A"
}
],
"title": "HTTPS Probe Success",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"min": 0,
"unit": "s"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"id": 2,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "max by (job, instance) (probe_duration_seconds{})",
"legendFormat": "{{job}} / {{instance}}",
"range": true,
"refId": "A"
}
],
"title": "HTTPS Probe Duration",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"min": 0,
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 8
},
"id": 3,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "max by (instance) (probe_http_status_code{job=\"blackbox-private-edge\"})",
"legendFormat": "{{instance}}",
"range": true,
"refId": "A"
}
],
"title": "Observed HTTP Status (403 Is Expected at the Private Edge)",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"min": 0,
"unit": "d"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 8
},
"id": 4,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "(probe_ssl_earliest_cert_expiry{} - time()) / 86400",
"legendFormat": "{{job}} / {{instance}}",
"range": true,
"refId": "A"
}
],
"title": "TLS Certificate Days Remaining",
"type": "timeseries"
}
],
"refresh": "30s",
"schemaVersion": 42,
"tags": [
"platform",
"observability",
"https"
],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "Platform / HTTPS Endpoints",
"uid": "platform-https-endpoints",
"version": 1
}
@@ -0,0 +1,266 @@
{
"annotations": {
"list": []
},
"editable": false,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"id": null,
"links": [],
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"max": 100,
"min": 0,
"unit": "percent"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "100 * (1 - avg by (instance) (rate(node_cpu_seconds_total{mode=\"idle\"}[5m])))",
"legendFormat": "{{instance}}",
"range": true,
"refId": "A"
}
],
"title": "Node CPU Used",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"max": 100,
"min": 0,
"unit": "percent"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"id": 2,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "100 * (1 - node_memory_MemAvailable_bytes{} / node_memory_MemTotal_bytes{})",
"legendFormat": "{{instance}}",
"range": true,
"refId": "A"
}
],
"title": "Node Memory Used",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"max": 100,
"min": 0,
"unit": "percent"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 8
},
"id": 3,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "100 * (1 - node_filesystem_avail_bytes{mountpoint=\"/\"} / node_filesystem_size_bytes{mountpoint=\"/\"})",
"legendFormat": "{{instance}} {{device}}",
"range": true,
"refId": "A"
}
],
"title": "Root Filesystem Used",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"max": 100,
"min": 0,
"unit": "percent"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 8
},
"id": 4,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "100 * (1 - node_filesystem_files_free{mountpoint=\"/\"} / node_filesystem_files{mountpoint=\"/\"})",
"legendFormat": "{{instance}} {{device}}",
"range": true,
"refId": "A"
}
],
"title": "Root Filesystem Inodes Used",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"min": 0,
"unit": "Bps"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 16
},
"id": 5,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum by (instance) (rate(node_network_receive_bytes_total{device!=\"lo\"}[5m]))",
"legendFormat": "{{instance}} receive",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum by (instance) (rate(node_network_transmit_bytes_total{device!=\"lo\"}[5m]))",
"legendFormat": "{{instance}} transmit",
"range": true,
"refId": "B"
}
],
"title": "Node Network Throughput",
"type": "timeseries"
}
],
"refresh": "30s",
"schemaVersion": 42,
"tags": [
"platform",
"observability",
"node"
],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "Platform / Kubernetes Node",
"uid": "platform-kubernetes-node",
"version": 1
}
@@ -0,0 +1,28 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: observability
generatorOptions:
disableNameSuffixHash: true
labels:
grafana_dashboard: "1"
observability.hyeonworks.com/instance: home
observability.hyeonworks.com/owner: platform-observability
configMapGenerator:
- name: grafana-dashboard-kubernetes-node
files:
- kubernetes-node.json
- name: grafana-dashboard-workload-health
files:
- workload-health.json
- name: grafana-dashboard-platform-services
files:
- platform-services.json
- name: grafana-dashboard-observability-backends
files:
- observability-backends.json
- name: grafana-dashboard-https-endpoints
files:
- https-endpoints.json
@@ -0,0 +1,384 @@
{
"annotations": {
"list": []
},
"editable": false,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"id": null,
"links": [],
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"min": 0,
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "max(prometheus_tsdb_head_series{})",
"legendFormat": "head series",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum(prometheus_engine_queries{})",
"legendFormat": "active queries",
"range": true,
"refId": "B"
}
],
"title": "Prometheus Head Series and Active Queries",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"min": 0,
"unit": "ops"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"id": 2,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum(rate(prometheus_notifications_errors_total{}[5m]))",
"legendFormat": "errors / second",
"range": true,
"refId": "A"
}
],
"title": "Prometheus Notification Error Rate",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"min": 0,
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 8
},
"id": 3,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum(loki_ingester_memory_chunks{})",
"legendFormat": "memory chunks",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum(loki_ingester_flush_queue_length{})",
"legendFormat": "flush queue",
"range": true,
"refId": "B"
}
],
"title": "Loki Ingester State",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"min": 0,
"unit": "s"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 8
},
"id": 4,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "time() - max(loki_objstore_bucket_last_successful_upload_time{})",
"legendFormat": "seconds since upload",
"range": true,
"refId": "A"
}
],
"title": "Loki Object Store Upload Age",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"min": 0,
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 16
},
"id": 5,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum(tempo_ingester_live_traces{})",
"legendFormat": "live traces",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum(tempo_ingester_flush_queue_length{})",
"legendFormat": "flush queue",
"range": true,
"refId": "B"
}
],
"title": "Tempo Ingester State",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"max": 1,
"min": 0,
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 16
},
"id": 6,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "min(alloy_config_last_load_successful{})",
"legendFormat": "last load successful",
"range": true,
"refId": "A"
}
],
"title": "Alloy Configuration State",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"min": 0,
"unit": "ops"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 24
},
"id": 7,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum(rate(loki_write_batch_retries_total{}[5m]))",
"legendFormat": "retry / second",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum(rate(loki_write_dropped_entries_total{}[5m]))",
"legendFormat": "dropped / second",
"range": true,
"refId": "B"
}
],
"title": "Alloy Delivery Retry and Drop Rate",
"type": "timeseries"
}
],
"refresh": "30s",
"schemaVersion": 42,
"tags": [
"platform",
"observability",
"backends"
],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "Platform / Observability Backends",
"uid": "platform-observability-backends",
"version": 1
}
@@ -0,0 +1,419 @@
{
"annotations": {
"list": []
},
"editable": false,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"id": null,
"links": [],
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"min": 0,
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 8,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "max(gitea_repositories{})",
"legendFormat": "repositories",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum(gitea_users{})",
"legendFormat": "users",
"range": true,
"refId": "B"
}
],
"title": "Gitea Inventory",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"min": 0,
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 8,
"x": 8,
"y": 0
},
"id": 2,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum(agroal_active_count{})",
"legendFormat": "active",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum(agroal_available_count{})",
"legendFormat": "available",
"range": true,
"refId": "B"
}
],
"title": "Keycloak Database Pool",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"min": 0,
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 8,
"x": 16,
"y": 0
},
"id": 3,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "min(cnpg_collector_up{})",
"legendFormat": "collector up",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum(cnpg_backends_total{})",
"legendFormat": "backends",
"range": true,
"refId": "B"
}
],
"title": "CloudNativePG Collector and Connections",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"min": 0,
"unit": "bytes"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 8
},
"id": 4,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum by (datname) (cnpg_pg_database_size_bytes{})",
"legendFormat": "{{datname}}",
"range": true,
"refId": "A"
}
],
"title": "CloudNativePG Database Size",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"max": 100,
"min": 0,
"unit": "percent"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 8
},
"id": 5,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "100 * max by (bucket) (minio_cluster_usage_buckets_total_bytes{bucket=~\"loki|tempo\"}) / max by (bucket) (minio_cluster_usage_buckets_quota_total_bytes{bucket=~\"loki|tempo\"} > 0)",
"legendFormat": "{{bucket}}",
"range": true,
"refId": "A"
}
],
"title": "AIStor Bucket Quota Used",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"min": 0,
"unit": "reqps"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 16
},
"id": 6,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum by (entrypoint) (rate(traefik_entrypoint_requests_total{}[5m]))",
"legendFormat": "{{entrypoint}}",
"range": true,
"refId": "A"
}
],
"title": "Traefik Request Rate",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"max": 100,
"min": 0,
"unit": "percent"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 16
},
"id": 7,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "100 * sum(rate(traefik_entrypoint_requests_total{code=~\"5..\"}[5m])) / clamp_min(sum(rate(traefik_entrypoint_requests_total{}[5m])), 1e-9)",
"legendFormat": "5xx ratio",
"range": true,
"refId": "A"
}
],
"title": "Traefik 5xx Ratio",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"max": 1,
"min": 0,
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 24
},
"id": 8,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "min by (job) (probe_success{})",
"legendFormat": "{{job}}",
"range": true,
"refId": "A"
}
],
"title": "Platform HTTPS Boundary Health",
"type": "timeseries"
}
],
"refresh": "30s",
"schemaVersion": 42,
"tags": [
"platform",
"observability",
"services"
],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "Platform / Services",
"uid": "platform-services",
"version": 1
}
@@ -0,0 +1,218 @@
{
"annotations": {
"list": []
},
"editable": false,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"id": null,
"links": [],
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"min": 0,
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum by (namespace, deployment) (kube_deployment_status_replicas_unavailable{})",
"legendFormat": "{{namespace}} / {{deployment}}",
"range": true,
"refId": "A"
}
],
"title": "Deployment Unavailable Replicas",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"min": 0,
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"id": 2,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "clamp_min(sum by (namespace, statefulset) (kube_statefulset_replicas{}) - sum by (namespace, statefulset) (kube_statefulset_status_replicas_ready{}), 0)",
"legendFormat": "{{namespace}} / {{statefulset}}",
"range": true,
"refId": "A"
}
],
"title": "StatefulSet Replicas Not Ready",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"min": 0,
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 8
},
"id": 3,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum by (namespace, pod) (increase(kube_pod_container_status_restarts_total{}[15m]))",
"legendFormat": "{{namespace}} / {{pod}}",
"range": true,
"refId": "A"
}
],
"title": "Container Restarts in 15 Minutes",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"min": 0,
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 8
},
"id": 4,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum(kube_persistentvolumeclaim_status_phase{phase=\"Pending\"})",
"legendFormat": "Pending",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum(kube_persistentvolumeclaim_status_phase{phase=\"Lost\"})",
"legendFormat": "Lost",
"range": true,
"refId": "B"
}
],
"title": "PersistentVolumeClaim Health",
"type": "timeseries"
}
],
"refresh": "30s",
"schemaVersion": 42,
"tags": [
"platform",
"observability",
"workloads"
],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "Platform / Workload Health",
"uid": "platform-workload-health",
"version": 1
}
+24
View File
@@ -0,0 +1,24 @@
# Grafana
Private Grafana for the home observability stack. The pinned Grafana chart is
rendered as the independent `grafana` release in `observability`; the
kube-prometheus-stack embedded Grafana remains disabled.
The workload consumes, but never renders, these existing Secret contracts:
- `grafana-admin`: `admin-user`, `admin-password`
- `grafana-keycloak-oidc`: `client-id`, `client-secret`
The `grafana` PVC is fixed to `observability-grafana-local-pv` (2Gi, Retain).
Host Nginx terminates the exact-SAN certificate and forwards plain HTTP through
Traefik; the Kubernetes Ingress therefore contains no TLS material.
The Pod does not automount a service-account token. A 3600-second projected
token, CA, and namespace are mounted only into `grafana-sc-dashboard`, whose
namespaced Role can read ConfigMaps but not Secrets. Dashboard ConfigMaps must
carry `grafana_dashboard: "1"` in `observability`.
Provisioned datasource UIDs are `prometheus`, `loki`, and `tempo`. Tempo service
map configuration is present for future use, but the metrics-generator and
Prometheus remote-write path are intentionally not enabled, so service-graph
availability is not an initial acceptance condition.
@@ -0,0 +1,27 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: grafana-dashboard-sidecar
namespace: observability
rules:
- apiGroups:
- ""
resources:
- configmaps
verbs:
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: grafana-dashboard-sidecar
namespace: observability
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: grafana-dashboard-sidecar
subjects:
- kind: ServiceAccount
name: grafana
namespace: observability
@@ -0,0 +1,37 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: grafana
namespace: observability
spec:
template:
spec:
automountServiceAccountToken: false
hostNetwork: false
hostPID: false
hostIPC: false
containers:
- name: grafana-sc-dashboard
volumeMounts:
- name: dashboard-sidecar-api-access
mountPath: /var/run/secrets/kubernetes.io/serviceaccount
readOnly: true
volumes:
- name: dashboard-sidecar-api-access
projected:
defaultMode: 420
sources:
- serviceAccountToken:
expirationSeconds: 3600
path: token
- configMap:
name: kube-root-ca.crt
items:
- key: ca.crt
path: ca.crt
- downwardAPI:
items:
- path: namespace
fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
@@ -0,0 +1,7 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: grafana
namespace: observability
spec:
ingressClassName: traefik
@@ -0,0 +1,24 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: observability
resources:
- dashboard-sidecar-rbac.yaml
helmGlobals:
chartHome: .helm/charts
helmCharts:
- name: grafana
repo: https://grafana-community.github.io/helm-charts
version: 12.10.1
releaseName: grafana
namespace: observability
valuesFile: values/home.yaml
includeCRDs: false
skipTests: true
patches:
- path: ingress-class-patch.yaml
- path: dashboard-sidecar-token-patch.yaml
@@ -0,0 +1,262 @@
replicas: 1
extraLabels:
observability.hyeonworks.com/instance: home
deploymentStrategy:
type: Recreate
revisionHistoryLimit: 3
automountServiceAccountToken: false
enableServiceLinks: false
rbac:
create: false
serviceAccount:
create: true
name: grafana
automountServiceAccountToken: false
image:
registry: docker.io
repository: grafana/grafana
tag: 13.1.1
sha: f33c692ba1a5ee15724cf6b22db65e9de39dde14d80f7d73a9546e3fc917270b
pullPolicy: IfNotPresent
testFramework:
enabled: false
securityContext:
runAsNonRoot: true
runAsUser: 472
runAsGroup: 472
fsGroup: 472
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
containerSecurityContext:
allowPrivilegeEscalation: false
privileged: false
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
initChownData:
enabled: false
persistence:
type: pvc
enabled: true
storageClassName: ssd-local-observability-grafana-retain
volumeName: observability-grafana-local-pv
lookupVolumeName: false
accessModes:
- ReadWriteOnce
size: 2Gi
finalizers:
- kubernetes.io/pvc-protection
admin:
existingSecret: grafana-admin
userKey: admin-user
passwordKey: admin-password
envValueFrom:
GF_AUTH_GENERIC_OAUTH_CLIENT_ID:
secretKeyRef:
name: grafana-keycloak-oidc
key: client-id
GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET:
secretKeyRef:
name: grafana-keycloak-oidc
key: client-secret
service:
enabled: true
type: ClusterIP
port: 80
targetPort: 3000
portName: service
serviceMonitor:
enabled: true
interval: 30s
path: /metrics
ingress:
enabled: true
annotations: {}
path: /
pathType: Prefix
hosts:
- grafana.learn.hyeonworks.com
tls: []
grafana.ini:
paths:
data: /var/lib/grafana/
logs: /var/log/grafana
plugins: /var/lib/grafana/plugins
provisioning: /etc/grafana/provisioning
analytics:
check_for_updates: false
check_for_plugin_updates: false
reporting_enabled: false
plugins:
preinstall_disabled: true
server:
domain: grafana.learn.hyeonworks.com
root_url: https://grafana.learn.hyeonworks.com/
enforce_domain: true
security:
cookie_secure: true
cookie_samesite: lax
auth:
disable_login_form: false
oauth_auto_login: false
login_maximum_lifetime_duration: 8h
login_maximum_inactive_lifetime_duration: 30m
auth.anonymous:
enabled: false
auth.basic:
enabled: true
auth.generic_oauth:
enabled: true
name: Keycloak
allow_sign_up: true
use_pkce: true
scopes: openid profile email
groups_attribute_path: groups
allowed_groups: /platform-observability-admins /platform-observability-viewers
role_attribute_strict: true
allow_assign_grafana_admin: false
skip_org_role_sync: false
validate_id_token: true
use_refresh_token: true
role_attribute_path: "contains(groups[*], '/platform-observability-admins') && 'Admin' || contains(groups[*], '/platform-observability-viewers') && 'Viewer' || null"
auth_url: https://id.learn.hyeonworks.com/realms/hyeonworks/protocol/openid-connect/auth
token_url: https://id.learn.hyeonworks.com/realms/hyeonworks/protocol/openid-connect/token
api_url: https://id.learn.hyeonworks.com/realms/hyeonworks/protocol/openid-connect/userinfo
signout_redirect_url: https://id.learn.hyeonworks.com/realms/hyeonworks/protocol/openid-connect/logout
datasources:
datasources.yaml:
apiVersion: 1
deleteDatasources:
- name: Prometheus
orgId: 1
- name: Loki
orgId: 1
- name: Tempo
orgId: 1
datasources:
- name: Prometheus
uid: prometheus
type: prometheus
access: proxy
url: http://observability-core-kube-pr-prometheus.observability.svc.cluster.local:9090
isDefault: true
editable: false
jsonData:
httpMethod: POST
timeInterval: 30s
- name: Loki
uid: loki
type: loki
access: proxy
url: http://loki.observability.svc.cluster.local:3100
isDefault: false
editable: false
jsonData:
derivedFields:
- name: trace_id
matcherRegex: '"trace_id"[[:space:]]*:[[:space:]]*"([0-9a-f]{32})"'
datasourceUid: tempo
url: '$${__value.raw}'
- name: Tempo
uid: tempo
type: tempo
access: proxy
url: http://tempo.observability.svc.cluster.local:3200
isDefault: false
editable: false
jsonData:
httpMethod: GET
nodeGraph:
enabled: true
serviceMap:
datasourceUid: prometheus
tracesToLogsV2:
datasourceUid: loki
spanStartTimeShift: -1m
spanEndTimeShift: 1m
tags:
- key: k8s.namespace.name
value: namespace
- key: k8s.pod.name
value: pod
filterByTraceID: true
filterBySpanID: false
sidecar:
image:
registry: quay.io
repository: kiwigrid/k8s-sidecar
tag: 2.10.0
sha: 129877c81acf2bc8c3fa000e89a62e020eb89d41ceb94767c657aef5bb0cc0d3
imagePullPolicy: IfNotPresent
resources:
requests:
cpu: 25m
memory: 64Mi
limits:
cpu: 100m
memory: 128Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
dashboards:
enabled: true
label: grafana_dashboard
labelValue: "1"
searchNamespace:
- observability
resource: configmap
watchMethod: WATCH
skipReload: true
provider:
name: sidecarProvider
orgid: 1
folder: ""
folderUid: ""
type: file
disableDelete: false
allowUiUpdates: false
foldersFromFilesStructure: false
imageRenderer:
enabled: false
networkPolicy:
enabled: false
assertNoLeakedSecrets: true
@@ -0,0 +1,15 @@
# kube-prometheus-stack
고정 chart `88.0.1``observability` namespace에 렌더한다. Grafana와
node-exporter, control-plane에 존재하지 않는 scheduler/controller-manager/etcd/
kube-proxy monitor는 끈다. Prometheus와 Alertmanager 데이터는 각각의 `Retain`
Local PV에 보존하며 CRD는 별도 root에서 먼저 적용한다.
Prometheus discovery는 namespace label
`observability.hyeonworks.com/enabled=true`와 resource label
`observability.hyeonworks.com/instance=home`의 교집합만 선택한다.
Alertmanager source는 selector discovery 대신 global
`alertmanagerConfiguration.name=platform-alertmanager`를 참조한다. 참조 대상의
Slack Secret이 준비되기 전에는 이 변경을 live에 적용하지 않고 기존 null 구성을
유지한다.
@@ -0,0 +1,37 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: observability
helmGlobals:
chartHome: .helm/charts
helmCharts:
- name: kube-prometheus-stack
repo: https://prometheus-community.github.io/helm-charts
version: 88.0.1
releaseName: observability-core
namespace: observability
valuesFile: values/home.yaml
includeCRDs: false
skipTests: true
images:
- name: quay.io/prometheus-operator/prometheus-operator
newName: quay.io/prometheus-operator/prometheus-operator
digest: sha256:64eb7914e4705dbb64438e3b3193da1226ad2ea4db2924983693999888cda9b2
- name: registry.k8s.io/kube-state-metrics/kube-state-metrics
newName: registry.k8s.io/kube-state-metrics/kube-state-metrics
digest: sha256:7661da8c99b733d43117e4cba12bd9865d335e5777191d0af3d789807aded9f4
patches:
- target:
group: monitoring.coreos.com
version: v1
kind: Alertmanager
name: observability-core-kube-pr-alertmanager
patch: |-
- op: remove
path: /spec/alertmanagerConfigSelector
- op: remove
path: /spec/alertmanagerConfigNamespaceSelector
@@ -0,0 +1,282 @@
commonLabels:
observability.hyeonworks.com/instance: home
crds:
enabled: false
grafana:
enabled: false
nodeExporter:
enabled: false
kubeApiServer:
enabled: true
kubelet:
enabled: true
coreDns:
enabled: true
kubeControllerManager:
enabled: false
kubeScheduler:
enabled: false
kubeEtcd:
enabled: false
kubeProxy:
enabled: false
defaultRules:
disabled:
Watchdog: true
rules:
etcd: false
kubeControllerManager: false
kubeProxy: false
kubeSchedulerAlerting: false
kubeSchedulerRecording: false
windows: false
prometheusOperator:
admissionWebhooks:
enabled: false
patch:
enabled: false
tls:
enabled: false
image:
registry: quay.io
repository: prometheus-operator/prometheus-operator
tag: v0.93.0
sha: 64eb7914e4705dbb64438e3b3193da1226ad2ea4db2924983693999888cda9b2
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
securityContext:
runAsNonRoot: true
runAsUser: 65534
runAsGroup: 65534
fsGroup: 65534
seccompProfile:
type: RuntimeDefault
containerSecurityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
serviceAccount:
automountServiceAccountToken: true
prometheusConfigReloader:
image:
registry: quay.io
repository: prometheus-operator/prometheus-config-reloader
tag: v0.93.0
sha: 65b90f44d5535b106015ac60bafb57803f65dc928c187874de6cd7a9ec6c8905
resources:
requests:
cpu: 25m
memory: 32Mi
limits:
cpu: 100m
memory: 64Mi
kubeStateMetrics:
enabled: true
kube-state-metrics:
image:
registry: registry.k8s.io
repository: kube-state-metrics/kube-state-metrics
tag: v2.19.1
sha: sha256:7661da8c99b733d43117e4cba12bd9865d335e5777191d0af3d789807aded9f4
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
securityContext:
enabled: true
runAsNonRoot: true
runAsUser: 65534
runAsGroup: 65534
fsGroup: 65534
seccompProfile:
type: RuntimeDefault
containerSecurityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
automountServiceAccountToken: true
collectors:
- certificatesigningrequests
- configmaps
- cronjobs
- daemonsets
- deployments
- endpointslices
- horizontalpodautoscalers
- ingresses
- jobs
- leases
- limitranges
- mutatingwebhookconfigurations
- namespaces
- networkpolicies
- nodes
- persistentvolumeclaims
- persistentvolumes
- poddisruptionbudgets
- pods
- replicasets
- replicationcontrollers
- resourcequotas
- services
- statefulsets
- storageclasses
- validatingwebhookconfigurations
- volumeattachments
prometheus:
monitor:
additionalLabels:
observability.hyeonworks.com/instance: home
alertmanager:
enabled: true
config:
global:
resolve_timeout: 5m
route:
group_by:
- namespace
group_wait: 30s
group_interval: 5m
repeat_interval: 12h
receiver: "null"
routes: []
receivers:
- name: "null"
serviceAccount:
automountServiceAccountToken: false
alertmanagerSpec:
replicas: 1
retention: 120h
automountServiceAccountToken: false
alertmanagerConfiguration:
name: platform-alertmanager
image:
registry: quay.io
repository: prometheus/alertmanager
tag: v0.33.1
sha: a89f8d4520954079275441eecdb71444328bd90633dd4eddfc33b9ed657f349b
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 250m
memory: 256Mi
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 2000
fsGroup: 2000
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
persistentVolumeClaimRetentionPolicy:
whenDeleted: Retain
whenScaled: Retain
storage:
volumeClaimTemplate:
spec:
storageClassName: ssd-local-observability-alertmanager-retain
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
prometheus:
enabled: true
prometheusSpec:
replicas: 1
retention: 15d
retentionSize: 16GB
scrapeInterval: 30s
evaluationInterval: 30s
walCompression: true
automountServiceAccountToken: true
image:
registry: quay.io
repository: prometheus/prometheus
tag: v3.13.2-distroless
sha: ce95cfa77eff5aad28bd7a65aff19868cf78d9e17e4c254da7dfe22ade78318b
resources:
requests:
cpu: 750m
memory: 2Gi
limits:
cpu: "2"
memory: 3Gi
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 2000
fsGroup: 2000
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
persistentVolumeClaimRetentionPolicy:
whenDeleted: Retain
whenScaled: Retain
storageSpec:
volumeClaimTemplate:
spec:
storageClassName: ssd-local-observability-prometheus-retain
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
serviceMonitorSelectorNilUsesHelmValues: false
podMonitorSelectorNilUsesHelmValues: false
probeSelectorNilUsesHelmValues: false
ruleSelectorNilUsesHelmValues: false
serviceMonitorNamespaceSelector:
matchLabels:
observability.hyeonworks.com/enabled: "true"
podMonitorNamespaceSelector:
matchLabels:
observability.hyeonworks.com/enabled: "true"
probeNamespaceSelector:
matchLabels:
observability.hyeonworks.com/enabled: "true"
ruleNamespaceSelector:
matchLabels:
observability.hyeonworks.com/enabled: "true"
serviceMonitorSelector:
matchLabels:
observability.hyeonworks.com/instance: home
podMonitorSelector:
matchLabels:
observability.hyeonworks.com/instance: home
probeSelector:
matchLabels:
observability.hyeonworks.com/instance: home
ruleSelector:
matchLabels:
observability.hyeonworks.com/instance: home
+16
View File
@@ -0,0 +1,16 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- kube-prometheus-stack
- loki
- tempo
- alloy
- node-exporter
- grafana
- blackbox-exporter
- network-policies
- targets
- rules/platform
- alerting
- dashboards/platform
+16
View File
@@ -0,0 +1,16 @@
# Loki
This root renders `grafana-community/loki` chart `18.7.1` as one Loki
`3.7.4` monolithic StatefulSet in `observability`.
- The image is pinned to the reviewed amd64 digest.
- Logs use the `loki` AIStor bucket through `loki-object-storage`; no root
credential is referenced.
- Retention is 30 days. The WAL and working data use the dedicated 5Gi
Retain Local PV.
- Gateway, canary, caches, embedded MinIO, tests, and rule sidecars are
disabled.
- The only client-facing service is ClusterIP port 3100.
Do not delete the PVC or bucket during rollback. Scale the StatefulSet down,
preserve its Retain volume, and investigate S3/WAL state before reapplying.
@@ -0,0 +1,17 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: observability
helmGlobals:
chartHome: .helm/charts
helmCharts:
- name: loki
repo: https://grafana-community.github.io/helm-charts
version: 18.7.1
releaseName: loki
namespace: observability
valuesFile: values/home.yaml
includeCRDs: false
skipTests: true
@@ -0,0 +1,146 @@
deploymentMode: Monolithic
commonLabels:
observability.hyeonworks.com/instance: home
defaults:
automountServiceAccountToken: false
serviceAccount:
automountServiceAccountToken: false
loki:
auth_enabled: false
image:
registry: docker.io
repository: grafana/loki
tag: "3.7.4"
digest: sha256:d80be589cf74b6d92747a1b155bec6f07bdd65e261f797fe93a78fb4e833032e
podSecurityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
containerSecurityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
commonConfig:
path_prefix: /var/loki
replication_factor: 1
schemaConfig:
configs:
- from: "2026-07-01"
store: tsdb
object_store: s3
schema: v13
index:
prefix: index_
period: 24h
storage:
type: s3
bucketNames:
chunks: loki
ruler: loki
admin: loki
s3:
endpoint: http://minio.object-storage.svc.cluster.local
s3ForcePathStyle: true
insecure: true
accessKeyId: ${AWS_ACCESS_KEY_ID}
secretAccessKey: ${AWS_SECRET_ACCESS_KEY}
useTestSchema: false
limits_config:
retention_period: 720h
ingestion_rate_mb: 4
ingestion_burst_size_mb: 6
max_line_size: 256KB
discover_log_levels: false
discover_service_name: []
ingester:
chunk_idle_period: 30m
max_chunk_age: 1h
wal:
checkpoint_duration: 5m
replay_memory_ceiling: 512MB
disk_full_threshold: 0.90
flush_on_shutdown: true
compactor:
retention_enabled: true
delete_request_store: s3
retention_delete_delay: 2h
retention_delete_worker_count: 10
max_compaction_parallelism: 1
singleBinary:
replicas: 1
sidecar: false
podLabels:
platform.hyeonworks.com/aistor-client: "true"
service:
type: ClusterIP
resources:
requests:
cpu: 350m
memory: 768Mi
limits:
cpu: 1500m
memory: 1536Mi
extraEnv:
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: loki-object-storage
key: access-key-id
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: loki-object-storage
key: secret-access-key
persistence:
enabled: true
size: 5Gi
storageClass: ssd-local-observability-loki-retain
enableStatefulSetAutoDeletePVC: true
whenScaled: Retain
whenDeleted: Retain
read:
replicas: 0
write:
replicas: 0
backend:
replicas: 0
ruler:
sidecar: false
gateway:
enabled: false
lokiCanary:
enabled: false
chunksCache:
enabled: false
resultsCache:
enabled: false
memcached:
enabled: false
minio:
enabled: false
test:
enabled: false
sidecar:
rules:
enabled: false
monitoring:
serviceMonitor:
enabled: true
interval: 30s
@@ -0,0 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- observability.yaml
- observability-agent.yaml
- object-storage-counterpart.yaml
@@ -0,0 +1,31 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: object-storage-allow-observability-s3
namespace: object-storage
labels:
app.kubernetes.io/component: observability-object-storage
app.kubernetes.io/part-of: platform
spec:
podSelector:
matchLabels:
aistor.min.io/objectStore: minio-aistor
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: observability
podSelector:
matchLabels:
platform.hyeonworks.com/aistor-client: "true"
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- loki
- tempo
ports:
- protocol: TCP
port: 9000
@@ -0,0 +1,140 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: observability-agent-allow-alloy-dns
namespace: observability-agent
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: alloy
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: observability-agent-allow-alloy-api
namespace: observability-agent
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: alloy
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 10.43.0.1/32
ports:
- protocol: TCP
port: 443
- to:
- ipBlock:
cidr: 192.168.0.107/32
ports:
- protocol: TCP
port: 6443
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: observability-agent-allow-alloy-backends
namespace: observability-agent
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: alloy
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: observability
podSelector:
matchLabels:
app.kubernetes.io/name: loki
ports:
- protocol: TCP
port: 3100
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: observability
podSelector:
matchLabels:
app.kubernetes.io/name: tempo
ports:
- protocol: TCP
port: 4317
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: observability-agent-allow-prometheus-to-alloy
namespace: observability-agent
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: alloy
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: observability
podSelector:
matchLabels:
app.kubernetes.io/name: prometheus
ports:
- protocol: TCP
port: 12345
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: observability-agent-allow-prometheus-to-node-exporter
namespace: observability-agent
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: prometheus-node-exporter
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: observability
podSelector:
matchLabels:
app.kubernetes.io/name: prometheus
ports:
- protocol: TCP
port: 9100
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: observability-agent-default-deny
namespace: observability-agent
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
@@ -0,0 +1,548 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: observability-allow-dns
namespace: observability
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: observability-allow-kubernetes-api
namespace: observability
spec:
podSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- kube-prometheus-stack-prometheus-operator
- kube-state-metrics
- prometheus
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 10.43.0.1/32
ports:
- protocol: TCP
port: 443
- to:
- ipBlock:
cidr: 192.168.0.107/32
ports:
- protocol: TCP
port: 6443
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: observability-allow-prometheus-egress
namespace: observability
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: prometheus
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app.kubernetes.io/name: kube-prometheus-stack-prometheus-operator
ports:
- protocol: TCP
port: 8080
- to:
- podSelector:
matchLabels:
app.kubernetes.io/name: kube-state-metrics
ports:
- protocol: TCP
port: 8080
- protocol: TCP
port: 8081
- to:
- podSelector:
matchLabels:
app.kubernetes.io/name: prometheus
ports:
- protocol: TCP
port: 9090
- protocol: TCP
port: 8080
- to:
- podSelector:
matchLabels:
app.kubernetes.io/name: alertmanager
ports:
- protocol: TCP
port: 9093
- protocol: TCP
port: 8080
- to:
- podSelector:
matchLabels:
app.kubernetes.io/name: loki
ports:
- protocol: TCP
port: 3100
- to:
- podSelector:
matchLabels:
app.kubernetes.io/name: tempo
ports:
- protocol: TCP
port: 3200
- to:
- podSelector:
matchLabels:
app.kubernetes.io/name: grafana
app.kubernetes.io/instance: grafana
ports:
- protocol: TCP
port: 3000
- to:
- podSelector:
matchLabels:
app.kubernetes.io/name: prometheus-blackbox-exporter
app.kubernetes.io/instance: blackbox-exporter
ports:
- protocol: TCP
port: 9115
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: observability-agent
podSelector:
matchLabels:
app.kubernetes.io/name: alloy
ports:
- protocol: TCP
port: 12345
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: observability-agent
podSelector:
matchLabels:
app.kubernetes.io/name: prometheus-node-exporter
ports:
- protocol: TCP
port: 9100
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: TCP
port: 9153
- to:
- ipBlock:
cidr: 192.168.0.107/32
ports:
- protocol: TCP
port: 10250
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: observability-allow-prometheus-to-operator
namespace: observability
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: kube-prometheus-stack-prometheus-operator
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: prometheus
ports:
- protocol: TCP
port: 8080
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: observability-allow-prometheus-to-ksm
namespace: observability
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: kube-state-metrics
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: prometheus
ports:
- protocol: TCP
port: 8080
- protocol: TCP
port: 8081
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: observability-allow-prometheus-self
namespace: observability
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: prometheus
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: prometheus
ports:
- protocol: TCP
port: 9090
- protocol: TCP
port: 8080
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: grafana
app.kubernetes.io/instance: grafana
ports:
- protocol: TCP
port: 9090
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: observability-allow-alertmanager
namespace: observability
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: alertmanager
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: prometheus
ports:
- protocol: TCP
port: 9093
- protocol: TCP
port: 8080
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: alertmanager
ports:
- protocol: TCP
port: 9094
- protocol: UDP
port: 9094
egress:
- to:
- podSelector:
matchLabels:
app.kubernetes.io/name: alertmanager
ports:
- protocol: TCP
port: 9094
- protocol: UDP
port: 9094
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: observability-allow-loki
namespace: observability
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: loki
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: prometheus
ports:
- protocol: TCP
port: 3100
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: grafana
app.kubernetes.io/instance: grafana
ports:
- protocol: TCP
port: 3100
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: observability-agent
podSelector:
matchLabels:
app.kubernetes.io/name: alloy
ports:
- protocol: TCP
port: 3100
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: loki
ports:
- protocol: TCP
port: 7946
- protocol: UDP
port: 7946
egress:
- to:
- podSelector:
matchLabels:
app.kubernetes.io/name: loki
ports:
- protocol: TCP
port: 7946
- protocol: UDP
port: 7946
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: observability-allow-tempo
namespace: observability
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: tempo
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: prometheus
ports:
- protocol: TCP
port: 3200
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: grafana
app.kubernetes.io/instance: grafana
ports:
- protocol: TCP
port: 3200
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: observability-agent
podSelector:
matchLabels:
app.kubernetes.io/name: alloy
ports:
- protocol: TCP
port: 4317
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: tempo
ports:
- protocol: TCP
port: 7946
- protocol: UDP
port: 7946
egress:
- to:
- podSelector:
matchLabels:
app.kubernetes.io/name: tempo
ports:
- protocol: TCP
port: 7946
- protocol: UDP
port: 7946
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: observability-allow-aistor-egress
namespace: observability
spec:
podSelector:
matchLabels:
platform.hyeonworks.com/aistor-client: "true"
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- loki
- tempo
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 10.43.124.248/32
ports:
- protocol: TCP
port: 80
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: object-storage
podSelector:
matchLabels:
aistor.min.io/objectStore: minio-aistor
ports:
- protocol: TCP
port: 9000
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: observability-allow-grafana-ingress
namespace: observability
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: grafana
app.kubernetes.io/instance: grafana
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
app.kubernetes.io/name: traefik
app.kubernetes.io/instance: traefik-kube-system
ports:
- protocol: TCP
port: 3000
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: prometheus
ports:
- protocol: TCP
port: 3000
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: prometheus-blackbox-exporter
app.kubernetes.io/instance: blackbox-exporter
ports:
- protocol: TCP
port: 3000
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: observability-allow-grafana-datasources
namespace: observability
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: grafana
app.kubernetes.io/instance: grafana
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app.kubernetes.io/name: prometheus
ports:
- protocol: TCP
port: 9090
- to:
- podSelector:
matchLabels:
app.kubernetes.io/name: loki
ports:
- protocol: TCP
port: 3100
- to:
- podSelector:
matchLabels:
app.kubernetes.io/name: tempo
ports:
- protocol: TCP
port: 3200
- to:
- ipBlock:
cidr: 192.168.0.107/32
ports:
- protocol: TCP
port: 443
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: observability-allow-grafana-dashboard-api
namespace: observability
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: grafana
app.kubernetes.io/instance: grafana
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 10.43.0.1/32
ports:
- protocol: TCP
port: 443
- to:
- ipBlock:
cidr: 192.168.0.107/32
ports:
- protocol: TCP
port: 6443
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: observability-default-deny
namespace: observability
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
@@ -0,0 +1,11 @@
# node-exporter
The standalone node-exporter runs in `observability-agent` with the embedded
kube-prometheus-stack copy and kube-rbac-proxy disabled. It mounts only
`/proc`, `/sys`, and `/` from the node, all read-only, and exposes an internal
ClusterIP metrics Service on port `9100`.
Host network, PID, and IPC namespaces remain disabled by the platform security
contract. Consequently, network-namespace metrics are treated as a known
single-node visibility limitation rather than authoritative host-network data.
@@ -0,0 +1,23 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: observability-agent
helmGlobals:
chartHome: .helm/charts
helmCharts:
- name: prometheus-node-exporter
repo: https://prometheus-community.github.io/helm-charts
version: 4.56.1
releaseName: node-exporter
namespace: observability-agent
valuesFile: values/home.yaml
includeCRDs: false
skipTests: true
images:
- name: quay.io/prometheus/node-exporter
newName: quay.io/prometheus/node-exporter
digest: sha256:da83fae85603c4e47e6c68369a7d746e2dda683dc35ea2e234b4f171e0d92798
@@ -0,0 +1,78 @@
fullnameOverride: node-exporter
namespaceOverride: observability-agent
image:
registry: quay.io
repository: prometheus/node-exporter
tag: v1.12.1
pullPolicy: IfNotPresent
kubeRBACProxy:
enabled: false
service:
enabled: true
type: ClusterIP
port: 9100
targetPort: 9100
portName: metrics
listenOnAllInterfaces: true
prometheus:
monitor:
enabled: true
additionalLabels:
observability.hyeonworks.com/instance: home
interval: 30s
podMonitor:
enabled: false
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 250m
memory: 256Mi
serviceAccount:
create: true
automountServiceAccountToken: false
rbac:
create: true
securityContext:
fsGroup: 65534
runAsGroup: 65534
runAsNonRoot: true
runAsUser: 65534
seccompProfile:
type: RuntimeDefault
containerSecurityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
privileged: false
readOnlyRootFilesystem: true
hostNetwork: false
hostPID: false
hostIPC: false
hostRootFsMount:
enabled: true
mountPropagation: None
hostProcFsMount:
mountPropagation: None
hostSysFsMount:
mountPropagation: None
nodeSelector:
kubernetes.io/os: linux
tolerations:
- effect: NoSchedule
operator: Exists
@@ -0,0 +1,10 @@
# Observability object storage
Loki와 Tempo는 AIStor에서 서로 다른 bucket, policy, user와 Kubernetes Secret을
사용한다. Loki quota는 200Gi, Tempo quota는 100Gi다. 정책은 자기 bucket의 위치·목록과
자기 object의 읽기·쓰기·삭제·multipart 처리만 허용한다.
운영 적용 전에는 K3s Secret 암호화와 격리 복구 증거, AIStor root credential의 암호화된
외부 복구 가능성을 각각 다시 확인한다. 기존 bucket·object·policy·user·quota는 자동
삭제하거나 credential을 자동 회전하지 않는다. 한쪽 Secret만 존재하는 partial 상태에서는
추가 mutation을 중단한다.
@@ -0,0 +1,29 @@
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetBucketLocation",
"s3:ListBucket",
"s3:ListBucketMultipartUploads"
],
"Resource": [
"arn:aws:s3:::loki"
]
},
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:AbortMultipartUpload",
"s3:ListMultipartUploadParts"
],
"Resource": [
"arn:aws:s3:::loki/*"
]
}
]
}
@@ -0,0 +1,29 @@
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetBucketLocation",
"s3:ListBucket",
"s3:ListBucketMultipartUploads"
],
"Resource": [
"arn:aws:s3:::tempo"
]
},
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:AbortMultipartUpload",
"s3:ListMultipartUploadParts"
],
"Resource": [
"arn:aws:s3:::tempo/*"
]
}
]
}
@@ -0,0 +1,87 @@
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: platform-certificate-probes
namespace: observability
labels:
app.kubernetes.io/name: platform-observability-rules
app.kubernetes.io/part-of: observability
observability.hyeonworks.com/instance: home
spec:
groups:
- name: platform.blackbox
rules:
- alert: PlatformPublicEdgeProbeFailed
expr: >-
probe_success{
namespace="observability",
"observability.hyeonworks.com/probe-group"="public-edge"
} == 0
for: 5m
labels:
severity: critical
annotations:
summary: 외부 엔드포인트 프로브 실패
description: '{{ $labels.instance }} 외부 엔드포인트 프로브가 5분간 실패했습니다.'
runbook_url: https://git.learn.hyeonworks.com/donghyeon.kang/project-infra/src/branch/main/docs/runbooks/2026-07-31-observability-access-cutover.md
- alert: PlatformPrivateEdgeProbeFailed
expr: >-
probe_success{
namespace="observability",
"observability.hyeonworks.com/probe-group"="private-edge"
} == 0
for: 5m
labels:
severity: critical
annotations:
summary: 비공개 경계 프로브 실패
description: '{{ $labels.instance }} 비공개 경계의 예상 응답이 5분간 실패했습니다. HTTP 403이면서 probe_success=1이면 정상입니다.'
runbook_url: https://git.learn.hyeonworks.com/donghyeon.kang/project-infra/src/branch/main/docs/runbooks/2026-07-31-observability-access-cutover.md
- alert: PlatformInternalHealthProbeFailed
expr: >-
probe_success{
namespace="observability",
"observability.hyeonworks.com/probe-group"="private-internal"
} == 0
for: 5m
labels:
severity: critical
annotations:
summary: 내부 상태 확인 프로브 실패
description: '{{ $labels.instance }} 내부 상태 확인 프로브가 5분간 실패했습니다.'
runbook_url: https://git.learn.hyeonworks.com/donghyeon.kang/project-infra/src/branch/main/docs/runbooks/2026-07-31-observability-access-cutover.md
- name: platform.certificates
rules:
- alert: PlatformCertificateExpiry
expr: >-
(
probe_ssl_earliest_cert_expiry{
namespace="observability",
"observability.hyeonworks.com/probe-group"=~"public-edge|private-edge"
} - time()
) >= 14 * 24 * 60 * 60
and
(
probe_ssl_earliest_cert_expiry{
namespace="observability",
"observability.hyeonworks.com/probe-group"=~"public-edge|private-edge"
} - time()
) < 30 * 24 * 60 * 60
labels:
severity: warning
annotations:
summary: TLS 인증서가 30일 이내 만료됨
description: '{{ $labels.instance }} 인증서 중 가장 이른 만료 시점이 14일 이상 30일 미만 남았습니다.'
runbook_url: https://git.learn.hyeonworks.com/donghyeon.kang/project-infra/src/branch/main/docs/runbooks/2026-07-31-observability-access-cutover.md
- alert: PlatformCertificateExpiry
expr: >-
probe_ssl_earliest_cert_expiry{
namespace="observability",
"observability.hyeonworks.com/probe-group"=~"public-edge|private-edge"
} - time() < 14 * 24 * 60 * 60
labels:
severity: critical
annotations:
summary: TLS 인증서가 14일 이내 만료됨
description: '{{ $labels.instance }} 인증서 중 가장 이른 만료 시점이 14일 미만 남았습니다.'
runbook_url: https://git.learn.hyeonworks.com/donghyeon.kang/project-infra/src/branch/main/docs/runbooks/2026-07-31-observability-access-cutover.md
@@ -0,0 +1,117 @@
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: platform-observability-core
namespace: observability
labels:
app.kubernetes.io/name: platform-observability-rules
app.kubernetes.io/part-of: observability
observability.hyeonworks.com/instance: home
spec:
groups:
- name: platform.storage
rules:
- alert: PlatformPersistentVolumeClaimNotBound
expr: kube_persistentvolumeclaim_status_phase{phase=~"Pending|Lost"} == 1
for: 10m
labels:
severity: warning
annotations:
summary: PersistentVolumeClaim이 바인딩되지 않음
description: 플랫폼 PersistentVolumeClaim이 Pending 또는 Lost 상태로 10분간 유지됐습니다.
runbook_url: https://git.learn.hyeonworks.com/donghyeon.kang/project-infra/src/branch/main/docs/runbooks/2026-07-31-observability-access-cutover.md
- alert: PlatformRootFilesystemUsage
expr: >-
100 * (1 -
node_filesystem_avail_bytes{job="prometheus-node-exporter",mountpoint="/",fstype!~"rootfs|tmpfs|ramfs|squashfs|overlay|nsfs|tracefs|debugfs|securityfs|proc|sysfs|cgroup2?"}
/
node_filesystem_size_bytes{job="prometheus-node-exporter",mountpoint="/",fstype!~"rootfs|tmpfs|ramfs|squashfs|overlay|nsfs|tracefs|debugfs|securityfs|proc|sysfs|cgroup2?"}
) > 75
for: 30m
labels:
severity: info
threshold: "75"
annotations:
summary: 루트 파일시스템 사용률이 75%를 초과함
description: 노드 루트 파일시스템 사용률이 정보 임계치인 75%를 30분간 초과했습니다.
runbook_url: https://git.learn.hyeonworks.com/donghyeon.kang/project-infra/src/branch/main/docs/runbooks/2026-07-31-observability-access-cutover.md
- alert: PlatformRootFilesystemUsage
expr: >-
100 * (1 -
node_filesystem_avail_bytes{job="prometheus-node-exporter",mountpoint="/",fstype!~"rootfs|tmpfs|ramfs|squashfs|overlay|nsfs|tracefs|debugfs|securityfs|proc|sysfs|cgroup2?"}
/
node_filesystem_size_bytes{job="prometheus-node-exporter",mountpoint="/",fstype!~"rootfs|tmpfs|ramfs|squashfs|overlay|nsfs|tracefs|debugfs|securityfs|proc|sysfs|cgroup2?"}
) > 85
for: 15m
labels:
severity: warning
threshold: "85"
annotations:
summary: 루트 파일시스템 사용률이 85%를 초과함
description: 노드 루트 파일시스템 사용률이 경고 임계치인 85%를 15분간 초과했습니다.
runbook_url: https://git.learn.hyeonworks.com/donghyeon.kang/project-infra/src/branch/main/docs/runbooks/2026-07-31-observability-access-cutover.md
- alert: PlatformRootFilesystemUsage
expr: >-
100 * (1 -
node_filesystem_avail_bytes{job="prometheus-node-exporter",mountpoint="/",fstype!~"rootfs|tmpfs|ramfs|squashfs|overlay|nsfs|tracefs|debugfs|securityfs|proc|sysfs|cgroup2?"}
/
node_filesystem_size_bytes{job="prometheus-node-exporter",mountpoint="/",fstype!~"rootfs|tmpfs|ramfs|squashfs|overlay|nsfs|tracefs|debugfs|securityfs|proc|sysfs|cgroup2?"}
) > 90
for: 5m
labels:
severity: critical
threshold: "90"
annotations:
summary: 루트 파일시스템 사용률이 90%를 초과함
description: 노드 루트 파일시스템 사용률이 심각 임계치인 90%를 5분간 초과했습니다.
runbook_url: https://git.learn.hyeonworks.com/donghyeon.kang/project-infra/src/branch/main/docs/runbooks/2026-07-31-observability-access-cutover.md
- alert: PlatformRootFilesystemInodes
expr: >-
100 * (1 -
node_filesystem_files_free{job="prometheus-node-exporter",mountpoint="/",fstype!~"rootfs|tmpfs|ramfs|squashfs|overlay|nsfs|tracefs|debugfs|securityfs|proc|sysfs|cgroup2?"}
/
node_filesystem_files{job="prometheus-node-exporter",mountpoint="/",fstype!~"rootfs|tmpfs|ramfs|squashfs|overlay|nsfs|tracefs|debugfs|securityfs|proc|sysfs|cgroup2?"}
) > 80
for: 30m
labels:
severity: warning
threshold: "80"
annotations:
summary: 루트 파일시스템 inode 사용률이 80%를 초과함
description: 노드 루트 파일시스템 inode 사용률이 경고 임계치인 80%를 30분간 초과했습니다.
runbook_url: https://git.learn.hyeonworks.com/donghyeon.kang/project-infra/src/branch/main/docs/runbooks/2026-07-31-observability-access-cutover.md
- alert: PlatformRootFilesystemInodes
expr: >-
100 * (1 -
node_filesystem_files_free{job="prometheus-node-exporter",mountpoint="/",fstype!~"rootfs|tmpfs|ramfs|squashfs|overlay|nsfs|tracefs|debugfs|securityfs|proc|sysfs|cgroup2?"}
/
node_filesystem_files{job="prometheus-node-exporter",mountpoint="/",fstype!~"rootfs|tmpfs|ramfs|squashfs|overlay|nsfs|tracefs|debugfs|securityfs|proc|sysfs|cgroup2?"}
) > 90
for: 15m
labels:
severity: critical
threshold: "90"
annotations:
summary: 루트 파일시스템 inode 사용률이 90%를 초과함
description: 노드 루트 파일시스템 inode 사용률이 심각 임계치인 90%를 15분간 초과했습니다.
runbook_url: https://git.learn.hyeonworks.com/donghyeon.kang/project-infra/src/branch/main/docs/runbooks/2026-07-31-observability-access-cutover.md
- name: platform.workloads
rules:
- alert: PlatformPodRestartBurst
expr: increase(kube_pod_container_status_restarts_total[15m]) > 3
for: 5m
labels:
severity: warning
annotations:
summary: 컨테이너 재시작 급증 감지
description: 한 컨테이너가 최근 15분 동안 3회를 초과해 재시작했습니다.
runbook_url: https://git.learn.hyeonworks.com/donghyeon.kang/project-infra/src/branch/main/docs/runbooks/2026-07-31-observability-access-cutover.md
- alert: PlatformPodOOMKilled
expr: kube_pod_container_status_last_terminated_reason{reason="OOMKilled"} == 1
for: 5m
labels:
severity: critical
annotations:
summary: 컨테이너가 OOM Killer에 의해 종료됨
description: 컨테이너의 최근 종료 사유가 OOMKilled인 상태로 5분간 유지됐습니다.
runbook_url: https://git.learn.hyeonworks.com/donghyeon.kang/project-infra/src/branch/main/docs/runbooks/2026-07-31-observability-access-cutover.md
@@ -0,0 +1,10 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: observability
resources:
- core-rules.yaml
- storage-quota-rules.yaml
- certificate-rules.yaml
- verified-service-rules.yaml
@@ -0,0 +1,55 @@
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: platform-aistor-storage-quota
namespace: observability
labels:
app.kubernetes.io/name: platform-observability-rules
app.kubernetes.io/part-of: observability
observability.hyeonworks.com/instance: home
spec:
groups:
- name: platform.aistor-storage-quota
rules:
- record: platform:aistor_bucket_quota_usage_percent
expr: >-
100
* max by (bucket) (
minio_cluster_usage_buckets_total_bytes{bucket=~"loki|tempo"}
)
/ max by (bucket) (
minio_cluster_usage_buckets_quota_total_bytes{bucket=~"loki|tempo"} > 0
)
- alert: PlatformAIStorBucketQuotaUsage
expr: >-
platform:aistor_bucket_quota_usage_percent >= 70
and
platform:aistor_bucket_quota_usage_percent < 85
for: 15m
labels:
severity: warning
annotations:
summary: AIStor 버킷 할당량 사용률 경고
description: '{{ $labels.bucket }} 버킷 사용률이 70% 이상 85% 미만으로 15분간 유지됐습니다.'
runbook_url: https://git.learn.hyeonworks.com/donghyeon.kang/project-infra/src/branch/main/docs/runbooks/2026-07-31-observability-access-cutover.md
- alert: PlatformAIStorBucketQuotaUsage
expr: >-
platform:aistor_bucket_quota_usage_percent >= 85
and
platform:aistor_bucket_quota_usage_percent < 95
for: 10m
labels:
severity: critical
annotations:
summary: AIStor 버킷 할당량 사용률 심각
description: '{{ $labels.bucket }} 버킷 사용률이 85% 이상 95% 미만으로 10분간 유지됐습니다.'
runbook_url: https://git.learn.hyeonworks.com/donghyeon.kang/project-infra/src/branch/main/docs/runbooks/2026-07-31-observability-access-cutover.md
- alert: PlatformAIStorBucketQuotaUsage
expr: platform:aistor_bucket_quota_usage_percent >= 95
for: 5m
labels:
severity: emergency
annotations:
summary: AIStor 버킷 할당량 사용률 긴급
description: '대응 절차에 따라 {{ $labels.bucket }} 버킷의 수집 경로를 중지하고 객체는 자동으로 삭제하지 마세요.'
runbook_url: https://git.learn.hyeonworks.com/donghyeon.kang/project-infra/src/branch/main/docs/runbooks/2026-07-31-observability-access-cutover.md
@@ -0,0 +1,88 @@
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: platform-verified-services
namespace: observability
labels:
app.kubernetes.io/name: platform-observability-rules
app.kubernetes.io/part-of: observability
observability.hyeonworks.com/instance: home
spec:
groups:
- name: platform.verified-services
rules:
- alert: PlatformCNPGCollectorDown
expr: >-
min by (cluster, namespace) (
cnpg_collector_up{
namespace="platform-data",
cluster="platform-postgres"
}
) == 0
for: 5m
labels:
severity: critical
annotations:
summary: CloudNativePG 수집기가 PostgreSQL 중단을 보고함
description: '{{ $labels.cluster }} CloudNativePG 수집기가 PostgreSQL 중단 상태를 5분간 보고했습니다.'
runbook_url: https://git.learn.hyeonworks.com/donghyeon.kang/project-infra/src/branch/main/docs/runbooks/2026-07-31-observability-access-cutover.md
- alert: PlatformAlloyConfigLoadFailed
expr: >-
min by (namespace) (
alloy_config_last_load_successful{namespace="observability-agent"}
) == 0
for: 5m
labels:
severity: critical
annotations:
summary: Alloy 설정 로드 실패
description: Alloy의 마지막 설정 로드 실패 상태가 5분간 유지됐습니다.
runbook_url: https://git.learn.hyeonworks.com/donghyeon.kang/project-infra/src/branch/main/docs/runbooks/2026-07-31-observability-access-cutover.md
- alert: PlatformAlloyLogDeliveryRetries
expr: >-
sum by (namespace) (
increase(loki_write_batch_retries_total{namespace="observability-agent"}[15m])
) > 0
for: 5m
labels:
severity: warning
annotations:
summary: Alloy 로그 배치 전송 재시도 발생
description: Alloy가 최근 15분 동안 Loki 쓰기 배치를 한 번 이상 재시도했습니다.
runbook_url: https://git.learn.hyeonworks.com/donghyeon.kang/project-infra/src/branch/main/docs/runbooks/2026-07-31-observability-access-cutover.md
- alert: PlatformAlloyLogEntriesDropped
expr: >-
sum by (namespace) (
increase(loki_write_dropped_entries_total{namespace="observability-agent"}[15m])
) > 0
for: 5m
labels:
severity: critical
annotations:
summary: Alloy 로그 항목 유실 발생
description: Alloy가 최근 15분 동안 재시도를 모두 소진한 뒤 로그 항목을 유실했습니다.
runbook_url: https://git.learn.hyeonworks.com/donghyeon.kang/project-infra/src/branch/main/docs/runbooks/2026-07-31-observability-access-cutover.md
- alert: PlatformLokiRuntimeConfigReloadFailed
expr: >-
min by (namespace) (
loki_runtime_config_last_reload_successful{namespace="observability"}
) == 0
for: 5m
labels:
severity: critical
annotations:
summary: Loki 런타임 설정 재로드 실패
description: Loki의 마지막 런타임 설정 재로드 실패 상태가 5분간 유지됐습니다.
runbook_url: https://git.learn.hyeonworks.com/donghyeon.kang/project-infra/src/branch/main/docs/runbooks/2026-07-31-observability-access-cutover.md
- alert: PlatformLokiWALDiskUsageHigh
expr: >-
max by (namespace) (
loki_ingester_wal_disk_usage_percent{namespace="observability"}
) >= 0.8
for: 15m
labels:
severity: warning
annotations:
summary: Loki WAL 디스크 사용률 높음
description: Loki WAL 디스크 사용률이 80% 이상으로 15분간 유지됐습니다.
runbook_url: https://git.learn.hyeonworks.com/donghyeon.kang/project-infra/src/branch/main/docs/runbooks/2026-07-31-observability-access-cutover.md
+24
View File
@@ -0,0 +1,24 @@
# Platform metric targets
This component declares the manually owned monitor resources and counterpart
NetworkPolicy needed to onboard existing platform services into the home
Prometheus instance.
The owners remain authoritative for their endpoint activation:
- Gitea chart: `/metrics` and its generated ServiceMonitor
- Keycloak Operator: management metrics and its generated ServiceMonitor
- CloudNativePG: the declared `metrics` Pod port through a manual PodMonitor
- AIStor: the v3 per-bucket usage endpoint through a manual ServiceMonitor
- packaged Traefik chart: its dedicated metrics Service and ServiceMonitor
The AIStor endpoint uses `MINIO_PROMETHEUS_AUTH_TYPE=public`, but no public
route is created. Port 9000 remains protected by the namespace default-deny and
the exact Prometheus/client allow policies. Because S3 and metrics share that
L4 port, an already-authorized S3 client can also reach the credential-less
metrics path; HTTP-path isolation is not expressible with NetworkPolicy.
The first inventory is captured before blackbox is deployed. Grafana already
exists and must be recorded explicitly as a preexisting, verify-only substrate
target; it is not owned or rolled back by this component.
@@ -0,0 +1,28 @@
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: aistor-bucket-usage
namespace: object-storage
labels:
app.kubernetes.io/name: minio-aistor
app.kubernetes.io/component: bucket-usage-metrics
app.kubernetes.io/part-of: platform
observability.hyeonworks.com/instance: home
spec:
jobLabel: aistor.min.io/objectStore
selector:
matchLabels:
aistor.min.io/objectStore: minio-aistor
namespaceSelector:
matchNames:
- object-storage
endpoints:
- port: http-minio
path: /minio/metrics/v3/cluster/usage/buckets
interval: 60s
scrapeTimeout: 10s
relabelings:
- action: keep
sourceLabels:
- __meta_kubernetes_service_name
regex: minio
@@ -0,0 +1,24 @@
apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
name: platform-postgres
namespace: platform-data
labels:
app.kubernetes.io/name: platform-postgres
app.kubernetes.io/component: database-metrics
app.kubernetes.io/part-of: platform
observability.hyeonworks.com/instance: home
spec:
jobLabel: cnpg.io/cluster
selector:
matchLabels:
cnpg.io/cluster: platform-postgres
namespaceSelector:
matchNames:
- platform-data
podMetricsEndpoints:
- port: metrics
path: /metrics
interval: 30s
scrapeTimeout: 10s
@@ -0,0 +1,108 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: traefik-preserve-ingress-and-allow-prometheus-metrics
namespace: kube-system
labels:
app.kubernetes.io/name: traefik
app.kubernetes.io/part-of: platform
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: traefik
app.kubernetes.io/instance: traefik-kube-system
policyTypes:
- Ingress
ingress:
# Preserve the pre-policy HTTP, HTTPS, and health reachability. The new
# policy isolates the selected Pod, so omitting these ports would break the
# existing NodePort and controller health paths.
- ports:
- protocol: TCP
port: 8000
- protocol: TCP
port: 8443
- protocol: TCP
port: 8080
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: observability
podSelector:
matchLabels:
app.kubernetes.io/name: prometheus
app.kubernetes.io/instance: observability-core-kube-pr-prometheus
ports:
- protocol: TCP
port: 9100
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: observability-allow-prometheus-platform-targets
namespace: observability
labels:
app.kubernetes.io/name: prometheus
app.kubernetes.io/part-of: platform-observability
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: prometheus
app.kubernetes.io/instance: observability-core-kube-pr-prometheus
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: gitea
podSelector:
matchLabels:
app.kubernetes.io/name: gitea
app.kubernetes.io/instance: gitea
ports:
- protocol: TCP
port: 3000
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: keycloak
podSelector:
matchLabels:
app: keycloak
app.kubernetes.io/instance: keycloak
app.kubernetes.io/managed-by: keycloak-operator
ports:
- protocol: TCP
port: 9000
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: platform-data
podSelector:
matchLabels:
cnpg.io/cluster: platform-postgres
ports:
- protocol: TCP
port: 9187
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: object-storage
podSelector:
matchLabels:
aistor.min.io/objectStore: minio-aistor
ports:
- protocol: TCP
port: 9000
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
app.kubernetes.io/name: traefik
app.kubernetes.io/instance: traefik-kube-system
ports:
- protocol: TCP
port: 9100
@@ -0,0 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- cloudnative-pg/pod-monitor.yaml
- aistor/service-monitor.yaml
- counterpart-network-policies.yaml
+15
View File
@@ -0,0 +1,15 @@
# Tempo
This root renders `grafana-community/tempo` chart `2.2.3` as one Tempo
`2.10.7` StatefulSet in `observability`.
- The image is rewritten to the reviewed amd64 digest.
- Traces use the `tempo` AIStor bucket through `tempo-object-storage`; no root
credential is referenced.
- Retention is seven days. WAL and local working data use the dedicated 5Gi
Retain Local PV.
- Only the query/metrics port 3200 and OTLP/gRPC port 4317 are exposed.
- OTLP/HTTP, Jaeger, Zipkin, metrics-generator, and tempo-query are disabled.
Do not delete the PVC or bucket during rollback. Stop the StatefulSet and
preserve its Retain volume while S3/WAL state is investigated.
@@ -0,0 +1,56 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: observability
helmGlobals:
chartHome: .helm/charts
helmCharts:
- name: tempo
repo: https://grafana-community.github.io/helm-charts
version: 2.2.3
releaseName: tempo
namespace: observability
valuesFile: values/home.yaml
includeCRDs: false
skipTests: true
images:
- name: docker.io/grafana/tempo
newName: docker.io/grafana/tempo
digest: sha256:6616b00287a4d7001951b5de117828ad5c6f93744935c1b7a5e044736373352c
patches:
- target:
group: ""
version: v1
kind: Service
name: tempo
patch: |-
- op: replace
path: /spec/ports
value:
- name: tempo-prom-metrics
port: 3200
protocol: TCP
targetPort: 3200
- name: grpc-tempo-otlp
port: 4317
protocol: TCP
targetPort: 4317
- target:
group: apps
version: v1
kind: StatefulSet
name: tempo
patch: |-
- op: replace
path: /spec/template/spec/containers/0/ports
value:
- containerPort: 3200
name: prom-metrics
protocol: TCP
- containerPort: 4317
name: otlp-grpc
protocol: TCP
@@ -0,0 +1,125 @@
global:
commonLabels:
observability.hyeonworks.com/instance: home
replicas: 1
config: |
stream_over_http_enabled: false
memberlist:
cluster_label: "{{ .Release.Name }}.{{ .Release.Namespace }}"
multitenancy_enabled: false
usage_report:
reporting_enabled: false
compactor:
compaction:
block_retention: 168h
distributor:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
ingester:
trace_idle_period: 10s
max_block_duration: 5m
max_block_bytes: 100000000
complete_block_timeout: 15m
flush_all_on_shutdown: true
server:
http_listen_port: 3200
storage:
trace:
backend: s3
s3:
bucket: tempo
endpoint: minio.object-storage.svc.cluster.local:80
insecure: true
forcepathstyle: true
access_key: ${AWS_ACCESS_KEY_ID}
secret_key: ${AWS_SECRET_ACCESS_KEY}
wal:
path: /var/tempo/wal
querier: {}
query_frontend: {}
overrides:
defaults:
ingestion:
rate_limit_bytes: 5000000
burst_size_bytes: 10000000
max_traces_per_user: 5000
global:
max_bytes_per_trace: 5000000
tempo:
registry: docker.io
repository: grafana/tempo
tag: 2.10.7
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: "1"
memory: 1Gi
memBallastSizeMbs: 0
reportingEnabled: false
metricsGenerator:
enabled: false
extraArgs:
config.expand-env: true
extraEnv:
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: tempo-object-storage
key: access-key-id
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: tempo-object-storage
key: secret-access-key
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
tempoQuery:
enabled: false
securityContext:
runAsUser: 10001
runAsGroup: 10001
runAsNonRoot: true
fsGroup: 10001
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
serviceAccount:
automountServiceAccountToken: false
service:
type: ClusterIP
serviceMonitor:
enabled: true
interval: 30s
additionalLabels:
observability.hyeonworks.com/instance: home
persistence:
enabled: true
enableStatefulSetAutoDeletePVC: false
storageClassName: ssd-local-observability-tempo-retain
accessModes:
- ReadWriteOnce
size: 5Gi
podLabels:
platform.hyeonworks.com/aistor-client: "true"