멀티 모듈을 적용한 계기
우리 팀이 진행한 캐치 테이블을 클론 코딩한 Dev-table 에서는 다음과 같은 기능을 구현하고자 하였다.
- 유저의 예약, 웨이팅
- 점주의 예약, 웨이팅
- 예약, 웨이팅을 알려줄 수 있는 알림
처음에는 이러한 기능들을 모두 하나의 모듈에서 처리하도록 구성을 해두었다. 즉, 다음과 같은 패키지 구조가 나타나게 된다.
devtable
ㄴ src
ㄴ main
ㄴ domain
ㄴ reservation
ㄴ application
ㄴ OwnerReservationService
ㄴ UserReservationService
ㄴ presentation
ㄴ OwnerReservationController
ㄴ UserReservationController
ㄴ waiting
ㄴ application
ㄴ OwnerWaitingService
ㄴ UserWaitingService
ㄴ presentation
ㄴ OwnerWaitingController
ㄴ UserWaitingController
ㄴ test
위와 같은 구조를 가지게 되었었다. 이렇게 구조를 가져갔을때는 단순히 보기에는 편하지만 이어서 유저의 기능, 점주의 기능이 점차적으로 확장되어 간다면 계속해서 User와 Owner의 기능이 하나의 도메인에 공존하게 되면서 해당 애플리케이션이 복합적인 기능을 다루게 된다.
그리고 이 시점에서 멘토님이 한 가지 피드백을 해주셨다.
보통 관리자, 점주, 회원의 기능들은 별도로 분리를 해서 관리를 해요.
한 번 이 번 주차에는 멀티 모듈을 적용해보시는 건 어떨까요!? 하지만 적용하는데에 생각보다 시간이 오래 걸릴거여서 현재 진행하고 있는 개발을 아마 중단하셔야 할 수도 있습니다 🥲
우리의 클론 코딩 목적은 협업 능력과 백엔드 개발 능력을 향상 시키는 것도 있지만, 무엇보다 최종적인 목표는 최종 프로젝트를 위해 최종 프로젝트에서 겪어봐야 할 문제들을 미리 겪어보자는 취지도 컸다.
그리고 멀티 모듈을 적용해본 경험이 있는 도연님과 함께 멀티 모듈을 밤새서 적용을 해보았다.
이제 해당 멀티 모듈로 분리 된 부분을 CD(Continuous Delivery, Continuous Deployment) 를 진행을 했어야 했다.
이 부분은 욕심으로 한 번도 경험해보지 않은 내가 진행을 해보기로 했다. 멀티모듈에 대한 배포.. 어려워보이긴 하지만 확실히 재밌어보이는 주제였다.
CD 적용 전
먼저 우리 팀의 멀티 모듈은 기존의 우리가 알고 있는 멀티 모듈과 조금 다른 양상을 띄고 있다.
하나의 공통 모듈을 어떠한 기능 별로 분리를 한 것이 아닌, 아예 별개의 애플리케이션을 멀티 모듈로 두어서 별개의 애플리케이션을 띄워야하는 단일 모듈은 아니고 MSA도 아닌 MSA로 변경해 나가야 할 그 이전의 단계라고 생각된다.
우선 전반적인 패키지 구조부터 대략적으로 살펴보자.
ㄴ devtable
ㄴ user
ㄴ UserApplication
ㄴ owner
ㄴ OwnerApplication
ㄴ common
ㄴ alarm
ㄴ AlarmApplication
common 을 제외한 다른 모듈에는 애플리케이션이 있음을 볼 수 있다. 우리의 프로젝트는 각기 다른 모듈이 따로 떠야하고, 이를 적용하기 위해 yml에서 포트번호를 각기 다르게 지정해주었다.
배포 도구 고민
이제 자동 배포를 만들기위해서 어떤 도구를 사용해야 할지에 대해서 고민을 할 차례이다.
- 우선 전제는 우리는 기존의 Github Actions는 그대로 가져가야겠다는 생각이 들었다.
우선 기본적으로 찾아본 배포 툴은 다음과 같이 3가지이다.
- AWS Code Deploy
- Docker
- Jenkins
3개의 차이를 좀 알아 볼 필요가 있을 것 같다. 그 차이점에 대해서 간단하게 알아보자.
Code Deploy
- 전반적으로 신뢰성 있고, 빠른 코드 배포가 자동화가 되어있다.
- EC2 Instnace의 롤링 업데이트를 수행하고 애플리케이션의 상태를 추적하여 애플리케이션 가용성을 극대화하는데 도움이 된다.
- AWS CLI 혹은 Console을 통해 배포 상태를 쉽게 시작하고 추적 할 수 있다.
- 스택으로 채택한 인원 : 389명
Docker
- 통합 개발자 도구이다.
- 개방형이고, 공유가 가능한 재사용 가능한 앱이다.
- 스택으로 채택한 인원 : 160.8K 명
Jenkins
- 쉬운 설치도구
- 쉬운 설정
- 변경 set에 대한 지원
- 스택으로 채택한 인원 : 56.1K 명
기본적으로 Jenkins, Docker, CodeDeploy를 모두 사용하는 배포 형태를 가장 많이 찾아볼 수 있었다.
제약 사항
위와 같은 차이점을 염두하고 저희는 제약된 자원에서 어떤 배포 툴을 사용 할지에 대해서 고려를 해야했습니다. EC2를 free-tier 를 사용함에 있어서, t2.micro - 물리(1 cpu, 2 thread, 1 GB Memory)
위와 같은 제약사항이 주어졌을 때 applicaiton이 3개나 올라가면서 그 서버 위에서 docker도 돌리고 jenkins도 돌린다..? 서버가 쉽게 죽을 것 같다는 생각이 들었다.
물론 EC2는 750시간 동안 무료 시간을 제공해주고 여러 개의 서버를 띄워주게 되면 시간이 각각 별도로 중첩되어 흘러가기 때문에 문제가 안될 수 있지만 가장 큰 문제는 EIP 부여문제이다.
AWS에서는 하나의 EC2에 대해서만 EIP를 무료로 제공해주는 정책을 펼치고 있기 때문에 Jenkins를 다른 서버에 띄워둔다면 소모되는 시간을 아끼기 위해 Jenkins가 있는 서버 인스턴스를 중지를 해놨다가 다시 켜야했고, 다시 키면 IP가 바껴서 계속 IP를 수정해주어야 한다 이는 좋은 방법이 아니라는 생각이 들었습니다.
선택
그 결과 저희는 비교적 가벼운 CodeDeploy만을 이용하여 배포를 진행하기로 하였고, 이 과정에서 S3의 힘을 빌리기도 한다.
기본적으로 주어진 요구 조건은 다음과 같다.
- 멀티 모듈로 구성이 되어있지만, 각 모듈은 서로 각기 다른 시기에 배포가 가능해야하며 따로 실행이 가능해야 한다.
이제 이를 해결해보자.
CD를 구축해보자.
먼저 기본적인 CD를 구축하기 위한 세팅은 향로님과 다른 블로거 분들의 포스팅을 많이 참고했다.
- https://github.com/jojoldu/jenkins-codedeploy-multi-module/blob/master/1_기본환경구성.md
- https://galid1.tistory.com/745
위의 내용 말고도 비교적 많은 블로그… 약 20개..? 넘게 참고를 했었지만 저희와 동일한 상황에 배포 상황은 찾을 수 없었다.
따라서, 기본적인 배포를 먼저 수행하고 난 뒤에 현재 주어진 요구사항을 해결하고자 했다.
여기서 한 가지 착오를 가져서 많은 시간을 허비했는데, 바로 다음과 같은 내용이다.
Code Deploy를 수행하기 위한 appspec.yml은 배포하고자 하는 모듈의 최상위에 있어야 한다.
첫 번째 트러블 슈팅 (CodeDeploy에 대한 낮은 이해도)
저 내용은 "appspec.yml은 배포하고자 하는 루트 모듈의 최상위에 있어야 한다." 를 얘기한 내용이었는데 이를 잘못 파악하여 "각 모듈에 있어도 되는건가?" 라는 생각을 하여 다음과 같은 구조로 배포를 위한 준비를 하였습니다.
devtable
ㄴ alarm
ㄴ scripts
ㄴ deploy.sh
ㄴ appspec.yml
ㄴ user
ㄴ scripts
ㄴ deploy.sh
ㄴ appspec.yml
ㄴ owner
ㄴ scripts
ㄴ deploy.sh
ㄴ appspec.yml
그리고, 이를 배포하기 위한 deploy.sh와 appspec.yml, githubAction_cd_yml 은 다음과 같은 형태를 가졌다.
다른 모듈 또한 내용이 공통된 부분이 많기 때문에 user 모듈에서의 appspec, deploy, cd.yml 을 살펴보자.
1. user appspec.yml
version: 0.0
os: linux
files:
- source: /
destination: /home/dev-table/user # 인스턴스에서 파일이 저장될 위치
permissions:
- object: /home/dev-table/user
owner: ec2-user
group: ec2-user
mode: 755
hooks:
ApplicationStart:
- location: scripts/deploy.sh
timeout: 60
runas: ec2-user
2. user deploy.sh
REPOSITORY=/home/dev-table/user
cd $REPOSITORY
APP_NAME=devtable
JAR_NAME=$(ls $REPOSITORY/build/libs/ | grep '.jar' | tail -n 1)
JAR_PATH=$REPOSITORY/build/libs/$JAR_NAME
CURRENT_PID=$(pgrep -f $APP_NAME)
if [ -z $CURRENT_PID ] #2
then
echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
else
echo "> kill -15 $CURRENT_PID"
sudo kill -15 $CURRENT_PID
sleep 5
fi
echo "> $JAR_PATH 배포" #3
nohup java -jar \
-Dspring.profiles.active=dev \
build/libs/$JAR_NAME > $REPOSITORY/nohup.out 2>&1 &
3. user-cd.yml
# workflow의 이름
name: github-action-user-cd
# 해당 workflow가 언제 실행될 것인지에 대한 트리거 지정
on:
push:
branches: [ develop ] # develop branch로 push 될 때 실행
# 해당 yml 내에서 사용할 key - value
env:
PROJECT_NAME: devTable
# workflow는 한개 이상의 job을 가지며, 각 job은 여러 step에 따라 단계를 나눌 수 있습니다.
jobs:
build:
name: github-action-user-cd
runs-on: ubuntu-latest
steps:
# 작업에서 액세스 할 수 있도록 저장소를 체크아웃 해줌.
- uses: actions/checkout@v2
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'corretto'
# ./gradlew 명령어를 수행 할 수 있도록 실행 권한 부여
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
shell: bash
# ./gradlew build 수행
- name: Build with Gradle
run: ./gradlew build:user
shell: bash
# 해당 코드들을 압축
- name: Make zip file
run: zip -r ./$GITHUB_SHA.zip .
shell: bash
- name: Configure AWS credentials
uses: aws-action/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID}}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY}
aws-region: ${{ secrets.AWS_REGION}}
- name: Copy script
run: cp ./scripts/*.sh ./deploy
- name: Upload to S3
run: aws s3 cp --region ap-northeast-2 ./$GITHUB_SHA.zip s3://$S3_BUCKET_NAME/$PROJECT_NAME/$GITHUB_SHA.zip
- name: Code Deploy
run: aws deploy create-deployment --application-name $CODEDEPLOY_NAME --deployment-config-name CodeDeployDefault.OneAtATime --deployment-group-name $CODEDEPLOY_GROUP --s3-location bucket=$S3_BUCKET_NAME,bundleType=zip,key=$GITHUB_SHA.zip
위와 같은 형태로 작성을 해두었다. Code Deploy의 진행 과정을 제대로 이해를 못했기 떄문에 다음과 같이 작성을 하게 되었는데, 위와 같이 작성을 하게 되었을 때 다음과 같은 오류를 만나게 된다.
읽어보면 CodeDeploy agent가 라이프사이클 이벤트에 이용가능한 상태가 아니고, CodeDeploy agent에 대한 접근 권한을 한 번 확인해보라는 내용이었다.
위 과정에서 IAM 규칙이 잘 못된 것일까? 라는 생각을 가지고 먼저 IAM을 다시 한 번 살펴봤다. 접근 권한에 대해서는 잘못된 부분이 없었고, S3도 EC2도 Code Deploy에 대한 권한 자체가 열려 있었기때문에 위와 같은 오류가 발생 할 이유는 없었다.
우리는 도연님의 Root 계정으로부터 생성된 사용자 IAM을 사용중이었는데, 도연님에게 혹시 전체 권한을 잠시 열어 줄 수 있는지 여쭤봤고, 적용되어 다시 테스트를 진행해봤다. 동일한 오류가 발생하였다.
무엇이 문제일까. S3에서 파일이 안 올라가는 것일까? 지금 현재 실패 된 기간을 보면 0초라고 나와있지만, 실제로는 해당 트리거가 실패하기까지 약 4~5분 정도의 시간이 소요되었다. 이는 테스트하는데 정말 많은 시간이 소요되었다. 현재까지 진행 된 프로젝트에서 압축되는 파일의 크기가 약 150MB 정도였다. 따라서, 프로젝트 파일이 너무 커서 압축 된 파일을 EC2로 옮기는 과정에서 발생하는 문제인가? 하여, 간단한 프로젝트를 만들어서 테스트를 수행했음에도 동일한 오류가 발생하는 것을 살펴 볼 수 있었다.
따라서, 다른 부분에 문제가 있다는 판단에 다시 한 번 문제를 검새해봤고, Code Deploy에서 발생하는 문제를 해결하기 위해 log를 보면 Code Deploy의 대부분의 문제를 해결 할 수 있다는 게시글을 보았다. 따라서, EC2에서 아래 경로로 이동하여 로그를 한 번 살펴봤다.
/opt/codedeploy-agent/deployment-root/{codeDeploy groupId}/{배포 ID}/logs
The AppSpec file was expected but not found at path "/opt/codedeploy-agent/deployment-root/0bb5a5aa-5894-4575-a69c-a7a4e79b4cdf/d-HQ5GBC7SW/deployment-archive/appspec.yml".
appspec을 찾을 수 없다는 얘기였다. 설마하여 하나의 appspec.yml을 루트 모듈로 이동을 시켜보았다. 배포가 정상적으로 되는 것을 볼 수 있었고, Download Bundle 부분이 넘어가는 부분에서도 1초도 안 걸리는 부분을 볼 수 있었다. 오류를 자세히 보지 않고 추측해서 이런 부분에서 오류가 발생했겠구나. 라고 생각했던 부분이 시간을 오래 잡아먹게 됐다.
배포가 정상적으로 된다는 것을 확인 할 수 있었고, 하나의 appspec 과 하나의 deploy.sh 만을 사용을 했어야 했다.
문제점을 해결하고 난 뒤에 해결해야 할 여러가지 문제들이 생겼다.
- 하나의 appspec만 써야한다면 기존의 appspec에서 각각 다른 폴더로 분리하여 모듈들이 배포되는 방식이 안된다는 것
- 이로 인해 한 번에 배포가 된다면 기존의 수행 중인 애플리케이션과 코드의 정합성이 달라 질 수 있다는 점
- github actions를 통해 모듈 별로 별개로 배포 되도록 하였는데, 나눌 이유가 없어졌다는 부분이다.
우선, 해결해야 할 문제들에 대해 더 오랜 시간을 혼자 소모하면 안 될 것 같다는 생각이 들었다. 따라서, 위에 필요한 부분들을 멘토님에게 하나 하나 질문을 드렸다.
현재 develop 브랜치에 push가 되면 자동으로 배포가 되는 형태로 보입니다. push가 되었다고 해서 해당 프로젝트가 안전하다고 할 수 있을까요? 항상 배포가 되어야할까요?
에 대해서 질문을 해주셨다 해당 내용에 대해서 다음과 같이 답변을 하였다.
- 해당 문제점을 위해서 CI를 도입을 하였고, develop에 merge가 될 때는 모든 테스트 코드가 통과된 상태라고 생각을 합니다. 이에 따라, 자동으로 배포가 되어도 상관이 없다고 생각을 했습니다.
이러한 내용에 대해서 다음과 같이 추가 답변을 해주셨다.
실제로, 테스트 코드는 전부 다 통과를 하였더라도, 어떠한 이유로 테스트 코드에 없는 다른 시나리오로 인해 문제가 발생 할 수 있는 가능성도 존재합니다. develop에 전체 push가 된 이후로 최종적으로 모든 테스트가 끝난 이후에 주로 배포를 하는 형태를 택하고 있습니다. 찾아보니 CD 에는 두 가지 용어가 있더라구요! Continuous Delivery, Continuous Deployment 지금 말씀하시는 내용이 Continuous Deployment를 말씀해주시는 것 같아요! 원하는 상황에서 배포를 할 수 있도록 바꿔보시는 건 어떤가요!?
Continuous Delivery와 Continuous Deployment의 차이는 프로덕션 업데이트에 대한 수동 승인에 대한 차이점인데, Continuous Deployment는 자동으로 해당 파이프 라인이 자동으로 수행되는 형태이고, Continuous Delivery는 수동적으로 수행을 해주어야 되는 형태를 말한다는 것을 알게 되었고, 이를 도입하기 위해서 github actions의 dispatch를 사용하였다.
따라서, github Actions의 코드를 다음과 같이 수정을 하였습니다.
# workflow의 이름
name: github-action-user-cd
# 해당 workflow가 언제 실행될 것인지에 대한 트리거 지정
# workflow 수동 실행
on:
workflow_dispatch:
inputs:
name:
description: 'CD를 수행하기 위하여 승인을 해주세요.'
required: true
default: 'CD를 수행하기 위한 Github Action 입니다.'
# 해당 yml 내에서 사용할 key - value
env:
PROJECT_NAME: user
# workflow는 한개 이상의 job을 가지며, 각 job은 여러 step에 따라 단계를 나눌 수 있습니다.
jobs:
build:
name: github-action-user-cd
runs-on: ubuntu-latest
steps:
# 작업에서 액세스 할 수 있도록 저장소를 체크아웃 해줌.
- uses: actions/checkout@v2
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'corretto'
# ./gradlew 명령어를 수행 할 수 있도록 실행 권한 부여
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
shell: bash
# ./gradlew build 수행
- name: Build with Gradle
run: ./gradlew build:user
shell: bash
# 해당 코드들을 압축
- name: Make zip file
run: zip -r ./$GITHUB_SHA.zip .
shell: bash
- name: Configure AWS credentials
uses: aws-action/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID}}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY}
aws-region: ${{ secrets.AWS_REGION}}
- name: Copy script
run: cp ./scripts/*.sh ./deploy
- name: Upload to S3
run: aws s3 cp --region ap-northeast-2 ./$GITHUB_SHA.zip s3://$S3_BUCKET_NAME/$PROJECT_NAME/$GITHUB_SHA.zip
- name: Code Deploy
run: aws deploy create-deployment --application-name $CODEDEPLOY_NAME --deployment-config-name CodeDeployDefault.OneAtATime --deployment-group-name $CODEDEPLOY_GROUP --s3-location bucket=$S3_BUCKET_NAME,bundleType=zip,key=$GITHUB_SHA.zip
그리고 이렇게 actions.yml 을 변경함으로써 아래와 같이 수동으로 actions를 실행을 할 수 있게 되었다.
Code Delivery 형태로 이제 원할 때 배포가 가능해졌다.
하지만 이어진 큰 문제들을 해결하는 것이 더 중요했다. 그 과정을 살펴보자.
모듈 별로 배포를 하도록 해서 애플리케이션이 별도로 수행 될 수 있도록 해보자.
우선 현재 상황에서 EC2에는 전체 프로젝트가 배포가 되는 형태이다. 이러한 상황에서 해결 할 수 있는 방법이 없을까? 를 다시 찾아봤고 다음과 같은 내용을 확인 할 수 있었다.
"Code Deploy를 사용하게 되면 "$DEPLOYMENT_GROUP_NAME" 이라는 변수를 deploy.sh에서 사용 할 수 있다."
"DEPLOY_GROUP_NAME을 기준으로 배포하기 위한 jar 파일을 찾아 분기를 나누어서 해당 애플리케이션이 실행되도록 하면 되겠다" 라는 생각을 가지고 appspec.yml은 아래와 같이 변경되었다.
version: 0.0
os: linux
files:
- source: /
destination: /home/dev-table/ # 인스턴스에서 파일이 저장될 위치
permissions:
- object: /
pattern: "**"
owner: ec2-user
group: ec2-user
hooks:
ApplicationStart:
- location: scripts/deploy.sh
timeout: 120
runas: ec2-user
deploy.sh 코드도 상단부가 다음과 같이 변경이 되게 되었다.
if [ "$DEPLOYMENT_GROUP_NAME" == "devtable-user" ]
then
JAR_NAME=$(ls /home/dev-table/user/build/libs/*.jar | grep 'user-' | tail -n 1)
echo "발견한 jar 이름 > $JAR_NAME"
elif [ "$DEPLOYMENT_GROUP_NAME" == "devtable-owner" ]
then
JAR_NAME=$(ls /home/dev-table/owner/build/libs/*.jar | grep 'owner-' | tail -n 1)
echo "발견한 jar 이름 > $JAR_NAME"
elif [ "$DEPLOYMENT_GROUP_NAME" == "devtable-alarm" ]
then
JAR_NAME=$(ls /home/dev-table/alarm/build/libs/*.jar | grep 'alarm-' | tail -n 1)
echo "발견한 jar 이름 > $JAR_NAME"
else
echo "Unknown DEPLOYMENT_GROUP_NAME: $DEPLOYMENT_GROUP_NAME"
exit 1
fi
destination에 전체 패키지가 배포가 될 것 이기 때문에 우선적으로 배포하고자 하는 github Actions에 따라서, 서로 각기 다른 jar 파일을 실행 할 수 있게 되었다. 하지만 아직까지 해결하지 못한 문제가 있다.
Github Actions으로 모든 모듈이 배포되는 문제
모든 github actions에서 하나의 모듈에 대해서 배포 하고자 할 때 모든 패키지가 배포되어서 하나의 모듈을 배포 할 때 루트 패키지부터 전체의 모듈이 배포가 되버린다. 비록 실행하고자 하는 애플리케이션에 대한 모듈은 정상적으로 실행이 되지만 이미 켜져있는 애플리케이션과 그리고 그 애플리케이션을 수행하는 코드 자체에 대한 정합성이 일치하지 않은 문제가 발생하게 되는 것이다.
물론, 이미 실행 중인 모듈에 대해서 애플리케이션을 수행중이기때문에 실행 중이었던 애플리케이션 자체에는 문제가 없었지만 피치 못할 이유로 서버를 껐다가 다시 수행하면 오류가 발생 할 가능성이 있다. 이를 해결하기 위해서 다음과 같은 의문이 들었다.
Appspec.yml은 루트 패키지에 있어야 한다.
흠.. 루트 패키지가 꼭 필요할까? Appspec.yml가 deploy.sh만 읽을 수 있으면 되는게 아닐까?
이전에 테스트 겸 만들어두었던 프로젝트에서 Appspec.yml 과 deploy.sh 만 압축하여 S3에 전달하고, 해당 내용을 Code Deploy가 읽어들여서 배포가 가능한지를 살펴보았다. 이를 위해서 github Actions의 cd 코드를 다음과 같이 수정하였다.
- name: Make zip file
run: |
files_to_compress="appspec.yml scripts/deploy.sh"
archive_name="$GITHUB_SHA.zip"
zip -r "$archive_name" $files_to_compress
해당 내용을 수행 한 결과 정상적으로 배포가 가능함을 확인해 볼 수 있었다. 이제 위를 해결하기 위한 방법이 자연스럽게 떠오르게 되었다.
전체 패키지를 압축하지 말고, 각 githubAction.yml 에서 해당 모듈의 jar와 appspec.yml scripts/deploy.sh 만 말아서 보내주면 해결되겠다!
이러한 근거를 가지고 githubActions의 코드를 다음과 같이 수정을 하였다.
# workflow의 이름
name: github-action-user-cd
# 해당 workflow가 언제 실행될 것인지에 대한 트리거 지정
# workflow 수동 실행
on:
workflow_dispatch:
inputs:
name:
description: 'CD를 수행하기 위하여 승인을 해주세요.'
required: true
default: 'CD를 수행하기 위한 Github Action 입니다.'
# 해당 yml 내에서 사용할 key - value
env:
PROJECT_NAME: user
# workflow는 한개 이상의 job을 가지며, 각 job은 여러 step에 따라 단계를 나눌 수 있습니다.
jobs:
build:
name: github-action-user-cd
runs-on: ubuntu-latest
steps:
# 작업에서 액세스 할 수 있도록 저장소를 체크아웃 해줌.
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'corretto'
# ./gradlew 명령어를 수행 할 수 있도록 실행 권한 부여
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
shell: bash
# ./gradlew build 수행
- name: Build with Gradle
run: ./gradlew :user:clean :user:build
shell: bash
# user의 jar 파일과 appspec을압축
- name: Make zip file
run: |
files_to_compress="user/build/libs/* appspec.yml scripts/deploy.sh"
archive_name="$GITHUB_SHA.zip"
zip -r "$archive_name" $files_to_compress
shell: bash
- name: Configure AWS credentials
uses: aws-action/configure-aws-credentials@v1
with:
aws-access-key-id: ${{secrets.AWS_ACCESS_KEY_ID}}
aws-secret-access-key: ${{secrets.AWS_SECRET_ACCESS_KEY}}
aws-region: ${{secrets.AWS_REGION}}
- name: Upload to S3
run: aws s3 cp --region ap-northeast-2 ./$GITHUB_SHA.zip s3://${{secrets.S3_BUCKET_NAME}}/$PROJECT_NAME/$GITHUB_SHA.zip
- name: Code Deploy
run: |
aws deploy create-deployment \
--application-name ${{secrets.CODE_DEPLOY_NAME}} \
--deployment-config-name CodeDeployDefault.OneAtATime \
--deployment-group-name ${{secrets.CODE_DEPLOY_GROUP_USER}} \
--file-exists-behavior OVERWRITE \
--s3-location bucket=${{secrets.S3_BUCKET_NAME}},bundleType=zip,key=$PROJECT_NAME/$GITHUB_SHA.zip \
--region ${{secrets.AWS_REGION}}
가장 크게 살펴볼 부분은 files_to_compress="user/build/libs/* appspec.yml scripts/deploy.sh" 이 부분이다.
이제 배포하고자 하는 모듈만 배포를 할 수 있는 상태가 되었다. 여기서 한 가지 의문이 들었다.
Alarm, User, Owner 세 개의 모듈이 전부 Common에 의존하고 있는데 이 때, Owner를 배포했을 때 만약 Common 부분이 수정되어서 기존의 User의 Common 코드 영역과 코드의 정합성이 일치하지 않는다면 어떡하지?
어떻게 고민해도 각 모듈 별로 common의 의존성을 배제 할 수 는 없다. 결국 멘토님에게 질문을 드렸다.
공통된 영역이 같이 수정이 된다면 보통 그걸 의존하고 있는 다른 모듈도 같이 배포해요~
정말 간단하게 생각했으면 되는 부분이었다. 공통되게 수정되는 일이 잦으면 안되겠지만, 피치못할 사정으로 공통된 부분이 수정이 된다면 의존하고 있는 두 개의 모듈 모두 배포를 하면 되는 부분이었다. 또 이러한 휴먼 에러가 두렵다면 보통 배포 서비스를 더욱 고도화 시킨다고 한다. 이제 배포가 되면 자동으로 이미 수행되고 있던 애플리케이션을 끄고 알 맞게 재 실행 해줄 수 있도록 deploy.sh 를 바꿔보자.
if [ "$DEPLOYMENT_GROUP_NAME" == "devtable-user" ]
then
JAR_NAME=$(ls /home/dev-table/user/build/libs/*.jar | grep 'user-' | tail -n 1)
echo "발견한 jar 이름 > $JAR_NAME"
elif [ "$DEPLOYMENT_GROUP_NAME" == "devtable-owner" ]
then
JAR_NAME=$(ls /home/dev-table/owner/build/libs/*.jar | grep 'owner-' | tail -n 1)
echo "발견한 jar 이름 > $JAR_NAME"
elif [ "$DEPLOYMENT_GROUP_NAME" == "devtable-alarm" ]
then
JAR_NAME=$(ls /home/dev-table/alarm/build/libs/*.jar | grep 'alarm-' | tail -n 1)
echo "발견한 jar 이름 > $JAR_NAME"
else
echo "Unknown DEPLOYMENT_GROUP_NAME: $DEPLOYMENT_GROUP_NAME"
exit 1
fi
CURRENT_PID=$(pgrep -f "$JAR_NAME")
if [ -z "$CURRENT_PID" ]
then
echo "> CURRENT_PID : $CURRENT_PID"
echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
else
echo "> CURRENT_PID : $CURRENT_PID"
echo "> kill -15 $CURRENT_PID"
sudo kill -15 "$CURRENT_PID"
sleep 5
fi
echo "JAR_NAME : > $JAR_NAME"
chmod +x $JAR_NAME
echo "> $JAR_NAME 배포"
if [ "$DEPLOYMENT_GROUP_NAME" == "devtable-user" ]
then
nohup java -jar \
-Dspring.profiles.active=dev \
"$JAR_NAME" > /home/log/user/nohup_log.out 2>&1 &
elif [ "$DEPLOYMENT_GROUP_NAME" == "devtable-owner" ]
then
nohup java -jar \
-Dspring.profiles.active=dev \
"$JAR_NAME" > /home/log/owner/nohup_log.out 2>&1 &
elif [ "$DEPLOYMENT_GROUP_NAME" == "devtable-alarm" ]
then
nohup java -jar \
-Dspring.profiles.active=dev \
"$JAR_NAME" > /home/log/alarm/nohup_log.out 2>&1 &
else
echo "Unknown DEPLOYMENT_GROUP_NAME: $DEPLOYMENT_GROUP_NAME"
exit 1
fi
JAR_NAME 에는 배포되었던 .jar 파일이 담겨져있다. 이를 nohup (백그라운드에서 실행) 되도록 하고 백 그라운드에서 실행되는 애플리케이션에 대한 로그도 확인 할 수 있어야 하므로 로그를 보관하는 폴더에서 따로 관리를 하도록 해주었다.
이렇게 배포를 하게 되면 정상적으로 배포가 되지만 애플리케이션 수행 부분에서 많은 부분이 정상적으로 수행되지 않음을 확인 할 수 있다. github Actions를 통해서 배포를 하기 위한 jar 파일을 추가하는 과정에서 애플리케이션을 수행하기 위한 yml 파일들을 .gitignore에 넣어두었기 때문에 이를 알지 못해서 발생하는 문제였다.
이를 해결하기 위한 방법을 찾아봤는데 2가지 방법을 찾게 되었다.
- Github Variable 을 사용하여 Secret으로 만들고 해당 Secret을 읽도록 하는 것
- EC2 서버 공간에 미리 yml 을 올려놓고 해당 yml 파일을 읽도록 하기.
이 두 가지 중에 우선 1번에다가 yml을 길게 늘여서 작성해놓는 것 보다 간단하게 서버에 yml을 올려놓고 읽는 방식이 더 간단한 해결책이라고 생각을 해서 2번 방식으로 진행을 했고, 그 결과 최종적인 deploy.sh가 다음과 같이 변경되게 되었다.
if [ "$DEPLOYMENT_GROUP_NAME" == "devtable-user" ]
then
JAR_NAME=$(ls /home/dev-table/user/build/libs/*.jar | grep 'user-' | tail -n 1)
echo "발견한 jar 이름 > $JAR_NAME"
elif [ "$DEPLOYMENT_GROUP_NAME" == "devtable-owner" ]
then
JAR_NAME=$(ls /home/dev-table/owner/build/libs/*.jar | grep 'owner-' | tail -n 1)
echo "발견한 jar 이름 > $JAR_NAME"
elif [ "$DEPLOYMENT_GROUP_NAME" == "devtable-alarm" ]
then
JAR_NAME=$(ls /home/dev-table/alarm/build/libs/*.jar | grep 'alarm-' | tail -n 1)
echo "발견한 jar 이름 > $JAR_NAME"
else
echo "Unknown DEPLOYMENT_GROUP_NAME: $DEPLOYMENT_GROUP_NAME"
exit 1
fi
CURRENT_PID=$(pgrep -f "$JAR_NAME")
if [ -z "$CURRENT_PID" ]
then
echo "> CURRENT_PID : $CURRENT_PID"
echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
else
echo "> CURRENT_PID : $CURRENT_PID"
echo "> kill -15 $CURRENT_PID"
sudo kill -15 "$CURRENT_PID"
sleep 5
fi
echo "JAR_NAME : > $JAR_NAME"
chmod +x $JAR_NAME
echo "> $JAR_NAME 배포"
if [ "$DEPLOYMENT_GROUP_NAME" == "devtable-user" ]
then
nohup java -jar \
-Dspring.profiles.active=dev \
-Dspring.config.location=/home/yml/user/application.yml,/home/yml/user/application-dev.yml \
"$JAR_NAME" > /home/log/user/nohup_log.out 2>&1 &
elif [ "$DEPLOYMENT_GROUP_NAME" == "devtable-owner" ]
then
nohup java -jar \
-Dspring.profiles.active=dev \
-Dspring.config.location=/home/yml/owner/application.yml,/home/yml/owner/application-dev.yml \
"$JAR_NAME" > /home/log/owner/nohup_log.out 2>&1 &
elif [ "$DEPLOYMENT_GROUP_NAME" == "devtable-alarm" ]
then
nohup java -jar \
-Dspring.profiles.active=dev \
-Dspring.config.location=/home/yml/alarm/application.yml \
"$JAR_NAME" > /home/log/alarm/nohup_log.out 2>&1 &
else
echo "Unknown DEPLOYMENT_GROUP_NAME: $DEPLOYMENT_GROUP_NAME"
exit 1
fi
최종적으로 이와 같이 deploy.sh 를 만들면서 서버의 배포가 정상적으로 되었다. 하지만 또 한 가지 문제가 발생했다.
서버의 메모리 부족
우리의 서버로는 AWS의 Free Tier를 사용을 했고 Free Tier의 자원은 1cpu 2thread, 1GB Memory 밖에 없기 때문에 3개의 애플리케이션이 동시에 수행이 되게 되면 사용가능한 메모리 자원을 초과하여 서버가 다운되는 현상이 있었다.
다행스럽게도 이는 예측 범위 안이었다. 원래는 3개의 애플리케이션을 실행해보고 free 명령어로 메모리 사용량을 확인하고, 메모리가 많이 부족하다고 판단이 되면 EBS 공간에 swap 영역을 만들어서 Suspend가 발생 시 swap 영역에 옮겨서 적은 메모리 용량에도 서버를 죽지 않도록 해야 겠다. 라는 판단을 가지고 있었는데, 이를 바로 수행해야 했다.
스왑 메모리 설정
# swapfile 메모리를 할당
sudo dd if=/dev/zero of=/swapfile bs=256M count=11
# swapfile 권한 세팅 (READ, WRITE)
sudo chmod 600 /swapfile
# swap 공간 생성 (Make swap)
sudo mkswap /swapfile
# swap 공간에 swapfile 추가하여 즉시 사용할 수 있도록
sudo swapon /swapfile
# /etc/fstab 에디터 열기
sudo vi /etc/fstab
# 파일의 맨 끝 다음줄에 아래 명령어 작성
/swapfile swap swap defaults 0 0
- swapfile 메모리를 할당
- swapfile은 서버의 가용 메모리에 따라 다르게 설정을 하지만 대체적으로 2배 또는 그 이상을 설정하는 것이 좋다고 한다. 이에 따라 약 2.8GB 를 스왑 메모리 영역으로 설정 하였다.
- swapfile 권한 세팅
- swap 공간 생성
- mkswap(make Swap) 을 통하여 swap 공간을 생성하고, swap memory에 1번에서 만든 swapfile을 추가해준다.
- swap 공간에 추가 된 swapfile 을 추가하여 즉시 사용 할 수 있도록 한다.
- /etc/fstab 에 스왑 명시
- etc/fstab는 파일 시스템 정보를 저장하는 것인데, 파티션 변경 및 디스크 추가 시에 이 파일에 등록해야 자동으로 마운트가 가능하기 때문이다.
- 추가적으로 외부에서 다른 EBS를 또 추가한다면 알아서 추가되는 것이 아닌 mount 명령어를 사용해야 해당 디스크를 추가하는 것이 가능하다.
현재는 EC2의 하나의 서버에 3개의 애플리케이션 서버, Redis, Prometheus, Grafana가 전부 다 띄워져있는 상황이다. 메모리가 부족해서 swap 영역의 많은 부분을 사용하는 것을 볼 수 있다.
결론
만약 서버의 가용 공간이 많고, 돈도 많았다면 여러 개의 서버를 띄우고 Jenkins와 Docker를 이용하여 CD를 구현 했을 것이다. 하지만 현실은 정해진 비용으로만 해결을 해야 하는 상황이 생길 수도 있다. 이런 정해진 제약조건에서 해결하는 것 또한 중요하다고 생각이 든다.
회고
배포를 진행하면서 역시나 느끼게 되었던 건 어떠한 기술을 사용하기 위해서는 우선 그 기술에 대해서 먼저 이해를 하고 도입을 하는게 중요하다는 생각을 다시금 되새길 수 있게 해주었다. 기술의 맥락을 이해하지 못하고, 우선 도입을 하려고 하니 잘못된 부분에서 많은 시간을 낭비하는 문제가 있었다. 또한, 많이 사용되지 않는 기술을 사용하니 공유된 자료가 없어서 문제를 해결하는 것도 여간 쉬운 일은 아니었다. 어떠한 기술을 도입하여야 할 때 그 기술이 커뮤니티에서 많이 대화가 이루어지는 기술인지 또한 살펴봐야 한다는 부분이 이런 부분이 아닐까 생각이 들었다. 백엔드 동기들끼리 이번 프로젝트를 진행하면서 많은 백둥이들이 도커를 최대한으로 활용하려고 하는 모습을 볼 수 있었다. 컨테이너 형식의 도커의 팔로우도 많고 사용하는 방식도 무궁무진 하다고 생각한다. 이번 주차에는 Docker를 알아보는 시간을 가져볼까 한다!
'프로그래머스 데브코스' 카테고리의 다른 글
최종 프로젝트 회고 (1) - 일정 관리 (1) | 2023.11.30 |
---|---|
지역 데이터를 스프링의 캐시 기반으로 조회하기 feat. 공공지역 데이터 (0) | 2023.11.18 |
프로그래머스 데브코스 14주차 회고 (0) | 2023.09.04 |
프로그래머스 데브코스 13주차 회고 (0) | 2023.08.27 |
프로그래머스 데브코스 12주차 회고 (0) | 2023.08.21 |