이번에 코인 프로젝트가 팀 단위로 분리되면서 신규 API 개발 수요가 급증했다. 신규 동아리원들이 Spring 3는 모르고 Spring Boot만 다룰 수 있는 시점에서 신규 API를 레거시(Spring 3) 환경에 작성하는 것은 일을 두 번 하며 능률도 떨어질 것이었다. 마이그레이션은 아직 완료까지 일정이 많이 남았기에 API 분할 배포를 수행하여 이 상황을 극복하고자 했다.
현재 모든 API는 기존 KOIN에 있고 마이그레이션중인 KOIN V2에는 몇몇 API가 구현되어 있다. 마이그레이션이 완료된 API부터 순차적으로 실서비스에 반영하여 레거시에서 작업하게 되는 문제를 최소화하고자 한다.
큰 구조는 위 그림과 같다.
현재 우리 서비스는 규모가 그리 크지 않고, EC2 인스턴스는 개수에 따라 비용이 배로 들기 때문에 하나의 인스턴스에서 NGINX를 사용하여 두 서버를 동시에 띄우기로 했다.
구상한 순서는 다음과 같다.
실제로 진행한 배포 준비 작업은 아래와 같이 진행되었다.
JDK 1.8을 사용하던 기존 KOIN과 달리 KOIN V2는 JDK 17을 사용한다. 따라서 아래 명령어를 사용하여 JDK 17을 설치했다. (참고 사이트)
wget -O - https://apt.corretto.aws/corretto.key | sudo gpg --dearmor -o /usr/share/keyrings/corretto-keyring.gpg && \
echo "deb [signed-by=/usr/share/keyrings/corretto-keyring.gpg] https://apt.corretto.aws stable main" | sudo tee /etc/apt/sources.list.d/corretto.list
sudo apt-get update; sudo apt-get install -y java-17-amazon-corretto-jdk
이제 2개의 WAS를 띄우기 위한 2개의 tomcat이 필요하다. 기존의 Spring 3를 띄우기 위한 apache-tomcat은 이미 서버에 설치되어 있었고, Spring Boot는 이미 빌드 파일(jar)에 tomcat이 내장되어 있기에 별도로 tomcat을 설치하지 않아도 구동할 수 있다. 따라서 tomcat은 별도의 추가 설치를 거치지 않았다.
WAS를 배포할 환경을 구성해야 한다. 아래와 같이 디렉터리를 생성한 뒤 후에 배포할 사용자에 대해 권한을 부여해준다.
mkdir koin_api_v2
chown tomcat:tomcat koin_api_v2
#!/bin/bash
if lsof -Pi :8081 -sTCP:LISTEN -t >/dev/null; then
echo "8081가 사용중입니다."
PID=$(lsof -Pi :8081 -sTCP:LISTEN -t)
kill -9 $PID
echo "사용 중인 8081 포트를 종료했습니다."
sleep 3
else
echo "8081포트가 사용 중이지 않습니다."
fi
nohup java -jar "KOIN_API_V2.jar" --server.port=8081 > log.txt 2>&1 &
준비한 배포 경로에 배포 스크립트(deploy.sh
)를 작성한다.
명령어에 관한 간단한 설명은 다음과 같다.
lsof -Pi :8081
: 8081 포트를 사용중인 프로세스를 찾는다.
>/dev/null
: 출력을 버린다.
kill -9 $PID
: 특정 프로세스를 강제종료한다.
nohup
: 백그라운드로 실행한다. 로그아웃해도 유지된다.
--server.port=8081
: 서버 포트를 8081로 실행한다.
>log.txt
: log.txt 파일에 출력한다.
2>&1
: 에러 출력을 표준 출력 경로에 함께 저장한다.
&
: 명령어를 백그라운드에서 실행한다.
마지막으로 실행 권한을 부여하기 위해 권한 조정을 진행한다.
chmod 755 deploy.sh
기존 서버에 WAS를 추가로 올리는 것이기 때문에 아이템을 처음부터 만들 필요는 없다. 아래 사진과 같이 선택하면 기존 아이템 설정 정보를 불러올 수 있다.
이후 진행한 설정은 다음과 같다.
jenkins는 github에서 소스 코드를 가져오기 때문에 설정 파일이 없다. 이대로는 빌드가 불가능하기 때문에 별도의 설정 파일을 첨부해야 한다. 동아리 프로젝트의 경우 .../jenkins/deploy/(ProjectName)/config/
에 설정 파일을 관리하고 있다. 또한 코마 프로젝트는 src/main/resources
경로에 설정 파일을 보관하고 있다. 위 명령어는 별도로 보관중이던 설정 파일을 자신이 있어야 할 경로로 복사해준다.
Invoke Gradle script
를 통해 gradle 빌드를 수행할 수 있다. Tasks에 빌드 명령어를 작성해준다. build -x test
는 test를 제외하고 빌드하는 명령어이다.
주의사항
배포 스크립트(
deploy.sh
) 내에서는 파일 지정 시 상대 경로를 사용하고 있다. 이 경우 파일의 경로는 배포 스크립트의 경로를 기준으로 지정되지 않고 스크립트를 호출한 유저의 현재 경로를 기준으로 지정된다. 즉 jenkins ssh 접속으로 /home/ubuntu에 접속되어 /usr/local/koin_api_v2/deploy.sh를 실행하면 /home/ubuntu/... 경로로 지정되는 것이다. 따라서 예상치 못한 문제가 발생할 수 있으므로 배포 스크립트에 상대 경로를 사용하고자 한다면 스크립트 실행 전 cd 명령어를 통해 해당 경로로 직접 이동하는 것이 바람직하다.
기존에 운영중이던 서버 인스턴스에 함께 올리는 것이기 때문에 NGINX 설정도 기존 설정에 부가하여 진행한다. 이번 작업의 목표는 특정 요청에 대해 포트를 다르게 리다이렉션해주는 것이다.
여기서 NGINX의 설정 파일을 수정하는데, 이번 작업에서는 전자의 경로를 사용한다.
#도메인별 설정
/etc/nginx/sites-enabled/(파일명).conf
#통합 설정
/etc/nginx/nginx.conf
# Default URL Mapping
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_pass http://localhost:8080;
}
# KOIN MIGRATION URL Mapping
location ~ ^(/tracks|/dept|/swagger-ui/|/v3/api-docs) {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_pass http://localhost:8081;
}
위의 기존 URL 매핑 정보를 보고 내용을 복사하여 새로운 location 정보를 작성한다. 이 부분의 핵심은 다음 문장이다.
proxy_pass http://localhost:8081;
기존 요청은 8080 포트로 연결했지만 새로 작성한 일부 요청에 대해서는 8081 포트로 연결한다는 뜻이다.
기존 요청과 새로운 요청을 가르는 기준은 다음 문장에 명시되어 있다.
location ~ ^(/tracks|/dept|/swagger-ui/|/v3/api-docs) {...}
/tracks
로 시작하거나 /dept
로 시작하는 등의 요청은 8081 포트로 매핑된다. /tracks/3
이나 /depts/4?pageNum=2
와 같은 요청이 매핑되는 것이다. /swagger-ui/
와 /v3/api-docs
는 Swagger 3를 매핑하기 위해 추가해주었다.
이번 API 분할 배포는 동일 출처 정책(SOP, Same Origin Policy)에 위반하여 CORS(Cross-Origin Resource Sharing) 에러가 발생한다. 이 정책은 다른 출처의 스크립트가 현재 출처의 리소스에 접근할 경우 제한하는 것으로, 출처의 판단 기준은 도메인, 프로토콜(HTTP, HTTPS 등), 포트이다. 여기서 포트가 나뉘기 때문에 두 출처가 다르다고 간주되는 것이다.
이번 작업에서 CORS 에러가 나는 이유
기존 요청은 server-url:8080으로 요청된다. 하지만 새로 추가된 WAS에 해당하는 요청의 경우 실제로는 server-url:8081의 리소스를 요청한다. 8080 포트에 8081의 리소스를 반환하는 것이기 때문에 CORS 에러가 발생하는 것이다.
location ~ ^(/tracks|/dept|/swagger-ui/|/v3/api-docs) {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '$http_origin';
add_header 'Access-Control-Allow-Methods' 'POST, GET, DELETE, PUT';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Max-Age' 3600;
add_header 'Access-Control-Allow-Headers' 'X-Requested-With, Origin, Content-Type, Accept, Authorization';
return 204;
}
add_header 'Access-Control-Allow-Origin' '$http_origin';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Headers' 'X-Requested-With, Origin, Content-Type, Accept, Authorization';
add_header 'Access-Control-Max-Age' 3600;
add_header 'Access-Control-Allow-Methods' 'POST, GET, DELETE, PUT';
...
}
CORS 에러를 예방하기 위해 위 스크립트를 추가로 작성했다. 이렇게 하면 CORS 에러가 발생하지 않고 정상적으로 8081 포트로 연결된다.
다만 WAS 단에서 CORS 관련 설정을 해주었다면 NGINX에서 CORS 관련 설정을 추가로 진행할 필요는 없다.
처음에는 하나의 URL로 매핑되는 서버에 어떻게 API에 따라 서로 다른 WAS에 매핑이 가능할지 설명을 들어도 확실히 이해가 가지 않았다. 하지만 이번 작업을 실제로 드라이빙해보면서 정말 많은 공부가 되었다. 어떻게 포트를 분리할 수 있었는지부터 시작해서 왜 CORS 에러가 나는지까지 명확하게 이해할 수 있었다. 정말 유익한 경험이었고, 인프라에 관해 평소 와닿지 않았던 부분들을 직접 느껴볼 수 있어서 좋았다.