반응형

준비 작업:  Elastic 8.0 및 Kibana 설치하기

아래 김종민 님의 설명을 읽고, ElasticsearchKibana 설치하는 것이 제일 쉽고 간단하다.

 

 

Elastic 8.0 설치하기 - Jongmin's Lifelog

정말 오랬만에 블로그 포스팅을 하네요. 얼마 전에 드디어 Elastic 8.0 이 출시되었습니다. 6.x 릴리스 까지는 보통 1년 ~ 18개월 정도의 텀을 두고 비교적 빠르게 버전 업데이트를 했는데 7.x 릴리스

kimjmin.net

 

##
## Elasticsearch 기동하기
##

$  bin/elasticsearch


##
## Kibana 기동하기
##   주의: 명령 옵션으로 --host를 지정하지 않으면, 기본값이 127.0.0.1로 설정된다.
##        만약 Web Browser가 kibana 서버와 다른 곳에 있다면 반드시 아래와 같이 
##        외부에서 접근 가능한 서버 주소를 지정해주어야 한다.

$ bin/kibana  --host=192.168.0.11

 

 


 

Elasticsearch 서버와 Kibana 서버를 설치했으면, 아래의 문서를 보면서 Python Example App을 작성한다.

 

Elasticsearch Python Client Example

2022년 11월 현재, 아래의 Web Docs가 가장 쉽게 설명된 것 같다.

Python PKG 설치, 인증/연결, 설정, Client Example Code 등 필요한 내용을 다 포함하고 있다.

 

 

Elasticsearch Python Client [8.5] | Elastic

 

www.elastic.co

 

 

아래  Docs는 초반에 "Elastic Cloud"를 먼저 예시로 설명하고 있는데,
만약 Private 환경(즉, self-managed cluster)에서 Client Example을 테스트할 것이라면
이 Docs의 아래 부분만 읽으면 된다.

[ 즉, 바로 이 부분부터 읽고 따라하면 된다 ]
https://www.elastic.co/guide/en/elasticsearch/client/python-api/master/connecting.html#connect-self-managed-new

 

 

Connecting | Elasticsearch Python Client [master] | Elastic

The async client shouldn’t be used within Function-as-a-Service as a new event loop must be started for each invocation. Instead the synchronous Elasticsearch client is recommended.

www.elastic.co

 

CRUD(Create, Read, Update, Delete) 예제를 보고 싶다면, 아래  Web Docs를 열람.

 

https://www.elastic.co/guide/en/elasticsearch/client/python-api/current/examples.html

 

Examples | Elasticsearch Python Client [8.5] | Elastic

Below you can find examples of how to use the most frequently called APIs with the Python client. Indexing a documentedit To index a document, you need to specify three pieces of information: index, id, and a body: from datetime import datetime from elasti

www.elastic.co

 

 

실제로 위 Web Docs를 보고 조금 변경해서 작성해본 예시이다.

그냥 Copy & Paste해서  `python3  myexample.py` 명령을 수행하면 된다.

 

Document 1개를 Elasticsearch에 저장하는 예제

from datetime import datetime
from elasticsearch import Elasticsearch

##
## NOTE : Configuration for multi node
##
NODES = [ "https://10.1.3.166:9200" ]

##
## Password for the 'elastic' user generated by Elasticsearch
##
ELASTIC_PASSWORD = "mypasswd"

##
## Create the client instance
##
es = Elasticsearch(
    NODES,
    ca_certs="/MyWorkSpace/elastic-stack-metal-install/elasticsearch-8.5.1/config/certs/http_ca.crt",
    basic_auth=("elastic", ELASTIC_PASSWORD)
)

## Create documents
doc = {
    'my-key-example-a': 'my-value-1',
    'my-key-example-b': 'my-value-2',
    'date': datetime.now(),
    'msg': "my log message example... hello~  world ^^",
}

resp = es.index(index="example-index-0", id=0, document=doc)
print(resp['result'])

 

Bulk로 많은 Document를 Elasticsearch에 저장하는 예제

from datetime import datetime
from elasticsearch import Elasticsearch
from randmac import RandMac


NODES = [ "https://10.1.3.166:9200" ]

##
## Password for the 'elastic' user generated by Elasticsearch
##
ELASTIC_PASSWORD = "mypasswd"

##
## Create the client instance
##
es = Elasticsearch(
    NODES,
    ca_certs="/MyWorkSpace/elastic-stack-metal-install/elasticsearch-8.5.1/config/certs/http_ca.crt",
    basic_auth=("elastic", ELASTIC_PASSWORD)
)

doc_id = 0
loop_cnt = 0

##
## Create documents
##
##   참고: 아래 for 문은 document 예시를 그럴듯하게 만들기 위함이다.
##        실제 Elasticsearch와는 아무런 관련이 없다. ^^
ip_networks = ["10", "172", "192"]

for ii in ip_networks:
    for xx in range(254):
        for yy in range(254):
            for zz in range(254):
                macaddress = str(RandMac())
                doc = {
                    'app': 'nac-server',
                    'level': 'info',
                    'date': datetime.now(),
                    'ip-address': ii + '.' + str(xx) + '.' +  str(yy) + '.' + str(zz),
                    'mac-address': macaddress,
                    'msg': 'Device ' + macaddress + ' is started',
                }

                doc_id += 1
                loop_cnt += 1
                resp = es.index(index="example-index-0", id=doc_id, document=doc)
            print("Count: " + str(loop_cnt) + "   " + str(resp['_index']) + "   " + str(resp['_id']) + "   " + str(resp['result']) + "   shard:" + str(resp['_shards']) + "   " + str(resp['_seq_no']))

print("\nTotal Document: " + str(doc_id))

 

Document를 조회하기

from datetime import datetime
from elasticsearch import Elasticsearch

NODES = [ "https://10.1.3.166:9200" ]

# Password for the 'elastic' user generated by Elasticsearch
ELASTIC_PASSWORD = "mypasswd"

# Create the client instance
es = Elasticsearch(
    NODES,
    ca_certs="/MyWorkSpace/elastic-stack-metal-install/elasticsearch-8.5.1/config/certs/http_ca.crt",
    basic_auth=("elastic", ELASTIC_PASSWORD)
) 

resp = es.get(index="test-index", id=5)
print(resp)

 

 

Kibana Web GUI로 결과 확인하기

Kibana Web GUI의 'dev tool'을 이용하여 아래와 같이 index, document를 조회할 수 있다.

Kibana Web UI

 

위 Web GUI에서 사용했던 Elasticsearch Query 참고.
##
## Index 정보를 가져온다.
##

GET _cat/indices?v

## 응답은 이런 모양이다.
health status index           uuid                   pri rep docs.count docs.deleted store.size pri.store.size
yellow open   example-index-0 Up1jqY3PTG2pLHdOLJcLGQ   1   1   18126912          739      2.9gb          2.9gb
yellow open   test-index      nGaLdNNORHKfJF1maBlNvw   1   1          2            0     11.2kb         11.2kb




##
## "example-index-0" 인덱스에 있는 모든 document 중에서 3개만 가져온다.
##

GET example-index-0/_search?size=3
{  
  "query": {
    "match_all": { }
  }
}
 

##
## "app" 항목 중에서 "server"라는 어휘가 포함돠ㅣㄴ 문서를 모두 가져온다.
##

GET example-index-0/_search
{  
  "query": {
    "match": { "app": "server" }
  }
}


##
## "mac-address" 항목 중에서 정확하게 "0e:c3:0d:97:ba:f0" 와 일치하는 document만 가져온다.
##

GET example-*/_search
{  
  "query": {
    "match_phrase": { "mac-address": "0e:c3:0d:97:ba:f0" }
  }
}


##
## Elasticsearch Cluster 상태 정보를 가져온다. 
##

GET /_cluster/state

GET /_cluster/state?filter_path=metadata.indices.*.stat*

GET /example-index-0/_stats


##
## Elasticsearch Cluster에 저장된 전체 Document 개수를 가져온다.
##

GET /_count


##
## 위 _count 정보 중에서 _shard 정보를 제외한 정보를 가져온다.
##

GET /_count?filter_path=-_shards

 

 

반응형

딥 러닝, 머신 러닝, AI 관련 도서를 읽은 것 중에서 괜찮았던 도서만 추려보았다.

 

파이썬 딥러닝 머신러닝 입문 (Python Deep Learning Machine Learning)

저자: 오승환

출판일: 2021년 01월 05일

Remark: eBook 형태로도 출판했음

 

 

딥러닝 텐서플로 교과서

저자: 서지영

출판일: 2021년 04월 30일

 

 

파이썬 텍스트 마이닝 완벽 가이드 (자연어 처리 기초부터 딥러닝 기반 BERT 모델까지)

저자: 박상언, 강주영, 정석찬

출판일: 2022년 02월 18일

반응형
작성일: 2024년 4월 18일

 

 

 

카카오 API 보다 Slack API를 사용하게 된 이유

 

개인적으로 자동으로 스마트폰 메신저에 Notification 메시지를 보낼 일이 있어서, Python으로 카카오톡 메시지 전송 Rest API를 사용했다. Python + 카카오 API 조합으로 구현하니까 참고할 예제가 많아서 큰 문제없이 개발을 할 수 있었다.

그런데 ...

AccessToken의 만료 시간이 4시간 밖에 안 되니까, Refresh Token을 이용해서 다시 AccessToken을 재발급해야 하는 귀찮은 일이 생겼다. 그냥 한번 코드를 작성하고 몇년 동안 잊고 살았으면 좋겠는데 말이다.

그래서 귀찮은 작업을 안 하려고 Slack API의 Webhooks를 이용하게 되었다.

 

 

 

Slack API의 Incoming Webhooks

Slack API를 이용해서 Slack Messenger에 메시지를 전달하기 위한 절차를 보면,

  • 당연히 Slack에 계정을 만들고, slack.com에 로그인해야 한다.
  • https://app.slack.com 접속해서 App을 추가한다. 
       + 나는 [Incoming Webhooks]라는 App을 추가했다.
       + 이때, 기존 channel을 사용해도 되고, 아래 화면처럼 새롭게 channel을 만들어도 된다.

 

Slack Incoming WebHooks App  생성하기

 

  +  위 화면처럼 channel을 선택하고 나면, 아래 화면이 보일 것이고 [Add Incoming WebHoos integration] 버튼을 누른다.

 

Add Incoming WebHooks integration

 

 

아래와 같이 App이 생성된 결과 및 설정 화면이 나온다.

그리고 Example 명령을 복사해서 Terminal에서 실행하면, 바로 Slack 메신저에 curl로 보낸 메시지가 보여진다.

 

 

 

아래와 같이 slack 메신저에 테스트로 보낸 메시지가 보여지면, Incoming-Webhook App은 정상 동작한 것이다.

 

 

 

Python의 requests package를 import해서 requests.post(url, headers, body)를 이용해도 curl과 동일한 효과를 낼 수 있다.

 

 

 

Python Code 작성

 

##
## FILENAME: my_webhook_test.py
##

import requests, json

## NOTE
##  MY_WEBHOOK_KEY는 "T7RURUGSK/.............................. 22i"
##  이런 형태의 문자열이다.

api_url = "https://hooks.slack.com/services/{MY_WEBHOOK_KEY}"
headers = {
    "Content-type": "application/json"
}
data = json.dumps({
    "text": "파이썬으로 테스트 메시지를 보내다 :) \n 룰루랄라 !!!"
})

response = requests.post(api_url, headers=headers, data=data)
print(response)

 

위와 같이 Python code를 작성하고, 아래와 같이 명령을 수행한다.

 

$ python3 my_webhook_test.py
<Response [200]>
$

 

위 Python 앱이 아래와 같이 메신저에 "파이썬으로 테스트 메시지를 보내다... 룰루랄라!!!"를 출력할 것이다.

 

Python App에서 보낸 메시지가 Slack 메신저 화면에 출력됨

 

 


 

 

그리고 아래 화면처럼 Slack API 매뉴얼이 잘 되어 있어서, 읽고 따라하면 잘 동작한다.

결국, AccessToken을 발급하지 않아도 이 Webhooks처럼 쉽게 메신저에 메시지를 보내는 것이 가능한다.

 

 

 

 

 

꼭 알아야 할 정보

 

내가 만든 Slack App에 관한 전체 목록을 열람하고, 설정 정보를 관리하고 싶다면 아래 주소를 사용한다.

- 내 App 목록을 확인할 수 있다.

- 각 App의 설정 정보,  REST API URL 등 정보를 직접 확인하고 제시해주는 예제 명령어를 통해 테스트할 수 있다.

 

https://api.slack.com/apps

 

Slack API: Applications | Slack

Your Apps Don't see an app you're looking for? Sign in to another workspace.

api.slack.com

 

반응형

 

YAML 내용 비교하기 (using Python)

 

 

`vim -d old_file new_file` 또는 diff 등 명령으로 파일에서 변경된 내용을 확인할 수 있지만,

이렇게 비교를 하면, 내용이 동일하더라도 특정 내용의 행이 위/아래로 이동한 경우에는 다른 파일로 간주된다.

즉, 내용을 비교하고 싶은데 문서의 Format이 변경된 것 때문에 다른 파일로 인식되는 문제가 생긴다.

 

따라서 서로 다른 버전의 YAML file을 비교할 때는 두 파일을 Parsing해서 구조체에 넣고, 특정 항목이 추가되었는지 또는 삭제되었는지 또는 변경되었는지를 일일히 확인하는 것이 제일 정확한다.

 

Istio project의 Utility code를 살펴보니, 이런 기능을 하는 python code가 있어서 인용해봤다.

 

https://github.com/istio/istio/blob/master/bin/diff_yaml.py

 

GitHub - istio/istio: Connect, secure, control, and observe services.

Connect, secure, control, and observe services. Contribute to istio/istio development by creating an account on GitHub.

github.com

 

 

 

혹시 GitHub에서 Folder 구조가 바뀌거나 diff_yaml.py 도구가 제거될 것을 우려해서 여기 blog에 흔적을 남긴다.

 

diff_yaml.py (source code 열람)

#!/usr/bin/env python
#
# Copyright 2018 Istio Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Compare 2 multi document kubernetes yaml files
# It ensures that order does not matter
#
from __future__ import print_function
import argparse
import datadiff
import sys
import yaml  # pyyaml

# returns fully qualified resource name of the k8s resource


def by_resource_name(res):
    if res is None:
        return ""

    return "{}::{}::{}".format(res['apiVersion'],
                               res['kind'],
                               res['metadata']['name'])


def keydiff(k0, k1):
    k0s = set(k0)
    k1s = set(k1)
    added = k1s - k0s
    removed = k0s - k1s
    common = k0s.intersection(k1s)

    return added, removed, common


def drop_keys(res, k1, k2):
    if k2 in res[k1]:
        del res[k1][k2]


def normalize_configmap(res):
    try:
        if res['kind'] != "ConfigMap":
            return res

        data = res['data']

        # some times keys are yamls...
        # so parse them
        for k in data:
            try:
                op = yaml.safe_load_all(data[k])
                data[k] = list(op)
            except yaml.YAMLError as ex:
                print(ex)

        return res
    except KeyError as ke:
        if 'kind' in str(ke) or 'data' in str(ke):
            return res

        raise


def normalize_ports(res):
    try:
        spec = res["spec"]
        if spec is None:
            return res
        ports = sorted(spec['ports'], key=lambda x: x["port"])
        spec['ports'] = ports

        return res
    except KeyError as ke:
        if 'spec' in str(ke) or 'ports' in str(ke) or 'port' in str(ke):
            return res

        raise


def normalize_res(res, args):
    if not res:
        return res

    if args.ignore_labels:
        drop_keys(res, "metadata", "labels")

    if args.ignore_namespace:
        drop_keys(res, "metadata", "namespace")

    res = normalize_ports(res)

    res = normalize_configmap(res)

    return res


def normalize(rl, args):
    for i in range(len(rl)):
        rl[i] = normalize_res(rl[i], args)

    return rl


def compare(args):
    j0 = normalize(list(yaml.safe_load_all(open(args.orig))), args)
    j1 = normalize(list(yaml.safe_load_all(open(args.new))), args)

    q0 = {by_resource_name(res): res for res in j0 if res is not None}
    q1 = {by_resource_name(res): res for res in j1 if res is not None}

    added, removed, common = keydiff(q0.keys(), q1.keys())

    changed = 0
    for k in sorted(common):
        if q0[k] != q1[k]:
            changed += 1

    print("## +++ ", args.new)
    print("## --- ", args.orig)
    print("## Added:", len(added))
    print("## Removed:", len(removed))
    print("## Updated:", changed)
    print("## Unchanged:", len(common) - changed)

    for k in sorted(added):
        print("+", k)

    for k in sorted(removed):
        print("-", k)

    print("##", "*" * 25)

    for k in sorted(common):
        if q0[k] != q1[k]:
            print("## ", k)
            s0 = yaml.safe_dump(q0[k], default_flow_style=False, indent=2)
            s1 = yaml.safe_dump(q1[k], default_flow_style=False, indent=2)

            print(datadiff.diff(s0, s1, fromfile=args.orig, tofile=args.new))

    return changed + len(added) + len(removed)


def main(args):
    return compare(args)


def get_parser():
    parser = argparse.ArgumentParser(
        description="Compare kubernetes yaml files")

    parser.add_argument("orig")
    parser.add_argument("new")
    parser.add_argument("--ignore-namespace", action="store_true", default=False,
                        help="Ignore namespace during comparison")
    parser.add_argument("--ignore-labels", action="store_true", default=False,
                        help="Ignore resource labels during comparison")
    parser.add_argument("--ignore-annotations", action="store_true", default=False,
                        help="Ignore annotations during comparison")

    return parser


if __name__ == "__main__":
    parser = get_parser()
    args = parser.parse_args()
    sys.exit(main(args))

 

 

diff_yaml.py 명령 따라하기

위 python code를 diff_yaml.py 파일명으로 저장하고, 아래와 같이 필요한 Python package를 설치하고, Run 한다.

$  pip install --upgrade pip

$  python -m pip install datadiff

$  ./diff_yaml.py /tmp/metallb-1.yaml /tmp/metallb-2.yaml

## +++  /tmp/metallb-2.yaml
## ---  /tmp/metallb-1.yaml
## Added: 1
## Removed: 1
## Updated: 0
## Unchanged: 14
+ apps/v1::Deployment_Test::controller     ## 변경된 내용을 보여준다.
- apps/v1::Deployment::controller          ## 변경된 내용을 보여준다.
## *************************

$

## 이것은 Linux diff 명령으로 확인한 내용이다.
## 실제로 yaml 내용은 변경된 것이 1개 이지만, 단지 행을 이동했다는 이유만으로
## 여러 곳이 다르다고 표기하고 있다. (즉, YAML을 사용하는 User 관점에서 보면 다 쓸데 없는 정보이다.)

$  diff /tmp/metallb-1.yaml /tmp/metallb-2.yaml
3d2
< kind: Namespace             ## <-- 실제로 이 부분은 내용이 바뀐 것이 아니라, 라인만 이동한 것이다.
5d3
<   name: metallb-system      ## <-- 실제로 이 부분은 내용이 바뀐 것이 아니라, 라인만 이동한 것이다.
8a7,8
>   name: metallb-system      ## <-- 실제로 이 부분은 내용이 바뀐 것이 아니라, 라인만 이동한 것이다.
> kind: Namespace             ## <-- 실제로 이 부분은 내용이 바뀐 것이 아니라, 라인만 이동한 것이다.
352d351
< kind: Deployment
400a400
> kind: Deployment_Test       ## <-- 이 부분만 내용이 변경된 것이다.

 

위 결과를 보면, 일반적인 diff와 내용이 다르다는 것을 알 수 있다.

가장 큰 차이점은 diff_yaml.py는 수정된 Line과 Column을 출력하지 않는다. 왜냐하면, 내용 자체의 변경 여부가 관심사이지 어떤 내용이 다른 행으로 이동했는지 또는 삭제, 추가되었는지 중요하지 않기 때문이다.

 

 

 

JSON 내용 비교하기 (using Python)

 

JSON 내용을 비교하는 CLI 도구는 아래 Web Docs를 참고하길 ~~~

 

 

GitHub - xlwings/jsondiff: Diff JSON and JSON-like structures in Python

Diff JSON and JSON-like structures in Python. Contribute to xlwings/jsondiff development by creating an account on GitHub.

github.com

 

 

json-diff

Generates diff between two JSON files

pypi.org

 

 

+ Recent posts