[네트워크] 리버스 프록시

 

리버스 프록시

스터디 준비 자료로 작성한 글입니다. 원본은 github에서 확인 가능합니다.

📡 프록시 서버란?

  • 프록시 서버는 통상적으로 포워드 프록시 서버를 뜻함.
  • 포워드 프록시 서버는 클라이언트와 서버 사이에 위치하여 클라이언트의 요청을 서버에 전달하고 서버의 응답을 클라이언트에 전달하는 역할을 함.
  • 프라이빗 네트워크와 퍼블릭 인터넷 사이의 중개자 역할을 함.

forward_proxy_flow

  • 클라이언트는 프록시 서버를 “서버”로 인식하고, 서버는 프록시 서버를 “클라이언트”라고 인식함

👍🏻 포워드 프록시 서버의 장점

  • 캐싱
    • 클라이언트와 서버 사이에서 중개자 역할을 하며 전달하는 내용들을 캐싱함.
    • CDN 서버와 유사하게 캐싱을 통해 빠른 응답을 할 수 있음.
    • 자주 요청하는 리소스를 캐싱해두면 불필요한 외부 요청을 줄일 수 있고 네트워크 병목 현상을 방지해줌.
  • 보안
    • 포워드 프록시 서버를 통해 서버에 요청을 보내면, 클라이언트의 요청 정보를 숨길 수 있음.
    • 서버 입장에서는 프록시 서버를 거쳐 요청이 오기 때문에 클라이언트의 정보를 알 수 없음. (IP 주소, User-Agent 등)

🔎 포워드 프록시 서버 사용 예시

  • 특정 사이트 접속 제한
    • 프록시 서버를 통해 요청을 보내기 때문에, 프록시 서버에서 요청을 제한할 수 있음.
    • 예를 들어, 특정 IP 주소나 User-Agent를 차단하거나, 특정 시간대에만 요청을 받을 수 있음.

🔥 리버스 프록시 서버란?

  • 포워드 프록시와 다르게 하나 이상의 웹 서버 앞에 위치하여 클라이언트의 요청을 가로채는 서버임.
  • 클라이언트가 서버에 요청을 보내면 리버스 프록시 서버가 요청을 가로채고, 웹 서버에 요청을 전달함.

reverse_proxy_flow

  • 클라이언트는 서버를 “서버”로 인식하고, 서버는 리버스 프록시 서버를 “클라이언트”라고 인식함

👍🏻 리버스 프록시 서버의 장점

  • 로드 밸런싱
    • 수백만 이상의 사용자를 가진 서비스의 경우 단일 서버로는 모든 트래픽을 처리하지 못할 수 있음.
    • 리버스 프록시 서버를 사용하면 여러 대의 서버를 두고 로드 밸런싱을 통해 트래픽을 분산시킬 수 있음.
    • 요청을 받은 서버가 과부하 상태라면 다른 서버로 요청을 전달함.
  • 공격 보호
    • 리버스 프록시를 사용하면 웹 사이트 또는 서비스에서 원본 서버의 IP 주소를 공개하지 않아도 됨.
    • DDOS 공격 등의 위협으로부터 원본 서버를 보호할 수 있음.
  • 캐싱
    • 리버스 프록시 또한 포워드 프록시와 유사하게 캐싱 기능을 제공할 수 있음.
    • 서버에 요청되는 주기가 짧은 리소스들을 리버스 프록시 서버에 캐싱해두면 서버의 부하를 줄일 수 있음.
  • SSL/TLS 암호화
    • 리버스 프록시 서버를 사용하면 웹 서버와 클라이언트 사이에 SSL/TLS 암호화를 적용할 수 있음.
    • 웹 서버는 SSL/TLS 암호화를 지원하지 않아도 됨.

NginX로 리버스 프록시 서버 구축하기

  • nginx로 로드밸런싱 기능을 하는 간단한 리버스 프록시 서버를 구축해보았음.

http 서버

  • 간단한 http 서버.
  • 서버 구분을 위해 uuid를 생성하고 보여주는 서버임.
package main

import (
		"net/http"
		"github.com/google/uuid"
		"github.com/labstack/echo"
		"github.com/labstack/echo/middleware"
	   )

func main() {
	e := echo.New()

	e.Use(middleware.Logger())
	e.Use(middleware.Recover())

	e.GET("/", hello)

	e.Logger.Fatal(e.Start(":3000"))
}

func hello(c echo.Context) error {
	u, err := uuid.NewRandom()
	if err != nil {
		panic(err)
	}
	return c.String(http.StatusOK, u.String())
}

NginX 설정

  • nginx.conf 파일 설정
worker_processes 4;

events { worker_connections 1024; }

http {
	upstream echo {
		least_conn;
		server server1:3000 weight=10 max_fails=3 fail_timeout=30s;
		server server2:3000 weight=10 max_fails=3 fail_timeout=30s;
		server server3:3000 weight=10 max_fails=3 fail_timeout=30s;
		server server4:3000 weight=10 max_fails=3 fail_timeout=30s;
		server server5:3000 weight=10 max_fails=3 fail_timeout=30s;
		server server6:3000 weight=10 max_fails=3 fail_timeout=30s;
	}

	server {
		listen 80;

		location / {
			proxy_pass http://echo;
			proxy_http_version 1.1;
			proxy_set_header Upgrade $http_upgrade;
			proxy_set_header Connection 'upgrade';
			proxy_set_header Host $host;
			proxy_cache_bypass $http_upgrade;
		}
	}
}
  • nginx는 이벤트 기반으로 동작함.
  • upstream 설정 -> upstream 안의 서버들에게 request를 보냄.
  • least_conn: 연결된 커넥션이 가장 적은 서버에게 request를 보냄. 그 이의 방식으로는
    • round-robin(default): 라운드로빈으로 분배
    • hash: 해시값을 기반으로 분배
    • ip_hash: 클라이언트의 ip를 기반으로 분배
    • random: 랜덤으로 분배
    • least_time: 가장 빠른 응답시간을 가진 서버에게 request를 보냄.

Docker image 빌드

  • echo/Dockerfile
FROM golang:1.18.1-alpine

ENV GO111MODULE=on

WORKDIR /src

COPY . /src

RUN go build -o main

EXPOSE 3000
CMD ["./main"]

` $ docker build -t echo .`

  • nginx/Dockerfile
FROM nginx:1.15-alpine

COPY nginx.conf /etc/nginx/nginx.conf

` $ docker build -t nginx_server .`

Docker compose

  • docker-compose.yml
version: '3.1'

networks:
  lb_network:
    driver: bridge

services:
  nginx:
    container_name: nginx_server
    image: nginx_server
    restart: always
    ports:
      - "8080:80"
    networks:
      - lb_network
    depends_on:
      - server1
      - server2
      - server3
      - server4
      - server5
      - server6
  server1:
    container_name: server1
    image: echo
    restart: always
    expose:
      - "3000"
    networks:
      - lb_network

  server2:
    container_name: server2
    image: echo
    restart: always
    expose:
      - "3000"
    networks:
      - lb_network

  server3:
    container_name: server3
    image: echo
    restart: always
    expose:
      - "3000"
    networks:
      - lb_network

  server4:
    container_name: server4
    image: echo
    restart: always
    expose:
      - "3000"
    networks:
      - lb_network

  server5:
    container_name: server5
    image: echo
    restart: always
    expose:
      - "3000"
    networks:
      - lb_network

  server6:
    container_name: server6
    image: echo
    restart: always
    expose:
      - "3000"
    networks:
      - lb_network
  • docker network 내부에서 자체적으로 DNS 서버를 가지고 있기 때문에 컨테이너 이름으로 접근 가능함.

결과

docker-compose

  • docker 컨테이너들과 네트워크가 생성된 것을 볼 수 있음

load-balancing

  • 앞에서 생성한 http 서버는 서버 생성 시 고유한 uuid 값을 받아 / 경로로 요청이 오면 해당 uuid를 반환하는 서버임.
  • nginx 서버에 접근하면 nginx 서버는 upstream 설정에 따라 http 서버로 로드밸런싱하여 request를 보냄.