0) CI/CD 환경 구축
6명의 팀원이 진행하는 프로젝트이다보니,
CI는 불가피한 선택이라고 생각했다.
자동 배포 환경 구축에 관심이 많았었는데
마침 팀원 중 한명이 예전에 사놓은 도메인이 있었고
그 도메인을 이용하여
CD까지 구현해보기로 의견이 모아졌다.
다음은 프로젝트의 Action 페이지이다.
GitHub - donsonioc2010/picasso: temp name
temp name. Contribute to donsonioc2010/picasso development by creating an account on GitHub.
github.com
1) CI with Github Action
여러 다른 선택지가 있으나
주어진 시간에 비해
해야 할 작업이 너무 많았다.
레퍼런스가 많고 이미 다른 프로젝트에서 접해본
Github Action으로 CI 파이프라인을 구축해 보았다.
1-1) ci.yml - on
name: Picasso Build
on:
pull_request:
branches: [ "dev", "feat" ]
types: [ opened, synchronize, reopened ]
dev 브랜치와
feat 하위 브랜치에서 PR을 할 때 마다
CI가 진행되도록 하기 위해 작성했다.
1-2) ci.yml - jobs
jobs:
build:
name: Picasso Build
runs-on: ubuntu-latest
steps:
- name: Github Action Version Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up JDK 17
uses: actions/setup-java@v1
with:
java-version: 17
distribution: 'zulu'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Test with Gradle
run: ./gradlew test
- name: Build with Gradle
run: ./gradlew build
- name: Cache Gradle packages
uses: actions/cache@v1
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: ${{ runner.os }}-gradle
NCP 서버가 우분투로 열려있기 때문에
ubuntu-latest 환경 위에서 CI가 수행되도록 설정했다.
CI에서 하는 작업은 CD에 비해서 많이 단순했다.
그냥 ./gradlew build
, ./gradlew test
로 build와 test 작업을 해준 뒤
Gradle package를 Github Action container에 캐싱한다.
마지막 캐싱 작업 자체는 이후 수행될 CI에서 더 빠른 작업을 위한 것이므로
만약 요금이 부과되는 private 모드였다면 아마 제외시키지 않았을까 싶다..
2) CI with DBDocs
CI의 장점을 다시 한번 생각해보면,
코드의 지속적인 통합을 통한 협업 능력 향상이 가장 먼저 떠올랐다.
이렇게 작은 MVP 프로젝트에서도
팀원마다 맡은 기능이 달라
DB에 대한 계속적인 수정이 필요한데,
더 큰 프로젝트에서는 어떨지 상상도 되지 않았다.
마침 필자의 팀이 사용하는 DB 툴인
DBDocs에서 Github Action을 이용한
DDL 코드 통합을 지원하여 사용해보기로 하였다.
2-1) DBDOCS_BUILD.yml 코드
name: DBDOCS_BUILD
on:
push:
branches: [ "dev" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
# DBDocs Install
- name: Install dbdocs
run: sudo npm install -g dbdocs
#DBDocs Install Check
- name: Check dbdocs
run: dbdocs
# Runs a set of commands using the runners shell
- name: Update dbdocs project
env:
DBDOCS_TOKEN: ${{ secrets.DBDOCS_TOKEN }}
run: dbdocs build ./Docs/Picasso.dbml --project=Picasso
이후 CD에 관한 설명에서 나오겠지만
dev에 push가 될 때 마다 자동 배포가 이루어 지기 때문에
dev에 push될 때 마다 DBDocs의 내용이 바뀌도록 설정했다.
DBDocs의 경우 npm으로 컨테이너에 라이브러리를 설치하고
Github Action Secret Key에 저장되어있는 DBDOCS_TOKEN으로
팀의 DBDocs에 접속하도록 하여
dbml 파일을 실제 팀의 DBDocs에 반영되도록 하였다.
3) CD with Github Action
정말 관심이 많았던 부분이었다.
같이 프로젝트를 진행하고 있던 형이 시켜서 해보았던
수동 배포에 대한 기억과
TIFY에서 하고있는 자동 배포에 대한 기억을 겹쳐서 생각해 보았을 때
자동 배포는 무조건 필요하다고 생각이 되었다.
시간이 없어 Github Action을 사용하지만,
다음 프로젝트에서는 Jenkins와 같은 다른 툴을 이용해 볼 것이다.
3-1) deploy.yml - on, permissions
name: Picasso CI/CD
on:
push:
branches: [ "dev", "main" ]
permissions:
contents: read
dev 브랜치와 main 브랜치에 push될 때 마다
자동 배포가 수행되도록 설정하였다.
우리 프로젝트의 설정 상 dev, main 브랜치에는
push가 direct하게 되지 않고
PR을 이용하여 merge하는 규칙을 설정했기 때문에
결국엔 [PR - 리뷰를 통한 merge]라는 과정 이후에
자동 배포가 이루어지게 된다.
permissions의 contents: read를 사용하여
Actions 페이지에서 커밋마다의 Action에 대해 자세히 볼 수 있게 설정했다.
3-2) deploy.yml - jobs
jobs:
build:
name: Picasso CI/CD
runs-on: ubuntu-latest
steps:
- name: Github Action Version Checkout
uses: actions/checkout@v2
- name: Setup jdk-17
uses: actions/setup-java@main
with:
java-version: 17
distribution: 'zulu'
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
- name: Test with Gradle
run: ./gradlew test
- name: Build with Gradle
run: ./gradlew build
- name: Move App.jar
run : mv ./Api/build/libs/app.jar ./
# Proxy Server Connect with SSH
# scp로 proxy server에 build된 Jar 파일을 보낸다
# jar 파일을 실행한다. 이 때 storage에 대한 VM Option을 추가하여 실행한다.
- name: Transfer BuildFile Git Actions To ApplicationServer
uses: appleboy/scp-action@master
with:
proxy_host: ${{ secrets.NCP_PROXY_HOST }}
proxy_username: root
proxy_key: ${{ secrets.NCP_PEM }}
proxy_port: 2222
host: ${{ secrets.NCP_APP_HOST }}
username: root
key: ${{ secrets.NCP_PEM }}
port: 22
source: app.jar
target: /var/app
- name: Run Application
uses: appleboy/ssh-action@master
with:
proxy_host: ${{ secrets.NCP_PROXY_HOST }}
proxy_username: root
proxy_key: ${{ secrets.NCP_PEM }}
proxy_port: 2222
host: ${{ secrets.NCP_APP_HOST }}
username: root
key: ${{ secrets.NCP_PEM }}
port: 22
script: |
cd /var/app
./app_run.sh
CI와 마찬가지로 NCP 서버가 ubuntu로 구성되어있기 때문에
ubuntu-latest로 설정했다.
배포 이전에도 코드의 실행 가능성에 대해 테스트를 해야하기 때문에
CI에서 했던 작업을 그대로 작성해주었다.
이후 Api/build 폴더에 있는 app.jar를
추후 프록시 서버와 어플리케이션 서버로 쉽게 보낼 수 있게
컨테이너의 home 디렉토리로 옮겨주었다.
이 때 발생했던 오류와 그에 대한 해결 과정은
아래 더보기에 표시해놓았다.
엄청난 과정이 있었지만
굵직한 실수와 오류만 적어놓았다....
1. scp부터 안된다...

scp 명령어로 app.jar를
프록시 서버를 거쳐 어플리케이션 서버로 보내야
프로그램을 실행시키든 뭘 하던지 하는데
scp 명령어부터 들어먹질 않았다.
이 때는 Action name에서부터 볼 수 있듯이
scp, ssh action-master를 동시에 수행했기 때문에
직접 ssh로 프록시, 어플리케이션 서버에 들어가
app.jar가 이동했는지 확인해보았다...
그렇게 확인해보니 파일은 전혀 이동하지 않았고
당연히 app.jar는 실행되지 않았다...
2) 어떻게 해결했을까?
scp가 안되는 이유를 생각해보았다.
혹시 프록시 서버를 들어갈 수 있는 다른 방법이 있는걸까?
scp-action 부분의 처음 코드이다.
- name: Deploy From Github Action to Application Server via Proxy Server
uses: appleboy/scp-action@master
with:
proxy_host: ${{ secrets.NCP_PROXY_HOST }}
proxy_username: root
proxy_key: ${{ secrets.NCP_PEM }}
proxy_port: 2222
host: ${{ secrets.NCP_APP_HOST }}
username: root
key: ${{ secrets.NCP_PEM }}
port: 22
source: ./Api/build/libs/app.jar
target: /var/app
script: |
pwd
cd /var/app
ls -al
cd ../..
java -jar ./var/app/app.jar -Dnaver.storage.access-key=${{ secrets.ACCESS_KEY }} -Dnaver.storage.secret-key=${{ secrets.SECRET_KEY }}
실패했다.
script의 run 까지는 하지 않고
scp만 (source, target 까지만) 해보았다.
그런데 이것도.. 안됐다
그래서 코드를 또 바꾸어 보았다.
- name: Deploy From Github Action to Application Server via Proxy Server
uses: appleboy/scp-action@master
with:
proxy_host: ${{ secrets.NCP_PROXY_HOST }}
proxy_username: root
proxy_key: ${{ secrets.NCP_PEM }}
proxy_port: 2222
host: ${{ secrets.NCP_APP_HOST }}
username: root
key: ${{ secrets.NCP_PEM }}
port: 22
source: ./Api/build/libs/app.jar
target: /var/app
run: java -jar ./var/app/app.jar -Dnaver.storage.access-key=${{ secrets.ACCESS_KEY }} -Dnaver.storage.secret-key=${{ secrets.SECRET_KEY }}
이걸 쓰고 같이 팀을 이룬 형한테 보여주었을 때
호되게 맞을 뻔 했다.
scp-action에서 run을 써버린 것이다 흑흑..
그래서 코드를 또 또 바꾸어 보았다.
- name: Transfer BuildFile Git Actions To ApplicationServer
uses: appleboy/scp-action@master
with:
proxy_host: ${{ secrets.NCP_PROXY_HOST }}
proxy_username: root
proxy_key: ${{ secrets.NCP_PEM }}
proxy_port: 2222
host: ${{ secrets.NCP_APP_HOST }}
username: root
key: ${{ secrets.NCP_PEM }}
port: 22
source: app.jar
target: /var/app
흠... 전혀 문제가 없어보이는데
다시 한 번 timeout 오류가 났다.
혹시 프록시 서버의 홈 디렉토리에
app.jar 파일이 없어 한번에 찾지 못하는 문제인가 싶어
mv 명령어로 app.jar를 옮겨주었다.
또 scp-action과 ssh-action을 분리하여
파일 이동과 파일 실행을 격리하기로 하였다.
- name: Move App.jar
run : mv ./Api/build/libs/app.jar ./
# Proxy Server Connect with SSH
# scp로 proxy server에 build된 Jar 파일을 보낸다
# jar 파일을 실행한다. 이 때 storage에 대한 VM Option을 추가하여 실행한다.
- name: Transfer BuildFile Git Actions To ApplicationServer
uses: appleboy/scp-action@master
with:
proxy_host: ${{ secrets.NCP_PROXY_HOST }}
proxy_username: root
proxy_key: ${{ secrets.NCP_PEM }}
proxy_port: 2222
host: ${{ secrets.NCP_APP_HOST }}
username: root
key: ${{ secrets.NCP_PEM }}
port: 22
source: app.jar
target: /var/app
- name: Run Application
uses: appleboy/ssh-action@master
with:
proxy_host: ${{ secrets.NCP_PROXY_HOST }}
proxy_username: root
proxy_key: ${{ secrets.NCP_PEM }}
proxy_port: 2222
host: ${{ secrets.NCP_APP_HOST }}
username: root
key: ${{ secrets.NCP_PEM }}
port: 22
script: |
nohup java -jar -Dspring.profiles.active=dev -Dnaver.storage.access-key=${{ secrets.ACCESS_KEY }} -Dnaver.storage.secret-key=${{ secrets.SECRET_KEY }} /var/app/app.jar > nohup.log 2> nohup.err < /dev/null &
appleboy의 깃헙에서 보았을 때
프록시 서버를 통하여 어플리케이션 서버로 접속하는 과정은
문제가 없었다.
그렇다면 문제는
Github Action Secret Key가 이상하다는 것..
혹시나 싶어 Secret Key를
제대로 된 값을 넣고 실행해 보았더니
scp가 작동했다...!
위같이 프록시 서버와 어플리케이션 서버에
접속하는 것이 원활히 되다 보니
다음 작업들은 문제없이 해낼 수 있었다.
(사실 스크립트 파일을 짜는데 어려움이 많았으나...
감사합니다 종원님..)
3) 결론
우선적으로 Secret Key에 제대로 된 값을 집어넣자.
이후로는 scp, ssh 과정이 잘 돌아가는지
커맨드별로 없애보며 단계별로 실행이 되는지 테스트해보자.
그러고도 안되면 오타이거나
Secret Key에 제대로 된 값이 들어가지 않았다는 것이다!
scp, ssh 접속을 위해 appleboy 라이브러리를 사용했고
Github 레포지토리 설정에 잡아놓은 Secret Key들을 값으로 주어
프록시 서버와 어플리케이션 서버에 접속이 가능하도록 하였다.
이전 단계에서 mv를 이용하여 홈 디렉토리로 옮겨놓았던
app.jar를 scp를 이용하여 로컬 -> 프록시 서버 -> 어플리케이션 서버 로 옮긴 후
어플리케이션 서버에서 app_run.sh 파일을 통해
app.jar를 실행한다.
이 때 스크립트 파일은 다음과 같다.
CURRENT_PID=$(pgrep -fl app | grep java | awk '{print $1}' )
if [ -z "$CURRENT_PID" ]; then
echo "> no applcation named java is running"
else
echo "> kill -9 java"
kill -9 $CURRENT_PID
sleep 5
fi
nohup java -jar\
-Dspring.profiles.active=dev\
-Dnaver.storage.access-key=${STORAGE_ACCESS_KEY}\
-Dnaver.storage.secret-key=${STORAGE_SECRET_KEY}\
-Dspring.mail.username=${EMAIL_ADDRESS}\
-Dspring.mail.password=${USER_PASSWORD}\
/var/app/app.jar > nohup.log 2> nohup.err < /dev/null &
스크립트 파일에선 java라는 이름으로 실행되고 있는 프로그램이 있다면
그것의 PID를 잡고 종료시킨 후
app.jar를 실행시켜 프로그램을 실행시킨다.
이 때 log를 파일마다 모아놓기 위해
nohup
으로 실행시킨다.
스크립트 파일 내의 ${} 안에 있는 변수들은
실제로는 직접 주입했으나 보안상의 문제로 위처럼 처리하였다.
ssh 경로로 서버에 접속하는 것은
어차피 pem키가 없다면 불가능한 루트이므로
스크립트 파일 내의 내용은 노출되어도 상관없다고 생각했다.
'🎉 프로젝트 > 🍀 Naver Cloud' 카테고리의 다른 글
[NCP] public area - private area 간의 통신 가능하게 하기 with NAT Gateway (1) | 2024.01.30 |
---|---|
[NCP] 무중단 배포 환경 구축하기 with Jenkins (0) | 2024.01.15 |
[NCP] Oriental Unity 프로젝트 (0) | 2024.01.15 |
[NCP] Multipart 파일 업로드 with Object Storage (2) | 2023.10.02 |
[NCP] 피카소 프로젝트 (0) | 2023.10.02 |