[Kubernetes] EFK 적용 과정 (Elasticsearch, Fluentd, Kibana) - 2
지난번에 작성한 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도 있으나 앞서 설명한 내용들과 크게 다르지 않아 여기서는 작성하지 않는다.