우선 원하는 구조는 위와 같으며, 흐름은 다음과 같다.
- nginx는 proxy역할을 하며, 별도의 인스턴스로 구성한다.
- WAS는 Spring Application이며, 총 두 대로 구성한다.
- 두 대의 WAS는 하나의 Database를 바라본다.
- 클라이언트는 정해진 도메인으로 접속하면, nginx에서 ip의 해시값에 따라 적절한 WAS로 요청을 분배하며, 각 WAS는 동일한 DB를 바라보고 있기 때문에 동일한 데이터를 응답할 수 있다.
사실 너무 간단해서 글로 쓰기도 뭣하지만.. 나중을 위해!
WAS 구축하기
Spring Application을 띄울 EC2 인스턴스를 하나 생성한다.
생성된 EC2에 접속한 뒤, 스프링을 띄울 수 있는 환경설정을 한다.
sudo apt update
sudo apt install default-jre
sudo apt install default-jdk
이후, 소스코드를 github에서 클론한다.
git clone -b {branch명} --single-branch {레포지토리 주소}
// 서브모듈이 포함되어있으며 레포지토리가 바라보고 있는 서브모듈을 가져올 경우
git clone -b {branch명} --single-branch {레포지토리 주소} --recurse-submodules
// 서브모듈이 포함되어있으며 최신의 서브모듈을 가져올 경우
git clone -b {branch명} --single-branch {레포지토리 주소} --remote-submodules
클론한 레포지토리에서 build.gradle이 있는 디렉토리로 이동한 뒤 소스코드 배포, 빌드를 진행한 뒤 실행한다.
./gradlew clean build
cd build/libs
java -jar -Dspring.profiles.active={activeProfile} *.jar &
이 과정을 두 개의 인스턴스에서 각각 진행한다.
이후 pull request 및 push에 따라 자동으로 빌드 및 실행이 진행되도록 하기 위해서는 jenkins에 연동하는 것이 좋다.
ps -ef | grep java
위 명령어를 통해 java -jar .. 이 프로세스로서 실행되고 있는지를 확인하는 것이 좋다. 빌드는 됐지만 실행이 안될수도..
nginx에 로드밸런싱 설정하기
두 대의 WAS를 모두 띄웠다면, nginx 역할을 담당할 인스턴스를 생성한다.
인스턴스가 생성되었다면, 먼저 도커를 설치한다.
sudo apt-get update && \
sudo apt-get install -y apt-transport-https ca-certificates curl software-properties-common && \
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - && \
sudo apt-key fingerprint 0EBFCD88 && \
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" && \
sudo apt-get update && \
sudo apt-get install -y docker-ce && \
sudo usermod -aG docker ubuntu && \
sudo curl -L "https://github.com/docker/compose/releases/download/1.23.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose && \
sudo chmod +x /usr/local/bin/docker-compose && \
sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
내가 진행할 nginx는 도커에서 설치하며, 우아한 테크코스에서 제공해주는 docker image를 사용한다. 나중에 서버에 직접 nginx를 설치해서 하는 방법도 시도해보아야겠다.
아무쪼록 도커가 설치되었다면, 이제 http://내도메인.한국 에 접속해서 사용할 무료 도메인을 생성한다. 그리고 TLS 설정을 진행한다.
TLS 설정
서버의 보안과 별개로 서버와 클라이언트간 통신상의 암호화가 필요하기에 TLS를 설정해준다. 평문으로 통신할 경우, 패킷을 스니핑할 수 있다.
letsencrypt를 통해 무료로 TLS 인증서를 사용해보자.
docker run -it --rm --name certbot \
-v '/etc/letsencrypt:/etc/letsencrypt' \
-v '/var/lib/letsencrypt:/var/lib/letsencrypt' \
certbot/certbot certonly -d '[도메인 주소]' --manual --preferred-challenges dns --server https://acme-v02.api.letsencrypt.org/directory
위 명령어를 통해 인증서를 생성하는 중, Please deploy a DNS TXT record under the name 이라는 문구가 나오면 다음을 진행한다.
TXT는 내도메인.한국의 도메인 설정에서 진행하는 것이며, 내도메인.한국에서 입력을 모두 마치고 저장한 뒤 약 5초정도 기다린 다음 letsencrypt 설정을 완료해주면 정상적으로 완료된다.
생성한 인증서를 현재 경로로 이동한다.
sudo cp /etc/letsencrypt/live/[도메인주소]/fullchain.pem ./
sudo cp /etc/letsencrypt/live/[도메인주소]/privkey.pem ./
vi Dockerfile 이라는 명령어를 통해 Dockerfile을 생성하고, 다음과 같이 작성한다.
// Dockerfile
FROM nginx
COPY nginx.conf /etc/nginx/nginx.conf
COPY fullchain.pem /etc/letsencrypt/live/[도메인주소]/fullchain.pem
COPY privkey.pem /etc/letsencrypt/live/[도메인주소]/privkey.pem
같은 디렉토리에서 vi nginx.conf 파일을 생성한 뒤, 다음과 같이 작성한다.
events {}
http {
# 생성한 두 대의 WAS private IP를 upstream app 아래에 작성한다.
upstream app {
server {WAS private IP}:{포트번호};
server {WAS private IP}:{포트번호};
}
# Redirect all traffic to HTTPS
server {
listen 80;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/[도메인주소]/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/[도메인주소]/privkey.pem;
# Disable SSL
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
# 통신과정에서 사용할 암호화 알고리즘
ssl_prefer_server_ciphers on;
ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;
# Enable HSTS
# client의 browser에게 http로 어떠한 것도 load 하지 말라고 규제합니다.
# 이를 통해 http에서 https로 redirect 되는 request를 minimize 할 수 있습니다.
add_header Strict-Transport-Security "max-age=31536000" always;
# SSL sessions
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
location / {
proxy_pass http://app;
}
}
}
nginx가 여러 서버에 분배해주므로 upstream 서버에 작성한다. 로드 밸런싱은 요청을 고루 분배해서 서버로 넘겨주는데, 이 때 어떻게 분배할 지 규칙을 정해줄 수 있다.
지금 위 nginx.conf처럼 아무 규칙도 upstream에 적지 않고 server ip만 작성한다면 default인 round-robin 방식으로 동작하게 된다.
upstream app {
ip_hash;
server {WAS private IP}:{포트번호};
server {WAS private IP}:{포트번호};
}
ip_hash를 적어주게 되면, ip를 해시값으로 나눠주게 된다. 이렇게 분배하게 되면 한번 접속했던 ip는 같은 해시값을 가지므로 매번 동일한 서버에게 요청을 보내게 된다. 세션으로 무언가를 관리하는 경우 이 방법을 사용하면 좋을 것 같다. (약간의 뇌피셜)
이 외에도 여러가지 방법이 있다.
- round-robin(default)
- hash - hash <키> 형태로 작성하며 해시한 값으로 분배한다. ex)hash $remote_addr (= 이는 ip_hash와 같다)
- ip_hash - 아이피를 해싱해 요청을 분배한다.
- random
- least_conn - 연결수가 가장 적은 서버에 가중치를 고려해서 분배
- least_time - 연결수가 가장 적으면서 평균 응답시간이 가장 적은 쪽을 선택해서 분배
자세한 내용은 nginx-stream-upstream-module 에서 확인할 수 있다.
방법을 설명하던 중 가중치라는 말이 나왔는데 특정 서버에 2배의 가중치를 주고 싶다면 다음과 같이 작성하면 된다.
upstream app {
server {WAS private IP}:{포트번호} weight=2;
server {WAS private IP}:{포트번호};
}
위와 같이 작성하면 규칙은 라운드 로빈으로 유지하되 첫번째 서버를 2배 더 많이 사용하게 된다.
nginx의 configuration을 통해 로드밸런싱 설정을 모두 마쳤으니, 도커 컨테이너를 띄우면 정상적으로 로드밸런싱이 가능해진다.
docker build -t nextstep/reverse-proxy:0.0.2 .
docker run -d -p 80:80 -p 443:443 --name proxy nextstep/reverse-proxy:0.0.2
끗