프로젝트를 할 때, EC2에서 백엔드 서버를 띄워두고 프론트엔드와의 연동을 진행하였다. 그런데 백엔드 개발이 완성된 상태가 아니어서 변경된 사항이 생기면 EC2에 접속하고, 변경사항을 pull 받고 다시 빌드한 후에 jar파일을 실행시켜야 하는 귀찮은 작업을 반복해야 했다.
앞으로도 이런 작업을 여러번 반복해야 할 것 같아서 자동화 시스템을 구축하기로 하였다.
CI/CD를 구축하기 위해서 Jenkins와 github action을 주로 사용한다고 들었는데, 아무래도 조금 더 친숙한 github action 을 사용하여 자동화 시스템을 구축하였다.
그럼 CI/CD가 무엇인지, 구축하는 방법은 무엇인지, 그 과정에서의 시행착오들은 무엇이 있었는지 살펴보자!
Continuous Integration
지속적 통합이라는 뜻으로, 새로운 소스코드의 빌드, 테스트, 병합 작업을 의미한다.
Continuous Delivery/Continous Deployment
지속적인 서비스 제공 및 지속적인 배포를 의미한다.
즉, CI/CD는 소프트웨어 개발 과정을 자동화하여 빠르게 배포할 수 있는 환경을 제공한다.
github action에서는 이벤트를 설정하고, 이벤트에 따라서 동작할 과정을 yml파일에 작성하여 설정할 수 있다. github action를 활용하기 전에 우선 어떤 구성요소들을 가지는지 살펴보도록 하자.
Workflows
하나 이상의 작업(Job)을 실행하는 구성 가능한 자동화 프로세스
워크플로우는 저장소 내에 YAML 파일로 정의하고, 하나의 저장소에 여러 워크플로우를 가질 수 있다.
Events
워크플로우 실행을 트리거하는 저장소 내의 특정 활동(pull request 생성, issue 생성, push 등)
Jobs
워크플로우 내에서 실행되는 단계들의 집합
Actions
GitHub Actions 플랫폼을 위한 커스텀 애플리케이션
다양한 자동화 작업을 손쉽게 설정할 수 있다.
actions/checkout@v3, actions/setup-node@v3 등과 같은 액션들을 제공한다.
Runners
워크플로우를 실행하는 서버
Ubuntu Linux, Microsoft Windows, macOS 환경을 제공한다.
각 워크플로우는 새로운 가상 머신에서 수행된다.
추가적인 내용은 GitHub Docs - GitHub Actions 설명서 에 해당 내용이 잘 설명되어 있다.
main 브랜치에 push가 발생 -> 변경 내용을 반영하여 Build -> 생성된 jar 파일을 실행
나는 위와 같이 동작하는 자동화 시스템을 구축하였다.
변경 내용을 반영하여 build하는 과정이 CI에 해당하고
build하여 만들어진 jar 파일을 실행하는 과정이 CD에 해당하는 것이다.
github action에서 제공하는 workflow를 기반으로 수정하여 구축하였다.
./gradlew clean build 로 이전 빌드 상태는 없애고 새로 빌드할 수 있도록 하였다.
스크립트를 변경하면서 해결한 문제들을 위주로 빌드 과정을 살펴보자!
gradlew를 찾을 수 없다는 오류가 발생하였다
./gradlew: No such file or directory
우리 프로젝트의 파일 구조는 다음과 같았다.
/
├── README.md
├── TWO/
│ ├── build/
│ ├── gradlew
│ ├── src/
│ ├── build.gradle
│ ├── gradlew.bat
│ ├── gradle/
│ └── settings.gradle
├── deploy.sh
깃허브 저장소의 root에서 Build를 하려고 하지만 gradlew는 TWO 디렉터리 내부에 존재해서 gradlew를 찾지 못해서 발생하려고 하는 오류였다.
따라서 TWO 디렉터리에서 gradlew를 실행시킬 수 있도록 설정이 필요하였다.
따라서 gradlew의 실행 권한 설정과 gradlew를 실행시킬 때에 working-directory를 TWO 라는 이름으로 아래와 같이 설정해주었다.
- name: Grant execute permission for gradlew
run: chmod +x gradlew
working-directory: TWO
- name: Build with Gradle
run: ./gradlew clean build
working-directory: TWO
properties 파일에 연결된 DB의 정보가 포함되어 있어서 이 내용은 깃허브에 올리지 않았다.
하지만 빌드를 진행하기 위해서는 꼭 필요한 정보들이다!
그래서 외부에는 드러나지는 않지만 빌드 시에 사용할 수 있도록 환경변수를 설정해주었다.
repository의 settings -> Secrets and variables에서
github action에 사용할 환경변수들을 다음과 같이 추가해놓을 수 있다.

스크립트에서는 ${{secrets.[환경변수명]} 형태로 사용 가능하다.
echo 명령어와 리다이렉트 >>(append) 를 사용하여 application.properties 파일에 환경변수에 저장해둔 db 관련 정보를 파일에 추가해주었다.
디버깅을 위해서 스크립트에 properties 파일을 출력해보는 명령어를 작성해보기도 하였는데, *로 표시되고 보이지는 않았다!
- name: make application-mysql.yml
run: |
cd ./TWO/src/main/resources
echo "${{ secrets.MYSQL_APPLICATION }}" >> ./application.properties
이렇게 working directory와 secrete 환경변수를 설정하여 빌드가 가능하게 만든 스크립트 파일은 다음과 같다.
# 1 워크플로의 이름 지정
name: main
# 2 워크플로가 시작될 조건 지정
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest # 3 실행 환경 지정
#4 실행 프로세스 지정
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '21'
- name: make application-mysql.yml
run: |
cd ./TWO/src/main/resources
echo "${{ secrets.MYSQL_APPLICATION }}" >> ./application.properties
- run: cat ./TWO/src/main/resources/application.properties
- name: Grant execute permission for gradlew
run: chmod +x gradlew
working-directory: TWO
- name: Build with Gradle
run: ./gradlew clean build
working-directory: TWO
다음과 같이 빌드가 성공한 모습을 볼 수 있다!
github action의 runners에서 빌드를 하여 생성된 jar 파일을 aws 인스턴스에 옮겨서 실행시키는 방식으로 CD를 구축하였다.
artifact는 github action을 실행하면서 생성된 파일이나 데이터를 저장하고 공유하는 방법이다.
아티팩트를 사용하면 워크플로의 작업 간에 데이터를 공유할 수 있으며, 워크플로 실행이 종료된 이후에도 빌드 및 테스트 출력을 저장할 수 있다.
사실 이 과정은 필요 없는데 다른 워크플로우 파일을 복붙해서 작성하느라 포함되었다...
하지만 다음에 사용할 수도 있으니 artifact에 대해서 알아두도록 하자!
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: build-artifact
path: build/libs/*.jar
scp(Secure Copy Protocol) 를 사용하여 로컬에서 원격 서버로 파일을 복사한다.
즉, github action의 runners에 존재하던 jar 파일을 EC2 인스턴스로 복사한다.
EC2에 접속하기 위한 ip를 EC2_HOST, EC2에 접속하는데 필요한 pem 키 내용을 SSH_PRIVATE_KEY에 등록해두었다. 이를 통해서 EC2 인스턴스에 접근할 수 있도록 설정해주었다.
strip_compenets 는 상위 디렉토리 경로를 제거하는 것이다.
해당 스크립트에서는 strip_components 가 2로 설정되어 있으므로 상위 2개의 디렉터리 경로를 제거하는 것이다.
현재 복사하는 source "./TWO/build/libs/*.jar" 로 지정되어 있다. 여기서 상위 두개의 디렉터리인 TWO와 build를 제외하고 libs/*.jar 까지만 복사되는 것이다.
- name: Copy jar file to remote
uses: appleboy/scp-action@master
with:
username: ubuntu
host: ${{ secrets.EC2_HOST }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: "./TWO/build/libs/*.jar"
target: "/home/ubuntu/cicd"
strip_components: 2
EC2에 접속하여 확인해보면/home/ubuntu/cicd/libs/*.jar 경로로 복사가된 것을 확인할 수 있다.

그리고 jar 파일을 실행을 시켜주어야 한다.
기존에 실행되던 jar 파일은 끄고, 새롭게 가져온 jar 파일을 실행시켜주어야 한다. 이 과정을 실행시켜주는 스크립트 파일인 deploy.sh 를 작성하여 github에 같이 올려 두었다. 그래서 이 deploy.sh 파일도 EC2로 복사해준다.
jar 파일을 복사한 것과 동일하게 deploy.sh 파일도 복사한다.
- name: Copy deploy script file to remote
uses: appleboy/scp-action@master
with:
username: ubuntu
host: ${{ secrets.EC2_HOST }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: "deploy.sh"
target: "/home/ubuntu/cicd"
deploy.sh 파일의 내용은 다음과 같다.
#!/bin/bash
echo "Deployment start"
# 현재 실행 중인 .jar 프로세스의 PID를 가져옵니다.
CURRENT_PID=$(pgrep -f .jar)
echo "Current PID: $CURRENT_PID"
# 실행 중인 프로세스가 있으면 종료합니다.
if [ -z "$CURRENT_PID" ]; then
echo "No running process found."
else
echo "Killing process $CURRENT_PID"
kill -9 $CURRENT_PID
sleep 3
fi
# 새로운 JAR 파일의 경로를 설정합니다.
JAR_PATH="/home/ubuntu/cicd/libs/*.jar"
echo "JAR Path: $JAR_PATH"
# JAR 파일에 실행 권한을 부여합니다.
chmod +x $JAR_PATH
# JAR 파일을 백그라운드에서 실행합니다.
nohup java -jar $JAR_PATH > /dev/null 2> /dev/null < /dev/null &
echo "Deployment successful."
ssh를 통해 원격 서버에 명령을 실행할 수 있도록 해주는 액션인 ssh-action를 이용하여 deploy.sh를 실행시켜준다. 위에서 옮겨둔 deploy.sh에 실행권한을 부여하고 실행시켜준다.
이 과정까지 마무리한다면 모든 과정이 마무리된 것이다!!!!
- name: Execute deploy script
uses: appleboy/ssh-action@master
with:
username: ubuntu
host: ${{ secrets.EC2_HOST }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script_stop: true
script: |
chmod +x /home/ubuntu/cicd/deploy.sh
sh /home/ubuntu/cicd/deploy.sh
CI/CD 모두 동작하는 workflow 파일은 다음과 같다.
# 1 워크플로의 이름 지정
name: main
# 2 워크플로가 시작될 조건 지정
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest # 3 실행 환경 지정
#4 실행 스텝 지정
# uses: uses 키워드는 지정한 리포지토리를 확인하고 코드에 대한 작업을 실행할 수 있습니다.
# action/check-out에는 checkout이라는 작업의 v3 버전을 실행합니다.
# name: 스텝의 이름을 지정합니다.
# run: run 키워드는 실행할 명령어를 입력합니다.
# ./gradlew clean build에는 그레들을 사용해 프로젝트를 빌드 이전 상태로 돌리고 다시 빌드하는 명령어를 실행합니다.
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '21'
- name: make application-mysql.yml
run: |
cd ./TWO/src/main/resources
echo "${{ secrets.MYSQL_APPLICATION }}" >> ./application.properties
- run: cat ./TWO/src/main/resources/application.properties
- name: Grant execute permission for gradlew
run: chmod +x gradlew
working-directory: TWO
- name: Build with Gradle
run: ./gradlew clean build
working-directory: TWO
# github artifact에 jar파일 복사
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: build-artifact
path: build/libs/*.jar
- name: Copy jar file to remote
uses: appleboy/scp-action@master
with:
username: ubuntu
host: ${{ secrets.EC2_HOST }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: "./TWO/build/libs/*.jar"
target: "/home/ubuntu/cicd"
strip_components: 2
- name: Copy deploy script file to remote
uses: appleboy/scp-action@master
with:
username: ubuntu
host: ${{ secrets.EC2_HOST }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: "deploy.sh"
target: "/home/ubuntu/cicd"
- name: Execute deploy script
uses: appleboy/ssh-action@master
with:
username: ubuntu
host: ${{ secrets.EC2_HOST }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script_stop: true
script: |
chmod +x /home/ubuntu/cicd/deploy.sh
sh /home/ubuntu/cicd/deploy.sh
push가 발생할 때마다 워크플로우가 잘 작동하는 것을 확인할 수 있다.

Refs