배포 과정에서 만난 문제들과 해결 과정

 

단기 심화 Java 부트캠프에 참여하면서, 개강 과제로 간단한 배포를 하게 되었다. SpringBoot 기반의 단일 애플리케이션을 AWS EC2 서버에 jar 파일로 배포하라는 요구사항이 있었고 관련 자료 또한 제공되었다.

 

참고 자료를 읽어보니, AWS RDS 서비스로 클라우드 DB를 연결하는 내용이었고 Filezilla라는 파일 전송 오픈소스 소프트웨어를 사용해 로컬 컴퓨터에 있는 파일을 EC2 서버에 복사하도록 권장하고 있었다. 자세한 설명이 있어서 그대로 진행했다면 빠른 배포가 가능했겠지만, 나는 이전에 배포 관련 문제로 밤을 샌 적이 있기 때문에 그 때 배웠던 내용들을 실제로 적용해보고 내가 잘 이해한 것인 지 다시 한 번 테스트해보고 싶다는 생각이 들었다.

내 노션..

 

 

내 첫 프로젝트에서 컨설턴트님이 다른 건 몰라도 배포 과정은 꼭 문서화하고 팀원들과 공유해야 한다고 하셨던 덕분에, 프로세스나 개발 과정을 정리하는 습관이 있다. 정리하면서 놓친 부분도 다시 볼 수 있고, 설명을 다듬는 과정에서 배우는 것도 있어서 좋은 것 같다. 아무튼

 

내 개발 원칙 중 하나가 '기술을 사용하기 위한 프로젝트는 지양하자'인데, 이 말은 내가 프로젝트에서 하는 모든 업무엔 이유가 있어야 한다는 말과 같다. 이번 과제에서 나는 도커를 활용해 Nginx와 MySQL서버를 띄우고 SpringBoot 웹 애플리케이션 서버의 경우는 jar 파일을 생성해 EC2에서 직접 배포하는 방식을 선택했다. 그렇다면 이 이유가 명확해야 할 것이다.

 

1. 해당 배포 방식을 선택한 이유

HTTPS 적용 및 보안 강화

클라이언트의 요청이 HTTP로 서버에 도착하면 보안 문제가 발생할 수 있다. 이에 Gabia에서 도메인을 구입해, DNS 레코드를 수정하여 내 EC2 Public ip와 연결한 후, 도메인에 대한 SSL 인증서를 발급 받고 HTTPS 통신을 구현하고자 했다. 또한, 브라우저에서 URL에 내부 포트를 명시해 직접 접근하는 것을 막기 위해 Nginx를 통한 리버스 프록시 설정이 필요했다.

 

API_KEY 유출 방지

강의를 들으며, Kakao나 Naver API를 사용하기 위한 key를 하드 코딩했었다... 이전에 아무 생각없이 Github에 key가 포함된 파일을 올렸다가 문제가 발생했던 적이 있어서, env 파일로 처리할까 하다가 jar 파일로 만들어서 실행하면 유출될 일이 웬만해선 없다고 하셔서 그 방법을 선택했다. 사실 도커 컨테이너로 Spring 서버를 띄워본 적은 있는데 jar 파일로 배포해 본 경험은 없어서 이렇게 한 것도 있다.

 

AWS RDS 과금

AWS RDS는 AWS에서 제공하는 관계형 데이터베이스 서비스로, 보안과 고가용성 관점에서 양질의 기능을 제공한다. DB에 대한 모니터링이나 복구 스토리지를 구성하는 등 클라우드 스토리지로서의 역할을 톡톡히 한다. 하지만, 2024년 2월 이후로 RDS에 Public ip가 자동으로 할당되어 과금이 발생할 수 있다고 한다. Private ip로 통신하고 H2 인메모리 DB를 사용하면 괜찮다고 하지만, 수 차례 AWS 과금으로 인해 커피 값을 까먹었던 과거가 있어서 원천 차단하고자 DB는 직접 띄우기로 했다.

 

 

이러한 이유들에서 시작한 내맘대로 배포하기는 이번에도 Nginx에서 난관에 부딪혔다. 

 

 

2. 502 Gateway Error

 

Nginx를 도커 컨테이너로 실행하면서, 볼륨을 마운트하는 부분을 이렇게 작성했었다.

 

 -v /home/ubuntu/nginx/conf:/etc/nginx/conf.d

 

 

볼륨 마운트란, 호스트 서버의 디렉토리/파일 위치와 컨테이너의 디렉토리/파일 위치를 매핑하는 것을 뜻한다. 볼륨 마운트 옵션을 주게 되면 도커 컨테이너를 내렸다가 다시 실행하더라도, 호스트 서버에 설정 정보를 백업해두고 사용할 수 있기 때문에 해당 옵션을 주었다.

 

호스트 서버의 /conf 디렉토리에 default.config 파일을 만들어두고 Nginx 컨테이너가 올라가게 되면 컨테이너에서 해당 파일을 설정 파일로 읽어들이게 했다.

 

 

server {

        location / {
		proxy_set_header X-Real-IP $remote_addr;
            	proxy_set_header X-Forwarded-For $http_x_forwarded_for;
            	proxy_pass http://127.0.0.1:{SERVER_PORT};
        }

        listen 443 ssl;
        listen [::]:443 ssl;
	ssl_certificate /etc/letsencrypt/live/{DOMAIN_NAME}/fullchain.pem; # managed by Certbot
    	ssl_certificate_key /etc/letsencrypt/live/{DOMAIN_NAME}/privkey.pem; # managed by Certbot

        server_name {DOMAIN_NAME};

}

 

 

HTTPS 보안 접속만을 허용하기 위해, 80 포트로 들어오는 요청을 443 포트 쪽으로 리다이렉션하도록 구성했었다. 이후 인증이 된 요청은 Spring 서버 쪽으로 중계하도록 했다. 

 

이렇게 설정을 하고 브라우저에서 접속을 하니, 502 gateway error를 마주하게 됐다. 서버 간 통신에 문제가 있을 때 발생하는 에러인데, Nginx 리버스 프록시 서버와 Spring 서버의 통신 부분에서 문제가 발생했던 것이다. 

 

문제의 원인은 127.0.0.1:{SERVER_PORT} 위치에 내가 배포한 Spring 서버가 없었다는 것이다. 나는 Spring 서버를 배포할 때, Nginx 컨테이너 내부가 아닌 EC2, 즉 호스트 서버에 직접 배포했었다. localhost나 127.0.0.1이 적혀있어도 문제가 없으려면, Nginx 컨테이너 내부에 Spring 서버를 배포하거나, Nginx를 EC2에 직접 설치해 구성했어야 했다.

 

그렇다면 해결 방법은?

 

proxy_pass http://{PUBLIC_IP}:{SERVER_PORT};

 

간단하다. 127.0.0.1이 아닌 EC2 호스트 서버의 Public ip 주소를 적어주면 된다. 만약 나처럼 도메인을 구입해 DNS 레코드를 등록해 둔 상황이라면 Public ip 대신 도메인을 적는 것도 가능하다.

 

하지만 결과적으로 난 Public ip도 도메인도 아닌, Private ip를 적어줬다. 그 이유는 다음 과정에서 설명하겠다.

 

3. 브라우저에서 Spring 서버 포트로 직접 접근 가능한 문제

 

배포에도 성공했고, HTTPS 전환까지 성공했는데 고민이 생겼다. 브라우저에서 포트 번호를 명시해 HTTP로 직접 Spring 서버로 들어오는 요청에 대해서는 어떻게 처리해야 하는 지? 처음엔 Nginx 설정 파일에 Spring 포트를 listen하면 404 페이지를 리턴하는 내용을 작성했었다.

 

server {

        location / {
                allow 127.0.0.1;
                deny all;
        }

        listen {SERVER_PORT} ;
        listen [::]:{SERVER_PORT} ;

        server_name {DOMAIN_NAME};
        return 404; # managed by Certbot

}

 

그러나 무슨 이유에선지 문제는 해결되지 않았고 log 파일을 통해 파악한 바로는, 내가 Spring 서버를 호스트 서버에 직접 배포했기 때문에 포트로 직접 들어오는 요청에 대해서는 Nginx 컨테이너가 인식하지 못했던 게 문제였다.

 

결과적으로 AWS EC2 인스턴스와 연결된 보안 그룹 인바운드 목록에서 해당 포트를 제거해서 문제를 해결했다. 하지만 이 경우, 내부 통신에서는 반드시 Private ip를 사용해야 한다. 

 

이런 저런 문제들을 해결하며 결국 최종적으로 작성한 Nginx 설정 파일은 다음과 같다.

 

server {

        location / {
		proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $http_x_forwarded_for;
                proxy_pass http://{PRIVATE_IP}:{SERVER_PORT};
        }

        listen 443 ssl;
        listen [::]:443 ssl;
	ssl_certificate /etc/letsencrypt/live/{DOMAIN_NAME}/fullchain.pem; # managed by Certbot
    	ssl_certificate_key /etc/letsencrypt/live/{DOMAIN_NAME}/privkey.pem; # managed by Certbot

        server_name {DOMAIN_NAME};

}

server {

        location / {
		proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $http_x_forwarded_for;
                return 301 https://{DOMAIN_NAME}$request_uri;
        }

        listen 80 ;
        listen [::]:80 ;

        server_name {DOMAIN_NAME};
	return 404; # managed by Certbot

}

 

 

4. Ubuntu SCP Permission Denied

 

또 다른 문제로 로컬 컴퓨터의 jar 파일을 EC2 서버의 특정 디렉토리 밑으로 복사하던 중 권한이 없다는 에러를 마주했다. 

scp -i {JAR_FILE} ubuntu@{PUBLIC_IP}.ap-northeast-2.compute.amazonaws.com:~/spring/

 

 

SCP는 SSH 기반으로 통신해서 서버 간 파일을 전송하는 프로토콜이다. 위 명령어는 Ubuntu 사용자 권한으로 EC2 서버에 접속해 jar 파일을 특정 디렉토리에 복사하도록 하는 내용이므로, Ubuntu 사용자에 대한 파일 권한을 먼저 설정해야 했다.

 

sudo chmod -R 777 ~/spring

 

파일 권한으로는 r(읽기), w(쓰기), x(실행)가 있으며 Ubuntu 운영체제에서는 chmod 명령어를 입력해 특정 사용자에게 특정 권한을 부여할 수 있다. 정보처리기사 시험을 준비하면서 리눅스 명령어를 학습한 적이 있는데, 리눅스 chmod 명령어와 사용법이 같다. user(사용자), root(루트), others(그 외 모두) 에 대해 각각 권한을 부여하는 명령어이고 -R 옵션은 하위 파일들에 대한 권한도 한 번에 설정한다는 의미이다.

 

파일에 대한 권한을 설정해두고 난 뒤, SCP 명령어를 입력하니 정상적으로 jar 파일이 복사되었으며 Spring 서버 배포에 성공할 수 있었다.

 

 

 

 

간단한 과제였지만, 프로그램이 아닌 실제 터미널 명령어만을 사용해 배포를 진행해보니 생각보다 빠르게 진행되진 않아서 이 과정을 기록해야겠다는 생각이 들었다.