본문 바로가기
IT/AWS

AWS EKS에 로그 트레이싱 구축(1)-kubernetes에 EFK 스택 설치

by twofootdog 2020. 8. 23.

이번 글에서는 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://springboot.cloud/15

 

ElasticSearch 설치시 확인할 사항

ElasticSearch 설치시 확인할 사항 올초부터 회사에서 요즘 Kafka, Elasticsearch, Ansible, k8s 등등 난생 처음 다뤄보는 것들을 하나하나 엮어서 올리다보니 설치부터 삽질한 경험이 많아서 정리 해 보려고

springboot.cloud

https://arisu1000.tistory.com/27848

 

쿠버네티스 권한관리(Authorization)

권한관리 기본 쿠버네티스 클러스터의 api에 접근하기 위해서는 우선 유효한 사용자 인지 인증(authentication)을 거처야 합니다. 인증이 됐으면 그 사용자가 접근하려고하는 api에 권한이 있는지 확�

arisu1000.tistory.com

https://www.digitalocean.com/community/tutorials/how-to-set-up-an-elasticsearch-fluentd-and-kibana-efk-logging-stack-on-kubernetes

 

How To Set Up an Elasticsearch, Fluentd and Kibana (EFK) Logging Stack on Kubernetes | DigitalOcean

When running multiple services and applications on a...

www.digitalocean.com

 

댓글