About

AWS를 이용하여 kinesis -> firehose -> elasticsearch로 보내는 방식으로 log를 많이 저장한다. 필자의 회사에서도 같은 방식으로 진행하였는데, firehose의 한가지 문제점은 dictionary의 모든 value를 String 타입으로 처리해서 elasticsearch에 밀어넣는다는 점이었다. Kibana에서 dashboard를 활용하기 위해서는 timestamp(date type의) 필드가 꼭 있어야 했기에 이 문제를 해결하였고 이번 블로그를 통해서 소개하고자 한다.

Solution

해결법은 사실 매우 간단하다. elasticsearch에서 제공하는 template을 사용하면 되는데, 필자는 cli 환경에서 elasticsearch를 다루는 것이 익숙하지 않아서, python 패키지 elasticsearch-dsl를 이용하여 해결하는 법을 소개하도록 하겠다.

이 블로그에서는 elasticsearch도 AWS에서 제공하는 것을 사용하고 있다는 가정하에 진행할 것임을 미리 밝혀둔다.

from elasticsearch import Elasticsearch, RequestsHttpConnection
from requests_aws4auth import AWS4Auth
from elasticsearch_dsl import Index

ELASTIC_HOST = 'elasticsearch-url'
AWS_REGION = 'ap-northeast-2'

def get_es():
   global ELASTIC_HOST, AWS_REGION

   awsauth = AWS4Auth(
       access_key, # IAM role or user를 통해서 얻을 수 있다.
       secret_key,
       AWS_REGION,
       'es',
       session_token=token,
   )

   return Elasticsearch(
       hosts=ELASTIC_HOST,
       port=9200,
       http_auth=awsauth,
       use_ssl=True,
       verify_certs=True,
       connection_class=RequestsHttpConnection,
   )

get_es는 elasticsearch 객체를 return 받는 method이다.

def set_template(
    es_client,
    index_patterns,
    template_name,
):
    body = {
        'index_patterns': index_patterns,
        'mappings': {
            'properties': {
                'timestamp': {
                    'type': 'date',
                    'format': 'yyyy-MM-dd HH:mm:ss'
                },
            },
        },
    }
    
    try:
        result = es_client.indices.put_template(
            template_name, body=body
        )

    except Exception as err:
        result = None
        print(err)

    return result

먼저 가장 중요한 set_template에 대해서 살펴보자. 가장 중요한 부분은 당연히 body에 해당하는 값이다.

  • index_patterns: list형식으로 index_patterns를 넣어주게 되는데, 여기에 해당하는 패턴의 index들은 이 template을 따르게 된다. (예: [‘app-log-*’])
  • timestamp: format은 elasticsearch에서 사용하는 painless 언어 문법을 따르게된다. 각자의 format에 맞추기위한 자세한 정보는 문서를 통해서 변경하도록 하자.

여기서 중요한 점은 위의 예제에서 설명하듯 app-log-* index가 있다고 가정할 때, 미리 들어가게 될 모든 field(예: user_id, action, etc..)들의 type을 template에 설정해야만 하는 것은 아니다. 물론 잘 사용하기 위해서 해주는 것도 좋겠지만, 아무래도 완벽한 기획이 있을 수 없기에 field는 가변성이 높다.

결론만 말하자면, timestamp 외의 다른 field에 대해서는 String으로 다 들어가게 될 것이란 점이니 미리 사용하게될 field들을 모두 적어 둘 필요는 없다는 점이다. (참고로 테스트를 다양하게 해본 것은 아니지만 int, float 등 값은 firehose를 통해서 elasticsearch에 들어갈 때 그냥 long 타입으로 변형되는듯 하다)

def get_template(
    es_client,
    template_name
):
    templates = es_client.indices.get_template(
        template_name
    )
    
    return templates


def delete_template(
    es_client,
    template_name
):
    result = es_client.indices.delete_template(
        template_name
    )
    
    return result

template 확인 및 삭제하는 법은 위의 코드처럼 단순하다.

이제 전체 코드를 살펴보고 마무리하도록 하자.

es_client = get_es()
template_name = 'test_template'

set_template(
  es_client=es_client,
  index_patterns=['app-log-*'],
  template_name=template_name,
)
"""
output: {'acknowledged': True}
"""

get_template(es_client, template_name)
"""
output: {'test_template': {'order': 0,
  'index_patterns': ['app-log-*'],
  'settings': {},
  'mappings': {'properties': {'timestamp': {'format': 'yyyy-MM-dd HH:mm:ss', 'type': 'date'}}},
  'aliases': {}}
}
"""

delete_template(es_client, template_name)
"""
output: {'acknowledged': True}
"""

Conclusion

어려운 문제는 아니지만, template이라는 기능을 모르면 어렵게 이 문제를 풀게될 수도 있다. 그 글을 찾지는 못하겠으나, 실제로 stack overflow의 한 글에서 답변으로 채택된 답변은 새로운 날이 시작되기 5분 전마다 cloudwatch를 이용하여 index를 자신이 만들어서 mapping을 지정해두는 방식을 택했다고한 것을 본적이 있다.

(firehose를 이용하면 같은 패턴으로 매일 새로운 index가 만들어진다. 예를 들어 app-log-2020-05-25, app-log-2020-05-26 등과 같은 방식으로 말이다. 그래서 firehose에 의해 만들어지기 5분 전에 스스로 mapping을 설정하여 index를 만드는 방식을 택한 것)

필자도 template을 발견하기 전까지는 어떻게 풀어야 할지 고민을 많이 했던지라 부디 누군가에게 도움이 되길 바란다.