들어가기 전에
지난 준비편을 통해 모든 준비가 끝났다면 이번 글에서 CI/CD 구축 작업을 직접 해보도록 하자. 참고로 이 글은 Github Actions 내의 모든 기능이나 개념에 대한 설명보다는 집중에 구현한 글이다. 만약 기초 개념에 대한 공부가 필요하다면 Microsoft에서 제공하는 Github Actions로 개발 주기 자동화라는 발표 영상을 추천한다.
목차
🌊 실전
1. CI
- workflow 생성
2. CD
- workflow 생성
- ssh-action 활용하여 서버 접속하기
- scp-action 활용하여 빌드된 아티팩트 복사하기
시작하기
1️⃣ CI
workflow 생성
/github/workflows 하위에 ci.yml 파일을 생성하고 내용을 커스텀한다.
name: CI
on:
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Run chmod to make gradlew executable
run: chmod +x ./gradlew
- name: Build with Gradle
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
with:
arguments: build
- name : 자유롭게 원하는 이름으로 작성하면 된다.
- on : 어떤 작업이 발생할 때 workflow가 실행될지에 대한 설정으로 나는 main 브랜치에 pull request가 발생할 때 실행되도록 했다.
- 프로젝트에서 사용하는 Java 버전이 17이기 때문에 버전 정보를 변경했고 gradlew 권한 설정 스텝을 추가해줬다. 참고로 기존 템플릿에서 권한 설정 스텝을 추가하지 않으면 에러가 발생한다.
2️⃣ CD
1. workflow 생성
템플릿만 먼저 작성해두고 작업을 하나 하나 추가해가야 한다.
name: CD
on:
push:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Run chmod to make gradlew executable
run: chmod +x ./gradlew
- name: Build with Gradle
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
with:
arguments: build
- on : 이번에는 main 브랜치에 push가 될 때 실행되도록 했다.
- 나머지 설정은 CI 파일과 같다.
2. ssh-actions 활용: 서버 접속
GitHub - appleboy/ssh-action: GitHub Actions for executing remote ssh commands.
GitHub Actions for executing remote ssh commands. Contribute to appleboy/ssh-action development by creating an account on GitHub.
github.com
서버에 ssh 키를 이용하여 접속하기 위해 ssh-action을 활용한다.
1) ssh 접속을 위해 rsa 키를 생성한다.
참고로 Best practice는 원격 서버가 아닌 로컬 서버에서 생성하는 거라고 한다. 나는 원격 서버에 직접 설치했다. 파일 이름은 자유롭게 기입하면 된다.
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
2) 생성된 파일을 확인한다.
입력한 파일 이름대로 private key 파일과 public key 파일, 총 2개가 생긴다.
- id_rsa : Private key
- id_rsa.pub : Public key
3) Public key를 .ssh/authorized_keys에 추가한다.
cat id_rsa.pub >> ~/.ssh/authorized_keys
4) Private key를 Github Secrets에 추가한다.
id_rsa 파일을 열어 전문을 복사하고 Github Secrets에 추가한다. 이때 주의할 점은 가운데 키로 보이는 문자열만 복사하는 게 아니라 처음부터 끝까지 파일 내의 모든 내용이 복사되어야 하는 것이다.
nano id_rsa
5) 연결 테스트를 한다.
Github Secrets에 서버 접속을 위한 정보를 추가한다.
- HOST : 서버 접속용 IP
- USERNAME : 서버 username
- PRIVATE_KEY : ssh PRIVATE KEY
- PORT : 서버 접속용 외부 포트
steps에 아래 작업을 추가해서 접속이 완료되고 script가 실행되는지 확인하면 된다.
- name: executing remote ssh commands using password
uses: appleboy/ssh-action@v0.1.10
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.PRIVATE_KEY }}
port: ${{ secrets.PORT }}
script: whoami
이때 접속이 안되는 문제가 발생하는 경우가 많은데 어떤 걸 시도해 볼 수 있을지 적어보겠다. 보통 이 과정을 거치면 접속 문제가 해결됐다.
- 접속을 위한 host, username, port가 제대로 기입됐는지 확인한다.
- 위의 정보가 제대로 기입됐다면 private_key 전문을 다시 복사해 secrets에 기존 키는 삭제하고 새로 추가한다. 복사하는 과정 중에 실수로 원치 않은 정보가 들어갔을 수도 있다.
- 그래도 안된다면 rsa 키 생성부터 다시 해보자.
3. scp-action 활용: 빌드된 아티팩트 복사
GitHub - appleboy/scp-action: GitHub Action that copy files and artifacts via SSH.
GitHub Action that copy files and artifacts via SSH. - GitHub - appleboy/scp-action: GitHub Action that copy files and artifacts via SSH.
github.com
파일이나 빌드된 아티팩트들을 서버에 복사하기 위해 scp-action을 활용하자.
[jar 파일 복사하기]
1) steps에 jar 파일 복사 작업을 추가한다.
- name: Deploy jar file to remote server
uses: appleboy/scp-action@v0.1.4
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USERNAME }}
key: ${{ secrets.SERVER_PRIVATE_KEY }}
port: ${{ secrets.SERVER_PORT }}
source: ./build/libs/buddy-wisdom-0.0.1-SNAPSHOT.jar
target: /budsdom
overwrite: true
- 대부분 기존에 저장해놓은 Secrets 정보를 활용하면 된다.
- source : jar 파일 생성 위치를 기입한다.
- target : 서버 내에 어떤 위치에 저장할 것인지 기입한다.
2) 빌드 결과를 확인한다.
3) 서버 내에 jar 파일이 복사되었는지 확인한다.
4) jar 파일이 실행되는지 확인한다.
java -jar [jar파일명].jar
[shell script 복사하기]
1) 프로젝트 내에 shell script 파일을 생성한다.
내용은 다음과 같다.
#!/bin/bash
DIRECTORY=/budsdom/build/libs
PROJECT_NAME=buddy-wisdom
echo ">>> Move the directory."
cd $DIRECTORY
echo ">>> Verify that any processes are currently running."
CURRENT_PID=$(pgrep -f ${PROJECT_NAME}.*.jar)
if [ -z "$CURRENT_PID" ]; then
echo ">>> No processes are currently running."
else
echo ">>> kill -15 $CURRENT_PID"
kill -15 $CURRENT_PID
sleep 5
fi
echo ">>> Run the jar file"
JAR_NAME=$(find ./ -maxdepth 1 -type f -name "*.jar" -printf "%f\n" | sort -r | head -n 1)
echo ">>> JAR NAME : $JAR_NAME"
nohup java -jar "$JAR_NAME" > /dev/null 2>&1 &
❶ cd $DIRECTORY
jar 파일이 있는 디렉토리로 이동한다.
❷ CURRENT_PID=$(prep -f ${PROJECT_NAME).*.jar)
현재 jar 파일을 실행 중인 프로세스 ID를 찾는다.
❸ if [ -z "$CURRENT_PID" ]; then
실행 중인 프로세스가 없을 때 무슨 작업을 할 건지 작성한다.
❹ kill -15 $CURRENT_PID
정상 종료되도록 -15를 사용해 프로세스를 종료한다. (스프링 부트 2.3 이후부터는 shutdown 설정을 graceful로 해야 적용이 가능하다고 한다.)
❺ sleep 5
다음 작업을 시작하기 전에 5초 간 일시 정지한다.
❻ JAR_NAME=$(find ./ -maxdepth 1 -type f -name "*.jar" -printf "%f\n" | sort -r | head -n 1)
현재 디렉토리 내의 파일 중에 jar 확장자를 가지며 가장 최근에 수정된 파일을 찾아 이름을 출력한다.
❼ nohup java -jar "$JAR_NAME" > /dev/null 2>&1 &
프로세스를 백그라운드에서 실행하기 위한 명령어로 표준 에러를 표준 출력으로 출력되게 해놓고 표준 출력은 출력되지 않게 했다.
❼에서 표준 출력이 출력되지 않게 한 이유는 CD 과정에서 아래와 같이 애플리케이션 실행 로그가 뜨면서 다음 과정으로 넘어가지 않는 문제점이 발생했기 때문이다. 이때는 scrpit 실행 명령어를 nohup으로 하고 표준 출력이 출력되지 않게 해도 해결이 되지만 script 내부에서 처리하는 게 나을 것 같다고 판단이 들어 이렇게 처리하게 됐다.
2) steps에 shell script 파일 복사 작업을 추가한다.
- name: Deploy script to remote server
uses: appleboy/scp-action@v0.1.4
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.PRIVATE_KEY }}
port: ${{ secrets.PORT }}
source: ./run.sh
target: ./
overwrite: true
- name: Run jar file use SSH
uses: appleboy/ssh-action@v0.1.10
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USERNAME }}
key: ${{ secrets.SERVER_PRIVATE_KEY }}
port: ${{ secrets.SERVER_PORT }}
script: chmod 755 run.sh && ./run.sh
- overwrite : 덮어쓰기 옵션을 true로 지정했다.
- script : 복사한 파일의 권한을 실행 가능하도록 변경하고 실행하도록 했다.
3) 테스트 경로를 만들어 화면이 제대로 뜨는지 확인한다.
위처럼 뒤에 포트 번호를 생략하고 싶다면 80번 포트를 8080번 포트로 리다이렉션 시키자.
sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 8080
정리
최종적으로 모든 작업을 적용한 CI/CD workflow는 이렇다.
name: CI
on:
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Run chmod to make gradlew executable
run: chmod +x ./gradlew
- name: Build with Gradle
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
with:
arguments: build
name: CD
on:
push:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Run chmod to make gradlew executable
run: chmod +x ./gradlew
- name: Build with Gradle
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
with:
arguments: build
- name: Deploy jar file to remote server
uses: appleboy/scp-action@v0.1.4
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USERNAME }}
key: ${{ secrets.SERVER_PRIVATE_KEY }}
port: ${{ secrets.SERVER_PORT }}
source: ./build/libs/buddy-wisdom-0.0.1-SNAPSHOT.jar
target: /budsdom
overwrite: true
- name: Deploy script to remote server
uses: appleboy/scp-action@v0.1.4
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.PRIVATE_KEY }}
port: ${{ secrets.PORT }}
source: ./run.sh
target: ./
overwrite: true
- name: Run jar file use SSH
uses: appleboy/ssh-action@v0.1.10
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USERNAME }}
key: ${{ secrets.SERVER_PRIVATE_KEY }}
port: ${{ secrets.SERVER_PORT }}
script: chmod 755 run.sh && ./run.sh
참고
- 스프링 부트와 AWS로 혼자 구축하는 웹 서비스
- [Git] Github Actions Error: Gradle script '/home/runner/work/*/gradlew' is not executable.
- Spring boot Graceful Shutdown
- ssh: handshake failed on every attempt
'노트 > F-lab' 카테고리의 다른 글
Github Actions: CI/CD 구축 (feat. Docker) (0) | 2023.06.21 |
---|---|
RabbitMQ를 활용한 메시지 처리 방식 구현: 설치부터 적용까지 (0) | 2023.06.17 |
Github Actions: CI/CD 구축 (feat.shell script) - 준비편 (0) | 2023.06.04 |
NCP 서버 생성부터 ssh 접속까지 (2) | 2023.05.29 |
Spring REST Docs를 선택한 이유 (0) | 2023.04.08 |