[ AWS ] Spring Boot 서버 AWS EC2에 배포하기
1. EC2 인스턴스 시작하기 1. IAM User로 로그인하고 Region을 서울로 설정한 뒤, EC2 서비스->인스턴스->인스턴스 시작을 눌러 인스턴스를 생성한다. - AWS는 root user 자격 증명이 필요한 작업이 아닐 경
jsyeo.tistory.com
Jenkins가 설치된 빌드 서버와 빌드된 jar 파일이 실행될 배포 서버를 분리하고 싶다면 새로운 EC2 인스턴스를 만들어 해당 인스턴스에 Jenkins를 설치해 jar 파일을 빌드하고 ssh를 통해 빌드된 jar 파일을 배포 서버에 전송한 뒤 배포해야 한다.
그러나 인스턴스를 하나 더 생성해 사용하는 것은 부담되었기 때문에, 도메인 주소와 https를 연결해 둔 인스턴스에 jenkins를 설치하여 CI/CD 구축을 실습해보기로 하였다.
EC2 인스턴스에 Jenkins 설치하기
EC2 인스턴스 보안 그룹 및 리스너 설정하기
먼저, EC2 인스턴스와 ELB의 보안 그룹에 8080 포트로 들어오는 인바운드 트래픽을 허용하도록 인바운드 규칙을 추가히고, 로드밸런서에HTTPS:8080으로 들어오는 요청을 EC2 인스턴스에 HTTP:8080으로 전달하도록 대상 그룹과 리스너를 추가한다.
EC2 인스턴스에 Jenkins 설치하기
jenkins.io->documentation->Installing Jenkins를 참고하여 인스턴스의 OS에 따라 맞는 방법으로 Jenkins를 설치한다.
만들어 둔 인스턴스의 OS가 ubuntu이므로 아래 링크의 Long Term Support release를 참고하여 Jenkins를 설치하였다.
Linux
Jenkins – an open source automation server which enables developers around the world to reliably build, test, and deploy their software
www.jenkins.io
설치가 완료되었다면 아래 코드를 통해 확인할 수 있다.
# Jenkins status
$ sudo systemctl status jenkins
# Jenkins home directory - jenkins와 관련된 모든 데이터가 저장되는 directory
$ ls /var/lib/jenkins/
# Jenkins User - jenkins와 관련된 서비스는 아래 유저로 실행된다.
$ id jenkins
Jenkins가 설치된 인스턴스의 퍼블릭 IP 주소를 이용해 젠킨스 서비스에 접속할 수 있다.
http://{퍼블릭 IP 주소}:8080로 접근하면 아래와 같은 페이지가 나온다.
이 때, 아래 코드를 인스턴스에서 실행하여 초기 비밀번호를 확인할 수 있다.
$ sudo cat /var/lib/jenkins/secrets/initialAdminPassword
플러그인은 젠킨스에 추가 기능을 제공해준다. 어떤 플러그인이 필요한지 알고 있다면 Select plugins to install을 눌러 직접 설치할 플러그인을 선택하고, 그렇지 않다면 Install suggested plugins를 눌러 제안되는 플러그인을 설치한다.
이 후 Create First Admin User 페이지에서 사용자를 생성한 뒤에는 http://{퍼블릭 IP 주소}:8080로 접근하여 생성한 사용자 이름과 비밀번호로 로그인 할 수 있다.
Jenkins와 Github 연동하기
Dashboard->Jenkins 관리->System->Github로 이동한다.
Github Server의 Name에 원하는 이름을 넣고, Github Enterprise를 사용하지 않고 기본적인 Public github를 사용한다면 default 값인 https://api.github.com을 API_URL에 넣어주면 된다.
이후 Credentials 아래의 +Add->Jenkins를 눌러 Credentials를 설정해주면 되는데, Kind를 Secret text로 설정하고 Secret에 발급받은 Github token을 넣어주면 된다(ID, Description은 자유).
설정을 완료한 뒤, Test connection을 눌러 설정이 제대로 완료되었는지 확인할 수 있다.
확인이 끝났다면 저장을 눌러 Github 연동을 완료한다.
참고) Github Token 발급받기
Github->Settings->Developer Settings->Personal access tokens->Tokens(classic)->Generate new Token(classic)으로 이동한 후, 원하는 토큰 이름과 토큰 유효 기간, 스코프를 설정하여 토큰을 발급받는다.
발급받은 토큰을 복사해 저장해놓은 뒤, 필요할 때 사용할 수 있다.
Jenkins를 이용한 빌드/배포 자동화
빌드 자동화
먼저, 스프링부트 프로젝트를 빌드하기 위해 필요한 JDK와 Gradle을 프로젝트를 빌드할 수 있는 버전에 맞게 설정하고 설치해주어야 한다.
Dashboard->Jenkins 관리->Tools->JDK installations로 이동하여 Add JDK를 눌러 JDK 이름과 JAVA_HOME을 설정한다.
JAVA_HOME은 다음 방법으로 확인한 뒤, /bin/java를 제외한 경로를 입력해주어야 한다(아래의 경우 /usr/lib/jvm/java-17-openjdk-amd64).
만약 JDK가 설치되지 않았다면 아래 코드를 EC2 인스턴스에서 입력해 JDK를 설치한다.
$ sudo apt update
$ sudo apt install openjdk-17-jdk -y
이후, Dashboard->Jenkins 관리->Tools->Gradle installations로 이동하고 Add Gradle을 눌러 그래들 이름과 그래들 버전을 설정한 뒤, Install automatically를 누르고 저장한다.
Tools 설정을 완료했다면 Dashboard->새로운 Item으로 이동한 뒤, 아이템 이름을 입력하고 Freestyle project를 눌러 새로운 Jenkins Item을 생성한다.
생성한 아이템->구성->소스 코드 관리로 이동하고, Git을 눌러 배포할 스프링부트 프로젝트의 Repository URL을 입력하고 Credentials->+Add를 눌러 Kind를 Username with password로 설정하고, Username에 Github 계정, Password에 Jenkins를 Github와 연동할 때 생성한 token을 입력한 뒤 저장한다.
Branch의 경우, main 브런치의 파일을 받아와 배포할 것이기 때문에 */main으로 설정했다.
연동한 Github Repository에 새로운 커밋이 push되면 새로 Build하여 배포하기 위해 빌드 유발에 Github hook tigger for GITScm polling을 체크해주었다.
참고) Git webhook 설정하기
Github->Jenkins와 연동한 Repository의 Settings->Webhook->Add webhook으로 이동
- Payload URL에 {Jenkins 서버 IP 주소:Port}/github-webhook/을 입력하고 Content type을 application/json으로 설정해준다.
- Just the push event에 체크해 push event가 발생하면 webhook을 트리거하도록 설정한 뒤 Active를 체크하고 저장해준다.
Recent Deliver를 확인하여 정상 동작하는지 확인한다.
빌드 유발 설정을 완료하였다면, Build steps를 설정해야 한다.
Invoke Gradle을 눌러 Tools에서 설정한 Gradle을 Gradle Version으로 설정하고, Tasks에 clean build를 입력한다.
참고) Github Repository의 하위 디렉토리에 gradle 파일이 있는 경우
Ex) repository인 daily-dev-cafe가 아닌, daily-dev-cafe/dailydevcafe에 gradle 파일(build.gradle 등)이 존재한다.
이 경우, Build Steps->고급->Root Build script에 build.gradle이 존재하는 절대 경로를 입력해주어야 한다.
설정을 완료한 뒤 저장하고 지금 빌드를 눌러 빌드가 정상적으로 수행되는지 확인한다.
참고) Exception in thread "main" java.lang.ClassNotFoundException이 발생하는 경우
buld.gradle에 다음과 같은 코드를 추가한다. 이 때, mainClass에 할당되는 클래스는 @SpringBootApplication 어노테이션이 붙은 클래스여야 한다.
bootJar {
mainClass = 'com.jsyeo.dailydevcafe.DailydevcafeApplication'
}
참고) 개발, 운영 환경에 따라 다른 properties 파일을 사용하는 경우
application.properties 파일에 적용할 profile을 설정한다. 이 때, application.properties에는 환경에 따라 변하지 않는 공통으로 적용되는 속성들을 설정한다.
spring.profiles.active=dev
application-dev.properties에는 개발 환경에서만 적용할 속성들을 설정하고, application-prod.properties에는 운영 환경에서만 적용할 속성들을 설정하여 실행환경에 따라 다른 속성들을 적용할 수 있다.
buld.gradle에 다음과 같은 코드를 추가한다.
bootRun {
String activeProfile = System.properties['spring.properties.active']
systemProperty "spring.profiles.active", activeProfile
}
운영 환경에서 빌드된 jar 파일 실행 시 -Dspring.properties.active=prod를 옵션으로 붙여 application-prod.properties를 적용해 실행할 수 있다.
# -Dspring.profiles.active={적용할 profiles}
sudo nohup java -jar -Dspring.profiles.active=prod /home/ubuntu/dailydevcafe/deploy/dailydevcafe-0.0.1-SNAPSHOT.jar 1>~/log.out 2>~/error.out &
참고) 빌드 최적화
빌드가 너무 오래걸린다면 EC2 인스턴스의 메모리가 부족하기 때문일 확률이 높다. 스왑 파일을 이용하여 메모리가 부족한 경우 인스턴스의 스토리지를 메모리로 대신 사용하도록 설정하였더라도 프리 티어인 t2.micro의 메모리는 1GiB 밖에 되지 않기 때문에 메모리 부족이 발생할 수 있다.
Jenkins는 default로 1개의 노드를 갖고, 노드는 2개의 executor를 갖는다(노드는 CPU, executor는 core의 역할). 즉, 2개의 executor가 병렬적으로 빌드를 실행할 수 있다.
따라서 Dashboard->Jenkins 관리->Nodes로 이동하여 Number of executors를 1로 수정한 뒤 저장하면, 한 번에 하나의 빌드만 수행되도록 하여 메모리 사용량을 줄이고, 빌드를 최적화할 수 있다.
배포 자동화
참고한 자료들의 대부분은 post build task 플러그인을 설치하고, 빌드가 완료되면 빌드된 jar 파일을 nohup &로 실행하여 자동으로 배포되도록 하였지만, 이유는 잘 모르겠지만 잘 동작하지 않아 SSH를 이용하는 방법을 이용해 배포 자동화를 할 수 있었다. 1
먼저, Publish Over SSH 플러그인을 설치한다.
설치가 완료되면 Dashboard->Jenkins 관리->System->Publish over SSH로 이동하여 SSH Servers를 추가한다.
Hostname에 빌드된 jar 파일을 전송받을 EC2 인스턴스의 퍼블릭 IP 주소를 입력하고, Remote Directory에 빌드된 jar 파일을 전송받을 EC2 인스턴스의 홈 디렉토리 경로를 입력한다.
이후, 고급을 눌러 Key에 EC2 인스턴스의 키페어 파일의 내용을 입력한다.
Ex) -----BEGIN RSA PRIVATE KEY-----과 -----END RSA PRIVATE KEY-----를 포함한 모든 내용을 복사해 Key에 붙여넣는다.
설정이 완료되었다면, 다시 Item의 구성->빌드 환경으로 이동하여 Send files or execute commands over SSH before the build starts를 선택한다.
Name에 위에서 만든 SSH Server의 Name을 눌러 적용하고, 소스 파일에 {build.gradle이 있는 디렉토리의 상대 경로}/*T.jar을 입력하고(*-SNAPSHOT.jar 파일만을 전송하기 위해), 접두사 제거에 {build.gradle이 있는 디렉토리의 상대 경로}/ 까지 입력한다.
(이 때, 절대 경로를 입력하면 올바르게 동작하지 않는 것 같으니 주의해야 한다.)
이후, Remote directory에 jar 파일을 전송받을 인스턴스의 경로를 입력하고,
Ex) {Publish over SSH->SSH Server->Remote Directory에 설정한 경로}/{상대 경로}에서 {상대 경로}만 입력하면 된다.
Exec command에 파일을 전송한 뒤 전송받은 인스턴스에서 실행할 Command를 입력한다.
Ex) 미리 실행할 쉘 스크립트를 init_server.sh 파일로 만든 뒤, sudo sh로 해당 파일을 실행하도록 했다.
#!/bin/bash
# 이미 실행된 **T.jar이 있다면 종료하고, 새로 빌드된 jar을 profiles.active=prod, nohup로 백그라운드에서 실행한다.
# 표준 출력은 log.out에, 표준 에러는 error.out에 출력하여 저장, 현재 시간 출력
var=$(ps -ef|grep '**T.jar')
echo peocess info: ${var}
get_pid=$(echo ${var} | cut -d " " -f2)
if [ -n "${get_pid}" ]
then
kill -9 ${get_pid}
echo process is terminated.
else
echo running process not running.
fi
nohup java -jar -Dspring.profiles.active=prod /home/ubuntu/dailydevcafe/deploy/*.jar 1>./log.out 2>./error.out &
date
참고) ERROR: Exception when publishing, exception message [Permission denied]
ssh를 통해 원격 서버의 디렉토리 or 파일에 접근 권한이 없어 발생하는 오류이다.
설정을 완료한 뒤 빌드를 실행하면, 정상적으로 빌드된 jar 파일이 배포 디렉토리로 이동한 뒤, 백그라운드로 실행되고 있음을 확인할 수 있다.
Ex) github push event -> Jenkins build 확인(GMT+9)
연동된 Repository에 push event가 발생한 후, webhook이 트리거되어 Jenkins에서 build가 실행된 것을 확인할 수 있다.
Ex) SSH 파일 전송 후, shell script 실행 확인
새로 빌드된 jar 파일이 deploy 디렉토리로 전송된 후, shell script가 실행되어 실행 중이던 jar 파일이 종료되고 새로 빌드된 jar 파일이 실행되는 것을 확인할 수 있다.
참고자료
Imran Teli, [DevOps Beginners to Advanced with Projects], (2024, Jan), Continuous Integratiin with Jenkins, Udemy, https://www.udemy.com/course/decodingdevops/
venh.log, (2021.8.20), Jenkins + Github 연동하기, venh.log, https://kitty-geno.tistory.com/88
venh.log, (2021.8.23), Jenkins(젠킨스) | Gradle, Jar 빌드&배포하기, venh.log, https://kitty-geno.tistory.com/91
broccoli, (2022.5.9), Jenkins를 활용한 스프링부트 앱 CI/CD 쉽게 시작하기, DEVOCEAN, https://devocean.sk.com/blog/techBoardDetail.do?ID=163889
종벌, (2023.3.27), [Jenkins] Git webhook 설정하기., 종벌, https://jong-bae.tistory.com/27
코_노, (2022.8.17), [Linux] 쉘 스크립트 - 파일 존재 여부 확인하기!, 코딩하는 주노 이야기, https://co-no.tistory.com/109
코딩공장공장장, (2022.11.21), 스프링 개발 배포 환경설정 파일 관리(spring.profiles.active), 코딩공장공장장, https://developer111.tistory.com/90
Hudi, (2022.8.9), 젠킨스 빌드 최적화를 향한 여정, hudi.blog, https://hudi.blog/jenkins-build-optimization/
디토20, (2023.3.3), [EC2 / linux] springboot 배포 오류 : Failed to start bean 'webServerStartStop, java.net.SocketException: Permission denied, 메타몽처럼, https://be-developer.tistory.com/115
- 80번 port에서 서버가 실행되도록 하였는데, "1024 이하의 port는 sudo(root)의 권한이 필요하다고 하는데, code deploy로 배포할 경우 ubuntu 권한으로 배포가 되기 때문에 배포에 권한이 없어서 Failed to start bean 'webServerStartStop,java.net.SocketException: Permission denied 에러를 뱉으며 실패하게 된다."고 한다. 이러한 이유가 연관이 있지 않을까 생각한다. [본문으로]
'DevOps' 카테고리의 다른 글
ArgoCD 설치 및 설정 (1) | 2024.09.01 |
---|---|
Terraform 이용해 EKS Cluster 구축하기 (0) | 2024.09.01 |
올리브영 온라인 쇼핑몰 public cloud(AWS) 인프라 구축 프로젝트 (5) | 2024.08.31 |
부하 테스트 및 조회 성능 개선(Ehcache, DB index) (0) | 2024.06.11 |
[ AWS ] Spring Boot 서버 AWS EC2에 배포하기 (0) | 2024.02.12 |