구축하고자 하는 최종 아키텍쳐는 다음과 같다.
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
'♾️DevOps > ♾️CI & CD' 카테고리의 다른 글
[CI/CD] - 무중단 배포 withCI/CD 3: github action을 활용한 CD 편 (1) | 2024.02.26 |
---|---|
[CI/CD] - 무중단 배포 with CI/CD 2: github action을 활용한 CI 편 (0) | 2024.02.26 |
[CI/CD] - 도커와 젠킨스를 사용한 CI/CD -4 (도커의 설치부터 자동배포까지) (5) | 2024.01.09 |
[Trouble Shooting] - 젠킨스 빌드 시 error: external filter 'git-lfs filter-process' failed 에러 해결하기 (1) | 2023.12.30 |
[CI/CD] - 도커와 젠킨스를 사용한 CI/CD -3 (도커의 설치부터 자동배포까지) (0) | 2023.10.22 |