Deploying Logstash to Kubernetes Tutorial


Logstash는 ELK Stack(Elastic Stack)에서 L에 해당하는 Application이다. (Elastic Search, Kibana가 나머지 2개) Logstash는 실시간 파이프라인 기능을 가진 오픈소스 데이터 수집 엔진이다.

예전에는 fluentd가 많이 사용되었는데, 요즘은 잘 관리가 되지 않는것 같다. 나도 이 작업을 처음에는 fluentd로 먼저 작업을 진행해보았는데, 원하는대로 쉽게 진행이 잘 되지 않았으며, 거기에 대한 troubleshooting 자료도 찾기 어려웠다. 그래서 logstash로 다시 작업을 진행했는데, 대부분의 자료들이 local에 logstash를 띄우는 예제가 많아서, 그냥 연습 정도만 가능했지 실제로 사용할 정도로 구축하기에는 쉽지않았다. 그래도 logstash는 문서가 잘 되어 있었으며, 관련자료 또한 fluentd보다는 많았으며, 지금도 지속적으로 관리가 되고 있는 프로젝트라서 지금 시작하는 유저라면 logstash를 사용하는게 더 쉬울것이라 생각한다. 신기하게도 logstash, fluentd 둘 다 Ruby로 작성된 프로젝트이다.

이 글은 logstash가 뭔지, kubernetes가 뭔지 등에 대한 개념적인 내용은 다루지 않는다. logstash를 처음 사용하는 사용자를 위한 튜토리얼(보고 따라하기)이다.

다음의 내용들을 소개한다.

  • MAC OS에 logstash 설치 및 사용
  • Ruby LogstashLogger를 사용하여 로그남기기
  • S3에 logstash 로그 upload
  • Logstash Docker Image를 이용하여 logstash docker image 생성 + AWS ECR
  • Kubernetes에 pod로 배포 및 Ruby App으로 제대로 동작하는지 확인하기

1. MAC OS에 logstash 설치 및 사용

이번 단계에 대한 내용은 다른 블로그에도 소개된 글이 엄청 많다. 오히려 그 글들에 더 자세히 소개되어 있다. 아래 내용 진행을 위해서 간단히 진행하겠다.

1-1. HomeBrew를 이용하여 logstash 설치

brew tap elastic/tap
brew cask install homebrew/cask-versions/adoptopenjdk8
brew install elastic/tap/logstash-full
  • 참고: [https://www.elastic.co/guide/en/logstash/7.6/installing-logstash.html#brew] (해당 페이지에 다른 OS에서의 설치방법도 나와있다.)

MAC OS에서 설치를 한다고 기본적으로 deamon형식으로 실행되지는 않는다. (너무 당연한 이야기 인가?) 테스트용으로 설치한 것이기 때문에 deamon으로 실행하는것에 대해서는 다루지 않겠다. 공식문서에 잘 소개되어 있다. commandline을 이용해서 1회적으로 실행하는 것을 다음 단계에서 하겠다.

1-2. logstash 실행 - commandline config

logstash  -e 'input { stdin { } } output { stdout {} }'

- e 옵션을 이용하면 별도의 config 파일 설정을 따르지 않고, commandline 에서 설정이 가능하다. 이걸 이용해서 console 상에서 input/output 이 모두 가능하도록 실행하였다. 실행한 뒤 Test Logstash LOG! 를 입력후 enter키를 눌렀다.

❯ logstash  -e 'input { stdin { } } output { stdout {} }'
Sending Logstash logs to /usr/local/Cellar/logstash-full/7.6.1/libexec/logs which is now configured via log4j2.properties
[2020-03-22T14:32:15,431][WARN ][logstash.config.source.multilocal] Ignoring the 'pipelines.yml' file because modules or command line options are specified
[2020-03-22T14:32:15,568][INFO ][logstash.runner          ] Starting Logstash {"logstash.version"=>"7.6.1"}
[2020-03-22T14:32:16,874][INFO ][org.reflections.Reflections] Reflections took 42 ms to scan 1 urls, producing 20 keys and 40 values
[2020-03-22T14:32:18,032][WARN ][org.logstash.instrument.metrics.gauge.LazyDelegatingGauge][main] A gauge metric of an unknown type (org.jruby.RubyArray) has been create for key: cluster_uuids. This may result in invalid serialization.  It is recommended to log an issue to the responsible developer/development team.
[2020-03-22T14:32:18,052][INFO ][logstash.javapipeline    ][main] Starting pipeline {:pipeline_id=>"main", "pipeline.workers"=>12, "pipeline.batch.size"=>125, "pipeline.batch.delay"=>50, "pipeline.max_inflight"=>1500, "pipeline.sources"=>["config string"], :thread=>"#<Thread:0x54cfbc81 run>"}
[2020-03-22T14:32:18,862][INFO ][logstash.javapipeline    ][main] Pipeline started {"pipeline.id"=>"main"}
The stdin plugin is now waiting for input:
[2020-03-22T14:32:18,929][INFO ][logstash.agent           ] Pipelines running {:count=>1, :running_pipelines=>[:main], :non_running_pipelines=>[]}
[2020-03-22T14:32:19,148][INFO ][logstash.agent           ] Successfully started Logstash API endpoint {:port=>9600}
 Test Logstash LOG!
/usr/local/Cellar/logstash-full/7.6.1/libexec/vendor/bundle/jruby/2.5.0/gems/awesome_print-1.7.0/lib/awesome_print/formatters/base_formatter.rb:31: warning: constant ::Fixnum is deprecated
{
      "@version" => "1",
    "@timestamp" => 2020-03-22T05:32:44.698Z,
          "host" => "SeokJoonYunui-MacBookPro.local",
       "message" => "Test Logstash LOG!"
}

위와 같이 입력한 문구가 아래에 JSON 형식으로 출력되면 정상적으로 설치 및 동작을 한다는 뜻이다. 종료하려면 contorl + C를 누르면 된다.

1-3. config 파일 생성 및 TCP/UDP로 입력을 받도록 설정

test.conf 란 이름으로 파일을 생성하여 아래의 내용을 입력한다.

input { 
  tcp {
    port => 9000
  }
  udp {
    port => 9000
  }
}

output {
  stdout { }
}

이제 이 config 파일을 적용하도록 logstash를 실행한다.

logstash -f test.conf

실행을 하였을 때 아래의 문구들을 확인할 수 있으면 성공적으로 실행된 것이다.

[2020-03-22T14:47:51,893][INFO ][logstash.inputs.tcp      ][main] Starting tcp input listener {:address=>"0.0.0.0:9000", :ssl_enable=>"false"}
[2020-03-22T14:47:51,964][INFO ][logstash.inputs.udp      ][main] Starting UDP listener {:address=>"0.0.0.0:9000"}
[2020-03-22T14:47:51,994][INFO ][logstash.agent           ] Pipelines running {:count=>1, :running_pipelines=>[:main], :non_running_pipelines=>[]}
[2020-03-22T14:47:52,068][INFO ][logstash.inputs.udp      ][main] UDP listener started {:address=>"0.0.0.0:9000", :receive_buffer_bytes=>"786896", :queue_size=>"2000"}

그런데, 이게 정상적으로 동작하는지 확인하는게 쉽지 않다. 필자는 Ruby App에서 Ruby LogstashLogger를 설치하여 확인하였다. 각자 주로 사용하는 언어의 logstash관련 라이브러리를 이용하여 확인하기 바란다. 필자가 확인한 방법을 아래에서 소개하겠다.

1-4. Ruby App에서 LogstashLogger를 사용하여 동작확인

Gemfile에 패키지 추가

gem 'logstash-logger'

설치를 위해 Bundle 실행

bundle install

RUBY_APP/config/initializers/logstash.rb 파일 추가

require 'logstash-logger'

# Defaults to UDP on 0.0.0.0
logger = LogStashLogger.new(port: 9000)

logger.info 'Server Start'

원하는 곳에 위 코드를 참고하여 log를 추가로 남기도록 하자. 필자는 기존에 logage를 이용하여 request log를 생성한 후 loggly에 log를 남기는 곳에 같은 로그를 logstash 로도 전송하도록 설정하였다.

require 'logstash-logger'

logstash_logger = LogStashLogger.new(
  type: :tcp,
  host: '0.0.0.0',
  port: 9000
)
logstash_logger.info(log_data)

그런후 rails server를 실행하여 2가지 요청을 보낸 후 정상적으로 log가 동작하는 것을 확인하였다.

{
  "host" => "localhost",
  "@timestamp" => 2020-03-22T06:03:07.568Z,
  "port" => 57829,
  "message" => "{\"message\":\"Server start\",\"@timestamp\":\"2020-03-22T15:03:02.559+09:00\",\"@version\":\"1\",\"severity\":\"INFO\",\"host\":\"SeokJoonYunui-MacBookPro.local\"}",
  "@version" => "1"
}
{
  "host" => "localhost",
  "@timestamp" => 2020-03-22T06:03:23.299Z,
  "port" => 57837,
  "message" => "{\"query\":\"{\\n    stores { id name openingTime status businessLicenseNumber\\n        floorPlans { id name rank }\\n    }\\n}\\n\",\"status\":200,\"controller\":\"v1/graphql/ceo\",\"duration\":1107.28,\"db\":82.93,\"view\":0.36,\"@timestamp\":\"2020-03-22T15:03:18.294+09:00\",\"@version\":\"1\",\"severity\":\"INFO\",\"host\":\"SeokJoonYunui-MacBookPro.local\"}",
  "@version" => "1"
}
{
  "host" => "localhost",
  "@timestamp" => 2020-03-22T06:03:40.127Z,
  "port" => 57845,
  "message" => "{\"query\":\"mutation updateStore($id: ID!, $store: StoreInput!) {\\n  updateStore(id: $id, store: $store) {\\n    store {\\n      ...store\\n    }\\n    errors {\\n      path\\n      message\\n    }\\n  }\\n}\\n\\nfragment store on Store {\\n  id\\n  name\\n  phone\\n  businessLicenseNumber\\n  ownerName\\n  vanTid\\n  status\\n  openingTime\\n  storeAddress {\\n    id\\n    roadAddress\\n    detailAddress\\n    zonecode\\n  }\\n  storeCategory\\n}\",\"variables\":\"{\\\\id\\\\:1,\\\\store\\\\:{\\\\businessLicenseNumber\\\\:\\\\1078155843\\\\,\\name\\\\:\\\\RCtest\\\\,\\\\openingTime\\\\:\\\\11:00\\\\,\\\\ownerName\\\\:\\\\ㅇㅇㅇ\\\\,\\\\phone\\\\:\\\\0123135642\\\\,\\\\status\\\\:\\\\active\\\\,\\\\storeAddress\\\\:{\\\\detailAddress\\\\:\\\\1층\\\\,\\\\roadAddress\\\\:\\\\서울 영등포구 국제금융로 110\\\\,\\\\zonecode\\\\:\\\\07326\\\\},\\\\storeCategory\\\\:\\\\레스토랑\\\\}}\",\"status\":200,\"controller\":\"v1/graphql/ceo\",\"duration\":87.46,\"db\":34.68,\"view\":0.29,\"@timestamp\":\"2020-03-22T15:03:35.118+09:00\",\"@version\":\"1\",\"severity\":\"INFO\",\"host\":\"SeokJoonYunui-MacBookPro.local\"}",
  "@version" => "1"
}

이로서 기본적인 logstash 동작에 대한 내용을 마치겠다.

2. S3에 Log 남기기

위에서는 logstash가 실행되는 머신에 파일로 log를 남기는 기본적인 사용법에 대해서 알아보았다. 이번에는 log를 AWS S3로 upload하는 방법에 대해서 알아보겠다. AWS에 S3를 생성하는건 여기서 다루지 않겠다. 먼저 logstash-plugin을 이용하여 logstash-output-s3를 설치한다.

logstash-plugin install logstash-output-s3

필자의 경우에는 Ruby Project 폴더에서 실행을 했을때는 오류가 발생했으며, 다른 폴더로 이동 후 실행을 했을때는 정상적으로 설치가 되었다. 아마 logstash-plugin에서 사용하는 bundle과의 버전 차이 때문일듯 하다.

test.conf를 아래 내용으로 수정한다.

input { 
	tcp {
		port => 9000
	}
  udp {
		port => 9000
	}
}

output {
	stdout { }
  s3 {
    access_key_id => "YOUR MONKEY"
    secret_access_key => "YOUR SECRET"
    region => "ap-northeast-2"
    bucket => "YOUR BUCKET"
    time_file => 1
    canned_acl => "public-read"
  }
}

  • Seoul Region이라 가정했다. 앞으로도 계속 그럴것이기에 다른 지역을 사용중이라면 알아서 수정해주면 된다.

위 내용을 참고하여 AWS Credential 정보 및 S3 bucket 이름만 수정하여 저장한다. 자세한 설정은 logstash-output-s3의 문서를 참조하기 바란다. 다시 logstash를 실행한다.

logstash -f test.conf

그런 후 위 1-4. Ruby App에서 LogstashLogger를 사용하여 동작확인에서 실행한 내용을 한번 더 하였다. 설정을 1분마다 파일을 남기는 것으로 했기 때문에 1분 정도 기다린 뒤 S3를 확인하면 파일이 생성된 것을 볼 수 있다. 아무런 설정을 하지 않았으므로 별도 폴더 없이 ls.s3.0972c831-229b-4918-9574-2956a3a5f260.2020-03-22T15.15.part0.txt 와 같은 형식으로 파일이 생성된다.

파일 내용은 다음과 같이 저장된다.

2020-03-22T06:15:12.544Z localhost {"message":"Server start","@timestamp":"2020-03-22T15:15:07.420+09:00","@version":"1","severity":"INFO","host":"SeokJoonYunui-MacBookPro.local"}
2020-03-22T06:15:18.762Z localhost {"query":"{\n    stores { id name openingTime status businessLicenseNumber\n        floorPlans { id name rank }\n    }\n}\n","status":200,"controller":"v1/graphql/ceo","duration":1092.85,"db":99.38,"view":0.36,"@timestamp":"2020-03-22T15:15:13.738+09:00","@version":"1","severity":"INFO","host":"SeokJoonYunui-MacBookPro.local"}
2020-03-22T06:15:20.062Z localhost {"query":"mutation updateStore($id: ID!, $store: StoreInput!) {\n  updateStore(id: $id, store: $store) {\n    store {\n      ...store\n    }\n    errors {\n      path\n      message\n    }\n  }\n}\n\nfragment store on Store {\n  id\n  name\n  phone\n  businessLicenseNumber\n  ownerName\n  vanTid\n  status\n  openingTime\n  storeAddress {\n    id\n    roadAddress\n    detailAddress\n    zonecode\n  }\n  storeCategory\n}","variables":"{\\id\\:1,\\store\\:{\\businessLicenseNumber\\:\\1078155843\\,\name\\:\\RCtest\\,\\openingTime\\:\\11:00\\,\\ownerName\\:\\ㅇㅇㅇ\\,\\phone\\:\\0123135642\\,\\status\\:\\active\\,\\storeAddress\\:{\\detailAddress\\:\\1층\\,\\roadAddress\\:\\서울 영등포구 국제금융로 110\\,\\zonecode\\:\\07326\\},\\storeCategory\\:\\레스토랑\\}}","status":200,"controller":"v1/graphql/ceo","duration":80.07,"db":29.0,"view":0.63,"@timestamp":"2020-03-22T15:15:15.052+09:00","@version":"1","severity":"INFO","host":"SeokJoonYunui-MacBookPro.local"}

이로서 log를 S3로 upload하는 방법까지 확인하였다.

3. Logstash Docker Image를 이용하여 logstash docker image 생성 + AWS ECR에 upload

Logstash를 Kubernetes라던지 AWS ECS 같은 곳에서 사용을 하려면 docker image가 필요하다. dockerhub에 Logstash Docker Image 공식 이미지가 있으므로 그걸 이용하면 된다. 여기서 몇가지 설정만 바꿔서 AWS ECR에 넣어두고 사용하면 편리하다.

먼저 아래 파일들을 모두 생성한다.

  • Dockerfile
FROM logstash:7.6.1

RUN logstash-plugin install logstash-output-s3

COPY test.conf /usr/share/test/
COPY logstash.yml /usr/share/logstash/config/logstash.yml

CMD ["logstash", "-f", "test.conf"]

위 내용을 보면 logstash.yml를 만들어서 기존 설정파일을 overwrite하고 test.conf 파일을 생성하여 이 설정대로 실행을 한다. test.conf는 위 2. S3에 Log 남기기에서 사용한 파일이다. logstash.yml 파일을 생성한다.

  • logstash.yml
    http.host: "0.0.0.0"
    xpack.monitoring.enabled: false
    

기본적으로 xpack license를 이용하도록 되어있어서 해당 옵션을 off한 것이다. 이걸 못찾아서 많은 시간을 해맸었다.

이 이미지를 윈하는 Repository에 저장한다. 필자의 경우에는 AWS ECR에 올렸다. ECR 생성 및 EKS pod로 배포하기 위한 파일을 생성해주는 **Terraform 파일은 다음과 같다.

  • ecr.tf
variable "ecr_lifecycle_count" {
  default = 10
}

resource "aws_ecr_repository" "test_logstash" {
  name = "test_logstash"
}

resource "aws_ecr_lifecycle_policy" "test_logstash_policy" {
  repository = aws_ecr_repository.test_logstash.name

  policy = <<EOF
{
    "rules": [
        {
            "rulePriority": 1,
            "description": "Expire images more than ${var.ecr_lifecycle_count} counts.",
            "selection": {
                "tagStatus": "any",
                "countType": "imageCountMoreThan",
                "countNumber": ${var.ecr_lifecycle_count}
            },
            "action": {
                "type": "expire"
            }
        }
    ]
}
EOF
}

output "ecr_test_logstash_url" { value = aws_ecr_repository.test_logstash.repository_url }

Terraform을 사용해도 되고, AWS Web Console에서 직접해도 된다. 어쨌든 위 내용을 참고해서 test_logstash라는 ECR을 생성한 후 우리가 만든 docker image을 올려주자. AWS CLI가 미리 설치되어 있어야하며, Credential 정보가 setting되어 있어야 한다.

참고로 AWS Credential 정보를 설정하는 방법은 여러가지가 있다. 원하는 방법 아무거나로 해도 된다.

  • ~/.aws/credential 파일에 저장
  • export AWS_ACCESS_KEY_ID="YOUR_MONKEY"; export AWS_SECRET_ACCESS_KEY="YOUR_SECRET" 를 직접 shell에서 실행
  • aws configure 를 실행하여 입력하는 방법

이제 docker image를 build하여 ECR에 push해보자.

$(aws ecr get-login --no-include-email --region ap-northeast-2)
docker build -t "AWS_ACCOUNT_NUMBER.dkr.ecr.ap-northeast-2.amazonaws.com/test_logstash":latest .
docker push "AWS_ACCOUNT_NUMBER.dkr.ecr.ap-northeast-2.amazonaws.com/test_logstash":latest

4. Kubernetes에 pod로 배포 및 Ruby App으로 제대로 동작하는지 확인하기

EKS 배포 및 kubectl를 EKS용으로 설정하는 방법에 대해서는 여기서 다루지 않겠다. 그것까지는 세팅되어 있다고 가정한 후 진행하겠다.

pod로 배포를 하기 위해 deployment 와 service 객체를 생성하도록 하겠다.

  • deployment : pod app 배포를 담당
  • service : pod all을 부에서 호출가능하도록 external dns를 제공

아래 2개의 파일을 생성한다.

  • test_logstash_deployment.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: test-logstash-deployment
    labels:
      app: test-logstash
    spec:
    replicas: 1
    selector:
      matchLabels:
        app: test-logstash
    template:
      metadata:
        labels:
          app: test-logstash
      spec:
        containers:
          - name: test-logstash
            image: AWS_ACCOUNT_NUMBER.dkr.ecr.ap-northeast-2.amazonaws.com/test_logstash:latest
            command: ["logstash"]
            args: ["-f", "test.conf"]
            imagePullPolicy: Always
            livenessProbe:
              exec:
                command:
                  - ls
              initialDelaySeconds: 0
              periodSeconds: 60
              timeoutSeconds: 10
            ports:
              - containerPort: 9000
                name: logstash
            resources:
              limits:
                cpu: 500m
                memory: 1Gi
              requests:
                cpu: 300m
                memory: 300Mi
    
  • test_logstash_service.yaml
kind: Service
apiVersion: v1
metadata:
  name: test-logstash-service
spec:
  selector:
    app: test-logstash
  ports:
    - protocol: TCP
      port: 9000
      targetPort: 9000
      name: logstash
  type: LoadBalancer

이제 kubectl 로 deployment와 service를 배포하자.

kubectl apply -f test_logstash_deployment.yaml
kubectl apply -f test_logstash_service.yaml

제대로 배포가 되었는지 확인을 해보자.

kubectl get pods; kubectl get deployments; kubectl get services

그러면 아래와 같은 정보를 얻을 수 있다.

NAME                                          READY   STATUS    RESTARTS   AGE
test-logstash-deployment-556c7d6f49-xjgvw     1/1     Running   0          20h
NAME                         READY   UP-TO-DATE   AVAILABLE   AGE
test-logstash-deployment     1/1     1            1           40h
NAME                   TYPE         CLUSTER-IP     EXTERNAL-IP                                   PORT(S)          AGE
kubernetes             ClusterIP    10.100.0.1     <none>                                        443/TCP          189d
test-logstash-service  LoadBalancer 10.100.133.177 아무거나URL.ap-northeast-2.elb.amazonaws.com    9000:31308/TCP   40h

이제 좀 전에 사용한 Ruby App에서 host: '0.0.0.0', 라고 입력한 부분에 test-logstash-service의 EXTERNAL-IP 부분을 복사하여 입력한 후 server를 재가동하고 다시 로그를 보내면 s3에 저장되는 것이 확인 가능하다.

참고로 위 yaml 파일 내의 TCP port 설정은 아래 블로그 내용을 참고하였다.

  • [https://towardsdatascience.com/the-basics-of-deploying-logstash-pipelines-to-kubernetes-94a470ad34d9]

마치며…

내가 진행한 이 모든 과정은 몇몇 블로그 검색 및 공식문서를 찬찬히 읽어보고 직접 시행착오를 거치면 굳이 이 내용을 참고하지 않고도 모두 가능하다. 하지만, 필자의 경우에는 처음 이 작업을 할 때 에러메세지를 보고 아무리 검색을 해도 이게 왜 나오는지 알지 못했으며, 그 원인을 알았을때도 해당 config 파일이 어디있는지 찾지 못했으며, 그 위치도 공식문서와는 다른 곳에 있었다. 그래서 이런식의 시행착오를 조금이라도 줄여주고자 이 글을 작업하였다. Terraform 문법과 Kubernetes yaml 파일 설정값등은 버전에 따라서 조금씩 바뀔수 있으니 그 점은 이해해주기를 바란다.

이 글이 도움이 되셨다면 공감 및 광고 클릭을 부탁드립니다 :)