네른

[Kubernetes] EFK 적용 과정 (Elasticsearch, Fluentd, Kibana) - 2 본문

DevOps

[Kubernetes] EFK 적용 과정 (Elasticsearch, Fluentd, Kibana) - 2

네른 2022. 5. 11. 19:14

지난번에 작성한 Fluentd 내용을 토대로, 실제 EFK 스택을 kube에 올릴 때 사용한 yaml을 정리하고자 한다.

 

해당 yaml들은 helm chart를 기반으로 작성되어있어 일부 변수가 포함되어있다.

실제로는 해당 부분을 직접 채워넣어 사용해도 되니까 우선은 helm chart 내용을 토대로 정리한다!

우선, 각 애플리케이션별 구성은 크게 다음과 같다.

 

Fluentd

 - Deployment (HTTP 로그 수집용 / Side-car 형태의 로그 수집용)

 - ConfigMap

 - Service (HTTP 로그 수집용 Pod를 위한 서비스)

 - Ingress

 

Elasticsearch

 - Deployment

 - Service

 - Ingress

 

Kibana

 - Deployment

 - Service

 - Ingress

 


우선, 셋이 공통으로 사용한 Ingress부터 보자.

kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
  annotations:
    kubernetes.io/ingress.class: 'nginx'
    nginx.ingress.kubernetes.io/proxy-read-timeout: '3600'
    nginx.ingress.kubernetes.io/proxy-send-timeout: '3600'
  name: {{ .Chart.Name }}-efk
  namespace: {{ .Values.namespace }}
  labels:
    app: {{ .Chart.Name }}-efk
    {{- if .Values.label.enabled }}
    environment: {{ .Values.label.environment }}
    {{- end }}
spec:
  rules:
    - host: {{ .Values.ingress.fluentd.kibana }}
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: {{ $.Chart.Name }}-kibana
                port:
                  number: 5601
    - host: {{ .Values.ingress.fluentd.elasticsearch }}
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: {{ $.Chart.Name }}-elasticsearch
                port:
                  number: 9200
    - host: {{ .Values.ingress.fluentd.fluentdhttp }}
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: {{ $.Chart.Name }}-fluentdhttp
                port:
                  number: 24224

사실 여기서는 별달리 중요한건 없다.

세 애플리케이션의 host명을 지정해주고 servicePort로 포트를 지정해서 넘겨주었을 뿐.

 

알아두면 좋은 내용으로는, apiVersion 값이 예전처럼 v1beta가 아닌  networking.k8s.io/v1으로 바뀌었다는 것과

ingress.class를 반드시 지정해주어야 한다는 것. (nginx 기반의 ingress controller를 쓰고 있어서 여기서는 nginx)

paths의 pathType은 Prefix/Exact/ImplementationSpecific 세 값이 있다

 - Prefix : URL 경로 중 / 이후 부분만 확인

 - Exact : URL 경로의 대소문자까지 정확하게 입력

 - ImplementationSpecific : Ingress Class에 따라 다름!

 

beta에서 정식 api?가 되면서 바뀐 것들이 있었음. paths.backend.service 이부분이 원래는 serviceName, servicePort 로 되어있었는데 저렇게 바뀌었음.

 

 

아래의 내용은 공식 document에 적힌 내용. 참고하면 좋을 듯.

Prefix / (모든 경로)
Exact /foo /foo
Exact /foo /bar 아니오
Exact /foo /foo/ 아니오
Exact /foo/ /foo 아니오
Prefix /foo /foo, /foo/
Prefix /foo/ /foo, /foo/
Prefix /aaa/bb /aaa/bbb 아니오
Prefix /aaa/bbb /aaa/bbb
Prefix /aaa/bbb/ /aaa/bbb 예, 마지막 슬래시 무시함
Prefix /aaa/bbb /aaa/bbb/ 예, 마지막 슬래시 일치함
Prefix /aaa/bbb /aaa/bbb/ccc 예, 하위 경로 일치함
Prefix /aaa/bbb /aaa/bbbxyz 아니오, 문자열 접두사 일치하지 않음
Prefix /, /aaa /aaa/ccc 예, /aaa 접두사 일치함
Prefix /, /aaa, /aaa/bbb /aaa/bbb 예, /aaa/bbb 접두사 일치함
Prefix /, /aaa, /aaa/bbb /ccc 예, / 접두사 일치함
Prefix /aaa /ccc 아니오, 기본 백엔드 사용함
Mixed /foo (Prefix), /foo (Exact) /foo 예, Exact 선호함

 


다음으로는 Service.

서비스는 사실 셋이 크게 다르지 않다. Kibana와 Fluentd는 필요한 포트만 각각 열어주면 됨.

단지, elasticsearch가 약간 다른게 있다. 포트를 두개 열어두어야 함.

 

apiVersion: v1
kind: Service
metadata:
  namespace: {{ .Values.namespace }}
  name: {{ .Chart.Name }}-elasticsearch
  labels:
    app: {{ .Chart.Name }}
    tier: elasticsearch
    {{- if .Values.label.enabled }}
    environment: {{ .Values.label.environment }}
    {{- end }}
spec:
  selector:
    app: {{ .Chart.Name }}
    tier: elasticsearch
    {{- if .Values.label.enabled }}
    environment: {{ .Values.label.environment }}
    {{- end }}
  ports:
    - name: elasticsearch-rest
      protocol: TCP
      port: 9200
      targetPort: 9200
    - name: elasticsearch-nodecom
      protocol: TCP
      port: 9300
      targetPort: 9300

포트가 두개인 이유는 다음과 같다.

 - 9200 : 일반적인 API 요청을 처리하기 위한 포트

 - 9300 : elasticsearch의 node간 통신을 위한 포트 (노드 조인, 클러스터 관리 등등)

 

이 외에는 별다르게 설정할 것도 없고, kibana와 fluentd도 동일하다. 

이 때의 fluentd service는 side-car가 아닌, http 로그 수집을 위해 열어두는 것.

 


다음으로는 각각의 Deployment를 보자.

 

우선 Kibana부터.

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: {{ .Values.namespace }}
  name: {{ .Chart.Name }}-kiabana
  labels:
    app: {{ .Chart.Name }}
    tier: kibana
    {{- if .Values.label.enabled }}
    environment: {{ .Values.label.environment }}
    {{- end }}
spec:
  selector:
    matchLabels:
      app: {{ .Chart.Name }}
      tier: kibana
      {{- if .Values.label.enabled }}
      environment: {{ .Values.label.environment }}
      {{- end }}
  replicas: 1
  template:
    metadata:
      labels:
        app: {{ .Chart.Name }}
        tier: kibana
        {{- if .Values.label.enabled }}
        environment: {{ .Values.label.environment }}
        {{- end }}
    spec:
      containers:
        - name: kibana
          image: {{ .Values.image.kibana.name }}
          imagePullPolicy: Always
          env:
            - name: SERVER_NAME
              value: kibana
            - name: ELASTICSEARCH_HOSTS
              value: http://{{ .Values.ingress.fluentd.elasticsearch }}
          ports:
            - containerPort: 5601
              protocol: TCP
          resources:
            requests:
              memory: "400Mi"
              cpu: "100m"
            limits:
              memory: "800Mi"
              cpu: "200m"

ENV로 환경변수를 넣어주는 부분이 필수적이다.

SERVER_NAME은 크게 의미는 없으나, logstash와 연계할 때에는 설정해주는 것이 좋다고 한다. 이 부분에 대해서는 추가 확인이 필요한 것으로 보임.

ELASTICSEARCH_HOSTS는 데이터를 가져올 elasticsearch의 주소를 입력하는 부분

 

다음으로는 Elasticsearch

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: {{ .Values.namespace }}
  name: {{ .Chart.Name }}-elasticsearch
  labels:
    app: {{ .Chart.Name }}
    tier: elasticsearch
    {{- if .Values.label.enabled }}
    environment: {{ .Values.label.environment }}
    {{- end }}
spec:
  selector:
    matchLabels:
      app: {{ .Chart.Name }}
      tier: elasticsearch
      {{- if .Values.label.enabled }}
      environment: {{ .Values.label.environment }}
      {{- end }}
  replicas: 1
  template:
    metadata:
      labels:
        app: {{ .Chart.Name }}
        tier: elasticsearch
        {{- if .Values.label.enabled }}
        environment: {{ .Values.label.environment }}
        {{- end }}
    spec:
      containers:
        - name: elasticsearch
          image: {{ .Values.image.elasticsearch.name }}
          imagePullPolicy: Always
          env:
            - name: discovery.type
              value: single-node
          ports:
            - containerPort: 9200
              protocol: TCP
            - containerPort: 9300
              protocol: TCP
          resources:
            requests:
              memory: "400Mi"
              cpu: "100m"
            limits:
              memory: "800Mi"
              cpu: "200m"

여기서도 ENV 부분만 확인하면 되는데, discovery.type의 경우 1대의 노드만 사용할 때는 single-node.

만약 클러스터를 구성한다면 discovery.seed_hosts와 cluster.initial_master_nodes 등의 옵션을 추가로 설정해 주어야 한다.

 

마지막으로 fluentd

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: {{ .Values.namespace }}
  name: {{ .Chart.Name }}-fluentdhttp
  labels:
    app: {{ .Chart.Name }}
    tier: fluentd
    {{- if .Values.label.enabled }}
    environment: {{ .Values.label.environment }}
    {{- end }}
spec:
  selector:
    matchLabels:
      app: {{ .Chart.Name }}
      tier: fluentd
      {{- if .Values.label.enabled }}
      environment: {{ .Values.label.environment }}
      {{- end }}
  replicas: 1
  template:
    metadata:
      labels:
        app: {{ .Chart.Name }}
        tier: fluentd
        {{- if .Values.label.enabled }}
        environment: {{ .Values.label.environment }}
        {{- end }}
    spec:
      imagePullSecrets:
        - name: {{ .Values.image.fluentd.imagePullSecrets }}
      containers:
        - name: fluentd-http
          image: "{{ .Values.image.fluentd.registry }}/{{ .Values.image.fluentd.repository }}:{{ .Values.image.fluentd.tag | default .Chart.AppVersion }}"
          imagePullPolicy: Always
          env:
            - name: FLUENT_UID
              value: root
            - name: FLUENT_CONF
              value: fluent.conf
            - name: FLUENTD_ARGS
              value: '-c /fluentd/etc/fluent.conf'
          lifecycle:
            preStop:
              exec:
                command: ["sh", "-c", "sleep 10"]
          resources:
            requests:
              memory: "400Mi"
              cpu: "250m"
            limits:
              memory: "2Gi"
              cpu: "2"
          volumeMounts:
            - name: {{ .Chart.Name }}-fluentdhttp-config
              mountPath: /fluentd/etc/
            - name: {{ .Chart.Name }}-log
              mountPath: {{ .Values.log.mountPath }}
      volumes:
        - name: {{ .Chart.Name }}-log
          emptyDir: {}
        - name: {{ .Chart.Name }}-fluentdhttp-config
          configMap:
            name: {{ .Chart.Name }}-fluentdhttp-config
            defaultMode: 420

여긴 뭐가 좀 많다.

앞과 마찬가지로 ENV에 필요한 설정값들을 집어넣는다. 이는 docker로 실행할 때 넘겨주는 인자들과 동일한데, 

 - FLUENT_UID : fluentd를 실행하는 유저의 UID. root로 주어 로그가 쌓이고있는 디렉토리에 접근할 수 있도록 함

 - FLUENT_CONF : config 파일 경로

 - FLUENTD_ARGS : 이 외에, fluentd를 실행할 때 들어갈 argument들인데, 여기서는 특정 conf 파일을 적용하라고 시킨 것.

 지금와서 보는건데, fluent_conf와 fluentd_args 둘 중 하나만 주어도 될 것 같은데 왜 두개로 했을까? 잘 모르겠다.

 

여기서, lifecycle 부분은 preStop이 적용되어있고, 내용은 10초간 기다리는 것인데 이유는 단순하다.

해당 POD가 endpoint에서 제거되기 전에 들어와서, 이미 처리중이던 내용들을 마저 안전하게 처리하고 종료하기 위해서.

 - kubernetes는 POD를 삭제할 때, 엔드포인트 컨트롤러가 유효한 엔드포인트 목록에서 해당 파드를 제거하여 서비스에서 제거하면서 동시에, pod 삭제를 진행한다고 함

 - 그런데 이 과정이 병렬로 진행될 수도 있어서 아직 엔드포인트가 살아있을 때 pod가 죽으면 미처 처리되지 못한 요청들이 있을 수 있음

 - 이를 방지하기 위해 pod 삭제를 잠깐 늦추는 것.

 

volumeMount와 volume 부분은 크게 다음과 같다

 - fluentdhttp-config : 필요한 config

 - log : 로그를 잠깐 쌓아둘 공간. 임시 공간이므로 emptyDir로 설정되어있음

 

또한, 이 외에, 사이드카 형식으로 구성된 fluentd들은 각 애플리케이션의 deployment의 containers 부분에 다음과 같이 정의된다.

        - name: fluentd
          image: "{{ .Values.image.fluentd.registry }}/{{ .Values.image.fluentd.repository }}:{{ .Values.image.fluentd.tag | default .Chart.AppVersion }}"
          imagePullPolicy: Always
          env:
            - name: FLUENT_UID
              value: root
            - name: FLUENT_CONF
              value: fluent.conf
            - name: FLUENTD_ARGS
              value: '-c /fluentd/etc/fluent.conf'
          resources:
            limits:
              cpu: "200m"
              memory: "800Mi"
            requests:
              cpu: "100m"
              memory: "400Mi"
          volumeMounts:
            - name: {{ .Chart.Name }}-fluentd-config
              mountPath: /fluentd/etc/
            - name: {{ .Chart.Name }}-log
              mountPath: {{ .Values.log.mountPath }}
      volumes:
        - name: {{ .Chart.Name }}-log
          emptyDir: {}
        - name: {{ .Chart.Name }}-fluentd-config
          configMap:
            name: {{ .Chart.Name }}-fluentd-config
            defaultMode: 420

앞선 내용과 다르지 않다!

 


마지막으로, 가장 중요하다고 생각되는 fluentd의 ConfigMap 부분이다.

 

kind: ConfigMap
apiVersion: v1
metadata:
  name: {{ .Chart.Name }}-fluentdhttp-config
  namespace: {{ .Values.namespace }}
  labels:
    app: {{ .Chart.Name }}
    {{- if .Values.label.enabled }}
    environment: {{ .Values.label.environment }}
    {{- end }}
data:
  fluent.conf: |
    <source>
      @type http
      port 24224
      bind 0.0.0.0
      keepalive_timeout 10s
      cors_allow_origins ["*"]
    </source>

    <match frontend.log>
      @type elasticsearch
      host {{ .Values.log.elasticIP }}
      logstash_format true
      logstash_prefix frontclient
    </match>

 

이 내용은 http 로그를 수집하는 fluentd의 configMap.

conf의 태그 내용은 앞서 설명했으니 넘어가도록 한다.

 

마찬가지로, 기본적인 fluentd의 configMap도 있으나 앞서 설명한 내용들과 크게 다르지 않아 여기서는 작성하지 않는다.

Comments