무중단 배포 기술을 경험하기 위해 CodeDeploy를 사용하여 배포를 진행해보려 합니다.
CodeDeploy를 사용하여 어떻게 배포해야 하는지, 배포하는 과정에서 어떤 오류를 만났고 어떻게 해결해야 하는지를 소개합니다..
사전 지식
구현 과정
로그
오류
⚡ Code Deploy
EC2, On-premise, Lambda, ECS로 Application 배포를 자동화하는 배포 서비스
해당 포스팅은 무중단 배포는 구현하지 않고, CD만을 다룹니다.
⚡ workflow
⚡ lifecycle
⚡ Appspec.yml
OS, Files, permission, hooks를 포함하고 있는 yaml파일입니다.
codedeploy-agent에게 배포, 실행파일들의 순서, 결로를 알려주는 역할을 담당합니다.
⚡ codedeploy-agent
EC2에 설치하는 프로그램입니다.
계정, 배포기록, 배포스크립트 등을 EC2 루트 디렉토리에 저장합니다.
/apt/codedeploy-agent/deployment-root
👉🏼 1. IAM role 생성
Code Deploy를 이용하기 위해서 IAM이 2가지가 필요합니다.
각 역할을 만들어줍니다.
S3
로부터 가져오는 작업을 위한 권한AmazonS3FullAccess
S3 List를 읽고 가져오는 권한만 있으면 됩니다!
AWSCodeDeployRole
👉🏼 2. EC2 IAM 수정
기존에 만들어놓은 EC2 인스턴스의 IAM을 위에서 만들어놓은 EC2 Role로 교체합니다.
인스턴스 작업 -> 보안 -> IAM 역할 수정
👉🏼 3. code deploy agent 설치
이제, EC2 SSH에 접속하여 code deploy agent
를 설치합니다.
해당 프로그램은 CodeDeploy
로부터 명령을 받아 직접 빌드 파일 다운로드 및 배포 스크립트 실행 과정을 수행합니다.
우선 SpringBoot로 API 테스트를 위해
테스트 환경 세팅을 진행합니다.
# JDK 설치
yum list | grep java-11
sudo yum install -y java-11-amazon-corretto.x86_64
이후 yum
을 이용하여 codedeploy-agent
를 설치합니다.
codedeploy-agent
는 ruby
로 동작하기 때문에 ruby
의 설치가 선행되어야 합니다.
yum
? Yellow dog Updater, Modified의 약자로 RPM 기반의 시스템을 위한 자동 업데이터 겸 패키지 설치/제거 도구wget
? 웹 서버로부터 콘텐츠를 가져오는 컴퓨터 프로그램, HTTP, HTTPS, FTP 프로토콜을 통해 내려받기를 지원앞으로 build 디렉토리 안에 빌드 파일을 다운로드 할 예정이니 build
디렉토리도 만들어줍니다.
sudo yum -y update
sudo yum -y install ruby
sudo yum -y install wget
mkdir build
cd /home/ec2-user
# wget을 통해 codedeploy-agent 파일 내려받기
wget https://aws-codedeploy-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/latest/install
# 권한 변경 후 설치
sudo chmod +x ./install
sudo ./install auto
# 설치 후 실행되었는지 확인
sudo service codedeploy-agent status
sudo service codedeploy-agent start
👉🏼 4. AWS CodeDeploy Application 생성
애플리케이션 생성 및 컴퓨팅 플랫폼 설정. EC2로 설정하면 됩니다.
👉🏼 5. 배포 그룹 생성
배포 그룹은 환경에 따라 나눌 수 있습니다. 보통 live, qa, dev.. 이렇게 나눕니다.
IAM은 저희가 만들어놓은 code-deploy용 IAM을 선택합니다.
배포 유형은 현재 위치로 하며
EC2 인스턴스를 선택하여 키, 값을 지정합니다.
👉🏼 6. appspec.yml 파일 생성
이후, codeBuild
처럼
codedeploy
에게 어떤 일을 수행하라는 명령들이 담긴 파일을 생성해야 합니다.
보통 lifecycle의 hook마다 쉘 스크립트를 따로 배치하여 수행합니다.
이를 위해 저희는 일단
를 설정하는 yaml
파일을 생성합니다.
version: 0.0
os: linux
files:
- source: /
destination: /home/ec2-user/build # 인스턴스에서 파일이 저장될 위치
hooks:
AfterInstall:
- location: deploy.sh
timeout: 60
runas: root
👉🏼 7. 배포 스크립트 생성
S3
로부터 빌드 파일을 다운로드 받은 이후
배포 스크립트가 없다면 아무일도 일어나지 않습니다.
저희는 shell에게 배포하라고 명령해야 합니다.
java -jar app.jar
이러한 명령들을 수행할 수 있는 쉘 스크립트를 제작합니다.
# deploy.sh
#!/bin/bash
BUILD_PATH=$(ls /home/ec2-user/build/*.jar)
JAR_NAME=$(basename $BUILD_PATH)
echo "> build 파일명: $JAR_NAME"
echo "> build 파일 복사"
DEPLOY_PATH=/home/ec2-user/
cp $BUILD_PATH $DEPLOY_PATH
echo "> springboot-deploy.jar 교체"
CP_JAR_PATH=$DEPLOY_PATH$JAR_NAME
APPLICATION_JAR_NAME=springboot-deploy.jar
APPLICATION_JAR=$DEPLOY_PATH$APPLICATION_JAR_NAME
ln -Tfs $CP_JAR_PATH $APPLICATION_JAR
echo "> 현재 실행중인 애플리케이션 pid 확인"
CURRENT_PID=$(pgrep -f $APPLICATION_JAR_NAME)
if [ -z $CURRENT_PID ]
then
echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
else
echo "> kill -15 $CURRENT_PID"
kill -15 $CURRENT_PID
sleep 5
fi
echo "> $APPLICATION_JAR 배포"
nohup java -jar $APPLICATION_JAR > /dev/null 2> /dev/null < /dev/null &
# healthCheck.sh
#!/bin/bash
echo "> Health check 시작"
echo "> curl -s http://localhost:8080/health "
for RETRY_COUNT in {1..15}
do
# RESPONSE body 데이터에 'UP'이라는 글자가 있다면 성공, 없다면 다시 체크
RESPONSE=$(curl -s http://localhost:8080/actuator/health)
UP_COUNT=$(echo $RESPONSE | grep 'UP' | wc -l)
if [ $UP_COUNT -ge 1 ]
then # $up_count >= 1 ("UP" 문자열이 있는지 검증)
echo "> Health check 성공"
break
else
echo "> Health check의 응답을 알 수 없거나 혹은 status가 UP이 아닙니다."
echo "> Health check: ${RESPONSE}"
fi
if [ $RETRY_COUNT -eq 10 ]
then
echo "> Health check 실패. "
exit 1
fi
echo "> Health check 연결 실패. 재시도..."
sleep 10
done
exit 0
이후 작성된 2개의 쉘 스크립트를 각 hook 역할에 맞춰서 실행할 수 있도록 appspec.yml
을 수정합니다.
version: 0.0
os: linux
files:
- source: /
destination: /home/ec2-user/build # 인스턴스에서 파일이 저장될 위치
# ec2-user로 설정해야함
permissions:
- object: /
pattern: "**"
owner: ec2-user
group: ec2-user
hooks:
ApplicationStart:
- location: deploy.sh
timeout: 60
runas: ec2-user
ValidateService:
- location: healthCheck.sh
timeout: 60
runas: ec2-user
scripts 디렉토리도 code-build의 대상이 되어 아티팩트에 같이 S3에 넣어질 수 있도록 buildspec.yml
수정합니다.
artifacts:
files:
- appspec.yml # code-deploy를 위해
- build/libs/*.jar
- scripts/** # 추가!
이렇게 생성한 총 4개의 파일을 프로젝트 최상단 경로에 배치하면 됩니다.
👉🏼 8. 배포 시도
성공적으로 배포가 완료되었습니다!
EC2에 접속하면 build
디렉토리 안에 성공적으로 buildspec.yml
에 적힌 아티팩트들이 다운로드 된 것을 알 수 있습니다.
성공적으로 배포되었는지 확인하기 위해서는 log
를 보는것이 필수입니다.
AWS CodeDeploy 가이드에서는 다음과 같이 log
데이터를 제공한다고 합니다.
# 배포 로그 확인
less /var/log/aws/codedeploy-agent/codedeploy-agent.log
# 스크립트 로그 확인
less /opt/codedeploy-agent/deployment-root/deployment-group-ID/deployment-ID/logs/scripts.log
자세한 확인 방법은 해당 링크를 참고해주세요.
CodeDeploy EC2/온프레미스 배포에 대한 로그 데이터 보기
❗ deploy.sh
빌드 스크립트가 실행되지 않는 오류
2023-04-26 12:16:26 WARN [codedeploy-agent(2693)]: InstanceAgent::Plugins::CodeDeployPlugin::HookExecutor: Script at specified location: deploy.sh is not executable. Trying to make it executable.
2023-04-26 13:02:27 WARN [codedeploy-agent(2693)]: InstanceAgent::Plugins::CodeDeployPlugin::HookExecutor: Script at specified location: healthCheck.sh is not executable. Trying to make it executable.
👉 해결방안1 : 권한 설정 오류
저와 동일한 오류가 발생하여 해결한 포스팅을 참조했습니다.
해당 오류는 deploy.sh
를 실행할 수 없어서 생긴 오류라고 진단합니다.
파일에 대한 권한을 755
로 새로 지정해주어야 합니다.
sudo chmod +x deploy.sh
해당 작업을 배포마다 수작업으로 추가하기엔 너무 번거로우니
codedeploy의 appspec.yml에 권한설정을 추가해줍니다.
permissions:
- object: /
pattern: "**"
mode: '755'
owner: ec2-user
group: ec2-user
하지만 위 포스팅에서는 permissions을 통해서 권한을 지정해도 상관없다고 합니다.
files.destination에 명시한 /home/ec2-user/build 경로에 복사합니다.
하지만, 실제로는 /deployment-archive/에 배포되며 (Amazon Linux의 deployment-root는 /opt/codedeploy-agent/deployment-root/) 배포후 실행되는 스크립트는 /home/ec2-user/build가 아닌 deployment-archive/hooks/deploy.sh 입니다.
하지만 이 이상으로 권한을 변경하는 방법을 찾지 못했고,
이전까지 잘 배포된 어플리케이션이고, 스크립트나 appspec 설정파일도 건들지 않았기 때문에 권한 문제가 원인이 아니라고 생각하게 되었습니다.
👉 해결방안2 : 파일 경로 혹은 파일 이름의 오타
파일 경로나 파일 이름이 오타가 생겼을 시 해당 hook에서 스크립트를 찾지 못해 오류가 발생할 수 있습니다.
하지만 이 역시 올바르게 되어있었습니다.
👉 해결방안3 : 어플리케이션 로그 확인
어플리케이션을 배포하는 도중에 springboot 자체를 실행하지 못할 경우 해당 오류가 발생할 수 있습니다.
저도 spring 배포 로그 파일을 확인한 결과 다음과 같이 오류가 발생한 것을 확인할 수 있었습니다.
13:48:39.134 [main] ERROR org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter -
***************************
APPLICATION FAILED TO START
***************************
Description:
Config data resource 'class path resource [aws.yml]' via location 'classpath:aws.yml' does not exist
Action:
Check that the value 'classpath:aws.yml' is correct, or prefix it with 'optional:'
이는, 아까 위에서 추가했었던 aws.yml 파일을 인식하기 위해 classpath를 추가 설정하였는데
gitignore에 포함되어 있어서 EC2 인스턴스 내에서 인식하지 못해 발생한 오류입니다.
SpringBootApplication 클래스에서 classpath를 수정한 이후
정상적으로 배포된 것을 확인할 수 있었습니다.
🗣️ codedeploy에서 발생한 오류면 deploy-log와 spring log 둘 다 분석해보아야 합니다.
이렇게 지속적인 배포 환경을 만들어보았는데
과정은 많이 어려웠지만 한번 만들고 나니 개발할 때 편리한 점을 많이 찾을 수 있었습니다.
스크립트 언어에 아직 익숙하지 않아서 구글링하며 많은 블로그들의 소스를 참고했습니다.
Pipeline을 통해서 배포를 진행하였기 때문에
기회가 된다면 Jenkins를 이용하여 CI/CD를 구축해볼 예정입니다.
AWS CodeDeploy - aws-documentation
Github Action과 AWS CodeDeploy를 사용하여 Spring Boot 프로젝트 CI/CD 파이프라인 구축하기 - 해어린
/usr/share/ruby3.2/logger/log_device.rb:83:in `exist?': no implicit conversion of Array into String (TypeError) if File.exist?(path) - stackoverflow
3) AWS로 배포하기 시리즈 - 3. AWS Code Pipeline으로 배포하기 - 향로