Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

Contents

Proxy에 대하여

Proxy 서버는 클라이언트가 자신을 통해서 다른 네트워크 서비스에 연결하게 중계해주는 소프트웨어다.

 프락시 서버 개요

웹 서비스를 예로 들어보자. 클라이언트(웹 브라우저)는 웹 서버에 직접 연결하는 대신에 프락시 서버에 연결해서 웹 페이지를 요청한다. 이 요청을 읽은 프락시 서버는 웹 서버에 요청을 전달하고, 응답을 받아서 클라이언트에 전송한다. 인터넷 서비스의 규모가 커지면서, 분산 시스템으로 서비스가 구성되는 경우가 많다. 프락시 서버를 이용하면 분산 시스템을 뒤에 숨기는 방식으로 시스템을 단순화 할 수 있다.

클라우드 기반 시스템에 서비스를 구축할 경우, 분산 시스템으로 구축하는 경우가 많다. 클라우드 시스템 구축의 핵심 요소며, 사용자 입장에서도 이래 저래 응용할 거리가 많다.

클라우드 환경에서의 프락시

가상화와 클라우드 관련된 일을 하고 있다. 과거에는 직접 만드는 일을 했었고, 최근 들어서는 클라우드 인프라 위에서 서비스를 만드는 일을 하고 있다. 클라우드를 기반으로 하는 서비스들은 높은 확률로 분산이 된다. 또한 서비스를 구성하는 자원(데이터베이스, 인스턴스, 컨테이너 등)은 인터넷으로 부터 격리된 공간에 만들어 진다. 따라서 인터넷으로 부터의 요청을 받아서 내부(AWS의 경우 VPC)의 분산된 자원에 요청을 전송하는 프락시가 매우 중요하다.

다양한 프락시 타입 중에서 HTTP를 기반으로 하는 리버스 프락시에 대해서 살펴볼 생각이다.

리버스 프락시

리버스 프락시는 일반적인 인터넷 서비스에서 널리 사용하고 있다. 리버스 프락시는 유저의 요청을 받아서 반대편(reverse)네트워크에 있는 인터넷 서버에 전달 하는 일을 한다. 리버스 프락시 서버는 단순히 요청을 전달하기만 할 뿐으로 요청의 처리는 뒷단에 있는 웹 서버들이 맡아서 한다. 따라서 하나의 리버스 프락시 서버가 여러 웹 서버로 요청을 전달하도록 구성 할 수 있다. 예컨데 로드 밸런서로의 역할을 수행 할 수 있다. 실제 HAProxy, NginX, Apache 웹서버들이 가지고 있는 리버스 프락시 기능을 이용해서 소프트웨어 기반의 로드밸런싱 환경을 구축하기도 한다.

소프트웨어 기반인 만큼 전용 로드밸런서 보다는 성능이 떨어질 수 있지만, 저렴한 비용과 이에 따르는 무지막지한 확장성으로 단점을 커버하고 있다. 클라우드 환경에서 사용할 로드밸런서라면 소프트웨어로 구축하는게 거의 당연하게 여겨진다.

리버스 프락시 테스트 환경

처음엔 오픈소스 프락시로 HAProxy를 생각했다. HAproxy는 로드밸런서로 사용하기에는 괜찮은 선택이었으나 다양한 활용이 필요한 리버스 프락시로 사용 하기에는 기능에 한계가 있었다. 그래서 NginX를 사용하기로 했다. NginX외에 Apache를 사용 할 수도 있었겠는데, 웹서버로서의 다양한 기능 보다는 NginX의 성능이 더 중요했다.

VirtualBox로 테스트 환경을 만들었다.

 Reverse Proxy를 위한 테스트 환경

  • 두 개의 호스트 전용 네트워크를 만들었다. 하나는 외부에 연결하기 위해서 사용하고 다른 하나는 내부에 있는 웹 서버에 연결하기 위해서 사용한다.
  • Proxy server는 유저의 요청을 받아서 밑에 있는 web-01과 web-02로 요청을 중계한다.

정적 리버스 프락시 서버 구성

가장 기본적인 리버스 프락시 구성이다. 일반적인 로드밸런서라고 보면 된다. upstream영역에 프락시 대상 호스트의 목록을 설정하면 된다. upstream은 proxy할 타겟 서버를 설정하기 위해서 사용한다.
upstream test_proxy {
    server web-01; 
    server web-02; 
}
test_proxy는 upstream의 이름이다. NginX는 하나 이상의 upstream으로 구성할 수 있으며, 이름으로 구분할 수 있다.

이제 test_proxy를 upstream으로 사용하도록 server 설정을 변경하면 된다. 아래는 완전한 설정파일이다.
# cat /etc/nginx/sites-available/default
upstream test_proxy {
    server web-01;
    server web-02;
}
server {
    listen 80 default_server;
    listen [::]:80 default_server;

    root /var/www/html;

    index index.html index.htm index.nginx-debian.html;

    server_name _;

    location / {
        proxy_pass http://test_proxy;
    }
}
location / 에 대한 모든 요청에 대해서, test_proxy로 중계하라고 설정했다.

로드 밸런싱 메서드

Nginx는 4개의 로드밸런싱 메서드를 제공한다.

라운드로빈(Round-robin)은 기본으로 사용하는 메서드로 모든 서버에 동등하게 요청을 분산한다.
upstream test_proxy {
    server web-01;
    server web-02;
}

least_conn은 연결이 가장 작은 서버로 요청을 보낸다.
upstream test_proxy {
    least_conn;

    server web-01;
    server web-02;
}

ip_hash는 클라이언트 IP주소를 기준으로 요청을 분배한다. IP주소가 같다면, 동일한 서버로 요청을 전송한다.
upstream test_proxy {
    ip_hash; 

    server web-01;
    server web-02;
}

hash는 유저가 정의한 key나 변수 혹은 이들의 조합을 해시해서 분산한다. key로 소스 IP나 포트 URI 등을 사용 할 수 있다.
upstream test_proxy {
    hash $request_uri consistent;

    server web-01;
    server web-02;
}
hashconsistent파라메터를 사용 할 수 있다. consistent를 사용하면 Ketama 컨시스턴시 해시 알고리즘을 이용해서 upstream 그룹에 서버가 추가 되거나 삭제 될 때, 키의 분배를 최소화 함으로써 캐시 실패를 줄일 수 있다.

least_time메서드는 NginX Plus에서 지원한다. 평균 레이턴시와 연결을 기준으로 검사해서 로드가 적은 서버로 요청을 보낸다.
upstream test_proxy {
    least_time header; 

    server web-01;
    server web-02;
}
header는 첫번째 바이트를 받을 때까지의 지연, last_byte는 모든 요청을 받을 때까지의 지연시간을 기준으로 삼는다.

프락시 응용

정적 프락시는 upstream 서버가 결정되 있다. 일반적인 웹 서비스라면 이 정도로 사용하는데 문제 없을 것이다. server가 추가되거나 삭제 할 경우 upstream 그룹을 수정해야 하겠지만 수정이 빈번하지 않으니 문제될게 없다.

하지만 클라우드 환경에서는 프락시 설정이 동적으로 이루어져야 할 필요가 있다. 컨테이너 기반으로 웹 서비스를 제공한다면, 빈번하게 프락시 설정이 바뀔 수 있을 것이다. 웹 서비스들이 수시로 올라올 테고, 웹 서비스를 위한 도메인 이름도 계속 변경될 것이다. 이런 환경에서의 프락시 응용에 대해서 살펴보려 한다.

도메인 이름 기반 dynamic Proxy

Slack이나 Jira와 같은 클라우드 기반의 인터넷 서비스를 한다고 가정해보자. 유저가 클라우드 서비스를 구축하면, 도메인도 함께 만들어줘야 할 것이다. 예컨데 jira라면, user-01.jira.com, user-02.jira.com과 같이 유저별로 도메인을 제공해야 한다. 대략 아래와 같은 구성이 될 것이다.

 Domain Name 기반 Proxy

이 방식의 서비스를 위해서는 DNS 서버의 도움이 필요하다. DNS 서버에 아스테리크(*)도메인에 대해서 Proxy 서버를 CNAME 혹은 A 레코드에 등록한다. 호스팅 업체를 통해서 DNS 서비스를 받는다면, 아스테리크를 지원하는지 확인을 해야 한다. 대부분이 아스테리크를 지원하지만 몇 몇 지원하지 않는 호스팅 업체도 있다.

이 서비스는 다음과 같이 작동 한다. Jira를 기준으로 설명한다.
  1. 유저가 user_01이라는 이름으로 jira 서비스를 구입했다. Jira는 user_01.jira.com 도메인을 유저에게 할당한다.
  2. Jira는 user_01.jira.com을 위한 서비스 인스턴스를 만들고 내부 DNS에 이 정보를 등록한다.
  3. 유저가 브라우저로 user_01.jira.com에 접속하려 한다.
  4. ns.jira.com에 네임 정보를 요청한다. ns.jira.com은 *.jira.com에 등록된 Proxy의 IP를 되돌려 준다.
  5. 유저는 Proxy 서버에 접속한다.
  6. Proxy Server는 HTTP의 Host 필드의 user_01.jira.com을 읽어서, 이 도메인에 대한 IP를 찾는다.
  7. user_01.jira.com에 대한 인스턴스 IP로 요청을 proxy 한다.
이 서비스를 테스트 하려면, DNS 서버를 구축해야 한다. 꽤나 복잡 할 것 같지만 dnsmasq를 이용하면 간단하게 테스트 환경을 만들 수 있다. 아래는 테스트 환경이다.

 dnsmasq를 이용한 네임서버 기반 proxy

테스트를 위해서 4개의 인스턴스를 만들었다. 먼저 DNS Instance에 dnsmasq를 설치한다.
# apt-get install dnsmasq

*.test.priv에 대해서 proxy server ip를 알려주도록 설정을 변경했다.
# cat /etc/dnsmasq.conf
....
address=/.test.priv/192.168.56.10
이제 *.test.priv 요청에 대해서 NginX Proxy server의 IP를 반환한다.

그리고 web-01.test.priv와 web-02.test.priv를 등록한후 dnsmasq 서버를 리로드 했다.
# cat /etc/hosts
...
192.168.57.3 web-01.test.priv
192.168.57.4 web-02.test.priv

호스트 운영체제에서 curl로 테스트 할 생각이다. /etc/resolv.conf 파일을 수정했다.
# cat /etc/resolv.conf
nameserver 192.168.56.10

# service nginx reload

네임을 제대로 찾는지 테스트를 했다.
# nslookup web-01.test.priv
Server:		192.168.56.10
Address:	192.168.56.10#53

Name:	web-01.test.priv
Address: 192.168.56.2

이제 proxy server를 설정한다. nginx를 설치 한 후 설정파일을 수정했다.
# cat /etc/nginx/sites-available/default
server {
        listen 80 default_server;
        listen [::]:80 default_server;

        resolver 192.168.56.10;
        location / {
                proxy_pass http://$request_uri;
        }
}
resolver로 앞서 만든 dns server를 설정했다. nginx를 restart 한 뒤, 호스트 운영체제에서 테스트를 수행했다.
# curl web-01.test.priv
<h1>WEB-01</h1>
# curl web-02.test.priv
<h1>WEB-02</h1>
잘 된다. 도메인 이름 기반 Proxy는 PaaS나 SaaS 환경을 구축 하려고 할 때, 특히 유용하게 사용 할 수 있을 거다.

이 방식의 유일한 문제점은 아마도 dnsmasq 서버의 유지 보수일 것이다. 새로운 도메인이 추가 되면 /etc/hosts를 변경 한 다음 realod를 해줘야 한다. 단순한 파일 관리기는 하지만 그다지 깔끔하지는 않다.

SaaS/PassS 환경을 구축하려면 service discovery 시스템을 만들어야 한다. 아마도 DNS 기반으로 만들어야 할 건데, dnsmasq 클러스터를 구성 해야 할 것이다. 주키퍼를 이용해서 구축 할 수 있겠다.

 key/value 기반의 proxy 환경

  1. 주키퍼는 dnsmasq 노드들을 관리한다.
  2. 도메인 정보는 주키퍼에 저장된다.
  3. dnsmasq가 주키퍼에 저장된 도메인 정보로 /etc/hosts를 재구성하기는 쉽지 않을 것이다. 별도의 애플리케이션으로 /etc/hosts를 재구성하고 dnsmasq를 reload 한다.
  4. 다이나믹하게 도메인 정보가 바뀌는 서비스에서 dnsmasq는 효율적이지 않다. 새로 만드는 것도 생각해봐야 겠다.
Saas/PaaS를 위한 DNS 기반의 service discovery 시스템은 간단하게 다룰 만한 내용은 아니다. 언젠가 제대로 고민을 해봐야 겠다. 지금은 이정도로 하고 넘어간다.

정규표현을 이용한 dynamic proxy

컨테이너를 기반으로 하는 SaaS 인프라를 개발한다고 가정해보자. 이 인프라에는 host_01, host_02, host_03 3개의 호스트가 있다. 유저가 SaaS 서비스인 워드프레스(wordpress)를 요청하면, 3개의 호스트 중 적당한 호스트를 골라서 워드프레스 컨테이너를 배치한다. 이제 유저 요청이 들어오면, 유저에게 할당한 컨테이너로 프락시해줘야 한다. 정규표현을 이용해서 프락시를 해보기로 하자.

 정규표현을 이용한 다이나믹 프락시

프로세스는 다음과 같다.
  1. 유저 컨테이너를 만들고 나면, 컨테이너를 위한 도메인을 등록한다. 도메인 이름에는 애플리케이션 이름, 컨테이너의 ID, 호스트 번호등이 들어간다. wordpress_container001_01.test.priv에는 host_01에 있는 container001로 프락시하라 라는 정보가 담겨있다. 정규표현식을 이용해서 처리 할 수 있을 것이다.
  2. 물론 *.test.priv는 로드밸런서(LB)로 향하도록 도메인 작업을 해둬야 한다. 약간 응용하면 dnsmasq로 만들 수 있다. 이 과정은 다루지 않겠다.
  3. 유저 요청은 로드밸런서 밑에 있는 NginX 프락시 서버로 이동한다. NginX 프락시 서버는 정규표현식을 이용해서 프락시할 호스트의 위치를 찾는다.
  4. 유저 요청은 NginX에 의해서 워드프레스 컨테이너가 있는 호스트로 전달된다. 이 호스트에는 역시 NginX 기반으로 된 Local Proxy가 있는데, 정규표현식을 이용해서 컨테이너의 이름을 찾아서 프락시 한다.
NginX Proxy Server의 설정은 대략 다음과 같다.
server {
    server_name ~^([a-zA-Z0-9]+)_([a-zA-Z0-9]+)_([0-9]+)\.test\.org$;
    resolver 127.0.0.1;
    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
        proxy_pass http://host_$3;
    }
 }
$1은 SaaS 애플리케이션 이름, $2는 컨테이너 ID, $3은 호스트 번호다. 예를 들어서 http://wordpress_container001_01.test.priv 요청이 들어오면, http://host_01로 요청을 프락시 한다.

역시 hosts는 관리해야 하는데, DNS, NIS, key/value 시스템 중 적당한 방식을 선택해야 한다. WAF(NAXSI) 모듈등이 붙어 있고, 검증된 NginX를 이용하는게 깔끔하긴 한데 key/value 시스템과 붙일려면 고민이 좀 필요하다. 직접 개발하면 key/value 시스템과 쉽게 연동할 수는 있겠지만, NAXSI등을 붙일 수 없다는게 맘에 걸린다. 물론 앞단에 (단지 WAF 기능을 위해서)NginX를 두는 방법도 있긴하지만 소프트웨어 레이어가 늘어나는게 또 맘에 걸리고..