쿠버네티스 시작하기(11) - Prometheus & Node-Exporter & AlertManager 연동
이전 장에서는 CI/CD를 구축하고 SonarQube & Jacoco로 코드정적분석 & 소스커버리지 관리 기능까지 적용해 보았다. 이번 장에서는 쿠버네티스 클러스터에 Prometheus(프로메테우스) & NodeExporter & AlertManager를 연동시켜 쿠버네티스 클러스터 및 서버환경를 모니터링할 수 있는 프로세스를 구축해보도록 하겠다.
1. 프로세스 구성도
이번 프로젝트에서 진행한 Prometheus & Node-Exporter & AlertManager를 활용한 모니터링 프로세스 구성도는 다음과 같다.
그림에 대해 간략하게 설명하자면, 우선 각 서버 및 각 쿠버네티스 노드에 Node-Exporter가 구동되고 있으며, Node-Exporter와 kube-state-metrics를 통해 Prometheus(프로메테우스)가 데이터를 주기적으로 수집한다(pull방식). 수집된 데이터는 Grafana에 전송되어 모니터링 대쉬보드로 현황 모니터링이 가능하며, 특정 이벤트 발생 시에는 AlertManager로 전송되어 Slack을 통해 알람이 전송된다. 위 그림에 표시된 프로세스 중 Grafana를 활용한 모니터링 대쉬보드는 다음 장에서 구현을 해 볼 것이며, 이번 장에서는 Grafana를 제외한 모니터링 프로세스만 구축할 것이다.
2. Prometheus, Node-Exporter, AlertManager는 무엇인가?
그렇다면 Prometheus, NodeExporter, AlertManager란 무엇인가? 자세하게 다루면 각각 한개의 포스트로 다루어야 하기 때문에 간단하게만 설명한다.
2-1. Prometheus
오픈소스 모니터링 툴로 지표 수집을 통한 모니터링이 주요 기능이다. 쿠버네티스 뿐만 아니라 애플리케이션이나 서버, OS등 다양한 대상으로부터 지표(Metric)를 수집하여 모니터링 할 수 있다. 기본적으로 Pull 방식으로 데이터를 수집하는데, 이 말은 모니터링 대상이 되는 자원이 지표정보를 프로메테우스로 보내는 것이 아니라, 프로메테우스가 주기적으로 모니터링 대상에서 지표를 읽어온다는 뜻이다(Push 방식으로 지표를 수집하는 모니터링 툴은 ELK스택 또는 Telegraf & InfluxDB 등이 있다). Pull 방식으로 지표정보를 읽어올때는 각 서버에 설치된 Exporter를 통해서 정보를 읽어오며, 배치나 스케쥴 작업의 경우에는 필요한 경우에만 떠 있다가 작업이 끝나면 사라지기 때문에 Pull 방식으로 데이터 수집이 어렵다. 그럴 경우 Push방식을 사용하는 Push gateway를 통해 지표정보를 받아오는 경우도 있다. 서버의 갯수가 정해져 있다면 프로메테우스에서 모니터링 대상을 관리하는데 어려움이 없지만, 오토스케일링이 많이 사용되는 클라우드 환경이나 쿠버네티스 클러스터에서는 모니터링 대상의 IP가 동적으로 변경되기 때문에 이를 일일이 설정파일에 넣는데 한계가 있다. 이러한 문제를 해결하기 위해 프로메테우스는 DNS나 Consul, etcd와 같은 다양한 서비스 디스커버리 서비스와 연동을 통해 모니터링 목록을 가지고 모니터링을 수행한다.
2-2. Exporter(node-exporter)
모니터링 대상이 프로메테우스의 데이터 포맷을 지원하지 않는 경우에는 별도의 에이전트를 설치해야 지표를 얻어올 수 있는데 이 에이전트를 Exporter라고 한다. Exporter 종류에는 node-exporter, mysql-exporter, nginx-exporter, redis-exporter 등 여러 종류가 있으며, 각 exporter를 모니터링 대상에 맞게끔 사용하면 된다. 필자는 서버의 CPU/메모리와 쿠버네티스 컨테이너 모니터링을 진행할 것이기 때문에 node-exporter를 선택하였다. 이와는 별개로 만약 java나 node.js와 같은 사용자 애플리케이션의 경우에는 Exporter를 사용하지 않고, 프로메테우스 클라이언트 라이브러리를 사용하게 되면 바로 지표를 프로메테우스 서버로 보낼 수 있다(예를 들면 springboot actuator나 micrometer와 연계하여 springboot api 정보 전달 등)
2-3. AlertManager
AlertManager는 프로메테우스로부터 alert를 전달받아 이를 적절한 포맷으로 가공하여 notify 해주는 역할을 한다. notify는 Slack이나 이메일을 통해 할 수 있다.
2-4. Prometheus 모니터링의 장/단점
프로메테우스는 지표(metric)정보를 pull 방식으로 데이터를 수집하고 일정 간격마다 데이터를 수집하기 때문에 로그 수집과 같은 모든 이벤트를 수집하는 일이나 배치 작업 등 단발적으로 발생하는 업무 모니터링을 할 때는 적합하지 않다. 또한 기본적인 구조로는 Scale-out을 구현하는데 한계가 있기 때문에 많은 이벤트가 발생하는 시스템을 모니터링 하는데는 적합하지 않다. 단 CPU/메모리/파일시스템 현황, 컨테이너 현황 등 특정 데이터의 흐름을 파악하는데 용이하며, 모든 이벤트에 데이터를 Push하는 ELK방식과는 달리 일정 간격마다 데이터를 수집하기 때문에 저사양의 Spec. 으로도 모니터링 시스템 구축이 가능하다. 또한 설정파일을 프로메테우스 서버 설정파일만 변경한 후 node-exporter는 배포만 하면 되기 때문에 관리자 입장에서 시스템 운영이 용이하다는 장점도 있다.
3. Prometheus & Node-Exporter 적용
그럼 지금부터 쿠버네티스 클러스터 환경에 Prometheus & NodeExporter & AlertManager를 적용해보도록 하겠다.
3-1. Prometheus & Node-Exporter 필요 리소스
쿠버네티스 클러스터에 프로메테우스 모니터링을 적용하기 위해선 다음과 같은 리소스가 필요하다.
- cluster-role : 프로메테우스 컨테이너가 쿠버네티스 api에 접근할 수 있는 권한을 부여한다.
- config-map : 프로메테우스가 기동되려면 프로메테우스 환경설정파일(prometheus.rules, prometheus.yaml)이 필요한데, 해당 환경설정파일을 정의해줌. 프로메테우스 컨테이너가 기동되면 config-map에 정의된 prometheus.yaml이 컨테이너 내부로 들어가게 된다. prometheus.rules에는 수집한 지표에 대한 알람조건을 지정하여 특정 조건이 되면 prometheus에서 AlertManager로 알람을 보낼 수 있고, prometheus.yaml에는 수집할 지표(Metric) 종류, 지표 수집 주기 등을 기입한다.
- deployment : 프로메테우스 deployment로 pod 가 구동될 때 필요하다.
- service : 컨테이너 외부에서 프로메테우스로 접근하기 위해 필요하다.
- daemonset(node-exporter) : 쿠버네티스 클러스터 정보를 수집하기 위해 node-exporter가 필요하며 해당 exporter는 각 노드당 1개씩만 올라가기 때문에 daemonset 타입으로 구동시킨다.
3-2. Prometheus & Node-Exporter 리소스 적용
그럼 아래와 같이 각 리소스 yaml 파일을 작성해보자.
prometheus-cluster-role.yaml :
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: prometheus
namespace: monitoring
rules:
- apiGroups: [""]
resources:
- nodes
- nodes/proxy
- services
- endpoints
- pods
verbs: ["get", "list", "watch"]
- apiGroups:
- extensions
resources:
- ingresses
verbs: ["get", "list", "watch"]
- nonResourceURLs: ["/metrics"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: prometheus
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: prometheus
subjects:
- kind: ServiceAccount
name: default
namespace: monitoring
prometheus-config-map.yaml :
apiVersion: v1
kind: ConfigMap
metadata:
name: prometheus-server-conf
labels:
name: prometheus-server-conf
namespace: monitoring
data:
prometheus.rules: |-
groups:
- name: container memory alert
rules:
- alert: container memory usage rate is very high( > 55%)
expr: sum(container_memory_working_set_bytes{pod!="", name=""})/ sum (kube_node_status_allocatable_memory_bytes) * 100 > 55
for: 1m
labels:
severity: fatal
annotations:
summary: High Memory Usage on {{ $labels.instance }}
identifier: "{{ $labels.instance }}"
description: "{{ $labels.job }} Memory Usage: {{ $value }}"
- name: container CPU alert
rules:
- alert: container CPU usage rate is very high( > 10%)
expr: sum (rate (container_cpu_usage_seconds_total{pod!=""}[1m])) / sum (machine_cpu_cores) * 100 > 10
for: 1m
labels:
severity: fatal
annotations:
summary: High Cpu Usage
prometheus.yml: |-
global:
scrape_interval: 5s
evaluation_interval: 5s
rule_files:
- /etc/prometheus/prometheus.rules
alerting:
alertmanagers:
- scheme: http
static_configs:
- targets:
- "alertmanager.monitoring.svc:9093"
scrape_configs:
- job_name: 'kubernetes-apiservers'
kubernetes_sd_configs:
- role: endpoints
scheme: https
tls_config:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
relabel_configs:
- source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name]
action: keep
regex: default;kubernetes;https
- job_name: 'kubernetes-nodes'
scheme: https
tls_config:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
kubernetes_sd_configs:
- role: node
relabel_configs:
- action: labelmap
regex: __meta_kubernetes_node_label_(.+)
- target_label: __address__
replacement: kubernetes.default.svc:443
- source_labels: [__meta_kubernetes_node_name]
regex: (.+)
target_label: __metrics_path__
replacement: /api/v1/nodes/${1}/proxy/metrics
- job_name: 'kubernetes-pods'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
- source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
action: replace
regex: ([^:]+)(?::\d+)?;(\d+)
replacement: $1:$2
target_label: __address__
- action: labelmap
regex: __meta_kubernetes_pod_label_(.+)
- source_labels: [__meta_kubernetes_namespace]
action: replace
target_label: kubernetes_namespace
- source_labels: [__meta_kubernetes_pod_name]
action: replace
target_label: kubernetes_pod_name
- job_name: 'kube-state-metrics'
static_configs:
- targets: ['kube-state-metrics.kube-system.svc.cluster.local:8080']
- job_name: 'kubernetes-cadvisor'
scheme: https
tls_config:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
kubernetes_sd_configs:
- role: node
relabel_configs:
- action: labelmap
regex: __meta_kubernetes_node_label_(.+)
- target_label: __address__
replacement: kubernetes.default.svc:443
- source_labels: [__meta_kubernetes_node_name]
regex: (.+)
target_label: __metrics_path__
replacement: /api/v1/nodes/${1}/proxy/metrics/cadvisor
- job_name: 'kubernetes-service-endpoints'
kubernetes_sd_configs:
- role: endpoints
relabel_configs:
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scheme]
action: replace
target_label: __scheme__
regex: (https?)
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
- source_labels: [__address__, __meta_kubernetes_service_annotation_prometheus_io_port]
action: replace
target_label: __address__
regex: ([^:]+)(?::\d+)?;(\d+)
replacement: $1:$2
- action: labelmap
regex: __meta_kubernetes_service_label_(.+)
- source_labels: [__meta_kubernetes_namespace]
action: replace
target_label: kubernetes_namespace
- source_labels: [__meta_kubernetes_service_name]
action: replace
target_label: kubernetes_name
prometheus-deployment.yaml :
apiVersion: apps/v1
kind: Deployment
metadata:
name: prometheus-deployment
namespace: monitoring
spec:
replicas: 1
selector:
matchLabels:
app: prometheus-server
template:
metadata:
labels:
app: prometheus-server
spec:
containers:
- name: prometheus
image: prom/prometheus:v2.12.0
args:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus/"
ports:
- containerPort: 9090
volumeMounts:
- name: prometheus-config-volume
mountPath: /etc/prometheus/
- name: prometheus-storage-volume
mountPath: /prometheus/
volumes:
- name: prometheus-config-volume
configMap:
defaultMode: 420
name: prometheus-server-conf
- name: prometheus-storage-volume
emptyDir: {}
prometheus-svc.yaml :
apiVersion: v1
kind: Service
metadata:
name: prometheus-service
namespace: monitoring
annotations:
prometheus.io/scrape: 'true'
prometheus.io/port: '9090'
spec:
selector:
app: prometheus-server
type: NodePort
ports:
- port: 8080
targetPort: 9090
nodePort: 30003
prometheus-node-exporter.yaml :
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-exporter
namespace: monitoring
labels:
k8s-app: node-exporter
spec:
template:
metadata:
labels:
k8s-app: node-exporter
spec:
containers:
- image: prom/node-exporter
name: node-exporter
ports:
- containerPort: 9100
protocol: TCP
name: http
---
apiVersion: v1
kind: Service
metadata:
labels:
k8s-app: node-exporter
name: node-exporter
namespace: monitoring
spec:
ports:
- name: http
port: 9100
nodePort: 31672
protocol: TCP
type: NodePort
selector:
k8s-app: node-exporter
작성을 완료하였으면 monitoring이란 namespace를 생성한 후, 해당 yaml파일을 쿠버네티스 클러스터에 적용시켜보자(프로메테우스 관련 리소스는 monitoring이란 namespace 위에 올라가도록 yaml에 설정되어 있으므로 namespace도 생성해야 함).
# kubectl create ns monitoring
# kubectl apply -f prometheus-cluster-role.yaml
# kubectl apply -f prometheus-config-map.yaml
# kubectl apply -f prometheus-deployment.yaml
# kubectl apply -f prometheus-node-exporter.yaml
# kubectl apply -f prometheus-svc.yaml
3-3. Prometheus & NodeExporter 적용 확인
쿠버네티스 클러스터에 yaml파일 적용이 완료되면 아래와 같이 컨테이너가 정상 수행됨을 확인할 수 있다. node-exporter같은 경우는 각 노드마다 하나씩 배포되어 각 노드 별 쿠버네티스 컨테이너 모니터링이 가능하다.
또한 http://[호스트서버IP]:[서비스포트] 로 접근하게 되면 프로메테우스 웹페이지를 확인할 수 있다(이 포스트와 동일하게 yaml파일을 작성했다면 http://[호스트IP]:30003 으로 접근하면 된다).
상단 메뉴의 Alerts를 선택하면 prometheus-config-map.yaml 내에 있는 prometheus.rules에 작성해 놓은 Alert에 대한 정의가 되어있는 것을 확인할 수 있다(해당 내용은 다음에 AlertManager와 연동시켜 해당 조건 충족 시 Slack을 통해 알람 받는 것을 테스트할 것이다).
또한 상단 메뉴의 Graph를 선택하고 밑에 콤보박스에서 보고싶은 지표(Metric)정보 한개를 선택한 후 Excute를 누르게 되면 해당 지표에 대한 시간 별 데이터를 그래프 혹은 수치값으로 확인할 수 있다. 수치값은 콤보박스에 있는 값으로 확인할 수도있지만, 에디트 창에 PromQL(Prometheus Query)을 작성해서 자기가 원하는 방식으로 조회를 할 수도 있다. 하지만 이 장에서는 PromQL에 대한 자세한 설명은 생략한다(일단 프로메테우스에서 쿠버네티스 클러스터의 지표정보가 수집되고 있다는 것만 알고 넘어가자). 수집된 지표정보는 다음 장에서 배울 Grafana 대쉬보드를 통해 좀 더 사용자 친화적으로 바꿀 것이다. 추가로 프로메테우스에 표시되는 시간은 한국시간을 쓰지 않기 때문에 맞지 않는다. 검색해 보아도 시간을 바꾸는 방법에 대해서는 찾기 어려웠으며, 어차피 해당 지표정보를 Grafana로 확인할 것이기 때문에 시간을 바꾸려 크게 노력하진 않았다.
다음으로 상단 메뉴의 Status -> Targets를 선택해보자. 그러면 프로메테우스가 모니터링 하고 있는 타겟을 확인할 수 있는데, 이 중 kube-state-metrics가 (0/1 up)으로 올라가지 않은 것으로 표시된다. kube-state-metrics는 쿠버네티스 클러스터 내의 API Server를 확인하며 클러스터 내 오브젝트(예를들면 Pod)에 대한 지표정보를 생성하는 서비스다. 따라서 Pod 상태정보를 모니터링하기 위해서는 kube-state-metrics가 떠 있어야 한다.
kube-state-metrics도 yaml파일을 만들어서 적용시켜보자.
cluster-role-binding.yaml :
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
labels:
app.kubernetes.io/name: kube-state-metrics
app.kubernetes.io/version: v1.8.0
name: kube-state-metrics
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: kube-state-metrics
subjects:
- kind: ServiceAccount
name: kube-state-metrics
namespace: kube-system
cluster-role.yaml :
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: kube-state-metrics
app.kubernetes.io/version: v1.8.0
name: kube-state-metrics
rules:
- apiGroups:
- ""
resources:
- configmaps
- secrets
- nodes
- pods
- services
- resourcequotas
- replicationcontrollers
- limitranges
- persistentvolumeclaims
- persistentvolumes
- namespaces
- endpoints
verbs:
- list
- watch
- apiGroups:
- extensions
resources:
- daemonsets
- deployments
- replicasets
- ingresses
verbs:
- list
- watch
- apiGroups:
- apps
resources:
- statefulsets
- daemonsets
- deployments
- replicasets
verbs:
- list
- watch
- apiGroups:
- batch
resources:
- cronjobs
- jobs
verbs:
- list
- watch
- apiGroups:
- autoscaling
resources:
- horizontalpodautoscalers
verbs:
- list
- watch
- apiGroups:
- authentication.k8s.io
resources:
- tokenreviews
verbs:
- create
- apiGroups:
- authorization.k8s.io
resources:
- subjectaccessreviews
verbs:
- create
- apiGroups:
- policy
resources:
- poddisruptionbudgets
verbs:
- list
- watch
- apiGroups:
- certificates.k8s.io
resources:
- certificatesigningrequests
verbs:
- list
- watch
- apiGroups:
- storage.k8s.io
resources:
- storageclasses
- volumeattachments
verbs:
- list
- watch
- apiGroups:
- admissionregistration.k8s.io
resources:
- mutatingwebhookconfigurations
- validatingwebhookconfigurations
verbs:
- list
- watch
- apiGroups:
- networking.k8s.io
resources:
- networkpolicies
verbs:
- list
- watch
service-account.yaml :
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
app.kubernetes.io/name: kube-state-metrics
app.kubernetes.io/version: v1.8.0
name: kube-state-metrics
namespace: kube-system
deployment.yaml :
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/name: kube-state-metrics
app.kubernetes.io/version: v1.8.0
name: kube-state-metrics
namespace: kube-system
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: kube-state-metrics
template:
metadata:
labels:
app.kubernetes.io/name: kube-state-metrics
app.kubernetes.io/version: v1.8.0
spec:
containers:
- image: quay.io/coreos/kube-state-metrics:v1.8.0
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
timeoutSeconds: 5
name: kube-state-metrics
ports:
- containerPort: 8080
name: http-metrics
- containerPort: 8081
name: telemetry
readinessProbe:
httpGet:
path: /
port: 8081
initialDelaySeconds: 5
timeoutSeconds: 5
nodeSelector:
kubernetes.io/os: linux
serviceAccountName: kube-state-metrics
service.yaml :
apiVersion: v1
kind: Service
metadata:
labels:
app.kubernetes.io/name: kube-state-metrics
app.kubernetes.io/version: v1.8.0
name: kube-state-metrics
namespace: kube-system
spec:
clusterIP: None
ports:
- name: http-metrics
port: 8080
targetPort: http-metrics
- name: telemetry
port: 8081
targetPort: telemetry
selector:
app.kubernetes.io/name: kube-state-metrics
yaml파일이 다 만들어졌으면 적용해보자.
# kubectl apply -f cluster-role-binding.yaml
# kubectl apply -f cluster-role.yaml
# kubectl apply -f deployment.yaml
# kubectl apply -f service-account.yaml
# kubectl apply -f service.yaml
쿠버네티스 pod 정보를 확인해보면 kube-state-metrics가 정상 구동되는 것을 확인할 수 있다.
또한 프로메테우스 웹페이지 내 Targets에서도 kube-state-metrics의 지표정보가 정상적으로 수집되는 것을 확인할 수 있다(상태 1/1 up으로 변경됨).
3-3. 서버에 Node-Exporter 적용
지금까지 node-exporter를 쿠버네티스 클러스터 내부에 적용하였고, 쿠버네티스 클러스터의 지표정보를 수집 & 모니터링이 가능하게 되었다. 그렇다면 서버의 지표정보는 어떻게 수집을 해야할까?
서버의 지표정보를 수집하려면 node-exporter를 쿠버네티스 pod가 아닌, 서버에 직접 구동시킨 후 쿠버네티스 클러스터에서 동작하고 있는 프로메테우스와 연동시켜야 한다.
서버용 node-exporter를 다운받는다.
# wget https://github.com/prometheus/node_exporter/releases/download/v0.16.0-rc.1/node_exporter-0.16.0-rc.1.linux-amd64.tar.gz
다운이 완료되면 해당 파일의 압축을 해제하고 디렉토리명을 node-exporter로 변경한다.
# tar -xzvf node_exporter-0.16.0-rc.1.linux-amd64.tar.gz
# mv node_exporter-0.16.0-rc.1.linux-amd64 node_exporter
node-exporter용 service 파일을 생성한다.
# cd /etc/systemd/system/
# vi node_exporter.service
node_exporter.service :
[Unit]
Description=Node Exporter
Wants=network-online.target
After=network-online.target
[Service]
User=prometheus
ExecStart=/home/admin/k8s/prometheus/server-node-exporter/node_exporter/node_exporter <-node_exporter가 설치된 디렉토리를 적는다
[Install]
WantedBy=default.target
systemd 시스템을 재기동하고, node_exporter를 시작하고, 시스템이 시작할때마다 node_exporter가 시작될 수 있게 enable처리한다.
# systemctl daemon-reload
# systemctl start node_exporter
# systemctl enable node_exporter
node_exporter가 정상 실행되고 있는지 확인한다.
# systemctl status node_exporter
# netstat -plntu
다음으로 쿠버네티스 클러스터에서 구동되고 있는 프로메테우스의 설정파일(prometheus-config-map.yaml 내 prometheus.yaml 부분)에 방금 구동시킨 node-exporter에 대한 설정을 추가한다. node-exporter가 적용된 서버의 ip와 포트(9100)를 추가해주면 된다. 필자는 두개의 서버에 적용했기 때문에 두 개 서버 정보를 추가했다.
prometheus-config-map.yaml :
scrape_configs:
- job_name: 'server-info'
static_configs:
- targets: ['66.42.43.41:9100', '139.180.196.145:9100']
그리고 변경한 설정파일을 적용해야 하므로, 프로메테우스를 내렸다가 다시 올려주면 서버 정보도 모니터링 되는 것을 확인할 수 있다(server-info (2/2 up)). Endpoint 항목을 클릭해보면 어떤 지표정보가 모니터링 되는지도 확인해볼 수 있다.
4. AlertManager 적용
마지막으로 AlertManager를 쿠버네티스 클러스터에 적용해보자. AlertManager의 작동 원리는 우선 프로메테우스에 수집된 지표정보가 prometheus.rules 에 적용된 알람 조건을 만족하게 되면 프로메테우스에서 AlertManager로 알람을 보내게 된다. 그러면 AlertManager는 config.yaml에 등록된 receivers에게 알람을 보내게 된다(config.yaml은 ConfigMap을 활용하여 정의하면 된다). receivers는 Slack이나 이메일을 등록한다. 필자는 이번 포스트에서 Slack을 사용할 것이다. AlertManager에 대한 자세한 설명은 https://prometheus.io/docs/alerting/configuration/ 를 참고하자
4-1. Slack에 Webhook 등록
우선 AlertManager의 config.yaml의 api_url에 등록할 Slack Webhooks url을 얻는 방법에 대해 알아보자.
처음에 Slack의 프로젝트로 이동한 후 Administraion -> Manage apps 선택한다.
Slack App Directory에서 webhooks 검색 후 Add to Slack 클릭 -> 채널명 선택하고 Add Incoming Webhooks integration 클릭
그러면 Webhook URL이 나오게 된다. 해당 URL은 config.yaml의 receiver 정보로 사용될 것이다. Save Setting을 클릭하고 설정을 저장하자.
4-2. AlertManager 설정 & 연동
AlertManager도 yaml파일을 활용해 쿠버네티스 클러스터에 적용시킬 것이다. AlertManager를 구동시키기 위해선 ConfigMap이 2개, Deployment, Service yaml파일이 필요하다. AlertManagerConfigMap.yaml 파일에는 AlertManager가 알람을 보낼 대상에 대한 정보(Slack Webhooks), AlertTemplateConfigMap.yaml 파일에는 알람 메시지 포맷, Deployment.yaml에는 AlertManager deployment 설정, Service.yaml에는 외부 포트 개방 설정이 있다.
다른 내용은 그대로 사용해도 되지만 AlertManagerConfigMap.yaml의 slack_configs의 api_url에는 위에서 등록한(아래에서 등록할 예정) Slack Webhooks url을 작성해야 한다.
AlertManagerConfigMap.yaml :
kind: ConfigMap
apiVersion: v1
metadata:
name: alertmanager-config
namespace: monitoring
data:
config.yml: |-
global:
templates:
- '/etc/alertmanager/*.tmpl'
route:
receiver: alert-emailer
group_by: ['alertname', 'priority']
group_wait: 10s
repeat_interval: 30m
routes:
- receiver: slack_demo
# Send severity=slack alerts to slack.
match:
severity: fatal
group_wait: 10s
repeat_interval: 1m
receivers:
- name: alert-emailer
email_configs:
- to: ekfrl2815@gmail.com
send_resolved: false
from: from-email@email.com
smarthost: smtp.eample.com:25
require_tls: false
- name: slack_demo
slack_configs:
- api_url: https://hooks.slack.com/services/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
channel: '#auth'
send_resolved: true
#text: 'container memory usage rate is very high( > 45%)'
AlertTemplateConfigMap.yaml :
apiVersion: v1
kind: ConfigMap
metadata:
creationTimestamp: null
name: alertmanager-templates
namespace: monitoring
data:
default.tmpl: |
{{ define "__alertmanager" }}AlertManager{{ end }}
{{ define "__alertmanagerURL" }}{{ .ExternalURL }}/#/alerts?receiver={{ .Receiver }}{{ end }}
{{ define "__subject" }}[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .GroupLabels.SortedPairs.Values | join " " }} {{ if gt (len .CommonLabels) (len .GroupLabels) }}({{ with .CommonLabels.Remove .GroupLabels.Names }}{{ .Values | join " " }}{{ end }}){{ end }}{{ end }}
{{ define "__description" }}{{ end }}
{{ define "__text_alert_list" }}{{ range . }}Labels:
{{ range .Labels.SortedPairs }} - {{ .Name }} = {{ .Value }}
{{ end }}Annotations:
{{ range .Annotations.SortedPairs }} - {{ .Name }} = {{ .Value }}
{{ end }}Source: {{ .GeneratorURL }}
{{ end }}{{ end }}
{{ define "slack.default.title" }}{{ template "__subject" . }}{{ end }}
{{ define "slack.default.username" }}{{ template "__alertmanager" . }}{{ end }}
{{ define "slack.default.fallback" }}{{ template "slack.default.title" . }} | {{ template "slack.default.titlelink" . }}{{ end }}
{{ define "slack.default.pretext" }}{{ end }}
{{ define "slack.default.titlelink" }}{{ template "__alertmanagerURL" . }}{{ end }}
{{ define "slack.default.iconemoji" }}{{ end }}
{{ define "slack.default.iconurl" }}{{ end }}
{{ define "slack.default.text" }}{{ end }}
{{ define "hipchat.default.from" }}{{ template "__alertmanager" . }}{{ end }}
{{ define "hipchat.default.message" }}{{ template "__subject" . }}{{ end }}
{{ define "pagerduty.default.description" }}{{ template "__subject" . }}{{ end }}
{{ define "pagerduty.default.client" }}{{ template "__alertmanager" . }}{{ end }}
{{ define "pagerduty.default.clientURL" }}{{ template "__alertmanagerURL" . }}{{ end }}
{{ define "pagerduty.default.instances" }}{{ template "__text_alert_list" . }}{{ end }}
{{ define "opsgenie.default.message" }}{{ template "__subject" . }}{{ end }}
{{ define "opsgenie.default.description" }}{{ .CommonAnnotations.SortedPairs.Values | join " " }}
{{ if gt (len .Alerts.Firing) 0 -}}
Alerts Firing:
{{ template "__text_alert_list" .Alerts.Firing }}
{{- end }}
{{ if gt (len .Alerts.Resolved) 0 -}}
Alerts Resolved:
{{ template "__text_alert_list" .Alerts.Resolved }}
{{- end }}
{{- end }}
{{ define "opsgenie.default.source" }}{{ template "__alertmanagerURL" . }}{{ end }}
{{ define "victorops.default.message" }}{{ template "__subject" . }} | {{ template "__alertmanagerURL" . }}{{ end }}
{{ define "victorops.default.from" }}{{ template "__alertmanager" . }}{{ end }}
{{ define "email.default.subject" }}{{ template "__subject" . }}{{ end }}
{{ define "email.default.html" }}
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<!--
Style and HTML derived from https://github.com/mailgun/transactional-email-templates
The MIT License (MIT)
Copyright (c) 2014 Mailgun
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-->
<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<head style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<meta name="viewport" content="width=device-width" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />
<title style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">{{ template "__subject" . }}</title>
</head>
<body itemscope="" itemtype="http://schema.org/EmailMessage" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 1.6em; width: 100% !important; background-color: #f6f6f6; margin: 0; padding: 0;" bgcolor="#f6f6f6">
<table style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6">
<tr style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<td style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
<td width="600" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; width: 100% !important; margin: 0 auto; padding: 0;" valign="top">
<div style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 0;">
<table width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background-color: #fff; margin: 0; border: 1px solid #e9e9e9;" bgcolor="#fff">
<tr style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<td style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; vertical-align: top; color: #fff; font-weight: 500; text-align: center; border-radius: 3px 3px 0 0; background-color: #E6522C; margin: 0; padding: 20px;" align="center" bgcolor="#E6522C" valign="top">
{{ .Alerts | len }} alert{{ if gt (len .Alerts) 1 }}s{{ end }} for {{ range .GroupLabels.SortedPairs }}
{{ .Name }}={{ .Value }}
{{ end }}
</td>
</tr>
<tr style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<td style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 10px;" valign="top">
<table width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<tr style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<td style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
<a href="{{ template "__alertmanagerURL" . }}" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize; background-color: #348eda; margin: 0; border-color: #348eda; border-style: solid; border-width: 10px 20px;">View in {{ template "__alertmanager" . }}</a>
</td>
</tr>
{{ if gt (len .Alerts.Firing) 0 }}
<tr style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<td style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
<strong style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">[{{ .Alerts.Firing | len }}] Firing</strong>
</td>
</tr>
{{ end }}
{{ range .Alerts.Firing }}
<tr style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<td style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
<strong style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">Labels</strong><br style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />
{{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}<br style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />{{ end }}
{{ if gt (len .Annotations) 0 }}<strong style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">Annotations</strong><br style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />{{ end }}
{{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}<br style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />{{ end }}
<a href="{{ .GeneratorURL }}" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #348eda; text-decoration: underline; margin: 0;">Source</a><br style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />
</td>
</tr>
{{ end }}
{{ if gt (len .Alerts.Resolved) 0 }}
{{ if gt (len .Alerts.Firing) 0 }}
<tr style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<td style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
<br style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />
<hr style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />
<br style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />
</td>
</tr>
{{ end }}
<tr style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<td style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
<strong style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">[{{ .Alerts.Resolved | len }}] Resolved</strong>
</td>
</tr>
{{ end }}
{{ range .Alerts.Resolved }}
<tr style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<td style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
<strong style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">Labels</strong><br style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />
{{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}<br style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />{{ end }}
{{ if gt (len .Annotations) 0 }}<strong style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">Annotations</strong><br style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />{{ end }}
{{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}<br style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />{{ end }}
<a href="{{ .GeneratorURL }}" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #348eda; text-decoration: underline; margin: 0;">Source</a><br style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />
</td>
</tr>
{{ end }}
</table>
</td>
</tr>
</table>
<div style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;">
<table width="100%" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<tr style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<td style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; text-align: center; color: #999; margin: 0; padding: 0 0 20px;" align="center" valign="top"><a href="{{ .ExternalURL }}" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 12px; color: #999; text-decoration: underline; margin: 0;">Sent by {{ template "__alertmanager" . }}</a></td>
</tr>
</table>
</div></div>
</td>
<td style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
</tr>
</table>
</body>
</html>
{{ end }}
{{ define "pushover.default.title" }}{{ template "__subject" . }}{{ end }}
{{ define "pushover.default.message" }}{{ .CommonAnnotations.SortedPairs.Values | join " " }}
{{ if gt (len .Alerts.Firing) 0 }}
Alerts Firing:
{{ template "__text_alert_list" .Alerts.Firing }}
{{ end }}
{{ if gt (len .Alerts.Resolved) 0 }}
Alerts Resolved:
{{ template "__text_alert_list" .Alerts.Resolved }}
{{ end }}
{{ end }}
{{ define "pushover.default.url" }}{{ template "__alertmanagerURL" . }}{{ end }}
slack.tmpl: |
{{ define "slack.devops.text" }}
{{range .Alerts}}{{.Annotations.DESCRIPTION}}
{{end}}
{{ end }}
Deployment.yaml :
apiVersion: apps/v1
kind: Deployment
metadata:
name: alertmanager
namespace: monitoring
spec:
replicas: 1
selector:
matchLabels:
app: alertmanager
template:
metadata:
name: alertmanager
labels:
app: alertmanager
spec:
containers:
- name: alertmanager
image: prom/alertmanager:v0.19.0
args:
- "--config.file=/etc/alertmanager/config.yml"
- "--storage.path=/alertmanager"
ports:
- name: alertmanager
containerPort: 9093
volumeMounts:
- name: config-volume
mountPath: /etc/alertmanager
- name: templates-volume
mountPath: /etc/alertmanager-templates
- name: alertmanager
mountPath: /alertmanager
volumes:
- name: config-volume
configMap:
name: alertmanager-config
- name: templates-volume
configMap:
name: alertmanager-templates
- name: alertmanager
emptyDir: {}
Service.yaml :
apiVersion: v1
kind: Service
metadata:
name: alertmanager
namespace: monitoring
annotations:
prometheus.io/scrape: 'true'
prometheus.io/path: /
prometheus.io/port: '8080'
spec:
selector:
app: alertmanager
type: NodePort
ports:
- port: 9093
targetPort: 9093
nodePort: 30005
작성한 yaml파일을 구동하면 AlertManager 웹페이지에 접속할 수 있다(주소는 http://[호스트IP]:[AlertManager서비스PORT 이며, 이 포스트에선 http://[호스트IP]:30005이다). 웹페이지로 가면 아무것도 뜨지 않는데, 만약 프로메테우스로부터 알람을 받게 되면 리스트에 받은 알람 정보가 뜨게 된다.
# kubectl apply -f AlertManagerConfigMap.yaml
# kubectl apply -f AlertTemplateConfigMap.yaml
# kubectl apply -f Deployment.yaml
# kubectl apply -f Service.yaml
4-3. AlertManager 테스트
이제 적용한 AlertManager가 잘 되는지 테스트를 진행해보자.
현재 프로메테우스에 저장된 alert rule(Prometheus-config-map.yaml 내 Prometheus.rules)은 아래와 같이 컨테이너 메모리 사용량이 55%가 넘거나, 컨테이너 CPU 사용량이 10%가 넘으면 AlertManager로 알람을 발송한다. AlertManager는 그 알람을 받은 후 Slack을 통해 사용자에게 알람을 보낸다.
테스트를 위해 소스 배포를 해보자. 소스 배포를 하게 되면 CPU/메모리 사용량이 늘어나 알람 조건을 충족하여 알람이 오게 된다. AlertManager 웹페이지에도 알람 리스트가 생기고,
아래와 같이 Slack으로도 알람 메시지가 오게 된다.
마치며
이번 장에서는 Prometheus & Node-Exporter & AlertManager를 활용한 모니터링 시스템을 구축하였다. 다음 장에서는 프로메테우스에 적재된 지표(metric)정보를 좀 더 쉽게 볼 수 있도록 Grafana와 연동하여 Dashboard를 만들어 보도록 하겠다.
참고
https://bcho.tistory.com/1270
https://devopscube.com/setup-prometheus-monitoring-on-kubernetes/
https://www.howtoforge.com/tutorial/how-to-install-prometheus-and-node-exporter-on-centos-7/