Why elastic stack?

기업의 서비스의 복잡도가 올라감에 따라 프록시 서버, 인증서버, 데이터베이스, api 서버 등 수많은 서비스들이 동작하고, 각자 다른 배포환경에서 관리되게 되었다. 이에 따라 하나의 서비스를 운영하더라도 여러 도메인에 걸쳐 많은 문제들이 생기게 되고, 여러 서버에 걸쳐 로그 및 데이터 분석의 필요성이 생겼고, 데이터를 관리하고 활용하기 위한 통합 솔루션의 필요성이 대두되었다. Elastic Stack 은 기업이 서비스를 운영함에 따라 나오는 모든 데이터를 한 곳에서 관리하고, 사용자가 직관적으로 이해할 수 있도록 시각화 시켜 가공하고, 수많은 데이터에서 원하는 데이터를 검색 및 분류해주는 플랫폼이다.

쉽게 이야기 하면, 기업이 운영하는 수십대의 서버 및 데이터베이스에서 나오는 모든 데이터를 한곳으로 모으고, 가공하고, 시각화해주는 통합 솔루션이라 할 수 있다.

이러한 기능을 수행하기 위해 elastic stack 을 구성하는 여러 서비스가 있는데, 대표적으로 각 서버에서 생기는 데이터를 한 곳으로 모으는 shipping 을 담당하는 beats(beat 에는 file beat 등 여러 종류의 beat 가 있으며, 기본적으로 file beat 를 사용하면 기업의 데이터 파일을 한곳으로 간편하게 이동시킬 수 있다.)와 데이터를 저장하고 분류하는 일종의 데이터 검색 엔진인 elastic search , 또, 이러한 사용자에게 가시적으로 보여주기 위한 kibana 등이 있다.

본 포스트에서는 이러한 elastic stack 의 다양한 기술을 활용하여 여러 서버의 로그 정보를 한 곳에 모으고 이를 모니터링 하기 위한 환경을 구축해 보도록 하겠다.

File beat

먼저 각 서버에서 나오는 로그들을 한곳으로 shipping 하기 위해서, 각 서버에 file beat 를 설치하고 운영해야 한다.

본 포스트에서는 각 서버와 filebeat 를 하나의 docker-compose 로 관리하여 하나의 서버에서 filefbeat 와 application 서버를 구축하고 이를 elastic search 서버로 전달해 보도록 한다.

어플리케이션 서버와 file beat 를 구성하는 docker-compose 파일은 다음과 같다.

docker-compose.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
version: '3'

services:
filebeat:
image: docker.elastic.co/beats/filebeat:${ELASTIC_VERSION:-6.5.0}
hostname: '{{.Node.Hostname}}-filebeat'
user: root
networks:
- default
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /var/lib/docker/containers/:/var/lib/docker/containers/:ro
- ./log/application:/var/log/application

command: ['--strict.perms=false']
deploy:
mode: global

server:
image: 'example-server'
build: './example-server'
volumes:
- ./log:/app/log
networks:
- default
ports:
- '3001:3001'

networks:
default:
driver: bridge

파일비트가 수집할 로그가 있는 디렉토리를 docker volume 으로 지정하고 application server 를 3001 번에서 동작시켰다.

또한, filebeat 의 설정을 담당하는 filebeat.yml 파일을 docker 내에 볼륨으로 지정하였으며, 그 내용은 다음과 같다.

filebeat.yaml

1
2
3
4
5
6
7
8
9
10
11
12
name: example filebeat
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/application/*.log

output.elasticsearch:
hosts: ['host.docker.internal:9200']

setup.kibana:
host: 'host.docker.internal:5601'

위처럼 9200번 포트에서 동작하는 elastic search 서버와 5601번에서 동작하는 kibana가 세팅된다.

deploy with beanstalk

Beanstalk 에서 multidocker deploy를 위한 Dockerrun.aws.json 파일을 작성하여 다음과 같이 배포할 수 있다.

여기서 중요한 점은 image 를 배포함에 있어 반드시 사용자id.dkr.ecr.ap-northeast-2.amazonaws.com/레포이름:latest 의 형태로 이미지 경로를 지정해 주어야 한다는 것이다.

즉, 멀티 도커를 사용할 때 필요한 이미지들을 미리 아마존의 repository 에 배포해 놓아야 한다.

이 과정에서 반드시 아마존에 로그인이 되어야 하며, 다음과 같은 명령어를 통해 ecr 에 별도로 로그인을 수행한다.

아래 명령어는 기본 aws 유저가 아닌 eb cli 접근이 가능한 프로그램 유저를 설정해 두고 해당 유저의 정보로 로그인을 함을 의미한다. 아래 명령어를 입력하면 AWS 에서 로그인을 하기 위한 명령어를 output 으로 제공하는데 해당 문자열을 복사하여 다시 cli에 입력하면 로그인이 완료된다.

1
aws ecr get-login --no-include-email --profile eb-cli

여기서 memory 는 매우 중요한데, 만약 memory 가 부족하다면 도커 컨테이너가 아무런 에러도 출력하지 않고 종료되어 버리니 이점에 유의해야 한다.

Dockerrun.aws.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
{
"AWSEBDockerrunVersion": 2,
"volumes": [
{
"name": "log",
"host": {
"sourcePath": "/var/log"
}
}
],
"containerDefinitions": [
{
"name": "filebeat",
"image": "docker.elastic.co/beats/filebeat:6.5.0",
"essential": true,
"memory": 128,
"mountPoints": [
{
"sourceVolume": "log",
"containerPath": "/var/log"
}
],
"command": ["--strict.perms=false"],
"links": ["server"]
},
{
"name": "server",
"image": "사용자id.dkr.ecr.ap-northeast-2.amazonaws.com/레포이름:latest",
"essential": true,
"memory": 512,
"mountPoints": [
{
"sourceVolume": "log",
"containerPath": "/app/log"
}
],
"portMappings": [
{
"containerPort": 3001,
"hostPort": 80
}
]
}
]
}

how does it works?

File beat 에는 input 과 harvest 라는 두가지 큰 개념이 있다.

먼저 input 이란 filebeat 가 데이터를 읽어오는 리소스를 찾고, 이를 실제로 읽어들이는 harvestor 를 관리하는 역할을 수행한다. 인풋으로 type 이 log 인 인풋이 들어오면 해당하는 패스에서 모든 파일을 찾아 각각에 harvestor 를 작동시킨다.

각 input 은 각자 독자적인 go routine 을 통해 돌아간다.

파일비트는 다음과 같은 종류의 여러 type 의 인풋을 제공한다.

log, stdin, redis, udp, docker, tcp, syslog

example

1
2
3
4
5
filebeat.inputs:
- type: log
paths:
- /var/log/*.log
- /var/path2/*.log

harvestor 란 파일을 하나하나 읽어서 수집하는 harvest 를 수행한다. harvestor는 각 파일을 라인바이 라인으로 읽어서 아웃풋으로 보내며, 파일을 열고 닫으면서 계속 수집을 수행한다. harvestor 가 로그를 수집하는 동안 file을 계속 열어놓는다.

harvestor 를 닫으면 리소스를 할당 해제한다.

파일의 상태관리는 어떻게 하나?

파일 비트는 각 파일의 읽기 상태를 레지스트리 파일 디스크에 기록해 두고 계속 트레킹한다.

이를 통해 어디까지 읽었는지 파악하고 output server 와의 연결상태가 불안정하다면 이를 계속 트레킹 하고 빠짐없이 보내준다. 파일비트는 무조건 최소한 한번은 정보가 전달되는 것을 보장하는 대신 대신에 두번 갈수는 있다. 이는 레지스트리에 저장된 값 덕분이다. 데이터가 두번 전송되는 문제는 shutdown_timeout 옵션을 통해 조절 할 수 있다.

가령, 파일비트가 종료되기 전에 시간을 두고 데이터를 전송을 완료하는 등의 동작을 수행하는 것이다.

또한, output으로의 연결 상태가 좋지 않은데 파일이 삭제된다면 이는 유실될 수 있다.

logstash

logstash 는 data flow engine 으로써 filebeat, dbms, message que 등 여러 데이터 소스로 부터 데이터를 받아 데이터를 가공하고 정제하는 역할을 수행한다.

Logstash 에는 pipeline 이라는 개념이 있으며, 여러 데이터들에 대해 일련의 작업을 수행하는 pipeline의 합집합으로써 동작한다.

elastic stack 내에서 일반적인 활용도는 각 서버의 filebeat 가 보낸 로그 데이터를 받아 각종 메타 정보를 기입하고 분류하고 정제하여 elastic search 서버로 보내주는 역할을 수행한다.

logstash의 기능은 크게 3가지로 이야기 될 수 있는데 바로 input, filter, output 이다.

먼저 input 은 데이터가 유입되는 근원지를 설정함으로 파일, 데이터베이스, 메세지 큐, 파일비트 등 데이터 유입 경로를 지정하고 주기적으로 데이터를 받는다.

filter 단계에서는 받은 데이터를 필터링하고 정제하여 메타데이터를 입히고 데이터를 전처리하는 등을 수행한다.

output 단계에서는 정제된 데이터를 목적으로 되는 서버로 전달하며 주로 검색엔진인 elastic search 등으로 데이터를 전달한다.

logstash 는 데이터를 정제하는 과정에서 codec 을 사용하는데, codec 이란 특정 데이터 소스의 형식에 맞는 데이터를 우리가 원하는 형태로 가공해 주는 역할을 하는 encode decode 를 수행한다고 보면 된다. 가령, nginx 서버에서 생성된 각종 nginx 서버 양식의 로그 데이터를 nginx codec 으로 decode 하여 다시 우리가 원하는 데이터 모델로 재생성 하는 것을 생각하면 된다.

이렇게 재가공된 데이터는 다시 filter 를 통해 분류되어 목적지 output 으로 전달된다.

installation

1
docker pull docker.elastic.co/logstash/logstash:6.6.0

run

1
docker run --rm -it -v ~/pipeline/:/usr/share/logstash/pipeline/ docker.elastic.co/logstash/logstash:6.6.0

여기서 logstash 는 파일비트로 부터 여러번 데이터를 받아 batch size 만큼을 체운 뒤에 elastic search 에 데이터를 전달하기 때문에 즉각적으로 데이터가 전송되지 않아 동작을 확인하기 어려울 수 있다.

가급적 input 에 tcp 를 추가하여 실시간으로 데이터를 확인할 수 있도록 설정해 놓으면 비교적 편리하게 테스트를 진행할 수 있다.

logstash.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 파이프라인 수를 의미한다.
# 더 많은 CPU 를 사용하기 위해 설정한다.
pipeline.workers: 2

# 배치 사이즈를 설정한다.
pipeline.bath.size: 125

# 배치 딜레이는 어떤 간격으로 요청하는지 한다.
pipeline.batch.delay: 5

# config 옵션이 변경되면 보고 감지하는 옵션이다.
config.reload.automatic: false

# config 변경을 감지하는 주기이다.
# 현재 설정에서는 5초에 한번 감지하는 것으로 되어있다.
config.reload.interval: 5s

Elastic Search

Elastic search 는 filebeat 가 수집해온 데이터를 분류하고 서치하는 검색 엔진이다.

installactio on docker

elastic search server 를 설치하고 실행한다.

1
docker pull docker.elastic.co/elasticsearch/elasticsearch:6.5.4
1
docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:6.5.4

docker 를 사용하여 구성을 할때 max virtual memory areas vm.max_map_count [65530] is too low, increase to at least 와 같은 에러가 나올 수 있다.

이는 도커에서 할당하는 최대 가상 메모리가 모자라서 생기는 문제로, 다음과 같은 명령어를 통해 이를 늘려주면 해결된다.

1
sudo sysctl -w vm.max_map_count=262144

Kibana

키바나는 내부적으로 노드서버를 띄우며, 사용자가 수집한 정보를 사용자에게 보여주기 위해 시각화 시켜주며, 사용자를 위한 관리자 대쉬보드를 제공한다.

또한, kibana 는 elastic search 를 조작하기 위한 dev console 또한 제공한다.

1
docker pull docker.elastic.co/kibana/kibana:6.5.4
1
2
3
4
5
6
version: '2'
services:
kibana:
image: docker.elastic.co/kibana/kibana:6.5.4
volumes:
- ./kibana.yml:/usr/share/kibana/config/kibana.yml

다운로드를 받았다면 설치를 풀어주고

exconfig/kibana.yml 의 설정을 해준다.

키바나를 실행하면 Localhost:5601 로 키바나가 붙는다.

Kibana query

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
POST /inspections/_doc
{
"business_address":"kwan ak goo",
"coordinates":{
"lat":21,
"lon":-123
},
"score":12
}
POST /inspections/_doc/_bulk
{ "index" :{ "_id" : 1 } }
{"business_address":"kwan ak goo"}
{"index":{"_id":2}}
{"business_address":"kwan ak goo"}

GET _search
{
"query": {
"match_all": {}
}
}
GET /inspections/_mapping/_doc
PUT /inspections/_mapping
{
"properties": {
"business_address":{
"type":"test",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"coordinates":{
"type":"geo_point"
}
}
}

GET /inspections/_doc/_search
{
"query": {
"range":{
"score":{
"gte":11
}
}
},
"aggregations":{
"inspection_score":{
"range":{
"field":"score",
"ranges":[
{
"key":"0-80",
"from":0,
"to":80
}
]
}
}
},
"sort":[
{
"_geo_distance":{
"coordinates":{
"lat":34.322345,
"lon":-122.234234
}
},
"order":"asc",
"unit":"km"
}
]
}


GET /inspections/_doc/_search
{
"query": {
"match":{
"business_address":"modified"
}
}
}
GET /inspections/_doc/_search
{
"query": {
"match_phrase":{
"business_address":"kwan"
}
},
"highlight":{
"fields":{
"business_address":{}
}
}
}
GET /inspections/_doc/_search
{
"query": {
"bool":{
"must":[
{
"match_phrase":{
"business_address":"kwan"
}
},
{
"match_phrase":{
"business_address":"ak"
}
}
],
"must_not":[
{
"match_phrase":{
"business_address":"goo"
}
}
]
}
}
}

PUT /inspections/_doc/t061gmgBoJ5OaX9VzeS8
{
"settings":{
"index.number_of_shards":1,
"index.number_of_replicas":0
},
"business_address":"modified"
}

PUT /inspections/_doc/t061gmgBoJ5OaX9VzeS8
{
"settings":{
"index.number_of_shards":1,
"index.number_of_replicas":0
},
"business_address":"modified"
}

POST /inspections/_doc/0k6Ug2gBoJ5OaX9VzuR6/_update
{
"settings":{
"index.number_of_shards":1,
"index.number_of_replicas":0
},
"business_address":"modified"
}

GET /inspections/_analyze
{
"tokenizer": "standard",
"text":"my email address test1234@example.com"
}




GET /inspections/_analyze
{
"tokenizer": "standard",
"filter": ["lowercase", "unique"],
"text":"My my email address test1234@example.com"
}

Docker 시작하기

현대의 대부분의 소프트웨어는 클라우드 환경에서 동작하게 되고, 서비스를 잘게 나누어 다양한 클라우드 환경에서 쉽게 배포하고 운영하기 위한 컨테이너 기술의 필요성이 대두되었다. 도커는 운영체제 위에서 프로세스를 독립적으로 실행가능하게 만들어 주는 리눅스 컨테이너 기술이다.

기본적으로 container 라는 환경을 구성하고 그 안에서 docker image를 실행시키는 방식이다.

여기서 docker image는 실행에 필요한 코드 및 패키지 등 모든 필요사항을 가진다.

도커는 운영체제 위에서 동작하며 같은 커널에서 어플리케이션이 동작하지만 메모리나 OS 등이 완벽하게 격리될 수 있다.

Installation

아래와 같이 brew 로 docker 를 설치하고, 권한을 할당하여 sudo 없이 docker 를 실행 할 수 있게 해준다.

1
2
brew install docker docker-compose docker-machine &&
sudo chmod 666 /var/run/docker.sock

먼저 도커를 실행하면 도커 엔진이 실행되며 도커 이미지를 도커 엔진이 실행하면 컨테이너가 생성된다.

먼저 container를 설정하기 위해서 우리는 dockerfile을 작성해야 하는데 기본적인 dockerfile의 형태는 다음과 같다.

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# set node.js environment
FROM node:8-alpine

# set working directory
WORKDIR /user/src/app

# 앱 의존성 설치
COPY package*.json ./

# docker image안에 앱의 소스코드를 넣음
COPY . .

RUN npm install

# Make port 80 available to the world outside this container
EXPOSE 8080

# Define environment variable
ENV NAME World

# Run app.py when the container launches
CMD ["npm","start"]


# RUN chmod +x ./docker-entry.sh
# ENTRYPOINT "./docker-entry.sh"

그 뒤에 다음 명령어를 통해 docker image를 만들어 준다.

1
2
docker build -t test . // image의 별명을 입력받아 docker image를 만들어 준다.
docker image ls // 이미지의 목록을 불러온다.

앱 실행하기

다음 명령어를 통해 해당 machine의 4000번 포트를 외부 80번 포트에 연결해 준다.

1
docker run -p 4000:80 -d test # test 라는 이미지를 실행한다.

Docker container 중지하기

1
docker container stop __container_name_or_id__

docker 로그인 하기

1
docker login

Docker image 배포하기

1
2
3
4
# docker image tag 붙이기
docker tag image username/repository:tag
# docker 배포하기
docker push username/repository:tag

도커 허브의 이미지 받아서 실행하기

1
docker run -p 4000:80 username/repository:tag

지우기

1
docker container rm __image_id__

모든 도커 컨테이너 삭제

docker rm $(docker ps -a -q)

모든 도커 이미지 삭제

docker rmi $(docker images -q)

실행중인 컨테이너의 쉘에 접근

docker exec -it container_id /bin/bash

docker 실행하면서 bash 실행하기

docker run -p 80:80 -it {컨테이너 이름} /bin/bash

Docker network

아래 옵션을 주면 네트워크 포트가 매핑된다.

하지만 아래와 같은 host 네트워크는 리눅스 운영체제에서만 동작하고 mac 이나 window 환경에서는 호스트의 포트에 매핑이 되지 않으니 주의하여야 한다.

1
docker run --network host

Docker Compose

container 내에서 여러개의 image가 돌아야 하고 서비스가 복잡해 질수록 많은 이미지들을 관리해야할 필요가 생기게 된다. 여기서 docker-compose.yml을 사용한다.

즉, docker compose 는 여러개의 도커 컨테이너가 동작하기 위한 방법을 기술한 것이다.

Docker-compose.yml 파일은 docker container가 배포 환경에서 어떻게 동작해야 할지 명시한다.

docker compose의 동작 순서는 다음과 같다.

  1. dockerfile 과 함께 어플리케이션의 환경을 정의하여 어디에든 배포할 수 있도록 설정한다.
  2. docker compose 파일에 서비스들을 정의하여 독립된 환경에서 함께 실행될 수 있도록 한다.
  3. docker compose 를 실행시킨다.

docker-compose example

1
2
3
4
5
6
7
8
version: '3'
services:
web:
build: .
ports:
- "5000:5000"
redis:
image: "redis:alpine"
1
2
3
docker swarm init
docker stack deploy -c docker-compose.yml getstartedlab
doocker service ls

앱과 swarm을 내리기

1
2
docker stack rm getstartedlab
docker swarm leave --force

여기서 swarm 이란 여러 서버에서 동작하는 앱처럼 분산환경에서 배포하기 위한 것이다. 여러 대의 machine을 swarm 이라 불리는 dockerized cluster로 합쳐준다.

swarm을 실행하면 전체 노드들의 관리자로써 swarm을 통제할 수 있게 된다.

다음은 해당 machine을 swarm의 관리자로 임명하고 초기화하는 명령어이다.

1
docker swarm init

Docker-machine을 통해 virtual machine을 생성할 수 있는 hypervisor를 만들 수 있다.

아래 명령어를 docker-machine을 통해 vm들을 생성하는 명령어이다.

1
2
3
docker-machine create --driver virtualbox myvm1
docker-machine create --driver virtualbox myvm2
docker-machine ls

다음 명령어를 통해 특정 vm이 swarm의 manager가 되도록 한다.

1
docker-machine ssh myvm1 "docker swarm init --advertise-addr <myvm1 ip>"

특정 vm이 swarm에 참여하도록 하려면 다음 명령어를 수행한다.

1
2
3
docker-machine ssh myvm2 "docker swarm join \
--token <token> \
<ip>:2377"

swarm에 포함된 node 들을 보는 명령어는 다음과 같다.

1
docker-machine ssh myvm1 "docker node ls"

One example of where this handcoded approach will fail is in detecting faces in images. Today, every smartphone can detect a face in an image.

The most successful kinds of machine learning algorithms are those that automate decision-making processes by generalizing from known examples

In this setting, which is known as supervised learning

the user provides the algorithm with pairs of inputs and desired outputs, and the algorithm finds a way to produce the desired output given an input.

scikit-learn is a very popular tool, and the most prominent Python library for machine learning.

A Python distribution made for large-scale data processing, predictive analytics, and scientific computing

1
pip install numpy scipy matplotlib ipython scikit-learn pandas pillow
1
scikit-learn` is built on top of the NumPy and SciPy scientific Python libraries. In addition to NumPy and SciPy, we will be using `pandas` and `matplotlib
1
2
3
4
import numpy as np

x = np.array([[1, 2, 3], [4, 5, 6]])
print("x:\n{}".format(x))

pandas is a Python library for data wrangling and analysis

classification

어떤 데이터들을 분류하는 문제이다.

여기서 분류의 대상의 data, 분류종류를 class 그리고 특정 데이터가 분류되었을때 그 분류 이름을 label이라고 부른다.

What is supertest

Node.js 에서 express 를 이용하여 서버를 개발함에 있어, rest api 를 테스트 하기 위해서는 실제로 서버에 요청을 보내고 클라이언트에 원하는 데이터가 전송되는 것을 확인해야 한다.

이러한 e2e 테스트를 위해서는 실제 서버를 띄운 뒤에 해당 서버로 요청을 날려 보면서 테스트를 진행해야 하며, 이렇게 가상의 서버를 띄워주고 express web server 에 대해 e2e 테스트를 가능하게 해주는 것이 바로 supertest 이다.

본 포스트에서는 그 중에서도 일반 javascript가 아닌 typescript 환경에서의 e2e 테스트 코드 작성법에 대해 알아보고자 하며, 테스트를 위해서는 페이스북에서 만든 잘나가는 프레임웍인 jesttypescript 배포판인 ts-jest 를 사용한다.

Prerequisition

먼저 타입스크립트로 작성된 프로그램은 javascript 기반의 테스트 환경인 jest 로는 테스트를 작동시킬 수가 없기 때문에, ts-jest 패키지를 dev 환경에 설치해 주어야 jest 가 typescript 코드를 읽고 실행할 수 있다.

다음과 같이 ts-ject, supertest, @types/supertest를 설치해 준다.

1
2
3
npm install --save-dev ts-jest
npm install --save-dev supertest
npm install --save-dev @types/supertest

여기서 한가지 문제가 있는데, 아직 typescript supertest 의 경우 여러 패키지들과의 문제를 보이고 있다.

단편적인 예로 XMLHttpRequest 를 찾지 못하는 현상이 있는데, 이는 다음 설정을 tsconfig.json 에 해줌으로써 해결된다.
다음과 같이 dom 을 앞에다 추가해 주자. 반드시 첫번째로 해야한다.

1
2
3
4
5
{
compilerOptions:{
"lib":["dom","es2015]
}
}

위처럼 ts-jest를 설치했다면 이제 typescript 코드를 jest 에서 돌릴수 있게 된다.

본 프로젝트에서는 ts-jest 는 내부적으로 js 의 es6 문법을 컴파일 하기 위해 babel loader 를 사용하기 때문에 다음과 같이 babel env 를 설치하고 babel configuration 을 다음과 같이 해주어야 한다.

1
npm install --save-dev babel-preset-env

.babelrc

1
2
3
{
presets:["env"]
}

Setup jest configuration

Jest 세팅은 tsconfig.json 에서도 할 수 있고 jest.config.js 파일에서도 할 수 있다.

이 경우 반드시 한쪽에서만 해야 문제가 발생하지 않기때문에 유념한다.

Jest 는 typescript 를 컴파일 하지 않고 파일별로 컴파일과 실행을 동시에 처리하기 때문에, module alias 를 사용하였다면, 해당 파일이 어디에 있는지를 가르쳐 주어야 한다.

아래 코드에서는 moduleNameMapper 를 통해 module alias 를 발견하면 어디에서 해당 파일을 찾을 수 있는지 jest 에게 가르쳐 준다.

또한 ts-jest preset 를 사용한다는 것을 명시하고 있다.

jest.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
moduleNameMapper: {
'^@domain/(.*)$': '<rootDir>/src/server/domain/$1',
'^@common/(.*)$': '<rootDir>/src/server/common/$1',
'^@utils/(.*)$': '<rootDir>/src/server/utils/$1',
'^@infra/(.*)$': '<rootDir>/src/server/infra/$1',
'^@api/(.*)$': '<rootDir>/src/server/api/$1',
'^@interfaces/(.*)$': '<rootDir/src/server/interfaces/$1',
'^@root/(.*)$': '<rootDir>/src/server/$1'
},
transform: {
'^.+\\.jsx?$': 'babel-jest',
'^.+\\.(ts|tsx)$': 'ts-jest'
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'node', 'json'],
testMatch: ['**/*.test.+(ts|tsx|js)']
};

여기서 moduleFileExtensions의 경우 typescript 테스트만을 진행하는 경우에는 tx, tsx, js 만 명시해주어도 되지만, 기본적인 node 가 제공하는 확장자인 json, node 등도 추가하여야 여러 패키지를 사용함에 있어 문제가 없다.

Getting started

how to setup express application

Supertest 는 express application 객체를 받아서 내부적으로 서버를 listen 하기 때문에 supertest를 할 때마다 이런 express application을 주입시켜 주어야 한다. 어찌보면 단순한 일이지만 사실 실제 서버의 경우 express application 을 초기화하는 일련의 과정이 존재하기 때문에 이 작업을 해주는 것이 다소 까다로울 수 있다.

또한, 대부분의 REST API 테스트의 경우 인증된 사용자에 대해서만 요청을 처리하는 authentication 기능이 있기 때문에, 테스트를 진행하기 전에 jwt 토큰 발급 및 회원가입 등 authentication 관련 문제들을 해결해야 한다.

때문에, 본 문서에서는 이런 기본적인 express application initializing 과 authentication 을 도와주는 helper 클래스인 TestHelper 클래스를 먼저 구현하여 supertest 를 하도록 한다.

app.ts

1
2
3
4
5
6
7
8
class App {
public async setup(): Promise<express.Express> {
let app = express();
app.use();

return app;
}
}

testHelper.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class TestHelper {
/**
* 테스트에 필요한 유저를 만들어 주는 함수이다.
*
* @param id 유저의 아이디
* @param password 유저의 비밀번호
*/
public async generateTestUser(
id: string,
password: string
): Promise<User> {
/**
* 필자는 일련의 회원가입을 담당하는 userService 에서
* 회원가입을 처리하고 있다.
* 이 부분은 독자가 임의적으로 회원가입 로직을 구현하면 된다.
*/
let user = await userService.createUser(
id,
password
);

return user;
}

/**
*
* @param id 유저의 아이디
*/
public async generateJwt(id) {
let jwtPayload = new JwtPayload(id);
return encodeJwt(jwtPayload);
}

public async getApp(): Promise<express.Express> {
let APP = new App();
let app = await APP.setup();

return app;
}
}

export const testHelper = new TestHelper();

위처럼 express appilcation 을 비동기적으로 세팅해주는 helper 함수들을 구현하였다면 아래와 같이 모든 테스트 코드를 작성하기 전에 위의 app을 초기화 해준다.

모든 테스트 이전에 다양한 환경을 세팅해주기 위해 beforeAll() 를 이용하는데, 이는 모든 테스트 전에 해야할 일들을 명시하고 세팅해 준다. 만약, 비동기적인 작업을 beforeAll 에서 해야 할 필요가 있다면 다음과 같은 방법이 있다.

  1. Promise 를 return 해주는 방법
  2. Async/Await 을 사용하는 방법
  3. Done 인자를 받아 사용하는 방법

아래 코드에서before all 부분에서 바로 promise 를 리턴하는 방식과 Async/Await 방식을 보여주고 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
describe('return promise',()=>{
let app;

beforeAll(()=>{
return testHelper.getApp().then(_app=>{
app = _app;
})
})

describe('POST: /users',()=>{
let res = await request(app).post('api/v1/users');

})
})
describe('example test',()=>{
let app;

beforeAll(async()=>{
app = await testHelper.getApp()
})

describe('POST: /users',async()=>{
let res = await request(app).post('api/v1/users');
})
})

이렇게 테스트를 돌리게 되면, 테스트는 끝이 났는데 프로세스는 계속해서 동작하고 있다는 에러를 볼 수 있다.

이는 데이터베이스 등처럼 connection 객체가 살아있는 경우 해당 프로세스가 종료되지 않기 때문인데, 이 때문에 다음과 같이 테스트가 끝난 이후에 application 내의 데이터베이스 커넥션을 끊어주면 해당 경고가 없어진다.

1
2
3
4
5
6
7
8
9
10
11
12
describe('',()=>{
let app;

beforeAll(async()=>{
app = await testHelper.getApp()
})

afterAll(async()=>{
app.closeApp()
})

})

여러 파일에서의 beforeAll 작업

위처럼 모든 테스트파일에서 setup 을 진행하면, 각 setup 은 jest 의 특성상 비동기적으로 동작하게 되기 때문에, 만약 db 등을 조작한다면 confict 이 날 수 있다.

즉, 만약 모든 테스트를 통틀어 한번 데이터를 세팅하고자 한다면 다음과 같이 별도의 setup.js 파일을 만들어 전체 테스트 실행 전에 돌려주는 것이 좋다.

setup.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var mysql = require('mysql');
console.log(mysql);
var connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'test1234',
database: 'test'
});

connection.connect();

connection.query('DELETE FROM user;', function(error, results, fields) {
if (error) throw error;
});
connection.end();

위 스크립트 파일을 시작하도록 package.json 에 등록해준다.

1
2
3
4
5
{
"scripts":{
"test":"node ./setup.js && jest"
}
}

How does it works

자바스크립트를 기본적으로 chrome 브라우저에서 실행되는 script 언어로 시작하였으나, node.js 라는 런타임 환경이 나온 이후로 브라우저 밖에서도 javascript를 사용할 수 있게 되었다.

아무래도 브라우저 내에서 사용되던 언어인 만큼 window 라는 전역 object 가 context의 root 역할을 수행한다.

런타임 환경 내에서 실행되는 모든 변수는 window 라는 전역 객체에 그 상태가 기록되며, 브라우저에서 동작하는 성격상 브라우저의 창 즉, window 가 런타임 환경에서 최상위 root context 가 된다. 기본적으로 html 이라는 사용자가 보는 가시적인 페이지와 연동되기 위해 event 라는 개념을 사용하며, 사용자가 브라우저에서 하는 모든 활동은 event 를 발생시키며, 각 이벤트가 발생하면 수행되는 일련의 함수의 집합으로 프로그램이 구성된다.

이러한 event 를 듣고 수행하는 것들을 일컬어 event target 이라고 하며, 이러한 event target 으로는 root context 인 window 객체 이외에도 document, element 객체가 있다.

각 객체들은 addEventListener 함수를 통해 특정 이벤트가 발생할때 실행할 함수들을 등록할 수 있으며, 이를 통해 web page 와 사용자가 상호작용을 하게 된다.

또한, javascript runtime environment 인 node.js 는 각종 패키지들을 사용할 수 있는데, 여기서 node 는 실행하는 루트의 package.json 파일을 참조하여 노드 모듈을 참조하고 실행하며, 때문에 실행하는 위치의 root 에 package.json 파일이 있어야만 패키지들을 찾고 실행할 수 있기 때문에 이를 유의하여야 한다.

Javascript modules

npm 패키지를 사용하기위한 설정을 package.json 에 설정해 준다.

path alias 사용하기

package.json

1
2
3
4
5
6
7
8
9
10
11
{
"_moduleAliases": {
"@domain": "dist/domain",
"@infra": "dist/infra",
"@util": "dist/util",
"@common": "dist/common",
"@interfaces": "dist/interfaces",
"@utils": "dist/utils",
"@api": "dist/api"
}
}

Multithreading

Javascript 는 기본적으로 single thread 방식으로 동작한다.

즉, 하나의 프로세스 내에서도 하나의 단일 스레드 내에서 동작하는 것을 기본으로 한다.

하지만, 브라우저 내에서 데이터를 로드하면서 화면을 전환하거나 혹은 화려한 ui 를 그리는 도중에도 사용자와의 interaction 을 해서 편의를 높이는 등 다양한 부분에 있어 병렬처리에 대한 필요성이 대두되었으며, 브라우저는 Web Worker 라는 개념을 통해 병렬처리를 가능하도록 하였다.

web worker 는 현재 실행중인 main thread 와 병렬적으로 background thread 에서 특정 작업을 수행하여 병렬적으로 특정 task 를 수행할 수 있게 해 준다.

1. codebase

모든 어플리케이션 코드는 version control system 을 통해 하나로 관리되어야 한다.

가령 여러개의 루트 repository 가 있다면 이것은 앱이 아니라 하나의 분산 시스템에 가까울 것이다.

하나의 코드베이스에서 여러개의 배포를 진행할 수 있고, 각 배포버전별로 코드 버전이 관리된다면 훌륭할 것이다.

Introduction of git flow

일반적인 프로젝트에서 이러한 버전관리는 훌륭한 버전관리 툴인 git 을 사용해서 진행하며 git flow 는 훌륭한 워크플로우를 제공한다.

install git flow on Mac

1
brew install git-flow

start git flow project

1
git flow init

다음은 git flow 에서 브랜치의 종류와 그 역할에 대해 알려준다.

master branch 는 실제 공식 배포버전을 관리하며 일반적으로 production deploy 버전을 관리한다.

feature branch 는 실제 각 개발자들이 작업을 하는 브랜치이며, 이는 develop 브랜치에서 갈라져 나오고 추후 다시 develop 브랜치로 병합된다.

develop 브랜치는 각 개발자들이 작업한 브랜치들이 머지되는 장소이며 모든 커밋의 내용이 기록된다.

release branch 는 develop 브랜치로 부터 당겨져오며 여기에서는 오직 버그 수정이나 혹은 documentation 수정 등 release 와 관련된 작업들만 수행된다. 이를 통해 특정 팀에서 해당 release 버전으로 테스트 혹은 출시준비과정을 진행하는 동안 다른 브랜치에서 작업을 수행하며, 다음 release 전까지 해결될 사항들을 정리할 수 있다. 해당 브랜치는 develop 으로부터 갈라져 나온다. 만약 코드리뷰를 한다면 해당 브랜치에서 하는 것이 가장 깔끔해 보인다.

hotfix branch hotfix 브랜치는 바로 master branch 에서 갈라져 나와 빠른 버그 수정 등을 수행하는 branch 이며, 버그가 수정되면 재빨리 다시 master 브랜치로 머지되어야 한다. 이 브랜치는 master 브랜치로부터 갈라져 나오는 유일한 브랜치이다.

Start programming with a feature branch

아래 명령어로 새로운 develop 브랜치로 부터 feature 브랜치를 만들어서 갈라져 나온다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 로컬에 새 브랜치 생성
git flow feature start branch_name

# 리모트 브랜치에 배포
git flow feature publish branch_name

# 새로운 내용을 커밋
git add .
git commit -m "content"
git push

# 머지를 요청하기 전에 반드시 리베이스를 받는다
git pull -r origin develop

# feature 브랜치 머지
git feature finish branch_name

release version

새로운 버전을 출시하기 위한 git flow 코드는 다음과 같다.

1
2
3
4
5
# from develop branch
git flow release start 0.0.1

# from release branch
git flow release finish 0.0.1

hotfix

공식 배포 버전에서 빠른 버그 수정을 위한 hotfix 브랜치에서 작업을 하는 과정은 다음과 같다.

1
2
git flow hotfix start hotfix_branch
git flow hotfix finish hotfix_branch

2. Dependencies

어플리케이션을 작성함에 있어 다양한 외부 라이브러리들에 의존하게 된다.

특히 각 언어별로 존재하는 패키지 매니저를 사용한다면 글로벌하게 설치되는 패키지도 있고 내부적으로 설치되는 패키지도 있는데 이처럼 글로벌하게 설치되는 패키지의 경우 시스템 환경에 어플리케이션이 의존하게 되는 문제가 있다.

12 factor app 에서는 이러한 시스템 환경에 전혀 의존하지 않는 프로그램을 작성함을 원칙으로 한다.

가령 node.js 환경에서 어플리케이션을 개발하는 경우 npm 패키지를 설치함에 있어 package.json 이라는 패키지 관리 documentation 을 하게 된다. 이처럼 의존하는 dependency 를 명확하게 명시하여서 다른 시스템 환경에서도 바로 빌드가 되어 오류없이 프로그램이 실행하도록 한다.

3. Configuration

대부분의 경우 어플리케이션은 배포할때마다 다른 configuration 이 필요하게 된다.

가령 develop server 의 경우 develop database 에 연결되어야 하고, production server의 경우 production database에 연결되어야 하는 것처럼 어플리케이션의 소스코드와는 달리 이러한 설정 값들은 배포마다 달라지게 된다.

때문에, 12 factor app 에서는 이러한 설정값을 코드베이스에 포함시키는 것을 엄격하게 금지하고 있으며, 이러한 모든 설정들을 environment variable 에 저장하는 것을 추천한다.

이러한 env variable 들은 여러 deployment setting 에 대해 혼합된 정보를 절대 가지지 않으며 각 배포환경마다 독립되게 존재하여야 한다.

4. Backing services

가령 이메일 서버나 혹은 데이터 베이스와 같이 어플리케이션이 동작하는데 필요한 부수적인 다른 서비스들을 backing service 라고 부른다.

이러한 backing service 들은 언제나 장애가 생길 수 있으므로 12 factor app 에서는 그 어떤 코드의 변경 없이 백업으로 준비된 backing service 에 붙을 부 있어야 한다.

참조 - 12 factor app

RabbitMQ 시작하기

Installation on ubuntu

ubuntu 에 rabbitmq 를 설치하고 실제 동작시켜 보자.

본 글에서는 실제 ubuntu 피씨가 아닌 docker ubuntu image 를 사용하도록 한다.

Ubuntu image 실행

아래 명령어를 통해 도커 우분투 이미지를 실행하고 콘솔에 들어가도록 하자.

1
2
docker pull ubuntu
docker run -it ubuntu

sudo 및 curl 설치

기본 우분투 이미지에는 sudo 와 curl 명령어가 없기 때문에 아래 명령어를 통해 sudo 와 curl 을 설치해 준다.

1
2
3
apt-get update&&
apt-get install -y sudo &&
apt-get -y install curl

Erlang/OTP 다운로드

Rabbitmq 를 내부적으로 erlang/otp 를 사용하기 때문에 적합한 버젼을 설치해 주어야 한다.

하지만 현재 erlang 버전은 너무 오래되었기 때문에 rabbitmq 팀에서 별도의 erlang 을 관리하고 배포하고 있는데 이를 설치하기 위해 다음과 같은 절차를 거친다.

먼저 apt-key 에 다음 키를 추가한다.

1
2
apt-get install -y gnupg2 &&
apt-key adv --keyserver "hkps.pool.sks-keyservers.net" --recv-keys "0x6B73A36E6026DFCA"

데비안 계열 운영체제가 레포지토리를 등록하기 위해서 다음 파일에 레포지토리를 등록해 주어야 한다.

이를 통해 apt-get 명령어가 해당 레포지토리를 찾고 패키지를 설치할 수 있다.

/etc/apt/sources.list.d/bintray.erlang.list 파일에

1
deb http://dl.bintray.com/rabbitmq-erlang/debian bionic erlang

를 추가하면 되며 아래와 같은 명령어를 통해 한번에 세팅할 수 있다.

1
2
3
4
5
cd /etc/apt/sources.list.d &&
touch bintray.erlang.list &&
echo "deb http://dl.bintray.com/rabbitmq-erlang/debian bionic erlang" >> bintray.erlang.list &&
echo register finished! &&
cat bintray.erlang.list

등록한 레포지토리를 설치해준다.

1
apt-get update

RabbitMQ 및 rabbitmq-server 설치

1
2

curl -s https://packagecloud.io/install/repositories/rabbitmq/rabbitmq-server/script.deb.sh | sudo bash && apt-get install -y rabbitmq-server

Getting started

서버 시작하기

1
2
3
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app

큐 비우기

1
sudo rabbitmqctl purge_queue queue_name

Final dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
FROM ubuntu

# set working directory
WORKDIR /

# update apt packages
RUN rm -rf /var/lib/apt/lists/*
RUN apt-get update

# install sudo and curl command
RUN apt-get install -y sudo && apt-get -y install curl

# register key for apt third party repository
RUN apt-get install -y gnupg2
RUN apt-key adv --keyserver "hkps.pool.sks-keyservers.net" --recv-keys "0x6B73A36E6026DFCA"

# register repository
RUN cd /etc/apt/sources.list.d && touch bintray.erlang.list && echo "deb http://dl.bintray.com/rabbitmq-erlang/debian bionic erlang" >> bintray.erlang.list && echo register finished! && cat bintray.erlang.list

# install repository
RUN apt-get update

RUN curl -s https://packagecloud.io/install/repositories/rabbitmq/rabbitmq-server/script.deb.sh | sudo bash

RUN apt-get install -y rabbitmq-server
EXPOSE 3001
EXPOSE 5672
EXPOSE 15672

CMD [ "rabbitmq-server","start" ]

Installation

Eb 를 설치하기 위해 awscli 와 awsebcli 를 설치해 준다.

1
2
3
pip install awscli
pip install awsebcli --upgrade --user
brew install awsebcli

elastic beanstalk 을 초기화 하고 생성 및 배포한다.

1
2
3
eb init
eb create
eb deploy

Configuration

Beanstalk 을 통해 어플리케이션을 배포하게 되면 여러개의

filebeat 와 함께 배포하기

만약 프로젝트 내에서 Elastic Stack 을 사용하여 어플리케이션에서 나오는 모든 로그 및 데이터를 filebeat 를 통해서 배포해야 할 필요가 있다면, filebeat 와 함께 multi docker 모드를 사용하여 beanstalk 에 배포할 수 있다.

먼저, filebeat 를 단독으로 ECR(Elastic Container Resistry) 에 올린다.

이 과정에서 반드시 아마존에 로그인이 되어야 하며, 다음과 같은 명령어를 통해 ecr 에 별도로 로그인을 수행한다.

아래 명령어는 기본 aws 유저가 아닌 eb cli 접근이 가능한 프로그램 유저를 설정해 두고 해당 유저의 정보로 로그인을 함을 의미한다. 아래 명령어를 입력하면 AWS 에서 로그인을 하기 위한 명령어를 output 으로 제공하는데 해당 문자열을 복사하여 다시 cli에 입력하면 로그인이 완료된다.

1
aws ecr get-login --no-include-email --profile eb-cli

아래 명령어를 통해 aws access key 와 secret key 를 등록하고 사용할 수 있다.

1
aws configure

다음과 같은 command option 을 통해 내가 어떤 사용자인지 알리고, 미리 configure 되어있는 해당 사용자의 정보로 authentication 을 진행할 수 있다.

1
aws [command] --profile 사용자이름

VS Code Debugging 시작하기

vs code 에서 디버깅을 함에 있어 Launch 와 Attach 모드가 존재한다.

우리는 .vscode 의 launch.json 설정파일을 수정함으로써 디버거를 attach 하시 전에 디버그 모드에서 어떻게 앱을 실행할지를 알려주는 것이다.

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×