Chapter 08 EC2 서버에 프로젝트를 배포해 보자

LeeKyoungChang·2022년 6월 4일
2
post-thumbnail

스프링 부트와 AWS로 혼자 구현하는 웹 서비스 를 공부하고 정리한 내용입니다.

 

1 ~ 5장까지는 스프링 부트로 서비스 코드를 개발했고, 6 ~ 7장까지는 배포 환경을 구성했다!
이제 이들을 조합해 실제로 서비스를 배포해보자!

책에서는 MariaDB를 사용했지만, Intellij Ultimate에서 사용하기 좋은 것 같다.
나는 Intellij Community을 사용해서 MYSQL을 사용했다.

 

📚 1. EC2에 프로젝트 Clone 받고 간단한 실행 테스트

먼저, EC2로 접속한다.

sudo yum install git : git 설치
mkdir ~/app && mkdir ~/app/step1 : 저장할 디렉터리 생성
cd ~/app/step1 : step1으로 이동

git clone 레포지토리주소

# 프로젝트 디렉터리로 이동한 후
./gradlew test

을 실행한다.

 

💡 참고

  • EC2엔 그레이들(Gradle)을 설치하지 않았다.
  • 하지만, Gradle Task(ex: test)를 수행할 수 있다.
  • 이는 프로젝트 내부에 포함된 gradlew 파일 때문이다.
  • 그레이들이 설치되지 않은 환경 혹은 버전이 다른 상황에서도 해당 프로젝트에 한해서 그레이들을 쓸 수 있도록 지원하는 Wrapper 파일이다.
  • 해당 파일을 직접 이용하기 때문에 별도로 설치할 필요가 없다.

 

 

📖 A. 오류를 만나다.

ec2에서 프로젝트 clone 후, ./gradlew build를 한다. (위에서는 ./gradlew test)
그리고 나서 잘실행되는지 테스트를 해본다.

java -jar springawsbook-0.0.1-SNAPSHOT.jar &

실행을 하니

bash 출력된 내용

org.h2.jdbc.JdbcSQLNonTransientConnectionException: Connection is broken: "java.net.ConnectException: Connection refused (Connection refused): localhost" [90067-200]

~

2022-06-04 13:25:52.940 ERROR 12540 --- [           main] o.s.b.web.embedded.tomcat.TomcatStarter  : Error starting Tomcat context. Exception: org.springframework.beans.factory.UnsatisfiedDependencyException. Message: Error creating bean with name 'sessionRepositoryFilterRegistration' defined in class path resource [org/springframework/boot/autoconfigure/session/SessionRepositoryFilterConfiguration.class]: Unsatisfied dependency expressed through method 'sessionRepositoryFilterRegistration' parameter 1; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.springframework.boot.autoconfigure.session.JdbcSessionConfiguration$SpringBootJdbcHttpSessionConfiguration': Unsatisfied dependency expressed through method 'setTransactionManager' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'transactionManager' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Initialization of bean failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jdbcSessionDataSourceScriptDatabaseInitializer' defined in class path resource [org/springframework/boot/autoconfigure/session/JdbcSessionConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.boot.autoconfigure.session.JdbcSessionDataSourceScriptDatabaseInitializer]: Factory method 'jdbcSessionDataSourceScriptDatabaseInitializer' threw exception; nested exception is java.lang.IllegalStateException: Unable to detect database type

2022-06-04 13:25:53.009  INFO 12540 --- [           main] o.apache.catalina.core.StandardService   : Stopping service [Tomcat]
2022-06-04 13:25:53.022  WARN 12540 --- [           main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.context.ApplicationContextException: Unable to start web server; nested exception is org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat
2022-06-04 13:25:53.051  INFO 12540 --- [           main] ConditionEvaluationReportLoggingListener :

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2022-06-04 13:25:53.102 ERROR 12540 --- [           main] o.s.boot.SpringApplication               : Application run failed


org.springframework.context.ApplicationContextException: Unable to start web server; nested exception is org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat

~~

Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'sessionRepositoryFilterRegistration' defined in class path resource [org/springframework/boot/autoconfigure/session/SessionRepositoryFilterConfiguration.class]: Unsatisfied dependency expressed through method 'sessionRepositoryFilterRegistration' parameter 1; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.springframework.boot.autoconfigure.session.JdbcSessionConfiguration$SpringBootJdbcHttpSessionConfiguration': Unsatisfied dependency expressed through method 'setTransactionManager' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'transactionManager' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Initialization of bean failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jdbcSessionDataSourceScriptDatabaseInitializer' defined in class path resource [org/springframework/boot/autoconfigure/session/JdbcSessionConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.boot.autoconfigure.session.JdbcSessionDataSourceScriptDatabaseInitializer]: Factory method 'jdbcSessionDataSourceScriptDatabaseInitializer' threw exception; nested exception is java.lang.IllegalStateException: Unable to detect database type

~~

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'transactionManager' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Initialization of bean failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jdbcSessionDataSourceScriptDatabaseInitializer' defined in class path resource [org/springframework/boot/autoconfigure/session/JdbcSessionConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.boot.autoconfigure.session.JdbcSessionDataSourceScriptDatabaseInitializer]: Factory method 'jdbcSessionDataSourceScriptDatabaseInitializer' threw exception; nested exception is java.lang.IllegalStateException: Unable to detect database type

~~


Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jdbcSessionDataSourceScriptDatabaseInitializer' defined in class path resource [org/springframework/boot/autoconfigure/session/JdbcSessionConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.boot.autoconfigure.session.JdbcSessionDataSourceScriptDatabaseInitializer]: Factory method 'jdbcSessionDataSourceScriptDatabaseInitializer' threw exception; nested exception is java.lang.IllegalStateException: Unable to detect database type

3시간정도 구글링을 했지만 찾을 수 없어 위 내용을 읽어봤다.
그런데? 위 출력 내용을 보면

org.h2.jdbc.JdbcSQLNonTransientConnectionException: Connection is broken: "java.net.ConnectException: Connection refused (Connection refused): localhost" [90067-200]
Factory method 'jdbcSessionDataSourceScriptDatabaseInitializer' threw exception; nested exception is java.lang.IllegalStateException: Unable to detect database type

위 두 문장이 유독 반복되거나 이유를 말해주는 것 같았다.

  • 연결이 끊겼다. localhost를 연결할 수 없다.
  • database 타입이 어떤건지 모르겠다?

문뜩, 프로젝트 application.yml에서 h2 database를 사용하기 위해

datasource를 작성했던 게 기억이 나서 확인했다.

나는 ec2 가상 서버를 만든 후, 프로젝트 서버를 실행시킨 상태이다.
즉, 그림에서 보이는 url jdbc:h2:tcp//localhost//Users/leekyoungchang/~~~ 경로가 ec2 가상서버에는 없다.

그래서, 위 두 문장 오류가 발생했다는 것을 알게 되었다.

datasource를 주석처리 git push 하여 ec2 가상서버에 전달한 후

./gradlew build로 build를 수정한 후

libs에서 실행하면, database에 접근도 가능하고, 책 배포 스크립트 만들기에서 말한 ClientRegistrationRepository 오류를 만나게 된다. (이 오류는 다음 3. 외부 Security 파일 등록하기를 통해 해결할 수 있다.)

 

 

📚 2. 배포 스크립트 만들기

🔔 배포란?

  • git clone 혹은 git pull을 통해 새 버전의 프로젝트 받음
  • Gradle이나 Maven을 통해 프로젝트 테스트와 빌드
  • EC2 서버에서 해당 프로젝트 실행 및 재실행

이전까지 배포할 때마다 개발자가 하나하나 명령어를 실행했다.
이제 쉘 스크립트로 작성해 스크립트만 실행하면 앞의 과정이 차례로 진행되도록 하겠다.

 

💡 참고
쉘 스크립트 : .sh라는 파일 확장자를 가진 파일이다.
vim : 리눅스 환경과 같이 GUI가 아닌 환경에서 사용할 수 있는 편집 도구

 

책에서는 빔으로 리눅스 환경에서의 편집을 진행한다.

vim ~/app/step1/deploy.sh : deploy.sh 파일을 하나 생성한다.

#!/bin/bash

# 자주 사용하는 값 변수에 저장
REPOSITORY=/home/ec2-user/app/step1
PROJECT_NAME=springawsbook/springawsbook

# git clone 받은 위치로 이동
cd $REPOSITORY/$PROJECT_NAME/

# master 브랜치의 최신 내용 받기
echo "> Git Pull"
git pull

# build 수행
echo "> project build start"
./gradlew build

echo "> directory로 이동"

cd $REPOSITORY

# build의 결과물 (jar 파일) 특정 위치로 복사
echo "> build 파일 복사"
cp $REPOSITORY/$PROJECT_NAME/build/libs/*.jar $REPOSITORY/

echo "> 현재 구동중인 애플리케이션 pid 확인"

CURRENT_PID=$(pgrep -f ${PROJECT_NAME}.*.jar)

echo "> 현재 구동중인 애플리케이션 pid: $CURRENT_PID"

if [ -z "$CURRENT_PID" ]; then
        echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
else
        echo "> kill -15 $CURRENT_PID"
        kill -15 $CURRENT_PID
        sleep 5
fi

echo "> 새 애플리케이션 배포"
JAR_NAME=$(ls -tr $REPOSITORY/ | grep jar | tail -n 1)

echo "> Jar Name: $JAR_NAME"

nohup java -jar $REPOSITORY/$JAR_NAME 2>&1 &
  • REPOSITORY=/home/ec2-user/app/step1
    • 프로젝트 디렉토리 주소는 스크립트 내에서 자주 사용하는 값이기 때문에 이를 변수로 저장한다.
    • 마찬가지로 PROJECT_NAME=freelec-springboot2-webservice도 동일하게 변수로 저장한다.
    • 쉘에서는 타입 없이 선언하여 저장한다.
    • 쉘에서는 $ 변수명으로 변수를 사용할 수 있다.
  • cd $REPOSITORY/$PROJECT_NAME/
    • 제일 처음 git clone 받았던 디렉토리로 이동합니다.
    • 바로 위의 쉘 변수 설명을 따라 /home/ec2-user/app/step1/freelec-springboot2-webservice 주소로 이동한다.
  • git pull
    • 디렉토리 이동 후, master 브랜치의 최신 내용을 받는다.
    • ./gradlew build
    • 프로젝트 내부의 gradlew로 build를 수행한다.
  • cp ./build/libs/*.jar $REPOSITORY/
    • build의 결과물인 jar 파일을 복사해 jar 파일을 모아둔 위치로 복사한다.
  • CURRENT_PID=$(pgrep -f springboot-webservice)
    • 기존에 수행 중이던 스프링 부트 애플리케이션을 종료한다.
    • pgrep은 process id만 추출하는 명령어이다.
    • -f 옵션은 프로세스 이름으로 찾는다.
  • if ~ else ~ fi
    • 현재 구동 중인 프로세스가 있는지 없는지를 판단해서 기능을 수행한다.
    • process id 값을 보고 프로세스가 있으면 해당 프로세스를 종료한다.
  • JAR_NAME=$(ls -tr $REPOSITORY/ | grep jar | tail -n 1)
    • 새로 실행할 jar 파일명을 찾는다.
    • 여러 jar 파일이 생기기 때문에 tail -n로 가장 나중의 jar 파일(최신 파일)을 변수에 저장한다.
  • nohup java -jar $REPOSITORY/$JAR_NAME 2>&1 &
    • 찾은 jar 파일명으로 해당 jar 파일을 nohup으로 실행한다.
    • 스프링 부트의 장점으로 특별히 외장 톰캣을 설치할 필요가 없다.
    • 내장 톰캣을 사용해서 jar 파일만 있으면 바로 웹 애플리케이션 서버를 실행할 수 있다.
    • 일반적으로 자바를 실행할 때는 java -jar라는 명령어를 사용하지만, 이렇게 하면 사용자가 터미널 접속을 끊을 때 애플리케이션도 같이 종료된다.
    • 애플리케이션 실행자가 터미널을 종료해도 애플리케이션은 계속 구동될 수 있도록 nohup 명령어를 사용한다.

 

chmod + x ./deploy.sh : 이제 이렇게 생성한 스크립트에 실행 권한을 추가한다.

./deploy.sh를 실행한다.

실행이 잘된다. 이제 nohup.out 파일을 열어서 로그를 보자.
nohup.out : 실행되는 애플리케이션에서 출력되는 모든 내용을 갖고 있다.

  • nohup.out 제일 아래로 가면 ClientRegistrationRepository를 찾을 수 없다는 에러가 발생하면서 애플리케이션 실행에 실패했다.

 

💡 참고
프로젝트 clone한 후, 간단한 실행테스트를 해본다.
build 디렉터리를 대상으로 gradlew을 해줘야 한다. (./gradlew build)

cd build/libs로 이동해 java -jar springawsbook-0.0.1-SNAPSHOT.jar & 실행

➡ 아마, application-oauth.yml이 github repository에 없으면 (.gitignore 에 추가했을 경우) ClientRegistrationRepository를 찾을 수 없다는 에러가 발생할 것이다. (실행 실패, 이러면 된거임)

 

 

📚 3. 외부 Security 파일 등록하기

ClientRegistrationRepository를 생성하려면 clientIdclientSecret가 필수이다.
로컬에서는 실행할 때, application-oauth.yml가 있어 문제가 없었다.

vim /home/ec2-user/app/application-oauth.yml

  • app 디렉터리에 yml 파일을 생성한다.
  • 로컬에 있는 application-oauth.yml 파일 내용을 그대로 붙여넣는다.

그리고 생성한 application-oauth.yml을 쓰도록 deploy.sh 파일을 수정한다.

nohup java -jar \
        -Dspring.config.location=classpath:/application.yml,/home/ec2-user/app/application-oauth.yml \
        $REPOSITORY/$JAR_NAME 2>&1 &
  • Dspring.config.location
    • 스프링 설정 파일 위치를 지정한다.
    • 기본 옵션들을 담고 있는 application.yml과 OAuth 설정들을 담고 있는 application-oauth.yml의 위치를 지정한다.
    • classpath가 붙으면 jar 안에 있는 resources 디렉토리를 기준으로 경로가 생성된다.
    • application-oauth.yml 은 절대경로를 사용한다. 외부에 파일이 있기 때문이다.

 

수정이 다 되었다면 다시 deploy.sh를 실행해본다.

실행 성공!

 

 

profile
"야, (오류 만났어?) 너두 (해결) 할 수 있어"

0개의 댓글