⚙️ 스프링부트 어플리케이션을 EC2와 GitHub Actions를 이용해 CI/CD를 해보자!
build.gradle 작성
스프링부트에서는 빌드 시 jar파일이 두 개가 생성되는데, plain jar 파일과 일반(fat) jar파일이 생성된다.
plain jar에는 의존성 라이브러리가 포함되어 있지 않기 때문에 독립 실행이 불가능하다.
배포 시에는 독립 실행이 가능한 fat jar를 사용하기 때문에 불필요한 plain jar를 생성하지 않도록 build.gradle에 아래 코드를 작성하자.
// plain이 아닌 의존성을 포함한 일반 jar 파일만 생성하도록 변경
jar {
enabled = false
}
CI/CD 스크립트 파일(.yml) 생성
깃허브 액션을 통해 CI/CD를 진행할 것이기 때문에 파일을 경로에 맞게 생성해주자.
1. 프로젝트 폴더 바로 아래에 .github 디렉터리 생성
2. .github 디렉터리 아래에 workflows 디렉터리 생성
3. workflows 디렉터리에 CI/CD 스크립트를 작성할 .yml 파일 생성
.yml 파일명은 상관없다! 나는 cicd.yml으로 생성했다.
환경변수 비밀값 GitHub Repository Secrets 등록
CI/CD 스크립트 파일에 쓰이는 환경변수와 application.yml(.properties)에 비밀값이 사용된다.
깃허브 레포지토리에 비밀값을 공개적으로 올리면 안 되니 깃허브 레포지토리 설정에 있는 secrets and variables에 등록해서 사용해야한다.
해당 프로젝트를 올려둔 레포지토리에 들어가 [Settings] - [secrets and variables] - [Actions]에서 secrets를 설정할 수 있다.
아래 두 항목을 secrets로 등록해야한다.
1. CI/CD 스크립트에 사용될 비밀값
2. application.yml
CI/CD 스크립트에 사용될 비밀값 등록
작성할 CI/CD 스크립트 파일에는 위 사진에 보이는 네 가지 비밀값들이 사용된다.
아래 값들을 secrets에 등록해주자! 예시처럼 하나씩 등록하면 된다.
Name은 마음대로 지정해도 된다. 나중에 CI/CD 스크립트에서 같은 Name으로 매핑하면 된다!
1. EC2_HOST: EC2 퍼블릭 IP 주소 (ex. 12.345.678.999)
2. EC2_USER: ec2-user
3. EC2_SSH_KEY
AWS 콘솔 키페어 - EC2에 사용한 키페어 선택 - 작업 - 키 페어 가져오기 - 키 페어 파일 찾아보기를 누르면 값이 나오는데,
—BEGIN—부터 —END—까지 다 포함한 전체 값을 복사해서 secrets에 작성한다!
문구 빼고 복사했다가 안 되서 헤맸었다😱
4. EC2_SSH_PORT: 22
요렇게 4개의 secrets를 등록하면 CI/CD 스크립트에 사용될 비밀값들은 완료된다!
application.yml 등록
어플리케이션이 제대로 실행되려면 application.yml이 필요한데 노출되어선 안 될 비밀값들(S3 시크릿키나 api 키 등등..)도 포함되어 있기 때문에 이것도 secrets에 등록해주어야한다.
application.yml 파일을 .gitignore에 등록해두었기 때문에 yml 파일 전체가 없는 상황에서 총 세 가지 방법을 시도했는데,
두 가지 방법은 실패하고 한 가지 방법만 성공했다🧐
1. application.yml 파일에 있는 비밀값만 ${__}으로 숨김 처리 후 깃허브에 application.yml 업로드하는 방법 -> 실패🥲
1-1. (X) 깃허브 레포지토리 secrets에 비밀값 지정한 뒤 secrets에서 값을 가져와 환경 변수를 설정하는 CI/CD 파이프라인 작성
1-2. (X) secrets에 지정해둔 비밀값들 다시 지우고 EC2 내부에 있는 .bashrc 파일에 비밀값 설정
-> 이건 생각해보니 jar 파일 자체가 깃허브 액션에서 만들어져서 ec2에 넣어지는 거라 애초에 불가능한 것 같다.
2. application.yml 파일을 아예 깃허브에 올리지 않는 방법 -> 성공!🥳
2-1. (O) 비밀값을 숨김 처리하지 않은 application.yml 내용 전체를 깃허브 secrets에 등록 후 CI/CD 파이프라인에서 secrets 내용을 가져와 applcation.yml 파일을 생성한 뒤 함께 빌드
프로젝트에 사용하는 yml 파일 내용 그대로 아무 처리 없이 전체 복사해서 secrets에 등록해주면 끝!
CI/CD 스크립트 파일(.yml) 작성
동작 흐름
1. develop 브랜치에 코드가 푸시되면 프로젝트가 빌드되고 jar 파일을 생성한다. (CI)
2. 빌드된 jar 파일을 EC2 서버에 배포한다. (CD)
CI 스크립트
스크립트 내에 브랜치명, MY_project로 써둔 프로젝트명이나 secrets.MY_YML 등으로 표현한 secrets 이름은 본인이 지정한대로 알맞게 바꿔주어야함!
1. Workflow 시작 조건
name: CI/CD
on:
push:
branches: [develop]
- 워크플로 이름과 해당 브랜치에 코드가 푸시될 때 워크플로를 시작한다고 지정
2. 빌드 전 작업
# 워크플로의 이름 지정
jobs:
build:
runs-on: ubuntu-latest # 실행 환경 지정
# 실행 스텝 지정
steps:
# 깃허브에서 제공하는 checkout 엑션 사용
- uses: actions/checkout@v4
# JDK 21 설정
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
# secrets 내용을 읽어 yml 파일을 특정 위치에 생성
- name: Make application.yml
run: |
mkdir -p ./src/main/resources
cd ./src/main/resources
touch ./application.yml
echo "${{ secrets.MY_YML }}" > ./application.yml
- jobs.build로 CI 작업 정의
- checkout 액션으로 레포지토리의 최신 코드를 가져옴
- 프로젝트가 JDK 21을 사용하기 때문에 자바 버전을 알맞게 설정
- /src/main/resources 위치에 application.yml 파일을 생성하고, yml 파일에 깃허브 secrets에 yml으로 등록해뒀던 내용을 붙여넣음
3. Gradle 빌드
# gradle wrapper 파일에 실행 권한을 부여
- name: Grant execute permission for gradlew
run: chmod +x gradlew
# Gradle 빌드 엑션을 이용해서 프로젝트 빌드
- name: Build with Gradle
uses: gradle/gradle-build-action@v2.6.0
with:
arguments: build
- name: List build directory
run: ls -la build/libs
# 빌드해서 생긴 JAR 파일을 깃허브 아티팩트로 업로드
- name: Upload build artifact
uses: actions/upload-artifact@v2
with:
name: MY_project-0.0.1-SNAPSHOT
path: build/libs/MY_project-0.0.1-SNAPSHOT.jar
- gradlew에 실행 권한을 주고 gradle을 사용해 프로젝트를 빌드
- 빌드 완료된 jar 파일을 name, path를 지정해서 GitHub Actions의 아티팩트로 업로드
- GitHub Actions의 upload-artifact는 임시 저장소 같은 기능! 워크플로가 완료되면 Actions 페이지에서 빌드 완료되어 업로드된 jar 파일을 확인할 수 있다.
CD 스크립트
1. 배포 시작 조건
# 배포
deploy:
needs: build
runs-on: ubuntu-latest # 실행될 인스턴스 OS와 버전
- needs: build로 위에 작성해둔 CI 작업이 성공해야 CD 작업이 실행되도록 함
2. 빌드 결과물(jar) 다운로드
# 위의 빌드작업한 JAR 파일 = 아티팩트를 다운로드
steps:
- name: Download build artifact
uses: actions/download-artifact@v2
with:
name: MY_project-0.0.1-SNAPSHOT
path: build/libs
- name: List downloaded files
run: ls -la build/libs
- 빌드 완료 후 아티팩트로 업로드해둔 jar 파일을 다운로드한 뒤, 정상적으로 다운로드되었는지 확인
3. EC2 접속용 SSH Key 저장
- name: Save SSH key to file
run: |
mkdir -p ~/.ssh
echo "${{ secrets.MY_EC2_SSH_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
- EC2에 접근하기 위한 SSH key를 secrets에서 불러와서 저장
4. 8080 포트 사용 중인 프로세스 종료
스크립트 계속 테스트 하다보니까 8080 포트가 이미 실행 중이라는 에러가 떠서 종료하는 명령어를 추가해줬더니 괜찮아졌음!
에러 안 뜨면 굳이 이 구문은 추가하지 않아도 될 듯..?
- name: Kill process using port 8080
run: |
ssh -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no ${{ secrets.MY_EC2_USER }}@${{ secrets.MY_EC2_HOST }} "sudo lsof -t -i:8080 | xargs sudo kill -9"
5. EC2로 jar 파일 전송 후 실행
- name: Copy JAR file to EC2
run: scp -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no build/libs/MY_project-0.0.1-SNAPSHOT.jar ${{ secrets.MY_EC2_USER }}@${{ secrets.MY_EC2_HOST }}:/home/ec2-user/app/MY_project/
- name: Deploy JAR to EC2
uses: appleboy/ssh-action@v0.1.9 # ssh 접속하는 오픈소스
with:
host: ${{ secrets.MY_EC2_HOST }} # EC2 인스턴스 IP
username: ${{ secrets.MY_EC2_USER }} # 아마존 리눅스 아이디 (보통 ec2-user)
key: ${{ secrets.MY_EC2_SSH_KEY }} # EC2 인스턴스 pem key
port: ${{ secrets.MY_EC2_SSH_PORT }} # 접속포트번호 (보통 22)
script: | # 실행될 스크립트
cd /home/ec2-user/app/MY_project
nohup java -jar MY_project-0.0.1-SNAPSHOT.jar > /home/ec2-user/app/MY_project/app.log 2>&1 &
- 다운로드 해둔 jar 파일을 EC2의 /home/ec2-user/app/MY_project/ 위치에 업로드
- EC2에 업로드된 jar 파일을 nohup 명령어를 통해 백그라운드로 실행 (EC2 터미널을 종료해도 어플리케이션이 종료되지 않음!)
스크립트 작성이 완료되었으니 이제 develop 브랜치에 푸시하면 자동으로 빌드 및 배포가 이루어진다!🥹
전체 CI/CD 스크립트 및 Actions 결과
스크립트 내에 브랜치명, MY_project로 써둔 프로젝트명, secrets.MY_YML 등으로 표현한 secrets 이름은 본인이 지정한대로 알맞게 바꿔주어야함!
# 워크플로의 이름 지정
name: CI/CD
# 워크플로가 시작될 조건 지정
on:
push:
branches: [develop]
jobs:
build:
runs-on: ubuntu-latest # 실행 환경 지정
# 실행 스텝 지정
steps:
# 깃허브에서 제공하는 checkout 엑션 사용
- uses: actions/checkout@v4
# JDK 21 설정
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
# secrets 내용을 읽어 yml 파일을 특정 위치에 생성
- name: Make application.yml
run: |
mkdir -p ./src/main/resources
cd ./src/main/resources
touch ./application.yml
echo "${{ secrets.MY_YML }}" > ./application.yml
# gradle wrapper 파일에 실행 권한을 부여
- name: Grant execute permission for gradlew
run: chmod +x gradlew
# Gradle 빌드 엑션을 이용해서 프로젝트 빌드
- name: Build with Gradle
uses: gradle/gradle-build-action@v2.6.0
with:
arguments: build
- name: List build directory
run: ls -la build/libs
# 빌드해서 생긴 JAR 파일을 깃허브 아티팩트로 업로드
- name: Upload build artifact
uses: actions/upload-artifact@v2
with:
name: MY_project-0.0.1-SNAPSHOT
path: build/libs/MY_project-0.0.1-SNAPSHOT.jar
# 배포
deploy:
needs: build
runs-on: ubuntu-latest # 실행될 인스턴스 OS와 버전
# 위의 빌드작업한 JAR 파일 = 아티팩트를 다운로드
steps:
- name: Download build artifact
uses: actions/download-artifact@v2
with:
name: MY_project-0.0.1-SNAPSHOT
path: build/libs
- name: List downloaded files
run: ls -la build/libs
- name: Save SSH key to file
run: |
mkdir -p ~/.ssh
echo "${{ secrets.MY_EC2_SSH_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
- name: Kill process using port 8080
run: |
ssh -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no ${{ secrets.MY_EC2_USER }}@${{ secrets.MY_EC2_HOST }} "sudo lsof -t -i:8080 | xargs sudo kill -9"
- name: Copy JAR file to EC2
run: scp -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no build/libs/MY_project-0.0.1-SNAPSHOT.jar ${{ secrets.MY_EC2_USER }}@${{ secrets.MY_EC2_HOST }}:/home/ec2-user/app/MY_project/
- name: Deploy JAR to EC2
uses: appleboy/ssh-action@v0.1.9 # ssh 접속하는 오픈소스
with:
host: ${{ secrets.MY_EC2_HOST }} # EC2 인스턴스 IP
username: ${{ secrets.MY_EC2_USER }} # 아마존 리눅스 아이디 (보통 ec2-user)
key: ${{ secrets.MY_EC2_SSH_KEY }} # EC2 인스턴스 pem key
port: ${{ secrets.MY_EC2_SSH_PORT }} # 접속포트번호 (보통 22)
script: | # 실행될 스크립트
cd /home/ec2-user/app/MY_project
nohup java -jar MY_project-0.0.1-SNAPSHOT.jar > /home/ec2-user/app/MY_project/app.log 2>&1 &
이제 develop 브랜치에 푸시하면 Actions 탭에서 자동으로 빌드 및 배포를 진행하고 워크플로 과정에 문제가 없다면 성공✅, 문제가 있었다면 실패❌로 나타내준다!🙂
스크립트 작성하고 테스트하면서 환경변수랑 경로 지정에서 고생했었다....🫠
'⚙️ DevOps' 카테고리의 다른 글
[AWS/EC2] AWS EC2 + RDS + S3 생성하고 배포하기 #3 빌드 및 배포 (0) | 2024.11.16 |
---|---|
[AWS] 2024년 2월부터 모든 퍼블릭 IPv4 주소에 대해 요금이 부과되도록 변경 - 프리티어임에도 비용이 발생했다면 (0) | 2024.11.06 |
[AWS/RDS] AWS EC2 + RDS + S3 생성하고 배포하기 #2 RDS + S3 (0) | 2024.10.28 |
[AWS/EC2] AWS EC2 + RDS + S3 생성하고 배포하기 #1 EC2 (0) | 2024.10.21 |
[AWS/S3] 이미지 업로드용 S3 버킷 및 IAM 사용자 생성하기 (0) | 2024.09.27 |