본문 바로가기

개발일지(일간)

도커로 프로젝트 배포하기 - 로드밸런싱

도커를 통한 cd 와 서비스 분리까지 끝냈으니 이제 도커와 nginx를 이용해 로드밸런싱을 해주는 것만 남았다.

도커 컴포즈를 사용하는게 여러대의 도커를 관리하기 편해보였으므로 도커컴포즈를 사용해서 로드밸런싱을 해 주었다.

1.docker-compose.yml 작성

우선 배포가 되는 ec2인스턴스에 docker-compose.yml을 작성해주고, nginx설정을 수정해 주어야 한다. 

docker-compose.yml

version: '3'
services:
  nginx:
    image: nginx:latest
    container_name: nginx
    volumes:
      - /etc/nginx/nginx.conf:/etc/nginx/nginx.conf  # Nginx 설정 파일을 컨테이너에 연결
      - /etc/letsencrypt:/etc/letsencrypt
    ports:
      - "80:80"
      - "443:443"
    networks:
      - app-network

  backend1:
    image: 2zeroempty/cepdocker:latest
    container_name: backend1
    environment:
      AWS_ACCESS_KEY: "${AWS_ACCESS_KEY}"
      AWS_SECRET_ACCESS_KEY: "${AWS_SECRET_ACCESS_KEY}"
      DB_SOURCE_URL: "${DB_SOURCE_URL}"
      DB_SOURCE_USERNAME: "${DB_SOURCE_USERNAME}"
      DB_SOURCE_PASSWORD: "${DB_SOURCE_PASSWORD}"
    ports:
      - "8083:8080"
    networks:
      - app-network

  backend2:
    image: 2zeroempty/cepdocker:latest
    container_name: backend2
    environment:
      AWS_ACCESS_KEY: "${AWS_ACCESS_KEY}"
      AWS_SECRET_ACCESS_KEY: "${AWS_SECRET_ACCESS_KEY}"
      DB_SOURCE_URL: "${DB_SOURCE_URL}"
      DB_SOURCE_USERNAME: "${DB_SOURCE_USERNAME}"
      DB_SOURCE_PASSWORD: "${DB_SOURCE_PASSWORD}"
    ports:
      - "8084:8080"
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

nginx.conf 

# Nginx Main Configuration
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /run/nginx.pid;

# Load dynamic modules
include /usr/share/nginx/modules/*.conf;

events {
    worker_connections 1024;
}

http {
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                     '$status $body_bytes_sent "$http_referer" '
                     '"$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log main;

    upstream backend {
        server backend1:8080;
        server backend2:8080;
    }

    sendfile on;
    tcp_nopush on;
    keepalive_timeout 65;
    types_hash_max_size 4096;

    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # Load modular configuration files
    include /etc/nginx/conf.d/*.conf;

    # Redirect HTTP to HTTPS
    server {
        listen 80;
        listen [::]:80;
        server_name eventprodbackend.store www.eventprodbackend.store;

        location / {
            return 301 https://$host$request_uri;
        }
    }

    # HTTPS server block
    server {
        listen 443 ssl;
        listen [::]:443 ssl;
        server_name eventprodbackend.store www.eventprodbackend.store;

        ssl_certificate /etc/letsencrypt/live/eventprodbackend.store/fullchain.pem; # managed by Certbot
        ssl_certificate_key /etc/letsencrypt/live/eventprodbackend.store/privkey.pem; # managed by Certbot
        include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
        ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

       location / {
         proxy_pass http://backend;
         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;
    }
    }

    # Default server block
    server {
        listen 80 default_server;
        listen [::]:80 default_server;
        server_name _;

        root /usr/share/nginx/html;
        # Load configuration files for the default server block
        include /etc/nginx/default.d/*.conf;

        error_page 404 /404.html;
        location = /404.html { }

        error_page 500 502 503 504 /50x.html;
        location = /50x.html { }
    }
}

 

 

 

 

1-1.nginx.conf

현재 ec2에 https 설정이 되어있는 nignx 설정 파일이 있고, docker-compose.yml에서 이 설정을 사용하게 할 것이기 때문에 로드밸런싱을 위해 수정해야한다.

    upstream backend {
        server backend1:8080;
        server backend2:8080;
    }

블록을 추가해 주고, https 서버블록의 프록시 패스를  proxy_pass http://backend; 로 설정해 줌으로써, 443 port로 들어온 요청을 backend블록의 컨테이너들에게 분산되게 해 주었다.

 

1-2. docker-compose.yml

docker-compose.yml에서는 docker compose를 통해 실행될 nignx와 컨테이너 설정을 해 주어야 한다. 

이때 nginx와 컨테이너는 같은 app-network를 사용해야한다.

nginx는 기존의 수정된 nginx설정과 https를 위한 인증서를 사용하게 해 주고, 컨테이너는 서로 다른 외부포트와 프로젝트 실행에 필요한 환경변수를 설정해 주었다.( ports: - "외부포트:내부포트" )

각 컨테이너에 서로 다른 외부포트를 설정하는 것은 호스트 시스템에서 하나의 포트는 단 한 번만 바인딩될 수 있기 때문이다. 서로 같은 외부포트를 사용하면 충돌이 생긴다.

그렇다면 내부포트는 같은것을 쓰는데 왜 충돌이 나지 않는가 하는 의문이 생기는데, 도커 컨테이너는 독립적인 환경에서 실행이 되기 때문에 서로 영향을 받지 않는다. 당연히 포트도 서로 영향을 받지 않기 때문에 같은 포트를 써도 충돌이 생기지 않는다. 

이를 이용해서 같은 내부포트로 실행하고 로드밸런싱을 하는 것이다.

로드밸런싱이 되는 과정을 보면,

 

1. 클라이언트 → Nginx

  •   클라이언트는 우리가 익히 쓰는 도메인 주소로 요청을 보낸다. ( ex) https://naver.com )  이때, 이 요청은 Nginx가 설정된 서버의 80 포트 또는 443 포트로 들어온다.

2.Nginx → 백엔드 서버(컨테이너):

  • Nginx는 클라이언트로부터 받은 요청을 미리 설정된 백엔드 서버에 따라 로드 밸런싱한다.
  • 위 설정으로는 이때, 내부적으로 8080 포트를 사용하는 컨테이너들(backend1, backend2)에 요청을 전달하게 된다.

3.컨테이너(backend1, backend2) → Nginx → 클라이언트:

  • Nginx가 로드밸런싱을 통해 선택한 컨테이너(backend1:8080 || backend2:8080)에서 요청을 처리하고, 그 결과를 Nginx에 응답으로 보낸다. Nginx는 해당 응답을 받아 클라이언트에게 다시 전달한다.

정리하자면, 로드밸런싱은 클라이언트가 Nginx로 요청을 보내면, Nginx가 그 요청을 8080 내부 포트를 사용하는 컨테이너들로 분배하는 방식으로 동작한다.

Nginx는 여러 백엔드 서버(컨테이너)의 8080 포트로 요청을 로드밸런싱하여 보내고, 각 컨테이너는 요청을 처리한 후 응답을 Nginx에 다시 보내어 클라이언트에게 요청에 대한 응답을 하게 된다.

 

2.cd.yml 수정

도커 관리를 일반 docker -> docker compoese로 바꾸었으니 당연히 cd워크플로우도 수정해 주어야한다. 다행히 이번에는 ec2배포 과정만 수정해 주면 된다.

      - name: Deploy to EC2 using Docker Compose
        run: |
          ssh -i ~/.ssh/ec2_key.pem -p 2200 -o StrictHostKeyChecking=no -t {{ secrets.AWS_SEVER_INFO }} << 'EOF'
            echo "Deploying with Docker Compose..."
            
            echo "AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY }}" > .env
            echo "AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }}" >> .env
            echo "DB_SOURCE_URL=${{ secrets.DB_SOURCE_URL }}" >> .env
            echo "DB_SOURCE_USERNAME=${{ secrets.DB_SOURCE_USERNAME }}" >> .env
            echo "DB_SOURCE_PASSWORD=${{ secrets.DB_SOURCE_PASSWORD }}" >> .env
            
            sudo docker system prune --all --force
            sudo docker volume prune --force
            sudo systemctl stop nginx
            cd ~
            sudo docker-compose down
            sudo docker-compose pull
            sudo docker-compose up -d
          EOF

환경변수를 .env파일로 만들어 docker-compose.yml이 실행될때 읽게 해 주었다.

그리고 워크플로우가 실행될때 nginx를 종료하게 해 주었는데, 실행되기 이전에 기존에 실행되고 있는 nginx가 있으면 포트충돌이 나기 때문이다.

그리고 실행할때 docker-compose down이란 명령어로 도커 컴포즈를 통해 관리되는 컨테이너를 모두 종료시키게 되는데, 이때 분리된 서비스로 실행되고 있는 스케줄 컨테이너는 docker-compose로 관리하고 있지 않기 때문에 꺼지지 않는다.

이렇게 관리하면 api서비스 업데이트를 할때 api서버만 종료시키고, 업데이트 할 수 있다.

 

이렇게 당초에 구상했던 워크플로우대로 프로젝트를 업데이트 하게 되었다.

프로젝트를 배포하고 관리하다가

ci/cd를 통해 배포 자동화를 하면 편하지않을까?

ci/cd 도 적용했으니 도커를 통해 여러개의 컨테이너를 실행하고 로드밸런싱을 통해 트래픽 분산을 적용해보는게 좋지 않을까?

여러개의 컨테이너를 실행했을때 스케줄러가 중복실행이 되는 문제가 생기지 않을까?

그렇다면 어차피 스케줄 서비스가 api서비스와 목적도 다르고, 업데이트 주기도 다르니 아예 스케줄 서비스와 api서비스를 분리하는 것이 효율적이지 않을까?

라는 의식의 흐름에 따라 업데이트하게 되었는데, 생각보다 어려웠고, 예상했던 문제를 해결하고 계획대로 작업을 해나가는 과정이 재미있었던 것 같다.

서비스 분리를 하고, 로드밸런싱을 하면서 컨테이너 관리에 대해서 조금이나마 공부하게 되었는데, 이를 이용해 무중단 배포도 가능하겠다는 생각이 들었다.

여유가 된다면 무중단 배포도 시도해 볼 생각이다.