Github + Jenkins + AWS Codedeploy로 무중단 배포 자동화 하기

개발해규스·2025년 1월 23일

인프라

목록 보기
4/5
post-thumbnail

들어가며

애플리케이션을 수정해서 반영하는 경우 필연적으로 애플리케이션을 재시작 해야하는데요, 이로 인해 서비스가 일시 중지되는 상황이 발생하게 됩니다.
거기다 배포하는 과정에서 뭔가 문제가 생길경우 서비스가 그만큼 더 오랫동안 일시 중지가 될 텐데, 이러한 상황에서 무중단 배포를 구성해둔다면 걱정을 조금은 덜 수 있을겁니다. 또 배포를 하기위해 스크립트를 개발자가 일일이 돌리지 않고, 자동화 해주는 프로그램을 통해 원클릭으로 배포가 이루어지고, 그 결과를 알림으로 받는다면 더 좋겠죠. 그래서 CI/CD 파이프라인을 구축해 무중단 배포 자동화를 해보겠습니다.

시작

제가 사용하는 환경과 툴은 아래와 같습니다
OS: MacOS / AppleSilicon
Tool: Docker, Jenkins(jdk21)
언어/프레임워크: Java 21 / SpringBoot3.x
서버 애플리케이션이 실행될 EC2는 PrivateSubnet에 위치하도록 구성하겠습니다.

진행 순서는 아래와 같습니다.

  1. Jenkins 구축 (로컬에서 Docker를 이용해 구축)
  2. Jenkins <-> Github 연동
  3. 애플리케이션에 배포 스크립트 작성
  4. 무중단 구성을 위한 인프라 구성
  5. AWS CodeDeploy 구성
  6. Jenkins <-> AWS CodeDeploy 연동
  7. Slack Webhook 연동

상당히 긴 내용이 될 것 같지만 최대한 간단하게 적어보겠습니다!

Jenkins 구축

터미널에 아래 커맨드를 입력해 젠킨스를 실행하겠습니다.

docker run --name jenkins -p 8080:8080 -p 50000:50000 -v {젠킨스 워크스페이스 공간과 공유할 내 PC 위치}:/var/jenkins_home jenkins/jenkins:jdk21

최초에 젠킨스를 실행했다면 로그에 adminPassword가 있는데, 이를 컨테이너에서 log를 확인해서 입력해주세요.
확인은 아래 커맨드를 터미널에 입력해서 확인하시면 됩니다.

docker logs jenkins


이제 브라우저에서 아래 링크를 접속해서 초기 세팅을 진행합니다.

http://localhost:8080

위에서 확인한 패스워드를 입력해주신 뒤 첫번째 옵션으로 젠킨스 초기 플러그인을 세팅하고, 최초 어드민 계정과 아까 접속한 url을 설정합니다.

젠킨스의 기본세팅에는 Git플러그인은 있지만, AWS CodeDeploy를 지원하는 플러그인은 없기때문에 아래 이미지 처럼 AWS CodeDeploy와 Slack Notification플러그인을 설치합니다.

또, Git에 SSH 연결시 HostKey를 어떻게 사용할지에 대한 전략도 설정 해야하는데, 아래 이미지처럼 Accept first connection으로 설정해줍니다. 이에 대한 내용은 Jenkins git-client 문서를 참조해주세요.

Jenkins <-> Github 연동

우선 Jenkins에서 Github 프로젝트에 SSH방식으로 접근할 것 이므로 SSH 접근을 위한 Key를 생성해 Github에 등록합니다.

ssh-keygen -t ecdsa

만약 특정 위치에 키를 만들고 싶다면
Enter file in which to save the key ({위치 및 파일명}): 문구에서 원하는 위치를 포함한 파일명을 입력하면 됩니다.
이제 깃헙에 등록해보겠습니다.
먼저 위에서 만든 암호화 파일 중 ~~~.pub 로 된 암호화키를 열어서 그 안의 내용을 모두 복사해서 Github Repository -> Settings -> Deploy keys 메뉴 클릭 -> Add deploy key 버튼 클릭후 붙여넣고 등록하기

그 다음 젠킨스 메인화면에서 아래 이미지처럼 Job을 생성해줍니다.

이때 이 Job에서 Github repository에 접근할때 사용할 Key를 등록해줘야 하는데요, 위에서 만든 파일 중 .pub이 붙어있지 않은 키를 확인해 내용을 붙여넣어 주세요.

Job을 실행시키면 정상적으로 Github Repository에 접근해 Clone한 것을 볼 수 있습니다.

애플리케이션에 배포 스크립트 작성 및 AWS CodeDeploy 구성

CodeDeploy는 배포하는 프로젝트(Artifact)의 클래스패스(최상위 Directory)에 작성된 appspec.yml 파일의 내용의 각 단계 순서대로 커맨드를 실행하는 방식으로 배포를 진행합니다.
순서는 아래 이미지를 보시면 간단히 아실 수 있고, 더 자세하게 보시려면 AWS CodeDeploy 공식문서를 확인 해 주세요.

여기서 저는 BeforeInstall, AfterInstall, ApplicationStart 세 단계에 대한 스크립트를 작성해 배포를 구성하겠습니다.
파일 및 폴더 구조는 아래 이미지와 같습니다.

appspec.yml

version: 0.0 # version은 필수값, 0.0 고정
os: linux # ec2에 배포하는 경우 필수 값이며, ec2인스턴스에서 사용하는 os 타입을 기입합니다

files:
  - source: /
    destination: /home/ec2-user/deploy-temp

hooks:
  BeforeInstall:
    - location: scripts/remove_before.sh # Application 생명주기(배포 사이클중에) remove_before.sh를 실행
  AfterInstall:
    - location: scripts/kill_process.sh # Application 생명주기(배포 사이클중에) kill_process.sh를 실행
  ApplicationStart:
    - location: scripts/run_process.sh # Application 생명주기(배포 사이클중에) run_process.sh를 실행

각 단계별 스크립트

BeforeInstall: remove_before.sh

#!/bin/sh
sudo rm -rf /home/ec2-user/deploy-temp
sudo mkdir /home/ec2-user/deploy-temp

AfterInstall: kill_process.sh

#!/bin/bash
PROCESS_NO=$(pgrep -f nohup)
if [ -z $PROCESS_NO ]; then
  echo "> 실행중인 프로세스가 없습니다"
else
  echo "> 실행중인 프로세스 Id = $PROCESS_NO"
  sudo kill -15 $PROCESS_NO # tcp $serverPort에 해당하는 port를 Kill함.
fi

# 프로세스가 down될때까지 대기
IS_PROCESS_KILLED=$(pgrep -f nohup)
while [[ -n $IS_PROCESS_KILLED ]]
  do
    echo "> 프로세스 종료 대기중"
    IS_PROCESS_KILLED=$(pgrep -f nohup)
    echo "> IS_PROCESS_KILLED = $IS_PROCESS_KILLED"
  done

ApplicationStart: run_process.sh

#!/bin/bash
# 임시폴더에서 deploy하는 폴더로 데이터 복사
sudo rm -rf /home/ec2-user/deploy
sudo mkdir /home/ec2-user/deploy
sudo cp -r /home/ec2-user/deploy-temp/* /home/ec2-user/deploy
# nohup을 그냥 실행하면 code deploy 콘솔에 로그가 찍히므로 nohup을 빠져나오도록 표준입출력관련된걸 모두 /dev/null로 보냄
# sudo nohup java -jar -Dspring.profiles.active=prod $JAR_FILE > /dev/null 2> /dev/null < /dev/null &
# 위 방법을 쓰면 배포는 정상적으로 되지만, 로그가 안남음. 따라서 EC2내부에 java 배포 쉘을 작성하고, 이를 CodeDeploy가 로그안찍고 배포하도록함
echo "> javadeploy.sh 백그라운드 실행. > /dev/null 2> /dev/null"
sudo sh /home/ec2-user/deploy-scripts/javadeploy.sh > /dev/null 2> /dev/null &

더 자세한 내용은 AWS CodeDeploy 공식문서를 확인해주세요

무중단 구성을 위한 인프라 구성

이제 실제로 서버 애플리케이션을 동작시키기 위한 인프라를 구성해보겠습니다.
ELB(ALB사용) -> EC2 2대 구조로 구성해보겠습니다. 구성도는 아래와 같습니다.

  1. EC2 2대를 Private Subnet에 구성
  2. EC2가 Public Network, SessionManager에 접근하기 위해 Public Subnet에 NatGateway를 구성
  3. Jenkins가 CodeDeploy에 배포요청을 보내면 CodeDeploy는 NatGateway를 통해 EC2에 설치된 Agent를 통해 배포

Private Subnet에 EC2 실행 및 SessionManager 연동으로 접속까지 구성

블로그 포스팅을 참고해주세요!

배포 스크립트 작성

Session Manager를 통해 EC2에 접속한 뒤 Application 실행을 위해 Swap Memory와 JRE부터 설정하겠습니다

sudo su - ec2-user
sudo yum install java-21-amazon-corretto-headless -y
sudo dd if=/dev/zero of=/swapfile bs=128M count=8
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
sudo swapon -s
sudo vim /etc/fstab

vim으로 열린 파일 맨 끝에 아래 텍스트를 붙여넣고 저장합니다

/swapfile swap swap defaults 0 0

free -h

위 커맨드를 입력했을때 아래 이미지처럼 확인 되면 설정이 다 된 것입니다. 생성한 모든 인스턴스에 적용해 주세요.

다음으로 아래 커맨드를 실행합니다.

mkdir deploy-scripts
vim ./deploy-scripts/javadeploy.sh

그 후 vim 명령어에 의해 이동한 창에 아래 스크립트를 입력 한 뒤 저장합니다.

#!/bin/bash

HOME=/home/ec2-user
APPLICATION_PATH=$HOME/deploy
JAR_FILE=$APPLICATION_PATH/deploy.jar

sudo nohup java -jar $JAR_FILE >/dev/null 2>&1 &

이미지처럼 javadeploy.sh이 위치하도록 주의해 주세요.

AWS CodeDeploy 구성

AWS CodeDeploy를 통해 배포를 하려면 CodeDeploy Agent에 접근이 가능한 역할이 있어야하는데요, 아래와 같이 생성합니다.

AWSCodeDeployRole

우리는 현재 오토스케일링까지는 적용하지 않았기 때문에 EC2 인스턴스에 적용된 태그를 갖고 배포를 해야합니다.
따라서 아까 생성한 인스턴스에 그 인스턴스들의 도메인 역할을 구분 짓도록 Type=API 라는 태그를 붙이도록 하겠습니다.

그 다음 CodeDeploy 화면으로 이동후 애플리케이션 메뉴 클릭 후 애플리케이션 생성으로 이동해 아래 이미지처럼 배포 애플리케이션을 생성합니다.

다음은 아래 이미지처럼 배포 그룹을 생성합니다.
IAM 역할이 검색이 안된다면 바로 위에서 생성한 CodeDeploy IAM 역할의 ARN을 복사해 서비스 역할에 붙여넣어 주세요.

S3 버킷 생성

Jenkins <-> CodeDeploy 연동

연동을 위해선 Jenkins가 우리의 CodeDeploy 배포 그룹에 접근할 권한이 있어야합니다. 이는 곧 사용자를 만들어야 하는데요, 아래 이미지처럼 권한을 가진 사용자를 하나 만듭니다.

사용자를 만들었다면 Jenkins가 이 사용자라는 것을 인증하기 위한 인증 수단을 만들어야 하는데요, 저는 액세스키를 만들어 인증하겠습니다.
만든 사용자 -> 액세스 키 -> 액세스 키 만들기로 생성하는데, 이때 .csv 파일을 잘 저장해두세요!

다음은 Jenkins의 설정을 진행하겠습니다.
위에서 설정한 프로젝트의 설정으로 들어가서 아래 Build Steps부터 작성하겠습니다.


CI/CD 파이프라인을 구성하는 목적이니 우선 테스트는 제외하고 빌드하겠습니다.

rm -rf deploy
./gradlew clean build -x test
mkdir deploy
cp build/libs/*-SNAPSHOT.jar deploy
cp -r scripts deploy
cp appspec.yml deploy

다음으로는 바로 아래에있는 빌드 후 조치를 추가합니다. 맨 마지막에 아까 다운받았던 사용자의 액세스키를 입력해주세요.

이제 Jenkins에서 빌드를 시작해보겠습니다.

우선 CodeDeploy가 배포 LifeCycle을 진행하는데 문제가 없는지부터 확인하기 위해 CodeDeploy로 이동해 가장 최신의 배포현황을 확인합니다.

정상적으로 처리가 됐네요. 혹시 만약 문제가 있다면 AWS CodeDeploy 공식문서를 보시고 수정하시면 됩니다.
또 실제로 배포가 정상적으로 됐는지도 봐야겠죠. 아까 설정한 SessionManager를 통해 애플리케이션이 정상작동중인지 확인해보죠.

확인 결과 정상적으로 빌드가되어 작동중입니다.(저는 다른 라이브러리들이 들어가있어서 조금 더 추가를해주는바람에 로그가 더 찍혔네요.)
혹시 애플리케이션 실행이 안된다면 deploy-scripts/javadeploy.sh 의 아래 내용을 변경하신 뒤

sudo nohup java -jar $JAR_FILE >/dev/null 2>&1 & -> sudo nohup java -jar $JAR_FILE >/home/ec2-user/logs 2>&1 &

/home/ec2-user/logs 파일에 찍힌 로그를 확인하셔서 애플리케이션 실행이 되도록 해주세요!

cat /home/ec2-user/logs

무중단 배포를 위한 로드밸런서 구성

위의 배포는 사실 배포를 진행할때 애플리케이션 중단이 발생하게 됩니다. 이를 막기 위해 EC2 앞에 로드 밸런서를 구성하겠습니다.
로드 밸런서는 health check를 하기 때문에 애플리케이션에 health check를 위한 API 혹은 static resource 등을 제공해야 됩니다. 저는 간단하게 API방식으로 구현하겠습니다.

물론 여기서 커밋도 한 다음 배포를 한번 더 해야 반영이 되겠죠?

그 다음은 대상 그룹을 지정하겠습니다.

다음은 로드밸런서를 구성하겠습니다. Application LoadBalancer를 선택해주세요.

다음으로 대상 그룹의 HealthCheck가 정상적으로 이루어지는지 확인하겠습니다.
잘 안된다면 Application이 실제 잘 떠있고, 상태 검사를 하는 endPoint가 제대로 잘 되어있는지, 보안그룹의 인바운드는 로드밸런서가 상태 검사를 하는 포트의 접근을 허용하는지 등을 확인해주세요.

그러면 위 사진처럼 Helathy가 뜨게됩니다. 이 상태에서 LoadBalancer에 적용한 보안그룹의 인바운드 규칙에서 80포트를 허용 한 뒤 브라우저의LoadBalancer의 DNS명을 입력하게되면 위에서 만들었던 HealthCheck용 API응답이 오게 됩니다.

CodeDeploy 배포 그룹에 로드밸런서 추가

기존에는 로드밸런서 없이 애플리케이션 배포를 즉시 했었지만, 무중단 배포를 위해서는 배포시 클라이언트의 요청을 처리할 수 있는 서버가 한 대 이상 유지되어야 하죠.
기존에 만들었던 배포 그룹에 로드밸런서를 추가하여 트래픽차단, 트래픽허용 등의 단계를 추가하도록 구성하면 무중단 배포가 구성이 가능해집니다.
먼저 기존에 만든 배포 그룹을 편집하는 페이지로 이동하여 아래 이미지 처럼 진행해주세요.

그 다음 다시 배포를 해보면

BlockTraffic / AllowTraffic 등의 단계가 추가되었습니다. 이는 배포의 한 단위마다 트래픽을 차단 -> 애플리케이션 배포 -> 트래픽 허용 이라는 과정을 거치기 때문에 배포 단위에 해당하지 않는 인스턴스들은 배포를 진행하지 않게됩니다. 따라서 무중단 배포 구성이 가능해집니다.

Jenkins Slack Webhook 연동

우선 Slack App에서 앱 추가 버튼을 누른 뒤 다음 이미지처럼 Jenkins CI를 설치해주세요.


그러면 친절하게 어떻게 연동해야하는 페이지가 나타납니다. 이를 확인해서 연동해주시면 아래 이미지 처럼 빌드, 배포결과에 따라 슬랙으로 알림을 받아볼 수 있습니다!

더 자세한 내용은 Slack 공식문서 를 확인 해 주세요

마치며

실제 회사에서 무중단 파이프라인을 구축하며 공식문서를 찾고 많은 예시를 참고하면서 이것 저것 시도하느라 고생 한 걸 잊어버리지 않게 정리했는데, 내용이 워낙 많아서 정리하는 것도 상당히 길고 쉽지 않은 느낌입니다. 조금 더 괜찮은 방법이 보인다면 다음 포스팅 글로 적어보겠습니다.
도움이 되셨으면 좋겠네요. 긴 글 읽어주셔서 감사합니다.

profile
개발, 기타 좋아하는 뒷단 개발자

0개의 댓글