EC2 서버에 프로젝트 배포하기 - 루타블의 개발일기

김주영·2022년 7월 20일
4
post-thumbnail

[본 글은 프로젝트 과정을 기록할 목적으로 작성되었으며 아래 교재에 기반하여 작성됨]

🌱 EC2에 프로젝트 Clone 받기


🌿 EC2와 git 연동

🔧 먼저 깃허브에서 코드를 받아올 수 있게 EC2에 깃을 설치하겠다.

sudo yum install git

🔧 설치가 완료되면 다음 명령어로 설치 상태를 확인

git --version

🔧 git이 성공적으로 설치되면 git clone으로 프로젝트를 저장할 디렉토리를 생성

mkdir ~/app && mkdir ~/app/step1

🔧 생성된 디렉토리로 이동

cd ~/app/step1

🔧 본인의 github 웹페이지에서 https 주소를 복사

🔧 복사한 https 주소를 통해 git clone을 진행

git clone 복사한 주소

🔧 git clone이 끝났으면 클론된 프로젝트로 이동해서 파일들이 잘 복사되었는지 확인

cd 프로젝트명
ll

🔧 코드들이 잘 수행되었는지 테스트로 검증

./gradlew test

만약, Permission denied라고 나온다면 권한을 확인한 후, x(실행)권한이 없다면 다음 명령어로 권한을 부여하면 된다.

chmod +x gradlew

테스트가 실패해서 수정하고 깃허브에 푸시를 했다면 프로젝트 폴더 안에서 다음 명령어를 사용하면 된다.

git pull

이것은 프로젝트의 최신 코드를 local repository와 비교 후 병합한다.

  • EC2에 gradle을 설치하지 않았지만 Gradle Task를 수행할 수 있는 이유

프로젝트 내부에 포함된 gradlew 파일 때문이다. gradle이 설치되지 않은 환경 혹은 버전이 다른 상황에서도 해당 프로젝트에 한해서 gradle을 쓸 수 있도록 지원하는 Wrapper 파일이다. 해당 파일을 직접 이용하기 때문에 별도로 설치할 필요가 없다.

🌱 프로젝트 배포


🌿 배포 스크립트 만들기

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

앞선 과정을 배포할 때마다 개발자가 하나하나 명령어를 실행하는 것은 불편함이 많다. 그래서 이를 쉘 스크립트로 작성해 스크립트만 실행하면 앞의 과정이 차례로 진행되도록 하겠다. 참고로 쉘 스크립트와 vim은 서로 다른 역할을 한다.

쉘 스크립트는 .sh라는 파일 확장자를 가진 파일이다. 쉘 스크립트는 리눅스에서 기본적으로 사용할 수 있는 스크립트 파일의 한 종류다.

vim은 리눅스 환경과 같이 GUI가 아닌 환경에서 사용할 수 있는 편집 도구다. 여기서는 리눅스에서 가장 대중적인 도구인 vim을 통해 편집을 하겠다.

🔧 ~/app/step1/에 deploy.sh 파일을 생성

vim ~/app/step1/deploy.sh

vim 튜토리얼

ref : https://github.com/johngrib/simple_vim_guide/blob/master/md/for_beginners.md#%EC%A0%80%EC%9E%A5%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95

#!/bin/bash

REPOSITORY=/home/ec2-user/app/step1
PROJECT_NAME=bulletinboard-webservice-2022

cd $REPOSITORY/$PROJECT_NAME/ # 디렉토리 이동

echo "> Git Pull"

git pull

echo "> 프로젝트 Build 시작"

./gradlew build

echo "> step1 디렉토리로 이동"

cd $REPOSITORY

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 &

📢 #!/bin/bash

script 파일을 bash 쉘로 실행

📢 REPOSITORY=/home/ec2-user/app/step1
PROJECT_NAME=bulletinboard-webservice-2022

자주 사용하는 프로젝트 디렉토리 주소를 변수로 저장한다. 쉘에서는 타입 없이 선언하여 저장할 수 있으며, $변수명으로 변수를 사용할 수 있다.

📢 git pull

master 브랜치의 최신 내용을 받는다.

📢 ./gradlew build

프로젝트 내부의 gradlew로 build 수행

📢 cp $REPOSITORY/$PROJECT_NAME/build/libs/*.jar $REPOSITORY/

build의 결과물인 jar 파일을 복사해 jar 파일을 모아둔 위치로 복사한다.

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

pgrep은 process id만 추출하는 명령어다. -f 옵션은 프로세스 이름으로 탐색을 한다. 즉, 해당 프로젝트의 pid를 찾아 변수에 저장하는 명령어다.

📢 JAR_NAME=$(ls -tr $REPOSITORY/ | grep jar | tail -n 1)

새로 실행할 jar 파일명을 찾는다. 여러 jar 파일이 생기기 때문에 tail -n로 가장 나중의 jar 파일(최신 파일)을 변수에 저장한다. 그리고 출력 파일을 파일이 수정된 시간을 기준(-t)으로 내림차순(-r) 정렬한다.

📢 nohup java -jar $REPOSITORY/$JAR_NAME 2>&1 &

찾은 jar 파일명으로 해당 jar 파일을 nohup으로 실행한다. 스프링 부트의 지원으로 내장 톰캣을 사용해서 jar 파일만 있으면 바로 웹 애플리케이션 서버를 실행할 수 있다. 일반적으로 자바를 실행할 때는 java -jar라는 명령어를 사용하지만, 이렇게 하면 사용자가 터미널 접속을 끊을 때 애플리케이션도 같이 종료된다. 애플리케이션 실행자가 터미널을 종료해도 애플리케이션은 계속 구동될 수 있도록 nohup 명령어를 사용한다.

여기서는 2(출력)을 &1(표준입력)으로 보내도록 했다. 즉, jar 파일을 nohup으로 실행한 결과를 지정한 디렉토리의 입력으로 들어가도록 한 것이다.

deploy.sh 파일이 생성되었다. 하지만 x(실행)권한이 없어 기타 사용자가 실행할 수 없다. 다음 명령어로 실행 권한을 주겠다.

chmod +x ./deploy.sh

🔧 이제 해당 스크립트를 다음 명령어로 실행하겠다.

./deploy.sh

🔧 잘 실행되었으니 nohup.out 파일을 열어 로그를 open

📢 nohup.out

실행되는 애플리케이션에서 출력되는 모든 내용을 갖고 있다.

하지만 nohup.out 제일 아래로 가면 에러가 발생한 것을 알 수 있다.

🌿 외부 Security 파일 등록

ClientRegistrationRepository를 생성하려면 clientId와 clientSecret가 필수이다. 로컬 PC에는 실행할 때는 application-oauth.properties가 있어 문제가 없었다.

하지만 이 파일은 .gitignore로 git에서 제외 대상이라 깃허브에는 올라가 있지 않다.

애플리케이션을 실행하기 위해 공개된 저장소에 ClientId와 ClientSecret을 올릴 수는 없으니 서버에서 직접 이 설정들을 가지고 있도록 하겠다.

🔧 먼저 step1이 아닌 app 디렉토리에 properties 파일을 생성

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

🔧 로컬에 있는 application-oauth.properties 파일 내용을 그대로 붙여넣기

🔧 생성한 application-oauth.properties을 쓰도록 deploy.sh 파일을 수정


...
nohup java -jar \
   -Dspring.config.location=classpath:/application.properties,/home/ec2-user/app/application-oauth.properties \
   $REPOSITORY/$JAR_NAME 2>&1 &

📢 -Dspring.config.location

스프링 설정 파일 위치를 지정한다. 기본 옵션들을 담고 있는 application.properties와 OAuth 설정들을 담고 있는 application-oauth.properties의 위치를 지정한다.

classpath가 붙으면 jar 안에 있는 resources 디렉토리를 기준으로 경로가 생성된다.

application-oauth.properties는 절대경로를 사용한다. 그 이유는 외부에 파일이 있기 때문이다.

🔧 수정이 다 되었다면 다시 deploy.sh를 실행한다.

🌱 스프링 부트 프로젝트로 RDS 접근

MySQL에서 스프링부트 프로젝트를 실행하기 위해선 몇 가지 작업이 필요하다.

🌿 RDS 테이블 생성

H2에서 자동 생성해주던 테이블들을 직접 쿼리를 이용해 생성한다. 먼저 RDS에 테이블을 생성하도록 하겠다. 여기선 JPA가 사용될 엔티티 테이블과 스프링 세션이 사용될 테이블 2가지 종류를 생성한다. JPA가 사용할 테이블은 테스트 코드 수행 시 로그로 생성되는 쿼리를 사용하면 된다.

🔧 테스트 코드의 로그를 복사하여 RDS에 반영

스프링 세션 테이블은 schema-mysql.sql 파일에서 확인할 수 있다.

🔧 Files 검색 후 해당 파일 내용을 RDS에 반영

CREATE TABLE bulletinboard_webservice_2022.SPRING_SESSION (
	PRIMARY_ID CHAR(36) NOT NULL,
	SESSION_ID CHAR(36) NOT NULL,
	CREATION_TIME BIGINT NOT NULL,
	LAST_ACCESS_TIME BIGINT NOT NULL,
	MAX_INACTIVE_INTERVAL INT NOT NULL,
	EXPIRY_TIME BIGINT NOT NULL,
	PRINCIPAL_NAME VARCHAR(100),
	CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (PRIMARY_ID)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC;

CREATE UNIQUE INDEX SPRING_SESSION_IX1 ON bulletinboard_webservice_2022.SPRING_SESSION (SESSION_ID);
CREATE INDEX SPRING_SESSION_IX2 ON bulletinboard_webservice_2022.SPRING_SESSION (EXPIRY_TIME);
CREATE INDEX SPRING_SESSION_IX3 ON bulletinboard_webservice_2022.SPRING_SESSION (PRINCIPAL_NAME);

CREATE TABLE bulletinboard_webservice_2022.SPRING_SESSION_ATTRIBUTES (
	SESSION_PRIMARY_ID CHAR(36) NOT NULL,
	ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
	ATTRIBUTE_BYTES BLOB NOT NULL,
	CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_PRIMARY_ID, ATTRIBUTE_NAME),
	CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_PRIMARY_ID) REFERENCES SPRING_SESSION(PRIMARY_ID) ON DELETE CASCADE
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC;

이로써 RDS에 필요한 테이블은 모두 생성하였다.

🌿 프로젝트 설정

🔧 MySQL 드라이버를 build.gradle에 등록

🌳 build.gradle

dependencies {
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.projectlombok:lombok')
    compile('org.springframework.boot:spring-boot-starter-data-jpa')
    compile('com.h2database:h2')
    compile('org.springframework.boot:spring-boot-starter-mustache')
    compile('org.springframework.boot:spring-boot-starter-oauth2-client')
    compile('org.springframework.session:spring-session-jdbc')
    runtimeOnly('mysql:mysql-connector-java') //추가
    testCompile('org.springframework.boot:spring-boot-starter-test')
    testCompile('org.springframework.security:spring-security-test')
}

🔧 서버에서 구동될 환경을 구성하기 위해 src/main/resources/에 application-real.properties 파일 추가

application-real.properties로 파일을 만들면 profile=real인 환경이 구성된다고 보면 된다. 실제 운영될 환경이기 때문에 보안/로그상 이슈가 될 만한 설정들을 모두 제거하며 RDS 환경 profile 설정이 추가된다.

spring.profiles.include=oauth,real-db
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.session.store-type=jdbc

🔧 모든 설정이 되었다면 깃허브로 푸시

🌿 EC2 설정

OAuth와 마찬가지로 RDS 접속 정보도 보호해야 할 정보이니 EC2 서버에 직접 설정 파일을 둔다.

🔧 app 디렉토리에 application-real-db.properties 파일을 생성

vim ~/app/application-real-db.properties

🔧 다음 내용을 추가

spring.jpa.hibernate.ddl-auto=none
spring.datasource.url=jdbc:mysql://rds주소:포트명(기본은 3306)/database이름
spring.datasource.username=db계정
spring.datasource.password=db계정 비밀번호
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

📢 spring.jpa.hibernate.ddl-auto=none

JPA로 테이블이 자동 생성되는 옵션을 None(생성하지 않음)으로 지정한다. RDS에는 실제 운영으로 사용될 테이블이니 절대 스프링 부트에서 새로 만들지 않도록 해야 한다. 이 옵션을 하지 않으면 자칫 테이블이 모두 새로 생성될 수 있다. (주의 바람!)

🔧 마지막으로 deploy.sh가 real profile을 쓸 수 있도록 개선

...
nohup java -jar \
   -Dspring.config.location=classpath:/application.properties,/home/ec2-user/app/application-oauth.properties,/home/ec2-user/app/application-real-db.properties,classpath:/application-real.properties \
   -Dspring.profiles.active=real \
   $REPOSITORY/$JAR_NAME 2>&1 &

📢 -Dspring.profiles.active=real

application-real.properties를 활성화

application-real.properties의 spring.profiles.include=oauth,real-db 옵션 때문에 real-db 역시 함께 활성화 대상에 포함된다.

🔧 다시 한번 deploy.sh를 실행

🔧 curl 명령어로 html 코드가 정상적으로 보인다면 성공이다.

curl localhost:8080

🌱 EC2에서 소셜 로그인하기


🌿 AWS 보안 그룹 변경

먼저 EC2에 스프링 부트 프로젝트가 8080 포트로 배포되었으니, 8080 포트가 보안 그룹에 열려 있는지 확인한다.

🔧 AWS 사이트에서 EC2 서비스에 들어간 후, 왼쪽 카테고리에서 [보안 그룹]을 선택하여 보안 그룹을 체크한다.

8080 열려 있다면 OK, 안 되어있다면 인바운드 규칙 편집으로 수정해 준다.

🌿 AWS EC2 도메인으로 접속

🔧 왼쪽 사이드바의 [인스턴스] 메뉴를 클릭하여 본인이 생성한 EC2 인스턴스를 선택하여 퍼블릭 DNS를 확인한다.

이 주소가 EC2에 자동으로 할당된 도메인이다. 인터넷이 되는 장소 어디나 이 주소를 입력하면 우리의 EC2 서버에 접근할 수 있다.

🔧 해당 도메인 주소에 8080 포트를 붙여 브라우저에 입력

해당 서비스가 이제 도메인을 가진 서비스가 되었다!

하지만 현재 상태에서는 해당 서비스에 EC2의 도메인을 등록하지 않았기 때문에 구글과 네이버 로그인이 작동하지 않는다. 그래서 차례로 서비스에 등록하도록 하겠다.

🌿 구글에 EC2 주소 등록

🔧 구글 웹 콘솔(https://console.cloud.google.com/home/dashboard)로 접속하여 본인의 프로젝트로 이동한 다음 [API 및 서비스 ➡ 사용자 인증 정보]로 이동

🔧 [OAuth 동의 화면] 탭을 선택하고 아래에서 승인된 도메인에 'http://'없이 EC2의 퍼블릭 DNS를 등록

🔧 [사용자 인증 정보] 탭을 클릭해서 본인이 등록한 서비스의 이름을 클릭

🔧 EC2 DNS 주소로 이동해서 다시 구글 로그인을 시도

로그인이 정상적으로 수행되는 것을 확인할 수 있다.

🌿 네이버에 EC2 주소 등록

🔧 네이버 개발자 센터(https://developers.naver.com/apps/#/myapps)로 접속해서 본인의 프로젝트로 이동

🔧 PC 웹 항목에서 서비스 URL을 수정하고 Callback URL을 추가

📢 서비스 URL

  • 로그인을 시도하는 서비스가 네이버에 등록된 서비스인지 판단하기 위한 항목

  • 8080 포트는 제외하고 실제 도메인 주소만 입력한다.

  • 네이버에서 아직 지원되지 않아 하나만 등록 가능

  • 즉, EC2의 주소를 등록하면 localhost가 안 된다.

  • 개발 단계에서는 등록하지 않는 것을 추천

  • localhost도 테스트하고 싶으면 네이버 서비스를 하나 더 생성해서 키를 발급받으면 된다.

📢 Callback URL

  • 전체 주소를 등록한다. (EC2 퍼블릭 DNS:8080/login/oauth2/code/naver)

🔧 2개 항목을 모두 수정/추가하였다면 구글과 마찬가지로 네이버 로그인을 시도

로그인이 정상적으로 수행되는 것을 확인할 수 있다.

구글과 네이버 로그인도 EC2와 연동 완료되었다~👍

스프링 부트 프로젝트를 EC2에 배포해 보았고, 스크립트를 작성해서 간편하게 빌드와 배포를 진행했다.

여기서 남은 문제점은 2가지 정도 있다.

  1. 수동 실행되는 Test
  • 본인이 짠 코드가 다른 개발자의 코드에 영향을 끼치지 않는지 확인하기 위해 전체 테스트를 수행해야만 한다.

  • 현재 상태에선 항상 개발자가 작업을 진행할 때마다 수동으로 전체 테스트를 수행해야만 한다.

  1. 수동 Build
  • 다른 사람이 작성한 브랜치와 본인이 작성한 브랜치가 합쳐졌을 때(Merge) 이상이 없는지는 Build를 수행해야만 알 수 있다.

  • 이를 매번 개발자가 직접 실행해봐야만 한다.

다음 챕터에는 이런 수동 Test & Build를 자동화시키는 작업을 진행하겠다. 🚀

깃허브에 푸시를 하면 자동으로 Test & Build& Deploy가 진행되도록 개선하는 작업이다.

0개의 댓글