만쥬의 개발일기
article thumbnail

구축하고자 하는 최종 아키텍쳐는 다음과 같다.

3편까지 모두 따라하고 나면 다음과 같은 아키텍쳐를 구축하게 된다.

(단, 추후 EC2가 한 개 더 필요해진다.)

먼저 이번장에서는 스프링으로 구축한 서버를 nginx로 로드밸런싱 하는 부분을 다뤄볼 것이다.

이번 장에서 목표로 하는 아키텍쳐는 다음과 같다.


위 그림이 현재 목표인 아키텍쳐이다. (물론 최선은 각각을 하나의 EC2에 두는 것이다.)

  • Docker를 사용해 한 컴퓨팅 자원에서 웹서버와 WAS를 분리한다.
    • 각각의 이미지가 분리되어있기에 인프라 구축에 용이하다.
  • Nginx를 사용한 로드 밸런싱
    • 서버로 들어오는 요청을 8081,8082에 띄워진 두 WAS로 나누어 부하를 분산시킬 수 있다.
  • 무중단 배포
    • 배포 시 하나의 서버에 배포를 진행한다면 반드시 다른 하나의 서버는 살아 있기 때문에 무중단 배포가 가능하다. (추후 Blue Green 아키텍쳐로 업그레이드 예정)

 

다룰 내용

docker
nginx
docker-compose

 

개발환경

Ubuntu 20.04 (개발 PC)
Spring boot 3.2.2
java17
gradle
Nginx
Ubuntu 20.04 (배포 PC)
Docker 24.0.2
Dokcer-compose 2.24.6

테스트를 위한 API 작성

먼저 나중에 할 테스트를 위해 Spring 서버에 test용 API를 작성한다.

이 API는 서버가 가지고 있는 randomUUID를 반환한다.

만약 로드밸런싱에 성공해, API를 요청할 때마다 요청하는 서버가 바뀌게 된다면, UUID 응답도 바뀔 것이다.

Spring Dockerfile 작성

먼저 Spring으로 작성된 프로그램은 이미 존재한다는 가정하에, Dockerfile을 작성해보자.

# 1. Gradle을 사용하여 소스 코드 빌드를 위한 이미지
FROM gradle:jdk17 as builder

# 소스 코드를 이미지에 복사
WORKDIR /app
COPY . .

# Gradle을 사용하여 애플리케이션 빌드
RUN gradle build

# 최종 이미지를 위한 OpenJDK 이미지
FROM openjdk:17-jdk

# 빌드된 JAR 파일을 복사
ARG JAR_FILE=./build/libs/nsfServer-0.0.1-SNAPSHOT.jar
COPY --from=builder /app/${JAR_FILE} app.jar

EXPOSE 8080

# 시스템 진입점 정의
ENTRYPOINT ["java","-jar","/app.jar"]

빌드와 실행을 위해 두개의 이미지를 만들게 되는데, 최종 이미지를 만들 때 빌드를 위해 만들어진 이미지는 도커가 알아서 삭제해주니 메모리는 염려치 말자.

맥과 리눅스의 멀티 플랫폼 아키텍쳐를 위해 다음과 같이 빌드해준다.

docker buildx build --platform linux/arm64/v8,linux/amd64 -t {dockerhub id}/{project name}:{tag} --push .

이후 배포서버에서 해당 이미지를 다운받고, 실행시켜준다.

이때, 8081포트와 8082 포트 두 개의 포트로 서버를 각각 띄워주자.

docker buildx build --platform linux/arm64/v8,linux/amd64 -t {dockerhub id}/{project name}:{tag} --push .
docker run -d -p 8081:8080 --name tserver1 {dockerhub id}/{project name}:{tag}
docker run -d -p 8082:8080 --name tserver2 {dockerhub id}/{project name}:{tag}

dockerfile의 Expose와 -p 옵션의 차이점
expose는 내가 어느 포트를 개방할 것인지를 명시하는 것일 뿐, 실제로 개방하는 것은 아니다.
반드시 -p 옵션으로 포트포워딩을 해주어야 한다.
ex) 로컬의 8081포트를 도커의 8080포트와 매핑시키려면, -p 8081:8080 옵션

docker ps로 확인해보면, 다음과 같이 잘 올라간 모습이다

$ docker ps
CONTAINER ID   IMAGE              COMMAND                   CREATED       STATUS       PORTS                                              NAMES
08a17cb549ef   {imagename}        "java -jar /app.jar"      2 hours ago   Up 2 hours   0.0.0.0:8082->8080/tcp                             tserver2
6764c57d102e   {imagename}        "java -jar /app.jar"      2 hours ago   Up 2 hours   0.0.0.0:8081->8080/tcp                             tserver1

nginx 설정

우선 배포 서버에 nginx 이미지를 다운받아준다.

docker pull nginx

nginx이미지를 컨테이너화하고, 실행시켜준다.

docker run --name nginx -d --rm -p 9090:80 nginx

nginx 쉘에 접속한다.

docker exec -it nginx bash

현재 nginx 컨테이너는 빈 깡통이므로, 필수적인 도구들을 설치해준다.

순서대로 sudo, vim, net-tools(netstat 도구) 설치

apt-get update && apt-get install -y sudo
sudo apt-get install vim
sudo apt-get install net-tools

이제 nginx의 conf 파일을 수정해줄 것이다.

다음 명령어로 vi 편집기로 접속한 뒤, gg + dG 명령어로 파일의 내용을 전부 삭제해준다.

vi /etc/nginx/conf.d/default.conf

그리고 다음 내용을 채워넣고 저장한다.

upstream blue {
    server {blue 배포 서버 IP}:8081;
    server {blue 배포 서버 IP}:8082;
}    
upstream green {
    server {green 배포 서버 IP}:8081;
    server {green 배포 서버 IP}:8082;
}   
#upstream 을 이용해서 클라이언트 요청을 보낼 서버 아이피와 포트를 지정한다.

server {
    listen       80; #nginx의 IPv4 포트
    listen  [::]:80; #IPv6 포트
    server_name  localhost;


    access_log  /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;    
    include /etc/nginx/conf.d/version-env.inc;  

    location / {
        proxy_pass http://$version_env;  

        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;  
        proxy_set_header X-Real-IP $remote_addr;   

        # root   /usr/share/nginx/html;
        #index  index.html index.htm;
    }
}

upstream은 라운드 로빈이 기본 설정이고, least_conn,ip_hash등 다양한 옵션과 같이 사용 가능하다.

추가적인 설정들이 많으니 찾아보고 필요한 것을 적용시키자.

   
옵션 설명
ip_hash 같은 방문자로부터 도착한 요청은 항상 같은 업스트림 서버가 처리 할 수 있게 한다.
weight=n 업스트림 서버의 비중을 나타낸다. 이 값을 2로 설정하면 그렇지 않은 서버에 비해 두배 더 자주 선택된다.
max_fails=n n으로 지정한 횟수만큼 실패가 일어나면 서버가 죽은 것으로 간주한다.
fail_timeout=n max_fails가 지정된 상태에서 이 값이 설정만큼 서버가 응답하지 않으면 죽은 것으로 간주한다.
backup 모든 서버가 동작하지 않을 때 backup으로 표시된 서버가 사용되고 그 전까지는 사용되지 않는다.

 

그리고 같은 경로에 [version-env.inc](http://version-env.inc) 파일을 만들어준다.

vi /etc/nginx/conf.d/version-env.inc

다음 내용을 적고 저장해준다.

현재 기본 upstream 서버의 이름이 blue이므로, 다음과 같이 적는다.

만약 upstream 서버의 이름을 바꿔주게 된다면 이 파일도 수정해주어야한다.

set $version_env blue;

그리고 nginx를 reload 해준다.

nginx -s reload

테스트

이제 API를 요청해보면, 요청할때마다 다른 서버ID를 반환하는 것을 확인할 수 있다.

수정한 nginx 컨테이너를 이미지화하기

이처럼 nginx를 새로운 환경에 배포할 때마다 위 작업을 반복하지 않으려면, 우리가 수정한 nginx를 새로운 이미지로 만들어 관리하면, 더이상 같은 작업을 반복하지 않아도 된다.

호스트에서 다음 명령어를 입력해주자.

docker commit nginx mynginx 

이 명령어는 nginx라는 이름의 컨테이너를 mynginx라는 이미지로 만들어주는 것이다.

실행결과:

$ docker images
REPOSITORY                TAG                       IMAGE ID       CREATED         SIZE
mynginx                   latest                    dd015c9a8763   3 seconds ago   251MB

주의사항

conf 파일을 작성할 때 server {배포 서버 IP}:8081; 의 배포서버 IP 부분에 localhost 혹은 127.0.0.1을 사용해선 안된다. 도커는 격리된 환경을 제공하여 호스트 머신의 localhost와는 다른 localhost를 가진다.

ifconfig를 통해 사설 IP를 확인한 후, 해당 IP를 적용해주면, 상위 IP로 취급되어 정상적으로 접근 가능하다.

ex) 192.x.x.x

물론 아예 다른 컴퓨팅 자원을 사용해 구축하는 것이 더 바람직하다. (이때는 inbound와 outbound를 잘 신경써줄것)

docker-compose 설정

이제 우리는 이 프로젝트를 배포하기 위해서 다음 세 개의 이미지를 컨테이너화 시켜주어야한다.

  • 새로 생성한 mynginx 이미지
  • 서버1
  • 서버2

그러나 매번 세 개의 컨테이너를 실행시킨다는 것은 여간 귀찮은 일이 아니므로 docker-compose를 활용해보자.

배포 서버에 docker-compose를 설치해준다.

sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

docker-compose.yml 파일을 만들고, 다음과 같이 내용을 작성해준다.

version: '3'

services:
    nginx:
        image: mynginx
        ports:
            - '9090:80'
        extra_hosts:
            - 'host.docker.internal:host-gateway'
        container_name: nginx
        volumes:
            - nginx:/logs

    tserver1:
        image: eogns47/nsfserver:v1
        ports:
            - '8082:8080'
        container_name: tserver1
        volumes:
            - nsf_log:/logs

    tserver2:
        image: eogns47/nsfserver:v1
        ports:
            - '8081:8080'
        container_name: tserver2
        volumes:
            - nsf_log:/logs
volumes:
    nginx:
    driver: local

  nsf_log:
    driver: local

이제 -d 옵션을 주고 docker-compose를 실행시키면 스크립트대로 컨테이너들이 실행되는 것을 확인할 수 있다.

$ docker-compose up -d
[+] Running 3/3
 ✔ Container tserver2  Started                                                                                                                                                         0.7s
 ✔ Container tserver1  Started                                                                                                                                                         0.5s
 ✔ Container nginx     Started

볼륨 설정

docker-compose up으로 컨테이너를 생성하면, docker-compose가 위치한 폴더명 + 내가 작성한 볼륨명으로 볼륨이 생성된 것을 볼 수 있다.

$ docker volume ls
DRIVER    VOLUME NAME
local     kdh_nginx
local     kdh_nsf_log

생성된 볼륨의 위치는 다음 명령어로 확인한다.

$ docker volume inspect kdh_nsf_log
[
    {
        "CreatedAt": "2024-02-19T13:14:42+09:00",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/nsf_log/_data",
        "Name": "nsf_log",
        "Options": null,
        "Scope": "local"
    }
]

권한을 주고, 해당 볼륨 데이터가 있는 위치에 접근하면 마운트한 데이터(로그 등)을 확인할 수 있다.

sudo chmod +777 /var/lib/docker/volumes/

지금까지는 로드밸런싱을 했지만, scale-out을 하려면 docker-compose 파일을 고쳐주고, 포트도 따로 설정해주어야 하는 불편함이 있었다.

나중에는 docker-compose의 scale 옵션을 통해 scale-out을 더 편리하게 하는 방법을 공부해봐야겠다.

다음 포스팅에서는 이 로드밸런싱된 구조를 기반으로 CI/CD를 구축해보자.

 

reference

profile

만쥬의 개발일기

@KangManJoo

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!