과제3.efk stack 구축을 통한 로그 수집
실습과제
- 주제: EFK stack 구축을 통한 로그 수집
- 컨테이너 환경에서의 로그 수집을 위해 EFK(ElasticSearch + Fluentbit + Kibana) Stack을 구축합니다.
- 로그 저장소인 ElasticSearch, 로그 수집기인 Fluentbit, 로그 시각화 툴인 Kibana를 KES 환경에서 구축합니다.
1) ElasticSearch 구축 2) Fluentbit 구축 3) Kibana 구축EFK Stack
EFK STack 구축을 통한 로그 수집
- 컨테이너 환경에서의 로그 수집을 위해 EFK Stack을 구축합니다.
- 로그 저장소인 ElasticSearch, 로그 수집기인 Fluentbit, 로그 시각화 툴인 Kibana를 EKS 환경에서 구축합니다.
EFK?
- ElasticSearch + Fluentbit + Kibana
- 쿠버네티스는 파드가 정상상태가 아니면 새로 생성 -> 죽은 파드에 있는 컨테이너가 남긴 로그는 어디로?
- 컨테이너의 로그를 로그 저장소에 수집 -> 죽은 컨테이너의 로그도 남는다.
Fluentbit
- 컨테이너의 스트림 로그를 수집하는 로그 수집기
- 모든 노드마다 동일하게 배포! -> Daemonset
😂Daemonset?
- 쿠버네티스 컨트롤러(쿠버네티스 기본 Object를 생성하고 관리하는 역할)(Deployment, ReplicaSet 등) 중 하나.
- Daemonset은 Pod가 각각의 노드에 하나씩만 배포되게 하는 Pod 관리 컨트롤러ElasticSearch
- 로그를 저장하기 위한 대용량 저장소
- 텍스트, 숫자, 위치 기반 정보, 정형 및 비정형 데이터 등 모든 유형의 데이터를 위한 분산형 오픈 소스 검색 및 분석 엔진 = 많은 양의 데이터를 보관하고 실시간으로 분석하는 엔진
- ElasticSearch 설치하려면 원래는 Java8 설치하고 elasticsearch install 하고 service 다시 시작! 하지만 쿠버네티스로 하면 yaml 파일로 작성하고 띄우면 됨!!!!
- ElasticSearch를 NodePort type의 서비스로 배포하여 2주차 때 생성한 Nginx의 LoadBalancer에 연결하여 통신 확인
Kibana
- ElasticSearchㅘ 연동하여 로그 시각화 -> 로그 시각화를 통한 문제 해결 및 예방 가능
1단계. 2주차 과제와 동일하게 EKS 구성하고 Nginx 실행
- 과정 동일! 2주차 참고
2단계 ElasticSearch 배포
1. elasticSearch.yaml로 elasticSearch 배포 -> NodePortType으로 배포
- 쿠버네티스 클러스터 안에서 Elasticsearch 컨테이너를 실행하고, VPC 내부의 다른 노드나 Bastion에서 9200/9300 포트로 접근할 수 있게 만든 설정
- 구조
1 2 3 4 5 6
[ EKS Cluster ] ├── Node (EC2 #1) │ └── Pod (Elasticsearch container) │ └── NodePort Service (30920/30930) ↳ Pod 내부의 9200/9300 포트로 라우팅 - 명령어 ``` cat«EOF > ~/elasticSearch.yaml apiVersion: apps/v1 kind: Deployment metadata: name: elasticsearch labels: app: elasticsearch spec: replicas: 1 selector: matchLabels: app: elasticsearch template: metadata: labels: app: elasticsearch spec: containers: - name: elasticsearch image: elastic/elasticsearch:6.4.0 env: - name: discovery.type value: “single-node” ports: - containerPort: 9200 - containerPort: 9300
apiVersion: v1 kind: Service metadata: labels: app: elasticsearch name: elasticsearch-svc namespace: default spec: ports:
- name: elasticsearch-rest nodePort: 30920 port: 9200 protocol: TCP targetPort: 9200
- name: elasticsearch-nodecom nodePort: 30930 port: 9300 protocol: TCP targetPort: 9300 selector: app: elasticsearch type: NodePort EOF ```
- Deployment — Elasticsearch 실행 정의
- Deployment: 쿠버네티스가 “이 앱을 항상 몇 개 띄워야 하는지” 관리해주는 컨트롤러.
- replicas: 1 → Elasticsearch Pod를 1개만 실행 (단일 노드).
- Pod 템플릿
- image: elastic/elasticsearch:6.4.0 → Elasticsearch 공식 Docker 이미지, 버전 6.4.0
- env: discovery.type=single-node → 클러스터 모드 비활성화 → “단일 노드”로 동작
- containerPort 9200: REST API 포트 (검색 요청용)
- containerPort 9300: 노드 간 통신 포트 (클러스터 구성용)
=> 즉, 단일 Elasticsearch 서버 1대를 쿠버네티스 Pod 안에 띄워주는 설정이야.
- Service — NodePort로 내부 접근 설정
- Service: Pod에 접근할 수 있는 고정 네트워크 엔드포인트
- selector: app: elasticsearch → 위 Deployment의 Pod와 연결됨
- type: NodePort
→ 각 워커 노드의 IP에 고정 포트를 열어줌 (VPC 내부에서 접근 가능)
- Port 설정
- port (9200/9300)-> Service(ClusterIP) 기준 포트
- targetPort (9200/9300)-> 실제 Pod 내부 컨테이너 포트
- nodePort (30920/30930)->각 노드(EC2)에서 열리는 포트 (NodePort 범위 30000~32767)
1
2
3
kubectl apply -f elasticSearch.yaml
kubectl get pod | grep elastic
kubectl get svc | grep elastic
- 정리
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
┌──────────────────────────────┐ │ Kubernetes Cluster │ │ │ │ ┌────────────────────────┐ │ │ │ Service Layer │ │ │ │ │ │ │ │ [my-nginx Service] <──┐ │ │ │ [elasticsearch-svc] <─┘ │ │ └────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌────────────────────────┐ │ │ │ Node(EC2) │ │ │ │ (ip-10-0-3-187 ...) │ │ │ │ ┌────────────┐ │ │ │ │ │ nginx Pod 1│ │ │ │ │ ├────────────┤ │ │ │ │ │ nginx Pod 2│ │ │ │ │ ├────────────┤ │ │ │ │ │ elastic Pod │ │ │ │ │ └────────────┘ │ │ │ └────────────────────────┘ │ └──────────────────────────────┘
2. LoadBalancer 리스너 추가
- 리스너란? 리스너는 정의한 프로토콜과 포트를 사용하여 연결 요청을 확인하는 프로세스. 리스너에 정의한 규칙에 따라 로드밸런서가 등록된 대상으로 요청을 라우팅 하는 방법 결정
- 설정
- [Ec2] -> [로드밸런서] -> [Listeners]탭 클릭 -> [Edit]
-> loadbalancer의 9200 port로 들어오면, instance의 30920으로 trafic 넘겨준다.
3. LoadBalancer의 9200 포트로 외부 접근 허용 위해 Loadbalancer Security group inbound rule 추가
- 설정
- [Ec2] -> [로드밸런서] -> [Security]탭 클릭 -> Security Group ID 클릭 -> Security group 화면에서 inbound rule 편집
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌────────────────────────────────────────────┐
│ AWS Load Balancer (ELB) │
│────────────────────────────────────────────│
│ Listener 1: TCP 80 → Instance Port 30430 │ ← nginx 서비스
│ Listener 2: TCP 9200 → Instance Port 30920 │ ← elasticsearch 서비스
└────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────┐
│ EC2 Instance (ip-10-0-3-187 ...) │
│ (= Kubernetes Node, 워커노드) │
│───────────────────────────────────────────│
│ kube-proxy │
│ ├─ NodePort 30430 → Pod:80 (nginx) │
│ └─ NodePort 30920 → Pod:9200 (elasticsearch) │
│───────────────────────────────────────────│
│ [Pods] │
│ ├─ my-nginx-xxxx :80 (컨테이너 포트) │
│ └─ elasticsearch-xxxx :9200 (REST API) │
└───────────────────────────────────────────┘
3단계 Kibana
- 키바나란?
- Elastic Stack을 기반으로 구축된 오픈 소스 프론트엔드 애플리케이션으로, ElasticSearch에서 색인된 데이터를 검색하고 시각화하는 기능을 제공 = 오픈 소스 기반의 분석 및 시각화 플랫폼
- Kibana배포
- Kibana.yaml으로 kibana 배포 -> NodePortType으로 배포
- Kibana를 Nodeport type의 서비스로 배포하여 2주차 때 생성한 Nginx의 LoadBalancer에 연결하여 외부 접근
1. Kiban.yaml로 kibana 배포 > Nodeporttype으로 배포
``` cat«EOF > ~/kibana.yaml apiVersion: apps/v1 kind: Deployment metadata: name: kibana labels: app: kibana spec: replicas: 1 selector: matchLabels: app: kibana template: metadata: labels: app: kibana spec: containers:
- name: kibana image: elastic/kibana:6.4.0 env:
- name: SERVER_NAME value: “kibana.kubernetes.example.com”
- name: ELASTICSEARCH_URL value: “http://elasticsearch-svc.default.svc.cluster.local:9200” ports:
- containerPort: 5601
- name: kibana image: elastic/kibana:6.4.0 env:
apiVersion: v1 kind: Service metadata: labels: app: kibana name: kibana-svc namespace: default spec: ports:
- nodePort: 30561 port: 5601 protocol: TCP targetPort: 5601 selector: app: kibana type: NodePort EOF ```
- Deployment/kibana
- replicas: 1 → Kibana 파드 1개.
- 라벨: app: kibana (Service가 이 라벨로 파드를 찾아요)
- 컨테이너: elastic/kibana:6.4.0
- 포트: 컨테이너 내부 5601
- 환경변수:
- SERVER_NAME=”kibana.kubernetes.example.com”(SERVER_HOST를 지정하지 않으면 기본 0.0.0.0로 뜨므로 보통 추가 불필요)
- ELASTICSEARCH_URL=”http://elasticsearch-svc.default.svc.cluster.local:9200”
↳ 클러스터 DNS로 ES에 붙어요. elasticsearch-svc(default 네임스페이스)의 9200으로 접속.
- Service/kibana-svc (NodePort)
- type: NodePort
- 서비스 포트 5601 ↔ NodePort 30561로 개방
- 셀렉터: app: kibana → 위 Deployment 파드로 라우팅
- 명령
1 2
kubectl apply -f kibana.yaml kubectl get service
2. LoadBalncer 리스너에 추가
앞의 과정과 비슷
3. Loadbalncer의 5601 포트로 외부 접근 허용 위해 Loadbalancer Security group inbound rule 추가
앞의 과정과 비슷
- 전체 구조
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
사용자 브라우저
│ http://<아무 노드 퍼블릭IP>:30561
▼
Kubernetes Node (EC2)
│ NodePort 30561
▼
kibana-svc (ClusterIP)
│ targetPort 5601
▼
Kibana Pod :5601
│ (내부에서)
▼
Elasticsearch Service (ClusterIP)
│ elasticsearch-svc.default.svc.cluster.local:9200
▼
Elasticsearch Pod :9200
4단계 Fluentd
- Fluentd란?
- 로그 수집기
- 다양한 데이터소스(HTTP, TCP 등) 로부터 원하는 형태로 가공되어 여러 목적지(ElasticSearch, s3) 등으로 전달 가능
🔥주의: Fluentd -> FluentBit 대체
EKS 1.24 이상의 버전에서부터는 Fluentd가 아닌 FluentBit으로 대체됩니다. EKS 1.24부터는 1.23과는 다르게 런타임 Containerd만 사용하기 때문에 기존에 사용하던 Fleuntd를 사용할 경우 정상적으로 로그 수집이 불가합니다.
1. fluentbit.yaml로 fluentd Daemonset Pod 배포
- 주의: 꼭 vi 편집기로 직접 생성!!! (cat«EOF» ~/fluentbit.yaml 으로 생성X)
- i로 vi 편집기 편집 모드로 변경 후 fluentbit.yaml 파일의 내용 복사 붙여넣기. vi 편집기에 붙여넣기 할 때는 마우스 우클릭을 이용. 붙여넣기 완료 후 esc로 vi 커맨드 모드로 변경 후 :wq 엔터로 파일 저장 ``` cd ~/ vi fluentbit.yaml
kubectl apply -f fluentbit.yaml kubectl get pod -n kube-system | grep fluent
1
2
3
4
5
- 왜 붙여야 해?
- 쿠버네티스는 리소스를 네임스페이스로 구분해 관리해.
- 네가 아무 옵션 없이 kubectl get pod를 치면 default 네임스페이스만 조회함.
- 그런데 fluent-bit(및 coredns, kube-proxy, aws-node 등 클러스터 컴포넌트)는 보통 kube-system 네임스페이스에 배포되어 있어.
- 그래서 -n kube-system을 붙여야 그 네임스페이스의 파드를 보는 거야.
apiVersion: v1 kind: ServiceAccount metadata: name: fluent-bit namespace: kube-system labels: app: fluent-bit
apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: fluent-bit labels: app: fluent-bit rules:
- apiGroups: [””] resources:
- pods
- namespaces verbs: [“get”, “list”, “watch”]
kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: fluent-bit roleRef: kind: ClusterRole name: fluent-bit apiGroup: rbac.authorization.k8s.io subjects:
- kind: ServiceAccount name: fluent-bit namespace: kube-system
apiVersion: v1 kind: ConfigMap metadata: name: fluent-bit-config namespace: kube-system labels: k8s-app: fluent-bit data: # Configuration files: server, input, filters and output # ====================================================== fluent-bit.conf: | [SERVICE] Flush 1 Log_Level info Daemon off Parsers_File parsers.conf HTTP_Server On HTTP_Listen 0.0.0.0 HTTP_Port 2020
1
2
3
@INCLUDE input-kubernetes.conf
@INCLUDE filter-kubernetes.conf
@INCLUDE output-elasticsearch.conf
input-kubernetes.conf: | [INPUT] Name tail Tag kube.* Path /var/log/containers/*.log Parser docker DB /var/log/flb_kube.db Mem_Buf_Limit 5MB Skip_Long_Lines On Refresh_Interval 10
filter-kubernetes.conf: | [FILTER] Name kubernetes Match kube.* Kube_URL https://kubernetes.default.svc:443 Kube_CA_File /var/run/secrets/kubernetes.io/serviceaccount/ca.crt Kube_Token_File /var/run/secrets/kubernetes.io/serviceaccount/token Kube_Tag_Prefix kube.var.log.containers. Merge_Log On Merge_Log_Key log_processed K8S-Logging.Parser On K8S-Logging.Exclude Off
output-elasticsearch.conf: | [OUTPUT] Name es Match * Host ${FLUENT_ELASTICSEARCH_HOST} Port ${FLUENT_ELASTICSEARCH_PORT} Logstash_Format On Replace_Dots On Retry_Limit False
parsers.conf: | [PARSER] Name apache Format regex Regex ^(?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
[PARSER]
Name apache2
Format regex
Regex ^(?<host>[^ ]*) [^ ]* (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^ ]*) +\S*)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)")?$
Time_Key time
Time_Format %d/%b/%Y:%H:%M:%S %z
[PARSER]
Name apache_error
Format regex
Regex ^\[[^ ]* (?<time>[^\]]*)\] \[(?<level>[^\]]*)\](?: \[pid (?<pid>[^\]]*)\])?( \[client (?<client>[^\]]*)\])? (?<message>.*)$
[PARSER]
Name nginx
Format regex
Regex ^(?<remote>[^ ]*) (?<host>[^ ]*) (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^\"]*?)(?: +\S*)?)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)")?$
Time_Key time
Time_Format %d/%b/%Y:%H:%M:%S %z
[PARSER]
Name json
Format json
Time_Key time
Time_Format %d/%b/%Y:%H:%M:%S %z
[PARSER]
Name docker
Format json
Time_Key time
Time_Format %Y-%m-%dT%H:%M:%S.%L
Time_Keep On
[PARSER]
# http://rubular.com/r/tjUt3Awgg4
Name cri
Format regex
Regex ^(?<time>[^ ]+) (?<stream>stdout|stderr) (?<logtag>[^ ]*) (?<message>.*)$
Time_Key time
Time_Format %Y-%m-%dT%H:%M:%S.%L%z
[PARSER]
Name syslog
Format regex
Regex ^\<(?<pri>[0-9]+)\>(?<time>[^ ]* {1,2}[^ ]* [^ ]*) (?<host>[^ ]*) (?<ident>[a-zA-Z0-9_\/\.\-]*)(?:\[(?<pid>[0-9]+)\])?(?:[^\:]*\:)? *(?<message>.*)$
Time_Key time
Time_Format %b %d %H:%M:%S
apiVersion: apps/v1 kind: DaemonSet metadata: name: fluent-bit namespace: kube-system labels: k8s-app: fluent-bit-logging version: v1 kubernetes.io/cluster-service: “true” spec: selector: matchLabels: k8s-app: fluent-bit-logging version: v1 template: metadata: labels: k8s-app: fluent-bit-logging version: v1 kubernetes.io/cluster-service: “true” spec: containers: - name: fluent-bit image: fluent/fluent-bit:1.3.11 imagePullPolicy: Always env: - name: FLUENT_ELASTICSEARCH_HOST value: “elasticsearch-svc.default.svc.cluster.local” - name: FLUENT_ELASTICSEARCH_PORT value: “9200” - name: FLUENT_ELASTICSEARCH_SCHEME value: “http” volumeMounts: - name: varlog mountPath: /var/log - name: varlibdockercontainers mountPath: /var/lib/docker/containers readOnly: true - name: fluent-bit-config mountPath: /fluent-bit/etc/ terminationGracePeriodSeconds: 10 volumes: - name: varlog hostPath: path: /var/log - name: varlibdockercontainers hostPath: path: /var/lib/docker/containers - name: fluent-bit-config configMap: name: fluent-bit-config serviceAccountName: fluent-bit tolerations: - key: node-role.kubernetes.io/master operator: Exists effect: NoSchedule - operator: “Exists” effect: “NoExecute” - operator: “Exists” effect: “NoSchedule”
1
┌──────────────────────────────┐ │ Kubernetes Cluster │ │ │ │ ┌────────────────────────┐ │ │ │ Node A (EC2) │ │ │ │ ├─ Pod 1 (nginx) │ │ │ │ ├─ Pod 2 (app) │ │ │ │ ├─ Pod 3 (api) │ │ │ │ └─ Fluent Bit Agent 🟢│ │ ← Node 단위로 1개 │ └────────────────────────┘ │ │ ┌────────────────────────┐ │ │ │ Node B (EC2) │ │ │ │ ├─ Pod 4 (db) │ │ │ │ └─ Fluent Bit Agent 🟢│ │ │ └────────────────────────┘ │ └──────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
### 2. Kibana 인덱스 패턴 생성
- [Management] -> [Index patterns] -> index pattern:logstash-* -> next step -> @timestamp -> create index pattern


-> 확인하기! 생성된 Fields에 Kubernetes.labels.run이 검색되지 않는 경우 로그 수집이 덜 되었기 때문!!
### 3. 로그 시각화
- [Discover] -> 검색필터 설정 -> kubernetes.labels.run is my-nginx(nginx 배포 때 사용한 Label)

## 5단계 자원 삭제
1. kubernetes 자원 삭제
kubectl delete -f elasticSearch.yaml kubectl delete -f kibana.yaml kubectl delete -f fluentbit.yaml kubectl delete -f nginx-service.yaml kubectl delete -f nginx-deployment.yaml
1
2. EKS cluster와 Node Group 삭제
eksctl delete cluster –region ap-northeast-2 –name=mission-cluster ```

