ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 쿠버네티스 로깅(kubernetes logging)
    Kubernetes 2018. 8. 29. 09:00
    반응형

    클러스터 환경에서 앱을 운영할때 주의해야할 것 중에 하나가 로그를 처리하는 것입니다. 컨테이너 오케스트레이터를 사용하는 환경에서 로그를 수집해야할 때 주의해야 할 점 중에 하나는 로그를 로컬디스크에 파일로 쌓지 않아야 한다는 것입니다. 
    전통적인 애플리케이션 운영환경에서는 로컬 파일시스템의 지정된 위치에 파일로 로그를 쌓도록 구성합니다. 그리고 로그 양이 너무 많아져서 디스크사용량을 전부 채울때까지 로그가 쌓이는 것을 방지하기 위해서, logrotate를 사용하거나 애플리케이션의 로그관련 라이브러리나 프레임워크에서 옵션을 지정해서 로그가 쌓이기 시작한지 일정 시간이 지나거나 로그양이 일정 크기에 도달하면 오래된 로그들을 자동으로 삭제하도록 처리합니다. 이런 운영방식은 앱들이 항상 지정된 장비에서 실행된다는 걸 가정으로 하고 있습니다. 그래서 문제가 발생했을때 운영중이던 장비에 들어가서 로그를 확인하고 이슈를 확인해서 조치를 합니다. 운영중인 장비가 많아지면 일일히 모든 장비에 들어가서 로그를 확인하는 작업이 어려운 작업이 될 수 있습니다.

    포드 로그 확인하기
    컨테이너 오케스트레이터를 사용하는 시스템쪽으로 오게 되면 이런 작업 방식이 더 어려워 질 수 있습니다. 컨테이너는 클러스터내에서 하나의 노드에서만 떠 있는게 아니라 상황에 따라 여러 노드를 옮겨 다니기 때문입니다. 이런 상황에서 특정 앱의 컨테이너의 로그를 확인하는건 전체 클러스터의 노드중에서 어떤 노드들에 컨테이너가 떠 있는지 확인한 후 그 노드에 들어가서 컨테이너의 로그를 확인해야 하기 때문에 어려운 작업이 됩니다. 쿠버네티스에서는 이런 작업을 좀 더 편하게 하기 위해서 개별 노드에 들어갈 필요없이 kubectl을 가지고 직접 포드의 로그를 확인할 수 있는 기능을 제공해주고 있습니다. 다음 처럼 kubectl logs 명령어를 사용하면 현재 실행중인 포드의 로그를 그 포드가 실행중인 노드에 들어가보지 않고도 로그를 직접 확인할 수 있습니다. -f 옵션을 줘서 실행중인 로그를 계속 테일링(tailing) 하면서 확인할 수 있습니다. 브라우저에서 이 앱에 접속해서 새로고침을 해보면 로그가 갱신되는걸 확인할 수 있습니다. kubectl logs의 상세 옵션은 -h 옵션을 주면 확인할 수 있습니다.

    kubectl logs -f kubernetes-simple-app-57585656fc-rvqdm
    [GIN-debug] [WARNING] Now Gin requires Go 1.6 or later and Go 1.7 will be required soon.

    [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

    [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
    - using env:    export GIN_MODE=release
    - using code:    gin.SetMode(gin.ReleaseMode)

    [GIN-debug] GET    /                         --> main.setupRouter.func1 (3 handlers)
    [GIN-debug] GET    /env                      --> main.setupRouter.func2 (3 handlers)
    [GIN-debug] GET    /volume-config            --> main.setupRouter.func3 (3 handlers)
    [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
    [GIN-debug] Listening and serving HTTP on :8080
    [GIN] 2018/08/25 - 10:26:21 | 200 |      3.7873ms |    192.168.65.3 | GET      /
    [GIN] 2018/08/25 - 10:26:21 | 404 |          11µs |    192.168.65.3 | GET      /favicon.ico

    ElasticSearch로 수집해서 모아보기
    하지만 이것만으로 끝이 아니라 로컬디스크에 로그를 쌓을 경우 로컬디스크의 용량 문제로 삭제한 예전 로그들은 확인할 수 없습니다. 트래픽이 큰 서버들은 쌓이는 로그의 양도 많기 때문에 한시간 단위로 로그로테이션이 이뤄지도 하기 때문에 문제가 생긴걸 인지하고 로그를 확인하려고 서버로 접속한 뒤에는 이미 로그가 지워지고 난 이후일 수도 있습니다. 그래서 이런 문제점들을 개선하기 위해서 몇년 전 부터는 각각의 장비에 기록된 앱들을 한 곳에서 모아서 보도록 구성을 하고 있습니다. 이렇게 로그를 모아보는 구성을 할때 많이 사용되는 오픈소스들로는 kafka, fluentd, logstash, elasticsearch, kibana 등이 있습니다. 퍼블릭클라우드를 사용중인 곳이라면 각 퍼블릭클라우드에서 제공해주는 로그 수집 서비스들이 있기 때문에 그러한 것들을 이용할 수도 있습니다. 하지만 그런 서비스들을 사용하지 못하는 경우에는 앞서 언급한 오픈소스들을 이용해서 직접 로그 수집 시스템을 구성해서 사용할 수 있습니다. 
    여기서는 쿠버네티스 클러스터 내부에 직접 로그수집시스템을 꾸리고 거기로 로그를 수집해서 확인해 보도록 하겠습니다.
    먼저, 로그 수집기에서 수집된 로그를 받아줄 elasticsearch와 로그를 조회할 kibana를 실행해 보겠습니다. elasticsearch는 클러스터 형태로 여러대의 노드에서 실행되도록 개발되어진 시스템입니다. 하지만 여기서는 실습용이기 때문에 노드 1대에서만 실행되는 형태로 구성해 보겠습니다.
    우선 다음 내용을 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

    브라우저에서 localhost:30920으로 접속해 보면 elasticsearch가 정상적으로 실행된걸 확인할 수 있습니다.


    elasticsearch만 있어도 여기에 데이터를 저장하고 검색해서 확인하는걸 reast api를 통해서 할 수 있습니다. 그렇지만 elasticsearch만들 사용하는 경우는 잘 없습니다. 대부분 UI인 kibana를 함께 사용해서 데이터 검색을 편하게 사용하고 있습니다. kibana는 다음 처럼 실행하면 됩니다.

    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.kubenetes.example.com"
            - name: ELASTICSEARCH_URL
            ports:
            - containerPort: 5601
    ---
    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

    kibana는 elasticsearch 에 있는 데이터를 검색해주는 역할을 하기 때문에 elasticsearch의 주소를 알아야하기 때문에 환경변수에 ELASTICSEARCH_URL 를 설정해 줬습니다. 그런데 여기서 값에 들어간건 elasticsearch-svc.default.svc.cluster.local 입니다. 이 주소는 쿠버네티스는 내부에서 제공해주는 dns 서비스를 이용하도록 되어 있습니다. 주소 체계를 보면 끝에서 부터 local cluster에 있는 서비스(svc)중 default 네임스페이스의 elasticsearch-svc를 가리키는 주소라는걸 알 수 있습니다. 그래서 이 주소로 접근하게 되면 elasticsearch-svc서비스가 가지고 있는 ClusterIP를 통해서 elasticsearch 포드에 접근할 수 있게 됩니다. 물론 여기에 elasticsearch-svc의 ClusterIP를 설정해 줄 수도 있지만 그렇게 되면 매번 elasticsearch-svc 를 만들때마다 ClusterIP가 변경되기 때문에 kibana.yaml의 ELASTICSEARCH_URL도 변경해 줘야 하지만 dns형식으로 지정해 두면 elasticsearch-svc의 ClusterIP에 상관없이 kibana.yaml을 동일하게 사용할 수 있습니다. kibana는 처음 실행되고 초기화되는데 시간이 좀 걸립니다. 브라우저를 통해서 localhost:30561로 접근해 보면 아래처럼 kibana 화면을 확인할 수 있습니다.



    이제 데이터를 저장하고 검색할 수 있는 elasticsearch와 kibana 가 준비되었습니다.

    fluentd를 이용해서 로그 수집하기
    이제 fluentd를 이용해서 elasticsearch에 쿠버네티스에서 발생한 로그를 쌓아보도록 하겠습니다. fluentd는 쿠버네티스와 같은 CNCF 재단에 속해 있는 범용 로그 수집용 오픈소스 프로젝트입니다. 루비 기반으로 개발되어 있고 다양한 플러그인들을 사용할 수 있기 때문에 로그 수집에 많이 쓰이고 있습니다. 쿠버네티스가 아닌곳에서도 사용할 수 있지만 여기서는 쿠버네티스에서 발생한 로그들을 수집에서 elasticsearch로 저장하도록 구성해 보겠습니다. 쿠버네티으와 같은 CNCF소속이니 만큼 쿠버네티스와의 연계가 잘 되어 있습니다. https://github.com/fluent/fluentd-kubernetes-daemonset 를 보면 이미 쿠버네티스용으로 만들어진 다양한 dockerfile들이 있고 쿠버네티스 배포용 yaml파일들까지 있습니다. 로그를 수집해서 다양한 외부 저장소로 보낼 수 있도록 되어 있지만 elasticsearch용 배포 파일인 fluentd-daemonset-elasticsearch.yaml 내용을 조금 수정해서 사용하겠습니다.
    apiVersion: extensions/v1beta1
    kind: DaemonSet
    metadata:
      name: fluentd
      namespace: kube-system
      labels:
        k8s-app: fluentd-logging
        version: v1
        kubernetes.io/cluster-service: "true"
    spec:
      template:
        metadata:
          labels:
            k8s-app: fluentd-logging
            version: v1
            kubernetes.io/cluster-service: "true"
        spec:
          tolerations:
          - key: node-role.kubernetes.io/master
            effect: NoSchedule
          containers:
          - name: fluentd
            image: fluent/fluentd-kubernetes-daemonset:elasticsearch
            env:
              - name:  FLUENT_ELASTICSEARCH_HOST
                value: "elasticsearch-svc.default.svc.cluster.local"
              - name:  FLUENT_ELASTICSEARCH_PORT
                value: "9200"
              - name: FLUENT_ELASTICSEARCH_SCHEME
                value: "http"
              - name: FLUENT_UID
                value: "0"
            resources:
              limits:
                memory: 200Mi
              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

    먼저 kind 부분을 확인해보면 다른 것들과는 다르게 daemonset으로 되어 있는걸 볼 수 있습니다. 여러대의 노드로 구성된 클러스터에서 로그 수집기는 모든 노드에서 실행되어서 로그를 수집해야 합니다. 로그수집기인 fluentd가 처음 실행됐을때 클러스터의 노드 갯수가 5대 였다가 나중에 10대로 늘어나게 된다 하더라도 데몬셋으로 실행되어 있다면 클러스터 관리자나 사용자가 신경쓰지 않아도 자동으로 로그 수집기가 새로 추가된 노드에 실행되게 됩니다. 그렇기 때문에 이런 용도에서는 데몬셋을 사용합니다. 그리고 로그 수집기를 사용자가 직접 실행한 앱과는 분리해서 관리하기 위해서 네임스페이스를 kube-system으로 분리한걸 확인할 수 있습니다. 이미지는 별도의 수정을 하지 않고 fluentd에서 제공하는 기본 이미지를 사용했습니다. 수정이 필요하다면 https://github.com/fluent/fluentd-kubernetes-daemonset 에 있는 Dockerfile을 수정한 다음 이미지를 직접 빌드해서 사용하면 됩니다. 그리고 노드에서 실제 로그가 쌓이는 위치인 /var/log와 /var/lib/docker/containers를 볼륨으로 마운트 했습니다. /var/log에는 쿠버네티스에서 사용중인 시스템용 프로세스들의 로그가 쌓입니다. 그리고 /var/lib/docker/containers에는 쿠버네티스에 띄워진 포드들에서 출력하는 로그가 쌓입니다. 아래처럼 fluentd 포드에 들어가서 실제로 거기에 로그가 쌓이고 있는지 확인할 수 있습니다.
    $ kubectl exec -ti fluentd-q692f -n kube-system -- sh
    /home/fluent # ls -alF /var/log
    total 116960
    drwxr-xr-x    4 root     root          4096 Aug 26 02:41 ./
    drwxr-xr-x    1 root     root          4096 Jul  5 14:46 ../
    -rw-r--r--    1 root     root       5124579 Aug 26 03:24 docker-ce.log
    -rw-r--r--    1 root     root            66 Aug 26 03:10 fluentd-cluster-autoscaler.log.pos
    -rw-r--r--    1 root     root          3422 Aug 26 03:24 fluentd-containers.log.pos
    -rw-r--r--    1 root     root            54 Aug 26 03:10 fluentd-docker.log.pos
    -rw-r--r--    1 root     root            52 Aug 26 03:10 fluentd-etcd.log.pos
    -rw-r--r--    1 root     root            62 Aug 26 03:10 fluentd-kube-apiserver.log.pos
    -rw-r--r--    1 root     root            71 Aug 26 03:10 fluentd-kube-controller-manager.log.pos
    -rw-r--r--    1 root     root            58 Aug 26 03:10 fluentd-kube-proxy.log.pos
    -rw-r--r--    1 root     root            62 Aug 26 03:10 fluentd-kube-scheduler.log.pos
    -rw-r--r--    1 root     root            55 Aug 26 03:24 fluentd-kubelet.log.pos
    -rw-r--r--    1 root     root            79 Aug 26 03:10 kube-apiserver-audit.log.pos
    -rw-r--r--    1 root     root       2932397 Aug 26 03:24 kubelet.log
    -rw-r--r--    1 root     root      10485773 Aug 21 12:33 kubelet.log.0
    /home/fluent # ls -alF /var/lib/docker/containers/
    total 120
    drwx------   30 root     root          4096 Aug 26 03:10 ./
    drwxr-xr-x    3 root     root          4096 Aug 26 03:10 ../
    drwx------    4 root     root          4096 Aug  7 12:27 034ca7d66cf3e75f72d39cbe998e5a8eae9157024dc88d6a4e801c75c9f7bd4b/
    drwx------    4 root     root          4096 Aug  7 12:27 0450848cab9db1f41106628b23bd9466446f178d6d2364c70c8cce96fe95cede/


    마지막으로 컨테이너의 env를 확인해 보면 FLUENT_UID 환경변수를 0으로 설정했습니다. fluentd를 실행하는 유저ID를 0으로 설정해서 로그가 쌓이고 있는 디렉토리에 접근 가능한 권한을 설정했습니다. FLUENT_ELASTICSEARCH_HOST 에는  elasticsearch-svc.default.svc.cluster.local를 줘서 앞서 설정했던 elasticsearch 에 로그를 보내도록 설정했습니다. fluentd를 실행한 후 조금 있으면 elasticsearch쪽으로 로그가 쌓이기 시작합니다. 이렇게 쌓인 로그를 kibana에서 조회해 보도록 하겠습니다. 
    kibana에서 로그를 조회하려면 먼저 인덱스패턴(index pattern)을 만들어 줘야 합니다. 
    kibana에서 왼쪽의 Management 메뉴를 선택한 다음에 다시 Kibana의 “Index Patterns”를 선택해 줍니다. 


    그러면 인덱스 패턴을 생성할 수 있는 화면이 나옵니다. 여기 보면 이미 logstash-2018.08.26 으로 인덱스가 만들어져 있는걸 확인할 수 있습니다. 이제 이 인덱스의 데이터를 조회하기 위해서 index pattern쪽에 logstash-*를 입력해서 logstash 접두사를 가진 모든 인덱스를 한꺼번에 조회할 수 있는 인덱스 패턴을 만듭니다. 이렇게 날짜별로 인덱스를 만드는게 elasticsearch와 kibana를 함께할때 많이 사용되는 패턴입니다. elasticsearch에는 날짜단위로 데이터를 저장하면 나중에 데이터가 많아 졌을때 오래된 날짜에 해당하는 인덱스만 삭제해서 디스크 용량을 확보할 수 있습니다. kibana에서는 logshash-* 형태로 여러개의 인덱스를 한꺼번에 조회할 수 있는 장점이 있습니다. “Next step”을 통해서 다음 화면으로 넘어갑니다.


    검색 가능한 시간대를 옵션으로 사용할 수 있게 하기 위해서 @timestamp를 선택해주고 “Create index pattern”버튼을 눌러서 인덱스 패턴을 생성합니다. 그러면 logstash-*라는 이름으로 인덱스 패턴이 만들어진걸 확인할 수 있습니다.


    이제 이 인덱스 패턴에서 데이터를 검색해 보기 위해서 왼쪽의 Discover를 선택해 보겠습니다. 그러면 이미 로그가 수집되어서 들어오고 있기 때문에 쌓여 있는 로그 데이터가 보이는걸 확인할 수 있습니다. 로그 내용을 확인해 보면 쿠버네티스의 클러스터 정보들이 kubernetes.* 항목으로 기록되어져 있는걸 확인할 수 있습니다. 상단의 검색창에서 조건을 넣어서 필요한 로그들만 필터링해서 보는 것도 가능합니다.



    참조 


    반응형

    댓글

Designed by Tistory.