본문 바로가기
Docker

Docker와 Nginx를 활용한 로드 밸런싱 구축

by sangyunpark99 2025. 2. 26.
Docker를 사용해서 다중 서버를 두어 트래픽을 분산 시키기 위해서 어떻게 해야할까?

 

 

이번글은 Docker환경에서 Nginx로 로드 밸런싱을 적용시키는 방법에 대해 설명한 글입니다.

 

로드 밸런싱은 무엇이고, 왜 필요할까요?

 

로드 밸런싱(Load Balancing)은 여러 서버에 트래픽을 분산하여 시스템의 성능과 안전성을 높이는 기술입니다.

로드 밸런싱을 사용하면 서버에 과부하가 걸리는 문제를 방지하고, 트래픽이 많아질 때 여러 서버가 나눠서 처리할 수 있도록 도와줍니다.

이를 통해서 서비스 중단 없이 빠르고 안정적인 응답을 제공할 수 있습니다.

 

아키텍처

전체적인 아키텍처를 알아보겠습니다. docker와 nginx를 사용한 전체적인 구조를 그림으로 간단하게 나타내면 다음과 같습니다.

 

Docker에 nginx, springboot server 3개, mysql을 순차적으로 구성해보겠습니다.

먼저, docker-compose를 사용해서 nginx, springboot server, mysql의 컨테이너를 만들어줍니다.

 

docker-compose가 무엇일까요?

 

여러개의 Docker 컨테이너를 한번에 관리할 수 있게 도와주는 도구입니다.

docker-compose.yml 파일을 사용해서 여러 개의 컨테이너들을 쉽게 정의하고 실행하게 해줍니다.

 

docker-compose를 사용하지 않은 경우엔 어떻게 될까요?

 

아키텍처 그림에서 보이는 각 컨테이너를 docker run이라는 명령어로 각각 실행해야 합니다. 

docker-compose를 사용하는 경우, docker-compose up이라는 명령어로 각 컨테이너를 한번에 실행시켜 줍니다.

실행 시킨 모든 컨테이너를 종료하는 경우에는 docker-compose down이라는 명령어로 모든 컨테이너를 종료시킵니다.

 

프로젝트 기준으로 infratest 디렉터리 하위에 있습니다.

 

docker-compose.yml 파일은 다음과 같습니다.

version: '3.8'

services:
  nginx:
    image: nginx
    container_name: nginx
    ports:
      - "80:80"
    volumes:
      - ../infratest/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - server1
      - server2
      - server3
    networks:
      - my-network

  server1:
    build:
        context: ../
        dockerfile: ./infratest/app/Dockerfile
    container_name: server1
    expose:
      - "8080"
    depends_on:
      mysql:
        condition: service_healthy
    networks:
      - my-network

  server2:
    build:
      context: ../
      dockerfile: ./infratest/app/Dockerfile
    container_name: server2
    expose:
      - "8080"
    depends_on:
      mysql:
        condition: service_healthy
    networks:
      - my-network

  server3:
    build:
      context: ../
      dockerfile: ./infratest/app/Dockerfile
    container_name: server3
    expose:
      - "8080"
    depends_on:
      mysql:
        condition: service_healthy
    networks:
      - my-network

  mysql:
    image: mysql:8.0
    container_name: mysql
    restart: always
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: test
    networks:
      - my-network
    healthcheck:
      test: ["CMD", "mysqladmin", "ping","-h","localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

networks:
  my-network:
    driver: bridge

 

docker-compose.yml에 작성된 내용은 각각 어떤걸 의미할까요?

 

먼저, nginx라고 작성된 부분부터 보겠습니다.

nginx:
    image: nginx
    container_name: nginx
    ports:
      - "80:80"
    volumes:
      - ../infratest/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - server1
      - server2
      - server3
    networks:
      - my-network

 

이 부분은 Nginx를 컨테이너로 실행하고, 로드 밸런서 역할을 하도록 설정한 것입니다.

한 줄씩 어떤 의미를 갖고 있는지 알아보겠습니다.

 

image : nginx

 

docker에서 제공하는 nginx 공식 이미지를 사용하겠다는 의미입니다.

Docker Hub에서 nginx 이미지를 가져와서 컨테이너를 실행합니다.

 

ports:
      - "80:80"

 

호스트(localhost)의 80포트를 컨테이너의 80포트와 연결한다는 의미입니다.

브라우저에서 http://localhost로 접속하는 경우 NginX 컨테이너로 요청이 전달됩니다.

(http://localhost은 http://localhost:80을 의미합니다.)

 

volumes:
      - ../infratest/nginx/nginx.conf:/etc/nginx/nginx.conf:ro

 

 

로컬에 선언한 nginx.conf파일을 docker 컨테이너 내부의 /etc/nginx.conf로 매핑한다는 의미입니다.

로컬에서 nginx.conf를 수정하는 경우 컨테이너 내부 설정도 자동으로 변경됩니다.

 

nginx.conf파일은 프로젝트 기준 /infratest/nginx 내부에 존재합니다.

 

nginx.conf 파일은 어떠한 내용을 가질까요?
user  nginx;
worker_processes auto;

error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    upstream appservers {
        server server1:8080;
        server server2:8080;
        server server3:8080;
    }

    server {
        listen 80;

        location / {
            proxy_pass http://appservers;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}

 

설정한 내용을 간단히 나타내면 다음과 같습니다.

 

  • user nginx; 는 Nginx 프로세스를 실행할 사용자를 의미합니다.
  • worker_processes auto; 는 CPU 코어 수에 맞게 자동 설정함을 의미합니다.
  • error_log /var/log/nginx/error.log warn; 는 에러 로그 저장함을 의미합니다.
  • pid /var/run/nginx.pid; 는 Nginx 프로세스 ID 저장함을 의미합니다.
  • worker_connections 1024; 는 하나의 워커 프로세스당 최대 1024개 연결 가능함을 의미합니다.
  • upstream appservers → server1, server2, server3에 트래픽을 분산함을 의미합니다.
  • listen 80; → 80번 포트에서 HTTP 요청 수신함을 의미합니다.
  • proxy_pass http://appservers; → 요청을 server1, server2, server3로 전달함을 의미합니다.
  • proxy_set_header → 원본 요청 정보를 유지하도록 헤더 설정함을 의미합니다.

 

이렇게 설정을 해주는 경우 NginX가 요청을 server1, server2, server3에 자동 분배하는 로드 밸런서 역할을 하게 됩니다.

 

nginx 설정을 이어서 보도록 하겠습니다.

 

depends_on:
      - server1
      - server2
      - server3​

 

nginx 컨테이너가 server1, server2, server3가 실행된 후에 실행되되록 설정합니다.

 

왜 각 server가 실행이 된 후, nginx 컨테이너가 실행되야 할까요?

 

server가 먼저 실행되지 않는 경우, nginx가 서버를 찾지 못하는 문제가 발생할 수 있습니다.

따라서, 서버들이 먼저 실행될때까지 기다렸다가 nginx를 실행해줍니다.

 

  networks:
      - my-network

 

컨테이너가 my-networ라는 Docker 네트워크에 연결하라는 의미입니다.

Docker에서는 같은 네트워크에 있는 컨테이너끼리 서로 통신이 가능하기 때문에, Spring 서버와 nginx를 같은 네트워크에 포함시켜야 nginx에서 로드밸런싱이 가능합니다.

 

네트워크는 어떻게 선언해줄까요?
networks:
  my-network:
    driver: bridge

 

Bridge 네트워크로 선언해줍니다.

 

Bridge 네트워크의 특징

  • 컨테이너 간 격리된 네트워크를 제공합니다.
  • 컨테이너 이름으로 서로 접근 가능합니다.

 

nginx를 설정해주는 방법을 알아봤습니다. spring boot는 어떻게 설정할까요?
  server1:
    build:
        context: ../
        dockerfile: ./infratest/app/Dockerfile
    container_name: server1
    expose:
      - "8080"
    depends_on:
      mysql:
        condition: service_healthy
    networks:
      - my-network

 

build로 선언한 부분부터 보겠습니다.

build:
        context: ../
        dockerfile: ./infratest/app/Dockerfile

 

context: ../ 는 현재 docker-compose.yml이 있는 디렉터리의 한 단계 위에서 빌드를 수행한다는 의미입니다.

dockerfile: ./infratest/app/Dockfile 은 Spring Boot 애플리케이션을 빌드할 Dockerfile의 위치를 의미합니다.

 

Dockerfile은 다음과 같습니다.

FROM openjdk:17

WORKDIR /app

COPY ../build/libs/*.jar app.jar

ENTRYPOINT ["java", "-jar", "/app/app.jar"]

 

Dockerfile은 Spring Boot 애플리케이션을 실행하는 Docker 컨테이너를 만들기 위한 설정입니다.

 

FROM openjdk:17

penjdk:17 이미지를 기반으로 컨테이너를 생성한다는 의미로, 쉽게 말해 Java 17이 이미 설치된 환경에서 실행한다는 의미입니다.

 

WORKDIR /app

모든 파일이 /app 폴더 내에서 관리된다는 의미입니다.

 

COPY ../build/libs/*.jar app.jar

로컬(build/libs/)에서 생성한 *.jar 파일을 컨테이너 내부(/app/app.jar)로 복사한다는 의미입니다.

 

ENTRYPOINT ["java", "-jar", "/app/app.jar"]

컨테이너가 시작할 때  java -jar /app/app.jar 명령어를 실행하게 됩니다.

즉, Spring Boot 애플리케이션이 자동으로 실행되게 됩니다.

 

⚠️ 주의할점

docker-compose up을 하기 전에 Spring Boot를 먼저 (gradle기준) ./gradlew build로 빌드를 해야합니다.

그래야 build/libs 하위에 jar파일이 생기게되고, 이 jar파일을 Docker 컨테이너 내부로 복사할 수 있습니다.

 

DockerFile이 왜 필요한가요?

 

DockerFile은 Spring Boot 서버를 컨테이너에서 빌드하고  실행할 때 필요한 파일입니다.

또한, Spring Boot 애플리케이션을 Docker 환경에서 실행하기 위한 하나의 설명서 역할을 합니다.

DockerFile이 없다면, Spring Boot 서버를 docker에서 띄울수가 없습니다.

 

DockerFile을 알아봤고, 이어서 server 설정에 대해서 보겠습니다.
    container_name: server1
    expose:
      - "8080"
    depends_on:
      mysql:
        condition: service_healthy
    networks:
      - my-network

 

container_name: server1

컨테이너 이름을 server1로 지정한다는 의미입니다.

 

expose:

   - "8080"

컨테이너 내부에서만 8080포트를 공개한다는 의미입니다. nginx 같은 다른 컨테이너가 server1:8080을 통해서 요청을 보낼 수 있습니다. 즉, 같은 네트워크 내의 컨테이너끼리 통신 가능하지만, 호스트에서는 접근이 불가능합니다.

 

depends_on:

     mysql:

         condition: service_health

 

server1이 실행되기 전에 mysql이 먼저 실행한다는 의미입니다.

이번엔 단순한 실행이 아닌, healthcheck를 통해서 mysql이 정상적으로 작동 할 때까지 기다렸다가 실행됩니다.

(이 부분은 mysql 설정 부분에 설명이 나옵니다.)

 

MySQL이 완전히 실행되기 전에 Spring Boot가 실행되면 어떻게 될까요?

 

Spring Boot가 DB 연결을 시도할때, MySql이 아직 준비되지 않았기 때문에 Connetion refuesd 오류가 발생합니다.

healthCheck를 사용하면, MySql이 정상적으로 응답하는 상태일 때만 server1이 실행됩니다.

 

networks:
   - my-network

nginx에서 사용했던 것과 마찬가지로 my-network라는 하나의 네트워크에 연결해줍니다.

 

 

Spring boot를 설정하는 방법을 알아봤습니다. MySql은 어떻게 설정해줄까요?

 

mysql을 설정해주는 부분은 다음과 같습니다.

  mysql:
    image: mysql:8.0
    container_name: mysql
    restart: always
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: test
    networks:
      - my-network
    healthcheck:
      test: ["CMD", "mysqladmin", "ping","-h","localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

 

image: mysql:8.0

Docker Hub에서 MySql 8.0 버전의 공식 이미지를 가져와 컨테이너를 실행한다는 의미입니다.

 

container_name: mysql

컨테이너 이름을 mysql로 지정한다는 의미입니다.

 

Spring boot에서 mysql에 접근하기 위한 url은 아래와 같습니다.

spring.datasource.url=jdbc:mysql://mysql:3306/test

 

기존 로컬 환경에서는 localhost:3306을 해주었지만, 컨테이너 내부 환경일 경우엔 mysql의 컨테이너 이름을 url에 명시해야 합니다.

 

restart: always

컨테이너가 예기지 않게 종료된은 경우 자동으로 다시 시작한다는 의미입니다.

 

 

ports:
    - "3306:3306"

호스트의 3306 포트를 컨테이너 내부의 3306포트와 연결한다는 의미입니다.

 

 

environment:

    MYSQL_ROOT_PASSWORD: password

    MYSQL_DATABASE: test

 

MySql이 시작될 때 기본적으로 사용하는 환경 변수를 의미합니다.

MYSQL_ROOT_PASSWORD: password는 루트 계정의 비밀번호를 password로 설정해줍니다.

MYSQL_DATABASE:test는 MySql 시작 시 자동으로 test 데이터베이스를 생성해줍니다.

 

네트워크는 앞서 이야기한 내용과 동일합니다.

 

 

healthcheck:
  test: ["CMD", "mysqladmin", "ping","-h","localhost"]
  interval: 10s
  timeout: 5s
  retries: 5

 

컨테이너가 정상적으로 실행 중인지 확인하는 설정입니다.

MySql이 정상적으로 작동을 하고  있는지 mysqladmin ping이라는 명령어를 사용해서 체크합니다.

 

interver: 10s 는 10초마다 상태를 확인한다는 것을 의미합니다.

timeout: 5s 는 응답이 5초 이상 지연되면 실패로 간주함을 의미합니다.

retires: 5 5번까지 재시도 후 실패하면 컨테이너 비정상 상태로 처리함을 의미합니다.

 

docker-compose.yml 파일에 작성해준 내용에 대해 알아봤습니다. 이제 직접 실행해보도록 하겠습니다.

 

 

먼저, docker-compose up 명령어를  사용해서 yml 파일에 명시된 컨테이너들을 실행시켜 줍니다.

 

mysql 실행이 잘 되었는지 health-check를 해줍니다.

 

 

health-check가 정상적으로 완료된 경우, mysql 컨테이너의 상태가 Healthy로 변경됩니다.

다른 컨테이너들도 정상적으로 실행이 되었습니다.

 

 

docker desktop에서도 실행이 잘 됩니다.

 

설정은 전부 마쳤습니다. 이제 로드밸런싱이 잘되는지 확인해보겠습니다.

nginx가 로드밸런싱을 성공적으로 해줄까요?

 

로드밸런싱이 잘 되는지 확인하기 위해서 임의의 api 하나를 10번 호출해줍니다. 호출해주는 api는 다음과 같습니다.

http://localhost/health-check

 

이 url은 제가 spring boot에서 정의한 controller가 요청을 받을 수 있도록 하는 url 입니다.

spring boot에서 정의한 controller는 다음과 같습니다.

package com.sangyunpark.redis_mysql_rock.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
public class HealthCheck {

    @GetMapping("/health-check")
    public ResponseEntity<?> healthCheck() {
        log.info("/health-check 요청 전달 완료");
        return ResponseEntity.ok("health-check success!");
    }
}

 

로그를 사용해서 api 요청이 controller까지 잘 전달이 되었는지 확인합니다.

 

 

아래의 명령어를 사용해서 10번의 api 요청을 해보겠습니다.

for i in {1..10}; do curl -v http://localhost/health-check; echo ""; done

 

docker에서 확인한 로그 결과는 아래와 같습니다.

2025-02-26 13:33:07 server2 | 2025-02-26T04:33:07.322Z INFO 1 --- [nio-8080-exec-1] c.s.r.controller.HealthCheck : /health-check 요청 전달 완료

2025-02-26 13:33:07 server3 | 2025-02-26T04:33:07.438Z INFO 1 --- [nio-8080-exec-1] c.s.r.controller.HealthCheck : /health-check 요청 전달 완료

2025-02-26 13:33:07 server1 | 2025-02-26T04:33:07.463Z INFO 1 --- [nio-8080-exec-3] c.s.r.controller.HealthCheck : /health-check 요청 전달 완료

2025-02-26 13:33:07 server2 | 2025-02-26T04:33:07.476Z INFO 1 --- [nio-8080-exec-2] c.s.r.controller.HealthCheck : /health-check 요청 전달 완료

2025-02-26 13:33:07 server3 | 2025-02-26T04:33:07.488Z INFO 1 --- [nio-8080-exec-2] c.s.r.controller.HealthCheck : /health-check 요청 전달 완료

2025-02-26 13:33:07 server1 | 2025-02-26T04:33:07.498Z INFO 1 --- [nio-8080-exec-4] c.s.r.controller.HealthCheck : /health-check 요청 전달 완료

2025-02-26 13:33:07 server2 | 2025-02-26T04:33:07.508Z INFO 1 --- [nio-8080-exec-3] c.s.r.controller.HealthCheck : /health-check 요청 전달 완료

2025-02-26 13:33:07 server3 | 2025-02-26T04:33:07.518Z INFO 1 --- [nio-8080-exec-3] c.s.r.controller.HealthCheck : /health-check 요청 전달 완료

2025-02-26 13:33:07 server1 | 2025-02-26T04:33:07.529Z INFO 1 --- [nio-8080-exec-5] c.s.r.controller.HealthCheck : /health-check 요청 전달 완료

2025-02-26 13:33:07 server2 | 2025-02-26T04:33:07.537Z INFO 1 --- [nio-8080-exec-4] c.s.r.controller.HealthCheck : /health-check 요청 전달 완료

 

10번의 api 요청이 각각 server1, server2, server3에 골고루 분포되었습니다. 트래픽 분산이 nginx의 로드 밸런싱을 통해서 잘 분산되었습니다.

 

nginx는 특별한 설정이 없는 경우엔 기본적으로 라운드 로빈 방식으로 로드 밸런싱을 수행합니다.

'Docker' 카테고리의 다른 글

docker 환경에서 DB migration하기 (Flyway편)  (0) 2025.02.27
Docker로 배포해보기  (1) 2024.08.28