이번 글에서는 AWS EKS로 구축된 쿠버네티스 클러스터에 EFK 스택을 구성하고, Jaeger를 설치하여 분산 환경에서 로그 트레이싱 진행할 수 있도록 구축해 보겠다.
첫번째 장에서는 EFK 스택을 구성할 것이고, 그 다음 장에서는 Jaeger를 설치할 것이다.
EFK스택은 EC2 인스턴스에 별도로 구성하는것이 아닌, 쿠버네티스 클러스터 내에 구성할 것이다.
이 글의 실습을 진행하기 전에 다음과 같은 실습 환경이 준비되어 있어야 한다.
1. AWS EKS 구축(AWS EKS를 활용한 쿠버네티스 클러스터 구축 참고)
2. AWS EKS에 스프링부트 서비스 배포(AWS EKS에서 CodePipeline을 활용하여 스프링부트 서비스 배포하기 참고)
이 글의 순서는 다음과 같이 진행된다.
1. 분산환경 로그 트레이싱 구조
2. Elasticsearch 설치하기
3. Fluentd 설치하기
4. Kibana 설치하기
1. 분산환경 로그 트레이싱 구조
이번 글에서 구축하는 로그 트레이싱의 구조를 보면 다음과 같다.
우선 AWS EKS를 구축한 후, 프라이빗 서브넷에는 EC2 인스턴스로 구성된 워커 노드 그룹이, 퍼블릭 서브넷에는 로드밸런서들이 생성된다. 그리고 각각의 EC2 인스턴스에 Fluentd를 데몬셋 형태로 띄워서 로그 정보를 수집해서 Elasticsearch로 전송하게 되면 사용자는 Kibana UI를 통해서 수집된 로그 정보를 확인한다.
Jaeger는 Jaeger Collector와 Jaeger Query를 Pod로 띄운 후, 스프링부트 서비스에 Jaeger Agent를 Sidecar 패턴으로 적용시킨다. 그 후 Jaeger Agent가 스프링부트 서비스의 로그 트레이싱 정보를 Jaeger Collector로 보내면 Jaeger Collector는 트레이싱 정보를 저장소(Elasticsearch)로 보낸다. 그리고 사용자가 Jaeger Web UI로 트레이싱 정보를 확인할 때 Jaeger Query가 호출되어 저장소(Elasticsearch)에 저장된 트레이싱 정보를 Jaeger Web UI에 보여준다.
스프링부트 서비스의 로깅 정책은 log4j2를 활용했고, log4j2.yml파일은 다음과 같이 작성했다.
log4j2.yml :
Configutation:
name: Default
status: warn
Properties:
Property:
name: log-path
value: "logs"
Appenders:
Console:
name: Console_Appender
target: SYSTEM_OUT
PatternLayout:
pattern: "[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n"
File:
name: File_Appender
fileName: ${log-path}/cafe_logfile.log
PatternLayout:
pattern: "[%-5level] %X{springAppName},%X{traceId},%X{spanId} %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n"
RollingFile:
- name: RollingFile_Appender
fileName: ${log-path}/cafe_rollingfile.log
filePattern: "logs/archive/rollingfile.log.%d{yyyy-MM-dd-hh-mm}.gz"
PatternLayout:
pattern: "[%-5level] %X{springAppName},%X{traceId},%X{spanId} %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n"
Policies:
SizeBasedTriggeringPolicy:
size: 1 KB
DefaultRollOverStrategy:
max: 30
Loggers:
Root:
level: info
AppenderRef:
- ref: Console_Appender
- ref: File_Appender
- ref: RollingFile_Appender
Logger:
- name: edu.project
additivity: false
level: debug
AppenderRef:
- ref: Console_Appender
- ref: File_Appender
- ref: RollingFile_Appender
2. Elasticsearch 설치하기
이제 Fluentd & Elasticsearch & Kibana (일명 EFK)를 구축하여 로그 정보를 수집해 보도록 하자.
Fluentd는 deamonset, Elasticsearch와 Kibana는 각각 Deployment 형태로 쿠버네티스 클러스터에 올릴 것이다.
우선 Elasticsearch 매니페스트(yaml파일) 파일을 작성해보자.
매니페스트 파일에서 생성하는 리소스는 namespace, Statefulset, Service, PVC(PVC는 volumeClaimTemplates 에 의해서 생성된다)이다.
Statefulset은 주로 상태값을 관리하는 경우(예를 들면 PVC를 사용하는 경우)에 Deployment 대신 사용되는데, Elasticsearch는 PVC를 통해서 스토리지를 사용하기 때문에 Statefulset으로 구성했다.
Elasticsearch.yaml :
apiVersion: v1
kind: Namespace
metadata:
name: kube-logging
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: es-cluster
namespace: kube-logging
spec:
serviceName: elasticsearch
replicas: 3
selector:
matchLabels:
app: elasticsearch
template:
metadata:
labels:
app: elasticsearch
spec:
containers:
- name: elasticsearch
image: docker.elastic.co/elasticsearch/elasticsearch:7.2.0
resources:
limits:
cpu: 1000m
requests:
cpu: 100m
ports:
- containerPort: 9200
name: rest
protocol: TCP
- containerPort: 9300
name: inter-node
protocol: TCP
volumeMounts:
- name: data
mountPath: /usr/share/elasticsearch/data
env:
- name: cluster.name
value: k8s-logs
- name: node.name
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: discovery.seed_hosts
value: "es-cluster-0.elasticsearch,es-cluster-1.elasticsearch,es-cluster-2.elasticsearch"
- name: cluster.initial_master_nodes
value: "es-cluster-0,es-cluster-1,es-cluster-2"
- name: ES_JAVA_OPTS
value: "-Xms512m -Xmx512m"
initContainers:
- name: fix-permissions
image: busybox
command: ["sh", "-c", "chown -R 1000:1000 /usr/share/elasticsearch/data"]
securityContext:
privileged: true
volumeMounts:
- name: data
mountPath: /usr/share/elasticsearch/data
- name: increase-vm-max-map
image: busybox
command: ["sysctl", "-w", "vm.max_map_count=262144"]
securityContext:
privileged: true
- name: increase-fd-ulimit
image: busybox
command: ["sh", "-c", "ulimit -n 65536"]
securityContext:
privileged: true
volumeClaimTemplates:
- metadata:
name: data
labels:
app: elasticsearch
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: gp2 #do-block-storage #gp2 #do-block-storage
resources:
requests:
storage: 5Gi
---
kind: Service
apiVersion: v1
metadata:
name: elasticsearch
namespace: kube-logging
labels:
app: elasticsearch
spec:
selector:
app: elasticsearch
ports:
- port: 9200
name: rest
- port: 9300
name: inter-node
설정값에 대해 간단히 설명하자면 Statefulset 의 initContainers 부분에는 Elasticsearch 컨테이너를 기동할 때 여러 설정값들을 초기화해준다. /usr/share/elasticsearch/data 디렉토리에 접근할 수 있게 chown 명령어로 소유자 및 그룹을 변경해주고, 운영환경에서 Elasticsearch를 문제없이 운영하기 위해 가상 메모리 설정값인 vm.max_map_count의 값을 셋팅해주고, Elasticsearch는 동시에 매우 많은 파일을 엑세스 하기 때문에 ulimit 명령어를 통해 File Descriptor 설정을 변경해준다. Elasticsearch는 클러스터로 구성해서 총 3개의 node가 활성화 된다.
elasticsearch 매니페스트 작성이 완료되었으면 kubectl 명령어를 통해서 리소스를 생성한 후, 각 리소스들이 정상적으로 생성되었는지 확인해보자.
$ kubectl apply -f elasticsearch.yaml
namespace/kube-logging created
statefulset.apps/es-cluster created
service/elasticsearch created
$ kubectl get statefulset -n kube-logging
kubecNAME READY AGE
es-cluster 3/3 2m27s
$ kubectl get pod -n kube-logging
NAME READY STATUS RESTARTS AGE
es-cluster-0 1/1 Running 0 2m41s
es-cluster-1 1/1 Running 0 2m16s
es-cluster-2 1/1 Running 0 108s
$ kubectl get pvc -n kube-logging
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
data-es-cluster-0 Bound pvc-a6fa4838-aa5b-4f7d-a717-7b7a5c3cc19f 5Gi RWO gp2 2m48s
data-es-cluster-1 Bound pvc-f0dbd10c-4081-4689-b0b1-56e018ac204f 5Gi RWO gp2 2m23s
data-es-cluster-2 Bound pvc-19214aaa-5942-464a-b0e7-68979119ded4 5Gi RWO gp2 115s
$ kubectl get svc -n kube-logging
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
elasticsearch ClusterIP 10.100.171.21 <none> 9200/TCP,9300/TCP 2m57s
3. Fluentd 설치하기
다음으로 Fluentd 매니페스트 파일을 작성해보자.
Fluentd는 각 워커 노드에 Daemonset 형태로 올라가서 스프링부트 서비스에서 생성되는 로그 정보를 수집한 후 Elasticsearch로 전송하는 역할을 담당한다.
Fluented 매니페스트 파일을 통해 생성되는 리소스는 ServiceAccount, ClusterRole, ClusterRoleBinding, Daemonset이다.
우선 ClusterRole은 쿠버네티스 클러스터에서 할 수 있는 역할이 명시된 리소스이다. ClusterRole을 만든 후, 특정 ServiceAccount에 ClusterRoleBinding 리소스를 통해서 ClusterRole을 부여하게 되면, 특정 ServiceAccount로 수행하는 리소스(예를 들면 Daemonset, Deployment 등)이 해당 역할을 수행할 수 있게 된다. ClusterRole과 Role의 다른점은 Role은 그 Role이 속한 네임스페이스에서만 적용되는 반면, ClusterRole은 쿠버네티스 클러스터 전체에 적용된다.
Fluentd.yaml :
apiVersion: v1
kind: ServiceAccount
metadata:
name: fluentd
namespace: kube-logging
labels:
app: fluentd
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: fluentd
labels:
app: fluentd
rules:
- apiGroups:
- ""
resources:
- pods
- namespaces
verbs:
- get
- list
- watch
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: fluentd
roleRef:
kind: ClusterRole
name: fluentd
apiGroup: rbac.authorization.k8s.io
subjects:
- kind: ServiceAccount
name: fluentd
namespace: kube-logging
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentd
namespace: kube-logging
labels:
app: fluentd
spec:
selector:
matchLabels:
app: fluentd
template:
metadata:
labels:
app: fluentd
spec:
serviceAccountName: fluentd
tolerations:
- key: node-role.kubernetes.io/master
effect: NoSchedule
containers:
- name: fluentd
image: fluent/fluentd-kubernetes-daemonset:v1.4.2-debian-elasticsearch-1.1
env:
- name: FLUENT_ELASTICSEARCH_HOST
value: "elasticsearch.kube-logging.svc.cluster.local"
- name: FLUENT_ELASTICSEARCH_PORT
value: "9200"
- name: FLUENT_ELASTICSEARCH_SCHEME
value: "http"
- name: FLUENTD_SYSTEMD_CONF
value: disable
resources:
limits:
memory: 512Mi
requests:
cpu: 100m
memory: 200Mi
volumeMounts:
- name: varlog
mountPath: /var/log
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
terminationGracePeriodSeconds: 30
volumes:
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
Fluentd 매니페스트 파일에 대해 간단히 설명하자면, Fluentd를 수행하는 ServiceAccount를 만들고, ClusterRole에 pod와 namespace에 읽기 엑세스 권한(get, list, watch)을 주고, 해당 ClusterRole을 Fluentd에 부여한다. 이렇게 함으로써 Fluentd는 클러스터 내부에 있는(네임스페이스를 넘나들며) 로그 수집을 할 수 있게 된다.
다음으로 매니페스트 파일에 Fluentd의 kind를 Daemonset을 작성했다. Daemonset과 Deployment의 차이점은, 리로스를 Daemonset으로 올릴 경우 활성화 된 워커 노드에 1개씩 Pod가 생성되게 된다(Deployment는 랜덤 노드에 replicas 수 만큼 생성된다). 그리고 spec내부에 있는 container 설정에는 환경변수를 명시해 놓았는데, FLUENT_ELASTICSEARCH_HOST 에는 elasticsearch의 호스트주소를 작성해 놓았다. 쿠버네티스 클러스터 내부에 있는 pod의 호스트 주소는 [서비스명].[네임스페이스],svc.cluster.local 이며, 이 글에서는 Elasticsearch의 서비스명은 elasticsearch, 네임스페이스는 kube-logging이기 때문에 elasticsearch.kube-logging.svc.cluster.local로 작성했다. FLUENT_ELASTICSEARCH_PORT는 Elasticsearch의 포트주소를 작성했다.
fluentd 매니페스트 파일을 작성이 완료되었으면 kubectl 명령어를 통해서 리소스를 생성한 후, 각 리소스들이 정상적으로 생성되었는지 확인해보자. fluentd pod는 daemonset이기 때문에 2개의 워커 노드에 각각 한개씩 생성되었다.
$ kubectl apply -f fluentd.yaml
serviceaccount/fluentd created
clusterrole.rbac.authorization.k8s.io/fluentd created
clusterrolebinding.rbac.authorization.k8s.io/fluentd created
daemonset.apps/fluentd created
$ kubectl get ds -n kube-logging
kubectlNAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
fluentd 2 2 2 2 2 <none> 143m
$ kubectl get pod -n kube-logging -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
es-cluster-0 1/1 Running 0 169m 192.168.231.13 ip-192-168-245-14.ap-northeast-2.compute.internal <none> <none>
es-cluster-1 1/1 Running 0 169m 192.168.130.110 ip-192-168-137-251.ap-northeast-2.compute.internal <none> <none>
es-cluster-2 1/1 Running 0 169m 192.168.247.49 ip-192-168-245-14.ap-northeast-2.compute.internal <none> <none>
fluentd-82td4 1/1 Running 0 145m 192.168.197.39 ip-192-168-245-14.ap-northeast-2.compute.internal <none> <none>
fluentd-bwcg6 1/1 Running 0 145m 192.168.183.251 ip-192-168-137-251.ap-northeast-2.compute.internal <none> <none>
kibana-866c457776-d5mdh 1/1 Running 0 3m17s 192.168.149.156 ip-192-168-137-251.ap-northeast-2.compute.internal <none> <none>
4. Kibana 설치하기
다음으로 Kibana 매니페스트 파일을 작성해보자.
Kibana를 올리게 되면 Elasticsearch에 적재된 로그 정보를 Kibana를 통해서 확인할 수 있다.
kibana.yaml :
apiVersion: v1
kind: Service
metadata:
name: kibana
namespace: kube-logging
labels:
app: kibana
spec:
ports:
- port: 5601
selector:
app: kibana
type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: kibana
namespace: kube-logging
labels:
app: kibana
spec:
replicas: 1
selector:
matchLabels:
app: kibana
template:
metadata:
labels:
app: kibana
spec:
containers:
- name: kibana
image: docker.elastic.co/kibana/kibana:7.2.0
resources:
limits:
cpu: 1000m
requests:
cpu: 100m
env:
- name: ELASTICSEARCH_URL
value: http://elasticsearch:9200
ports:
- containerPort: 5601
kibana매니페스트 파일을 작성이 완료되었으면 kubectl 명령어를 통해서 리소스를 생성한 후, 각 리소스들이 정상적으로 생성되었는지 확인해보자.
$ kubectl apply -f kibana.yaml
kubectlservice/kibana unchanged
deployment.apps/kibana configured
$ kubectl get pod -n kube-logging
NAME READY STATUS RESTARTS AGE
es-cluster-0 1/1 Running 0 171m
es-cluster-1 1/1 Running 0 171m
es-cluster-2 1/1 Running 0 170m
fluentd-82td4 1/1 Running 0 147m
fluentd-bwcg6 1/1 Running 0 147m
kibana-866c457776-d5mdh 1/1 Running 0 5m7s
$ kubectl get svc -n kube-logging
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
elasticsearch ClusterIP 10.100.171.21 <none> 9200/TCP,9300/TCP 171m
kibana LoadBalancer 10.100.146.209 ace88e4cd03b742faac85a4debc78bde-1447862165.ap-northeast-2.elb.amazonaws.com 5601:31534/TCP 5m21s
kibana svc는 로드밸런서 타입으로 생성되었고, EXTERNAL-IP와 포트번호도 정상적으로 생성되었다.
이제 EXTERNAL-IP와 포트번호(이 글에서는 5601)를 활용하여 kibana 웹페이지에 접속해보자(로드밸런서가 생성되어야 하기 때문에 접속이 가능하기까지 약간의 시간(약 30초?)이 소요된다).
kibana 웹페이지에 접속해서 [management] -> [Index Patterns] -> [Create index pattern]을 선택한다.
[Index Pattern]에 logstash* 라고 입력하고 [Next Step]을 누른다.
[Time Filter field name] 은 @timestamp를 선택하고 [Create index pattern]을 누른다.
그리고 [Discover]로 이동하면 Fluentd에서 Elasticsearch로 보낸 로그 정보를 Kibana를 통해서 확인할 수 있다.
마치며
이번 장에서는 AWS EKS로 구축된 쿠버네티스 클러스터에 EFK 스택을 구축해보았다.
다음 장에서는 동일한 클러스터에 jaeger를 설치해서 로그 트레이싱 환경을 구축해보겠다
(https://twofootdog.tistory.com/74)
참고
https://arisu1000.tistory.com/27848
'IT > AWS' 카테고리의 다른 글
AWS SSM으로 Private Subnet EC2 접근하기 (1) | 2020.09.28 |
---|---|
AWS EKS에 로그 트레이싱 구축(2)-kubernetes에 Jaeger 설치 (0) | 2020.08.23 |
AWS EKS에서 CodePipeline을 활용한 스프링부트 서비스 배포 (0) | 2020.08.22 |
AWS EKS를 활용한 쿠버네티스 클러스터 구축 (0) | 2020.08.06 |
AWS RDS MariaDB 한글 깨짐 현상 해결(character_set, collation) (1) | 2020.04.12 |
댓글