private subnet에 spring boot, public subnet에 nginx 띄우고 연동하기
로컬에서 spring boot로 작업중인 서버를 private subnet의 ec2에 띄워보고, public subnet에는 nginx를 띄워서 둘을 연결해본다. 즉 nginx로 보내는 요청이 spring boot로 가게끔 설정해본다. (즉 nginx를 프록시로 활용!)
private subnet에 띄운 ec2에 스프링부트 서버 배포
1. ubuntu베이스의 ec2를 생성한다 (amazon linux 베이스여도 됨). 보안그룹 적용시 public subnet에 둘 nginx로부터 오는 트래픽에 대한 인바운드를 열어야 하며, rds나 elastiCache를 사용하는 경우 그 놈들로 가는 트래픽에 대한 아웃바운드를 열어야 한다. (당연히 nginx쪽에서의 아웃바운드와 rds & elastiCache쪽에서의 인바운드도 열어야 함). 또한 bastion host를 통한 ssh접속을 위해 bastion host로부터 오는 22번 포트 인바운드도 열어둬야 한다.
2. ssh등을 활용해 private subnet에 띄운 ec2에 접속한다. ssh로 접속하는 경우 키가 필요하며, scp(secure copy)를 통해 로컬에 있는 키를 public subnet에 띄운 ec2로 보내서 ssh를 두 번 거치는 방법도 있으나 사실 이는 비밀번호가 걸린 현관문 바로 앞에 열쇠를 두는 행위나 마찬가지다. 따라서 나는 ssh jump라는 방식을 통해 ec2에 접속했다.
Host somsatang-webserver-1
HostName 13.124.18.245
User ubuntu
IdentityFile /Users/chosanghyun/Downloads/sst.pem
Host somsatang-was-1
HostName 10.0.1.88
User ubuntu
IdentityFile /Users/chosanghyun/Downloads/sst.pem
ProxyJump somsatang-webserver-1
3. 다음 공식문서 링크를 참고해 docker를 설치한다
https://docs.docker.com/engine/install/ubuntu/
4. rds나 elastiCache를 사용한다면 application.properties에 적어준 설정에서 엔드포인트들을 적어줘야한다.
// localhost를 rds 엔드포인트로 바꿔준다. 실험결과 3306 포트는 따로 명시 안해도 되긴 함
spring.datasource.url=jdbc:mysql://localhost:3306/wooyoungsoo
// localhost를 elastiCache redis 엔드포인트로 바꿔준다
spring.data.redis.host=localhost
// 나머지 설정들은 생략했음
5. Intellij에서 다음과 같이 우측의 Gradle을 누르고 bootJar을 눌러 jar를 만들어준다
6. /build/libs에 jar파일이 생긴다. /build/libs, 즉 jar가 있는 디렉토리로 가서 Dockerfile을 만들어주고 다음과 같이 작성한다
FROM openjdk:17
ARG JAR_FILE=*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
openjdk는 프로젝트에서 사용중인 자바 버전에 맞게 11이나 17등을 써주면 된다.
- FROM openjdk:17: Docker 이미지의 기본 이미지를 지정. 이 경우에는 OpenJDK 17 이미지를 사용하는 것이며, 이 이미지는 Java 17 환경을 제공한다
- ARG JAR_FILE=*.jar: Docker 빌드 시에 사용할 변수를 정의. JAR_FILE이란 변수를 정의한 것이며, 기본값은 현재 디렉토리에 있는 모든 .jar 확장자를 가진 파일 중 하나
- COPY ${JAR_FILE} app.jar: COPY 명령을 사용하여 호스트 머신에서 Docker 컨테이너로 JAR 파일을 복사하라고 하는 것. ${JAR_FILE} 변수는 방금 정의한 그 변수가 맞다. 이 파일은 컨테이너 내부에 app.jar로 복사된다.
- ENTRYPOINT ["java","-jar","/app.jar"]: 컨테이너가 시작될 때 실행할 명령을 정의하는 것. 이 경우에는 Java를 실행하여 app.jar 파일을 실행한다는 의미다. 즉, Docker 컨테이너가 시작되면 Java 애플리케이션을 실행하고 jar 파일(= app.jar)을 실행한다.
즉 정리하면
"자바 17버전 환경에서 컨테이너를 실행할 건데",
"현재 디렉토리에서 .jar파일이 있으면 걜 JAR_FILE이란 변수에 저장하고",
"app.jar라는 이름으로 복사한 다음",
"컨테이너를 시작할 때 'java -jar'라는 커맨드를 활용해 app.jar를 구동할 거야"
라는 의미다.
7. 다음 커맨드를 입력해 도커 이미지를 빌드한다
docker build {옵션} {도커계정}/{프로젝트명}:{버전} {경로}
ex) docker build -t jofe/toyproject:0.1.0 .
-t는 --tag와 같은 의미이며 이미지에 부여할 이름과 태그를 지정할 수 있게끔 한다
참고로 맥북 m1이나 m2를 사용중인 유저라면 빌드 플랫폼이 arm기반이 되는데, aws에서 만든 ec2가 amd기반이면 호환성 문제가 생긴다. 따라서 만약 aws에서 만든 ec2가 amd기반이라면 다음과 같이 플랫폼을 지정해 빌드한다
docker build --platform linux/amd64 {옵션} {도커계정}/{프로젝트명}:{버전} {경로}
하지만 다음과 같이 ec2를 만들 때 arm기반으로 만들었다면 맥북도 방금 말한 거 안해도 된다.
8. 도커허브에 방금 생성한 도커 이미지를 푸시한다
docker push {이미지명}
ex) docker push jofe/toyproject:0.1.0
도커허브로 접속해서 보면 내가 푸시한 이미지가 잘 올라와있는 걸 다음과 같이 확인 가능함
9. ssh로 접속한 private subnet ec2에서 docker pull로 이미지를 땡겨온다
sudo docker pull {이미지명}
ex) sudo docker pull jofe/toyproject:0.1.0
10. 다음 커맨드를 입력해 컨테이너를 돌린다
sudo docker run --name {컨테이너명} -d -p 8080:8080 {이미지명}
ex) sudo docker run --name "jofe-service" -d -p 8080:8080 jofe/toyproject:0.1.0
-p 옵션 : -p 호스트포트:컨테이너포트 형식으로 사용되며, 이 옵션을 통해 호스트 머신과 컨테이너 간의 포트 포워딩(매핑)을 설정한다. 즉 호스트 머신의 몇 번 포트로 보내면 컨테이너의 몇 번 포트로 보낸다 이런 의미.
-d 옵션 : 컨테이너를 백그라운드에서 돌리게끔 하는 역할
참고 : 호스트의 8080번 포트로 설정한 만큼, 보안그룹에서 nginx 쪽에서 8080포트로 보내는 아웃바운드와 spring boot쪽에서 8080으로 받는 인바운드를 열어야 한다.
public subnet에 nginx 띄우고 연결
1. ssh로 public subnet에 띄운 ec2에 접속한다. 당연히 외부에서 22번 포트로 오는 ssh접속에 대한 인바운드가 열려있어야 한다
2. nginx를 설치한다
sudo apt install nginx
3. nginx를 설정할 차례다. 우선 /etc/nginx/sites-available 디렉토리로 이동한다
cd /etc/nginx/sites-available
4. 이 디렉토리는 nginx의 설정 파일들이 위치했으며, 처음에 들어가면 default라는 기본적인 설정 파일이 있을 것이다. 우리가 사용할 설정파일을 새로 만들어준다. (vi로 해도 상관없음)
sudo nano {원하는 설정파일명}
5. 새로 만든 설정파일의 내용은 다음과 같이 설정한다
server {
listen 80;
server_name {nginx가 띄워진 ec2의 ip 주소 또는 도메인};
location / {
proxy_pass http://{spring boot를 배포한 서버의 ip 또는 도메인}:8080;
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;
}
}
- listen 80 : 80번 포트로 웹서버를 서비스한다는 의미. 이를 통해 http://xxx.xxx.xxx.xxx:80이 아닌 http://xxx.xxx.xxx.xxx로 접속 가능하다
- server_name : 이 값으로는 도메인 or 호스트 or IP주소 작성. server 블록을 여러 개 둘 수 있으며, 클라이언트가 보낸 request의 header안에 있는 Host값이 이 server_name값과 일치하는 server 블록이 활성화된다. 만약 Host값과 매칭되는게 없다면 기본적으로 설정된 server블록 또는 첫 번째로 작성된 server 블록이 활성화된다
- location / {블라블라..} : /에 해당하는 URL, 즉 모든 요청에 대한 설정을 {}안에 정의한다는 뜻 (만약 /api로 했다면 해당 경로로 오는 요청들에 대한 설정을 정의한다는 뜻이 되겠져)
- proxy_pass : /에 해당하는 URL, 즉 모든 요청을 이 URL로 리다이렉트한다
- proxy_set_header : 헤더 설정
6. 방금 작성한 설정파일을 nginx가 읽을 수 있도록 설정할 차례. /etc/nginx/sites-enabled로 이동한다.
cd /etc/nginx/sites-enabled
이 디렉토리는 /etc/nginx/sites-available에 있는 설정 파일 중 활성화할 설정 파일을 "링크(심볼릭 링크)"로 관리하는 디렉토리다. ls를 누르면 default가 링크돼있는 걸 확인할 수 있고, 다음 커맨드를 통해 default를 지운다
sudo rm default
7. 우리가 아까 만든 설정파일에 링크를 걸어준다
sudo ln -s /etc/nginx/sites-available/{아까 만든 설정파일명}
심볼릭 링크 : 파일 시스템에서 다른 파일이나 디렉토리를 가리키는 일종의 포인터. 링크된 파일의 내용을 복제하는 개념이 아닌 단순히 참조만 하는, 걍 포인터라고 생각하면 됨
8. nginx를 재시작해 설정 파일을 적용한다
sudo service nginx restart
9. nginx가 띄워진 ec2의 ip주소로 요청을 보내면 private subnet에 띄운 spring boot로 전달되는 걸 볼 수 있다