서비스를 운영할 때 단일 서버로 구성된 애플리케이션이 속도는 가장 빠르다. 하지만 여러 가지 애로사항이 존재한다. 물리적인 한계에 도달했을 때 확장이 불가능하다.


가장 단순한 방법은 서버 부품(Cpu, Memory, Disk)를 더 빠른 부품으로 교체한다. 기존 업무프로세스에 변동이 없는 대신 큰 비용을 사용해야 한다. 대기업에서 주로 사용하는 방법이며, 수직 확장 Scale up이다.


소규모 기업은 보통 비용 사용에 부담을 느끼기 때문에 다른 방법을 모색하는데, 서버를 같은 구성으로 추가하는 모습을 띠게 된다. 여기서 한가지 짚고 넘어가야 할 부분이 있다. 진입점이 여러 개가 되는데 한곳으로 만들어줘야만 클라이언트가 손쉽게 이용할 수 있다. 그렇지 않다면 직접 판단해서 접근해야 한다. 수평 확장 Scale out이다.


하나의 진입점에서 여러 개의 서버로 분배해주는 것을 Load Balancing 이라고하며 OSI 7 Layer 중 L4 Layer 이상이 되어야 tcp, udp등 프로토콜을 구분할 수 있다. 추가로 L7 Layer 애플리케이션 간의 프로토콜을 확인할 수 있으며 보통 http, https, smtp등 을 사용한다. 훨씬 많은 정보를 얻기 때문에 체계적인 Load Balancing이 가능하다.


HAProxy는 Scale out으로 확장할 때 진입점을 관리해주는 L4/L7 로드밸런서이며 tcp, http, https를 지원한다.



무료로 사용할 수 있는 HAProxy Community Edition이 있으며, 기업을 대상으로는 로드밸런서 HAProxy Enterprise Edition를 제공한다. 추가로 ALOHA Load Balancer는 하드웨어 장비에 HAProxy를 결합해서 판매하는 상품이다.


마이크로서비스로 아키텍처를 사용할수록 진입점이 많아지게 되는데, 이 부분을 HAProxy를 사용하면 효과적으로 관리할 수 있다.

GitHub, Instagram, Stackoverflow, Tumblr, Amazon web service, Openstack등 글로벌 기업들도 많이 사용하고 있다.


HAProxy Community Edition을 설치해보도록 하겠다. 사용하려면 아래 모듈이 설치되어 있어야 한다.


yum install gcc gcc-c++ pcre-devel openssl-devel


준비가 완료되면 아래 명령어를 이용하여 설치한다. Https 사용이 필요하다면 USE_OPENSSL 옵션을 활성화하도록 한다.


wget http://www.haproxy.org/download/1.8/src/haproxy-1.8.3.tar.gz
tar zxvf haproxy-1.8.3.tar.gz
make TARGET=linux2628 USE_PCRE=1 USE_OPENSSL=1 USE_ZLIB=1 
make PREFIX=/usr/local/haproxy DESTDIR= install


설정은 global, defaults, frontend, backend, listen 총 5가지 그룹으로 관리된다.



global은 프로세스 전체에 적용된다. 모든 영역에 적용하려는 값만 선언해야 한다.

defaults는 frontend, backend, listen 3가지를 선언했을 때 기본적으로 적용되는 옵션이다. 진입점을 1개만 선언해서 사용할 때에는 기본값에 명시해도 되지만, 진입점이 여러 개인 경우 각각 선언하는 편이 좋다.


frontend는 진입점을 선언하는 곳이며, 조건이 일치하는 경우 backend로 연결해주는 역할을 한다.

backend는 frontend가 실질적으로 연결할 server들을 선언하여 관리한다.


listen은 frontend, backend를 합쳐서 선언할 수 있다.


실제로 설정을 frontend, backend 2가지를 나눠서 하는 편이좀 더 명확한 것 같다. 


설치가 완료되면 /etc/haproxy.cfg을 생성하고 아래 샘플을 참고하여 작성하자.

실제 테스트 하면서 작성했던 설정 값이며, 필요한 옵션만 간략하게 정리하였다.


global
        daemon # 백그라운드에서 동작
 
        user    haproxy 
        group   haproxy
        log     127.0.0.1   local0
        log     127.0.0.1   local1 notice
        maxconn 4096 # 프로세스 별 최대 connection 갯수
 
        ssl-default-bind-options no-sslv3 # Bind SSL 프로토콜 정의
        ssl-default-bind-ciphers ECDHE-RSA-AES128-GCM-SHA256:... # Bind SSL 사용 알고리즘 목록 정의
 
        ssl-default-server-options no-sslv3 # Server SSL 프로토콜 정의
        ssl-default-server-ciphers ECDHE-RSA-AES128-GCM-SHA256:... # Server SSL 사용 알고리즘 목록 정의
 
        tune.ssl.cachesize 100000 # SSL Session Cache 크기 설정
        tune.ssl.lifetime 600 # SSL Session 유효시간 설정
        tune.ssl.default-dh-param 2048 # Diffie-Hellman 키 교환 시 최대 크기 설정
 
defaults
        maxconn 4096 # 프로세스 별 최대 connection 갯수
 
frontend https-in
        mode    http
        log     global
        option  httplog
        option  dontlognull
 
        bind *:80 # listening 되어야하는 ip, port 설정
        bind *:443 ssl crt ./key.pem ca-file ./key.ca verify optional alpn h2,http/1.1 # (listening 되어야하는 ip, port + ssl + http/1.1, http/2) 설정
 
        timeout client  30s # client에서 haproxy 접속 시 timeout 설정
        timeout http-keep-alive 5s # 새로운 http 요청이 들어오기까지의 대기시간
 
        # host 지정 모든문자열이 일치 할 경우
        acl stats hdr(host) -i test.haproxy.com 
        acl host_api_cluster hdr(host) -i test.server.com
 
        # host가 일치할 때 지정 된 backend로 진행
        use_backend haproxy_stats if host_haproxy_stats 
        use_backend test_server if host_api_cluster
 
backend stats
        mode    http
        log     global
        option  httplog
        option http-server-close # 응답이 완료되면 server 측에서 연결을 종료함
 
        stats enable # 모니터링 활성화
        stats auth id:password # 모니터링 인증 활성화 basic auth 아이디, 비밀번호 설정
        stats uri /stats # 모니터링 페이지 url 설정
        stats hide-version # 버전 숨김
 
backend test_server
        mode    http
        log     global
        option  httplog
 
        balance leastconn # 요청 분배 알고리즘 설정
 
        timeout connect 10s # haproxy에서 server 접속 시 timeout 설정
        timeout check   5s # haproxy에서 check 목적으로 접속 시 timeout 설정
        timeout server  30s # haproxy에서 server 접속 시 timeout 설정
        timeout http-keep-alive 5s # 새로운 http 요청이 들어오기까지의 대기시간
 
        acl valid_method method GET POST PUT DELETE # 허용할 메소드 설정
        http-request deny if ! valid_method # 메소스 조건 확인
 
        http-request replace-value Host (.*):.* 1 # server에 client에서 요청한 Host 전달
        http-request set-header x-forwarded-for %ci # server에 remoteAddress 전달
 
        option httpchk GET /status # server에 지정된 상태 체크 url로 요청하여 확인
        http-check expect status 200 string OK # 응답 시 상태코드 200 + 문자열 OK 확인
 
        default-server inter 100 rise 2 fall 5 # 검사 조건 (확인 주기 100/ms, 2번이상 성공시 정상 확인, 5번이상 실패 시 제외)
        server server-001 192.168.0.1:8080 check # 서버 alias, ip:port, 검사 여부
        server server-002 192.168.0.2:8080 check 
        server server-003 192.168.0.3:8080 check


작성이 완료되었다면 /usr/local/haproxy/sbin/haproxy -f /etc/haproxy.cfg 명령어를 입력하여 시작하자.

자세한 내용은 http://cbonte.github.io/haproxy-dconv/ HAProxy 1.8 Documentation를 참고 하도록한다.


참고 사이트

  • http://www.haproxy.org/
  • http://www.haproxy.com/
  • http://cbonte.github.io/haproxy-dconv/
  • https://networkel.com/osi-model-7-layer-network-communication/
  • http://d2.naver.com/helloworld/284659
  • https://www.haproxy.com/documentation/aloha/7-0/traffic-management/lb-layer7/health-checks/
  • https://www.haproxy.com/blog/ssl-client-certificate-management-at-application-level/
  • https://www.haproxy.com/documentation/aloha/6-5/haproxy/tls/
  • http://inor.tistory.com/35
  • https://www.haproxy.com/blog/haproxy-amazon-aws-best-practices-part-1/


프로젝트가 운영단계로 접어들게 되면, 안정성과 신뢰성이 중요하게 작용한다. 장애, 재해 등 발생했을 때 데이터 복구 작업이 필요한데 준비되어 있지 않아, 데이터가 유실되거나, 초기화되는 경우 신뢰성은 추락하게 된다.


대비하려면 반드시 백업/복구에 대해 준비를 해야 한다. MariaDB 백업/복구는 mysqldump, xtrabackup 2가지 방법이 있다.


mysqldump는 설치 시 기본적으로 제공되는데, SQL 기반으로 백업되기 때문에 대응이 xtrabackup 보다는 편리하다.

운영 초기에는 별문제 없이 사용할 수 있는데, 데이터양이 많아졌을 때 한계점에 부딪치게 된다. 

모든 데이터를 메모리에 올려놓고 SQL로 변환하는 만큼 서버에 부담이 많이 가고, 백업도 많은 시간이 소요된다. 마찬가지로 복구도 모든 SQL이 실행되어야 하므로 같은 문제점을 가지고 있다.


xtrabackup은 Mysql, MariaDB 모든 버전을 지원하는 백업 솔루션이다. facebook은 엄청난 양의 데이터를 생성하는데 백업은 xtrabackup를 사용하고 있다.


“Facebook users create a vast amount of data every day. To make sure that data is stored reliably, we back up our databases daily. Facebook was an early adopter of incremental backup in Percona XtraBackup.”


대표적인 기능은 빠르고 안정적으로 백업/복구가 가능하고, 백업 중 중단없이 트랜젝션 처리가 가능하다. 모든 데이터를 가공하는 게 아닌, data 파일을 복제하는 것을 기반으로 하기 때문이다. 추가로 증분 백업을 지원하기 때문에 데이터양이 많아도 시점별로 관리가 가능하다.


사용하려면 별도로 설치해야 한다. linux에서만 지원하기 때문에, windows에서는 사용할 수 없다.

설치 가이드: https://www.percona.com/doc/percona-xtrabackup/LATEST/installation.html


설치가 완료되었다면 innobackupex 명령어를 통해서 전체 백업을 진행해보자.


백업 하기 전 주의사항이 있는데 엔진이 InnoDB인 경우 백업을 완료하는 시점에서 현재 스키마를 저장하기 위해 일시적으로 'Global Lock'을 걸게 되는데, 그 시간 동안 write 작업은 대기 상태가 되고, Lock이 해제되는 순간 진행된다. 데이터 파일이 크다면 길어질 수 있기 때문에 "--no-lock" 옵션을 사용하게 되면 이슈는 사라진다. 처리 시간은 DISK I/O의 영향을 많이 받으며, 가능하다면 별도의 디스크에 백업하는 것을 권장한다. 


전체 백업 시에는 MariaDB 서버의 설정 파일을 "--default-file"에 명시해야 한다. 서버 메모리가 여유가 된다면 "--use-memory"를 늘려서 처리하면 빠르게 진행할 수 있다. 백업이 완료된 후 "/backups"로 이동하여 파일을 확인해보면 data 파일이 이동된 것을 확인할 수 있다.


innobackupex \
        --default-file=my.cnf \
        --backup \
        --host=${backup_host} \
        --port=${backup_port} \
        --user=${backup_user} \
        --password=${backup_password} \
        --parallel=4 \
        --no-lock \
        --use-memory=4gb \
        /backups


데이터 복구를 진행하기 전 반드시 MariaDB 서버를 종료시키고, data 파일은 비워줘야 한다. 복구 시 파일 덮어쓰기는 불가능하다.

"/backups"를 data 파일 경로에 복사하여 복구 하므로 MariaDB 접속은 하지 않아도 된다.


복구 하기 전 "--apply-log" 옵션을 넣어서 준비상태로 만들어야 한다.


innobackupex \
        --use-memory=4gb \
        --apply-log \
/backups


완료되면 "--copy-back" 옵션을 넣고 복구를 진행한다.


innobackupex \
        --default-file=my.cnf \
        --parallel=4 \
        --copy-back \
        --use-memory=4gb \
        /backups


data 경로의 권한을 mysql로 변경해야 시작 시 권한 문제가 발생하지 않는다.


chown -R mysql. /mariadb/data


MariaDB를 시작하면 복구된 데이터를 확인할 수 있다.


만약 시간이 허락한다면, xtrabackup를 도입해서 장기적으로 장애 및 재해에 미리 대비하는 편이 안전하고 신뢰성을 높이는 길이라고 생각된다.


참고 사이트

  • https://www.percona.com/doc/percona-xtrabackup/LATEST/index.html
  • [리얼타임] MariaDB 실전 활용 노하우 (6.1. xtrabackup 참고)


Java Application을 원격지에서 감시하려면 JMX를 활성화해야 하고, Client로는 jConsole, VisualVM으로 접근하여 확인해야 합니다. GUI 환경이 갖추어지지 않는 CLI 환경은 jstatd (Virtual Machine jstat Daemon)을 활성화하고 rmi (Java Remote Method Invocation)을 이용하여 확인해야 합니다.


jstatd -10080 -J-Djava.security.policy=./java.policy
jps ---v rmi:192.168.0.2:10080
jstat -gcutil -h20 -t rmi:17755@192.168.0.2:10080 1000 1000


Jolokia는 원격지에서 JMX에 접근을 목적으로 사용되는 Java Specification Request (JSR 160)을 구현해놓은 Agent이며, Client와 Agent간의 통신은 HTTP/JSON으로 요청, JSON으로 정보를 받습니다. 



Java Application에 결합된 형태로 배포할 수 있기 때문에 사용성이 좋습니다. 예시는 Spring Boot 기준으로 합니다. Jolokia 활성화를 위해 모듈을 추가합니다.


dependencies {
    compile 'org.jolokia:jolokia-core'
}


Http Endpoint, MBean name 중복방지를 위해 추가 설정이 필요합니다. application.properties에 추가합니다.


management:
    context-path
/manage
    security
:
        enabled
false
 
endpoints
:
    beans
:
        sensitive
false
    jmx
:
        unique-names
true
        enabled
true
    jolokia
:
        enabled
true


Jolokia Protocol은 https://jolokia.org/reference/html/protocol.html에서 확인할 수 있습니다.


버전 확인
GET /manage/jolokia/version
 
Heap Memory 사용량 확인
GET /manage/jolokia/read/java.lang:type=Memory
 
Thread 수 확인
GET /manage/jolokia/read/java.lang:type=Threading/ThreadCount
 
Thread Dump 확인
POST /manage/jolokia
{
   "type":"EXEC",
   "mbean":"java.lang:type=Threading",
   "operation":"dumpAllThreads",
   "arguments":[true,true]
}
 
GC 현황 확인
POST /manage/jolokia
{
    "mbean": "java.lang:type=GarbageCollector,*",
    "type": "read"
}


대표적으로는 Memory, Thread, GC를 확인했으며, 자세한 내용은 Jolokia Protocol 문서를 참고하시기 바랍니다.


참고 사이트

  • https://jolokia.org/
  • https://jolokia.org/reference/html/protocol.html
  • https://docs.spring.io/spring-boot/docs/1.5.9.RELEASE/reference/htmlsingle/#production-ready-jolokia


어렸을 때부터 지속되어 온 생활습관들이 세월이 흘러 중년이 되면 몸에서 질병으로 나타난다. 프로젝트의 요구사항을 기반으로 개발자가 탄생시킨 프로그램도 마찬가지로 에러 메시지로 상황을 알려준다. 


지속해서 눈감고 모른 체하게 되면 프로그램이 중단되는 사태가 발생하는데 질병을 무시했다가 큰 병을 얻는 경우와 같은 상황이다. 

사람은 정기적으로 건강검진을 받는 것처럼, 프로그램도 정기적으로 관리를 해야 한다. 


프로그래밍 언어마다 감시하는 방법이 존재하지만, 예시는 Java 프레임워크 Spring Boot 기준으로 하겠다. 


Spring Boot는 Actuator를 사용하면 프로그램의 상황을 감시할 수 있는데, Http Endpoint, JMX, 원격 SSH 3가지 방법으로 확인할 수 있다.


Actuator를 활성화하려면 모듈을 추가해야 한다. 


dependencies {
    compile 'org.springframework.boot:spring-boot-starter-actuator'
    compile 'org.springframework.boot:spring-boot-starter-security'
    compile 'org.springframework.boot:spring-boot-starter-remote-shell'
}


모듈이 정상적으로 컴파일되면 Http Endpoint, 원격 SSH가 활성화된다. port는 설정하지 않으면 기본값 2000이다.

Http Endpoint의 중복을 방지하기 위해 추가 설정이 필요하다. application.properties에 추가한다.


management:
    context-path: /manage
    security:
        enabled: false
    shell:
        ssh:
            enabled: true
            port: 18080
    health:
        db:
            enabled: true
        diskspace:
            enabled: true
 
endpoints:
    beans:
        sensitive: false


Http Endpoint 목록은 /manage/mappings로 요청하면 확인할 수 있다.


{
  "{[/manage/metrics/{name:.*}],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.MetricsMvcEndpoint.value(java.lang.String)"
  },
  "{[/manage/metrics || /manage/metrics.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  },
  "{[/manage/auditevents || /manage/auditevents.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public org.springframework.http.ResponseEntity<?> org.springframework.boot.actuate.endpoint.mvc.AuditEventsMvcEndpoint.findByPrincipalAndAfterAndType(java.lang.String,java.util.Date,java.lang.String)"
  },
  "{[/manage/configprops || /manage/configprops.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  },
  "{[/manage/mappings || /manage/mappings.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  },
  "{[/application || /application.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public org.springframework.hateoas.ResourceSupport org.springframework.boot.actuate.endpoint.mvc.HalJsonMvcEndpoint.links()"
  },
  "{[/manage/trace || /manage/trace.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  },
  "{[/manage/heapdump || /manage/heapdump.json],methods=[GET],produces=[application/octet-stream]}": {
    "bean": "endpointHandlerMapping",
    "method": "public void org.springframework.boot.actuate.endpoint.mvc.HeapdumpMvcEndpoint.invoke(boolean,javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse) throws java.io.IOException,javax.servlet.ServletException"
  },
  "{[/manage/autoconfig || /manage/autoconfig.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  },
  "{[/manage/beans || /manage/beans.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  },
  "{[/manage/env/{name:.*}],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EnvironmentMvcEndpoint.value(java.lang.String)"
  },
  "{[/manage/env || /manage/env.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  },
  "{[/manage/health || /manage/health.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint.invoke(javax.servlet.http.HttpServletRequest,java.security.Principal)"
  },
  "{[/manage/dump || /manage/dump.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  },
  "{[/manage/info || /manage/info.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  },
  "{[/manage/loggers/{name:.*}],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.LoggersMvcEndpoint.get(java.lang.String)"
  },
  "{[/manage/loggers/{name:.*}],methods=[POST],consumes=[application/vnd.spring-boot.actuator.v1+json || application/json],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.LoggersMvcEndpoint.set(java.lang.String,java.util.Map<java.lang.String, java.lang.String>)"
  },
  "{[/manage/loggers || /manage/loggers.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  }
}


대표적으로 많이 쓰이는 것은 2가지이다. 

상태 값 확인은 /manage/health를 사용하면 되고, 추가 설정을 하게 되면 redis, mysql 저장소도 확인할 수 있다.


{
  "status": "UP",
  "diskSpace": {
    "status": "UP",
    "total": 214734073856,
    "free": 195815051264,
    "threshold": 10485760
  }
}


Cpu, Memory, Threads, GC, Datasource를 감시하기 위해서는 /manage/metrics를 사용한다.


{
  "mem": 12496579,
  "mem.free": 8820382,
  "processors": 8,
  "instance.uptime": 81630586,
  "uptime": 81641671,
  "systemload.average": 0.05,
  "heap.committed": 12340992,
  "heap.init": 12582912,
  "heap.used": 3520609,
  "heap": 12340992,
  "nonheap.committed": 159528,
  "nonheap.init": 2496,
  "nonheap.used": 155587,
  "nonheap": 0,
  "threads.peak": 342,
  "threads.daemon": 234,
  "threads.totalStarted": 3086,
  "threads": 287,
  "classes": 14031,
  "classes.loaded": 14031,
  "classes.unloaded": 0,
  "gc.parnew.count": 522,
  "gc.parnew.time": 9718,
  "gc.concurrentmarksweep.count": 2,
  "gc.concurrentmarksweep.time": 605,
  "httpsessions.max": -1,
  "httpsessions.active": 0,
  "datasource.primary.active": 1,
  "datasource.primary.usage": 0.033333335,
  "datasource.slave.active": 0,
  "datasource.slave.usage": 0.0,
}


원격 SSH는 Spring Boot 2.0에서는 제거되었다. 하지만 기능이 매우 괜찮아서 아쉽기도 하다. 

별도로 유저, 비밀번호 설정을 하지 않으면, 최초 시작 시 로그에서 무작위로 생성되는 비밀번호를 확인해야 한다.


Using default security password: 6a37007e-01c9-465c-b9d8-95a7c88afd38


정상적으로 접속되면 help를 입력하여 확인하자.


$ ssh -18080 user@localhost
Password authentication
Password:
 
 :: Spring Boot ::  (v1.5.8.RELEASE) on localhost
> help
Try one of these commands with the -h or --help switch:
 
NAME       DESCRIPTION
autoconfig Display auto configuration report from ApplicationContext
beans      Display beans in ApplicationContext
cron       manages the cron plugin
dashboard  a monitoring dashboard
egrep      search file(s) for lines that match a pattern
endpoint   Invoke actuator endpoints
env        display the term env
filter     a filter for a stream of map
java       various java language commands
jul        java.util.logging commands
jvm        JVM informations
less       opposite of more
mail       interact with emails
man        format and display the on-line manual pages
metrics    Display metrics provided by Spring Boot
shell      shell related command
sleep      sleep for some time
sort       sort a map
system     vm system properties commands
thread     JVM thread commands
help       provides basic help
repl       list the repl or change the current repl
 
>


dashboard를 입력하면 Thread, Memory 현황을 실시간으로 확인할 수 있다.



Java에서 제공하는 jmap, jstat등을 이용한다면 기본적인 감시는 가능하겠지만, Spring Boot Actuator는 기능이 확장되어 좀 더 쉽게 상황을 진단할 수 있다. 


프로젝트 일정에 너무 치이다 보면 실수를 인지하지 못한 상태로 운영단계에 들어가게 되는데, 반드시 검수 일정을 확보하여 Spring Boot Actuator 기능을 기반으로 실수를 사전에 감시하여 제거했으면 한다.


참고 사이트

  • https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#production-ready


대부분 사람은 일상 속에서 아이디, 비밀번호를 입력합니다. 그 행동이 로그인입니다.


모두에게 공개되어도, 문제되지 않는 콘텐츠라면 로그인은 실효성이 없습니다. 네이버에서 로그인하고 나서 볼 수 있는 콘텐츠를 보십시요.


이메일, 캘린더, 메모, 주소록 등 공개되는 경우, 정상적으로 생활 할 수 없을 것입니다. 보호를 받아야하는 콘텐츠는 소유자인 것을 증명하는 수단이 필요합니다. 그 개념이 바로 Authentication(인증)이고, 로그인을 통해서 행해집니다. 전통적으로 사용하는 방법입니다.


소유자만 콘텐츠를 이용할 때에는 사용에 불편함이 없었습니다. 세월이 흘러 페이스북, 트위터, 인스타그램과 같은 SNS가 활성화되고 스마트폰도 활성화되면서 애플리케이션이 많이 생겨났습니다. 그로 인해 콘텐츠 사용도 필요하게 되었고 기존 방법을 사용하려면 콘텐츠 소유자가 아이디, 비밀번호를 매번 알려줘야 합니다. 신뢰할 수 없는 애플리케이션의 경우 어떻게 해야 할지, 망설여지게 됩니다.


결국 소유자는 허용하고 싶은 콘텐츠만 전달하고 싶은 상황에 직면하게됩니다. OAuth 2.0은 그 역할을 대신 해주는 권한 부여 프레임워크 입니다. 


소유자를 증명하는 Authentication(인증)과, Authorization(권한 부여)가 함께 구성되어야만 정상적으로 사용할 수 있습니다. 실제 사례를 한가지 들어보겠습니다.



게임 커뮤니티 사이트인 루리웹의 로그인 화면입니다. 그림 하단에 네이버, 다음 로그인 버튼이 보입니다.

소유자의 정보를 제공받는 조건으로 루리웹 로그인을 대체하는 기능입니다. 네이버 아이디로 로그인 버튼을 클릭하면 아래와 같이 로그인 화면으로 이동합니다.



소유자를 증명하기 위해 Authentication(인증)을 해야 합니다. 아이디, 비밀번호를 입력하여 로그인합니다. 



Authentication(인증)이 성공하면 소유자에게 동의를 구합니다. 동의하게 되면 Authorization(권한 부여)가 되고 네이버에 있는 프로필 정보를 루리웹에게 제공합니다.


위 과정이 모두 끝나면 네이버 프로필 정보로 루리웹을 사용할 수 있게 됩니다. 소유자는 아이디, 비밀번호를 알려주지 않고, 루리웹에게 프로필 정보를 전달하였습니다.


OAuth 2.0은 Authorization(권한 부여) 기능을 담당하며, 사용하려면 반드시 소유자를 증명할 수 있는 Authentication(인증)이 함께 구성되어야 합니다.


RFC 6749는 위에서 설명한 OAuth 2.0의 규칙이 정의된 문서입니다. 각 역할을 아래와 같이 설명하고 있습니다.


Resource Owner(리소스 소유자)

보호된 콘텐츠(예: 이메일, 캘린더, 메모, 주소록)에 접근할 수 있는 권한을 부여하는 소유자를 의미합니다.


Resource Server(리소스 서버)

보호된 콘텐츠를 관리하는 서버이며, 리소스 소유자가 권한을 부여했다는 것을 증명하는 Access Token을 이용하여 보호된 콘텐츠를 받을 수 있습니다.


Client(클라이언트)

리소스 소유자를 대신하여 리소스 서버에 보호된 콘텐츠를 요청하는 애플리케이션입니다. 


Authorization Server(권한 부여 서버)

리소스 소유자의 권한 부여를 위임받아서 진행하고, 성공적으로 인증된 클라이언트에게 그 결과로 Access Token을 발급합니다.

NPM (Node Package Manager)


패키지를 관리하는 매니저이며, 각 언어별로 pip, gem, maven등이 같은 역활을 수행하고 있습니다.

불편한점, 버그는 github, twitter를 사용하여 문의를 합니다. 필자가 사용을 시작했을때는 주로 NPM V2를 사용하였는데 dependence tree가 가장 큰 문제점이였습니다. "npm install package" 형태로 설치를 진행하는데 각 패키지가 하위로 꼬리를 물고 포함되어 있기 때문입니다.

아래 그림은 패키지의 관계를 설명하고 있습니다. A v1.0 → B v1.0 하위로 패키지가 의존하게 되는데, A → B → C → N 까지 의존하여도 제약이 없었고, N 까지 내려가는 경우, 너무 깊숙히 폴더를 생성하기 때문에 문제 발생은 빈번했습니다. 

이 점을 극복한게 NPM V3으로 패키지는 모두 동일선상에 위치하고, 버전이 다른경우에만 하위로 패키지를 이동시켜서 해소되었습니다.




패키지가 하위로 서로 의존하고 있기때문에, 하위 패키지가 취약점을 가지고 있는 경우 쉽게 발견하기가 어렵습니다. 패키지를 도입할때에는 "package.json"의 "dependencies"를 꼭 참고하여 추가확인하는 습관을 갖는편이 좋습니다.

NSP (Node Security Platform)


패키지의 취약점을 발견하여 알려주는 플랫폼입니다. 하위 패키지 정보를 모두 검색하여 취약점을 찾습니다.

프로젝트 경로에서 "nsp check" 명령어를 실행하면 하위 패키지와 관련정보가 기록된 주소를 출력합니다.


$ nsp check --output summary
(+) 1 vulnerabilities found
 Name    Installed   Patched   Path                                             More Info
 growl   1.9.2       None      mocha@3.5.0 > growl@1.9.2                        https://nodesecurity.io/advisories/14


웹 브라우저로 들어가서 확인하면 아래와 같은 화면이 나오는데, Command Injection 으로 분류되어 있고, Critical 9.8점을 기록하고 있습니다.

growl 라이브러리는 알람을 지원하는 기능을 지원하는데, 임의의 명령을 수행할 수 있는 취약점이 있습니다. 

취약점을 해결한 버전은 나오지 않았으며, 신뢰할 수 없는 명령어를 사용할 수 있다면 패키지 사용을 권장하지 않고 있습니다.

다른 취약점들도 https://nodesecurity.io/advisories 권고사항 목록을 참고하면 사전에 예방할 수 있을 것 입니다.



Retire 


NSP와 동일한 기능을 제공하며, 하위 패키지 정보를 모두 검색하여 취약점을 찾습니다.

프로젝트 경로에서 "retire" 명령어를 실행하면 동일하게 실행합니다.

출력방식만 다르고 관련정보는 nsp와 동일한 nodesecurity를 사용하고 있습니다.


$ retire
growl 1.9.2 has known vulnerabilities:  severity: high; summary: growl_command-injection; https://nodesecurity.io/advisories/146
growl 1.9.2


패키지를 도입할 때에는 마지막 Commit이 언제인지, 이슈는 정상적으로 처리되고 있는지 확인이 필요합니다.

위 조건이 성립했다면, NSP, Retire 툴을 이용하여 추가로 검사한다면 크리티컬한 위험요소는 많이 상쇄될 것으로 기대합니다.


관련 링크


  • npm: https://www.npmjs.com/
  • nodesecurity: https://nodesecurity.io


'Nodejs' 카테고리의 다른 글

Node.js Error 처리방법  (0) 2017.05.18
Node.js 설치하기  (0) 2017.03.26
Node.js certificate has expired Error 해결하기  (0) 2016.06.17
Node.js Callback에 관한 고찰  (0) 2016.05.30
Node.js v6 버전 업그레이드 후기  (0) 2016.05.29

Node.js는 Javascript로 개발한다. 아래 코드를 주목해보자.


'use strict';

try {
    null.error;
} catch (e) {
    console.error('catch error: ', e.stack);
}


평상시 에러 잡는 코드를 작성할 때 사용한다. 아무런 의구심도 들지 않는다.
자연스럽게 catch에서 실행될 것이란 걸 알기 때문이다.


아이러니하게도, 비동기 방식으로 개발하게 된다면 상황은 달라진다.
순차적으로 진행되는 코드에 익숙한 게 대부분이다. 들쑥날쑥 동작하는 코드를 보는 순간 당황할 수 있다.


'use strict';

const fs = require('fs');

try {
    fs.readFile('bad', (err) => {
        throw new Error('test error');
    });
} catch (e) {
    console.error('catch error: ', e.stack);
}


비동기 방식으로 파일을 읽고 있으며, 동일하게 에러를 발생시킨다.
catch로 들어오지 못하고, process exit 되는 상황에 놓이게 된다.
callback function은 try ~ catch scope에서 벗어나기 때문에 잡을 수 없다.


'use strict';

const fs = require('fs');

try {
    fs.readFile('bad', (err) => {
        try {
            throw new Error('test error');
        } catch (e) {
            console.error('fs catch error: ', e.stack);
        }
    });
} catch (e) {
    console.error('catch error: ', e.stack);
}


사용하던 대로, 벗어나고 있는 scope에 새로운 try ~ catch를 사용하면 생각대로 진행된다.


전적으로 의존한다면, try ~ catch 천국이 될 것이다.


좀 더 나은 방법으로 고민해보면, node.js의 domain module을 사용하면 된다.
현재도 Deprecated로 명시가 되어있으나, 마땅한 대안이 없어 유지되고 있는 것으로 보인다.


쉽게 보면, global scope, local scope 2가지가 전부다.


'use strict';

const fs = require('fs');
const domain = require('domain');

const fsProtect = domain.create();

fsProtect.on('error', (err) => {
    console.log(err);
    console.error('domain error: ', err.stack);
});

fsProtect.run(() => {
    fs.readFile('bad', (err) => {
        throw new Error('test error');
    });
});


run 함수는, try ~ catch scope와 동일하게 보면 이해가 쉽게 된다.
callback function의 영역도 scope에 포함 되기 때문에, error 이벤트로 진입하게 된다.


{ Error: test error
    at ReadFileContext.fs.readFile [as callback] (error.js:14:15)
    at FSReqWrap.readFileAfterOpen [as oncomplete] (fs.js:365:13)
  domain:
   Domain {
     domain: null,
     _events: { error: [Function] },
     _eventsCount: 1,
     _maxListeners: undefined,
     members: [] },
  domainThrown: true }


error 객체를 자세히 살펴보면, 다른 점이 한가지 있다. run 함수 scope 안에서 에러가 발생하는 경우,
domain 객체가 property로 제공되며, _events에 error 이벤트가 등록되어 있다. 이 점 때문에 error 이벤트를 사전에 등록해줘야만 한다.


간편하게 사용할 수 있는 대표적인 방법이며, 대부분 위와 같은 방식을 사용한다.
한가지 의구심이 드는 부분은, 원하는대로 scope 조절은 불가능 하다는 것이다.


'use strict';

const fs = require('fs');
const domain = require('domain');

const fsProtect = domain.create();

fsProtect.on('error', (err) => {
    console.log(err);
    console.error('domain error: ', err.stack);
});

fs.readFile('bad', fsProtect.intercept((err) => {
    throw new Error('test error');
}));


try ~ catch scope를 생각해보자. 벗어나는 부분만, intercept 함수로 scope 영역을 확대했으며,
동일하게 error 이벤트가 등록되어 있다. scope 조절은 입맛에 맞게 할 수 있다.


일반적으로 프로젝트에 투입되게 되면, web server app, scheduler app을 개발하게 된다.


'use strict';

const http = require('http');
const server = http.createServer();

const domain = require('domain');

server.on('request', (req, res) => {
    const httpProtect = domain.create();

    httpProtect.add(req);
    httpProtect.add(res);

    httpProtect.on('error', (err) => {
        res.statusCode = 500;
        res.setHeader('Content-Type', 'text/plain');
        res.end(`${err.message}\n`);
    });

    httpProtect.run(() => {
        // throw new Error('test error');

        res.statusCode = 200;
        res.setHeader('Content-Type', 'text/plain');
        res.end('Hello World\n');
    });
});

server.listen(3000, '0.0.0.0', () => {
    console.log('server listening');
});


위 코드는 global scope 형태를사용하고 있는데, add 함수는 scope 영역에 객체를 포함한다는 의미이며, 에러가 발생하는 경우, 등록된 error 이벤트로 진입하게 된다.


추가로 고민해야 할 부분이 있는데, 주석을 풀고 요청을 해보자. process exit 될 것이다. error 이벤트로 진입 시 추가적인 에러가 발생했기 때문이다. 보통 error 처리 함수로 정의하고 구현하는데, scope에서 벗어난 영역이므로, 다시 scope를 정의하려고 하는 생각이 일반적이다.


'use strict';

const http = require('http');
const server = http.createServer();

const domain = require('domain');

server.on('request', (req, res) => {
    const httpProtect = domain.create();

    httpProtect.add(req);
    httpProtect.add(res);

    httpProtect.on('error', (err) => {

        const httpErrProtect = domain.create();
        httpErrProtect.add(req);
        httpErrProtect.add(res);

        httpErrProtect.on('error', (err) => {
            console.error(err);
        });

        httpErrProtect.run(() => {
            res.statusCode = 500;
            res.setHeader('Content-Type', 'text/plain');
            res.end(`${err.message}\n`);
        });
    });

    httpProtect.run(() => {
        throw new Error('test error');

        res.statusCode = 200;
        res.setHeader('Content-Type', 'text/plain');
        res.end('Hello World\n');
    });
});

server.listen(3000, '0.0.0.0', () => {
    console.log('server listening');
});


별로 문제가 없어 보이긴 한다. scope는 중첩되는 순간, 새로 선언한 scope로 error 이벤트는 등록된다. 상위 scope의 error 이벤트는 동작하지 않는다. run 함수 중첩사용은 시나리오를 예측해서 작성해야만, 대응이 수월할 것이며, 그렇지 않은 경우, error 이벤트의 호출 시점을 예측 할 수 없다.


'Nodejs' 카테고리의 다른 글

Node.js Package 취약점 분석하기  (0) 2017.08.16
Node.js 설치하기  (0) 2017.03.26
Node.js certificate has expired Error 해결하기  (0) 2016.06.17
Node.js Callback에 관한 고찰  (0) 2016.05.30
Node.js v6 버전 업그레이드 후기  (0) 2016.05.29

Http 인증 방식 중 주소를 기반으로 한 인증방식을 제공한다.


authority = [ userinfo "@" ] host [ ":" port ]


rfc3986의 3.2 Authority에 따르면 userinfo는 user:password 형태를 준수해야 하며, 일반문자열은 보안이 취약하므로, 암호화된 문자열을 사용해야 한다. 예시는 아래와 같다.


http://user:password@test.com


참고


+ Recent posts