위 글에서 이어지는 내용입니다!
Github Actions Workflow 작성
/. github/workflows/라는 경로는 반드시 지켜줘야 한다
# /.github/workflows/dev_deploy.yml
name: Dev Deploy
# release 브랜치로 push 되거나 pr이 날아가는 경우 workflow가 수행된다
on:
push:
branches:
- release
pull_request:
branches:
- release
# 본인이 설정한 리전, 버킷 이름, CodeDeploy 앱 이름, 배포그룹 이름을 채워 넣는다
env:
AWS_REGION: ap-northeast-2
S3_BUCKET_NAME: iluvit-dev-actions-s3-bucket
CODE_DEPLOY_APPLICATION_NAME: iluvit-app
CODE_DEPLOY_DEPLOYMENT_GROUP_NAME: iluvit-dev-deployment-group
permissions:
contents: read
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
environment: production
steps:
# (1) 기본 체크아웃
- name: Checkout
uses: actions/checkout@v2
# (2) JDK 17 세팅
- name: Set up JDK 17
uses: actions/setup-java@v2
with:
java-version: '17'
distribution: 'temurin'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
# (3) gitignore한 파일 직접 생성해주기
- name: make resources file
run: |
cd ./src/main/resources
touch ./application.properties
echo "${{ secrets.PROPERTIES }}" > ./application.properties
touch ./application-dev.yml
echo "${{ secrets.DEV_YML }}" > ./application-dev.yml
touch ./application-http.yml
echo "${{ secrets.HTTP_YML }}" > ./application-http.yml
touch ./application-map.yml
echo "${{ secrets.MAP_YML }}" > ./application-map.yml
touch ./application-s3.yml
echo "${{ secrets.S3_YML }}" > ./application-s3.yml
touch ./application-secret.yml
echo "${{ secrets.SECRET_YML }}" > ./application-secret.yml
touch ./application-security.yml
echo "${{ secrets.SECURITY_YML }}" > ./application-security.yml
# (4) Gradle build
- name: Build with Gradle
run : ./gradlew clean build --exclude-task test
# (5) AWS 인증 (IAM 사용자 Access Key, Secret Key 활용)
- name: Configure AWS credentials
uses: aws-actions/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: ${{ env.AWS_REGION }}
# (6) 빌드 결과물을 S3 버킷에 업로드
- name: Upload to AWS S3
run: |
aws deploy push \
--application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
--ignore-hidden-files \
--s3-location s3://$S3_BUCKET_NAME/$GITHUB_SHA.zip \
--source .
# (7) S3 버킷에 있는 파일을 대상으로 CodeDeploy 실행
- name: Deploy to AWS EC2 from S3
run: |
aws deploy create-deployment \
--application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
--deployment-config-name CodeDeployDefault.AllAtOnce \
--deployment-group-name ${{ env.CODE_DEPLOY_DEPLOYMENT_GROUP_NAME }} \
--file-exists-behavior OVERWRITE \
--s3-location bucket=$S3_BUCKET_NAME,key=$GITHUB_SHA.zip,bundleType=zip
(3) gitignore 한 파일 ec2에 직접 생성해 주기
이 단계가 다른 글들에서는 나와있지 않아 많이 시간이 걸렸다
우리 프로젝트에서는 설정파일들을 여러 가지로 쪼개서 application.yml을 제외하고는 모두 gitignore 시켜주었는데 CI는 깃허브 상에 있는 코드로 빌드를 하는 것이므로 문제가 생겼다
github actions secrets에 gitignore 한 설정 파일들을 넣어놓고 이를 이용해 빌드를 할 때./src/main/resources 에 설정 파일을 생성하여 문제를 해결하였다
(4) Gradle build
--exclude-task test : 테스트 코드를 제외하고 빌드한다. 이는 옵션이고 테스트 코드까지 빌드하고 싶다면 이를 제외하고 써주면 된다.
(5) AWS 인증
AWS에 접근하기 위해 인증하는 단계이다
우리가 IAM 사용자를 만든 후 저장한 Access Key와 Secret Key를 secrets 변수를 통해 가져와서 사용할 수 있다
(6) 빌드 결과물을 S3 버킷에 업로드
원하는 파일들을 압축해서 AWS S3에 업로드하는 단계이다
- --application-name: CodeDeploy 애플리케이션 이름
- --s3-location: 압축 파일을 업로드할 S3 버킷 정보
- --ignore-hidden-files (optional): 숨겨진 파일까지 번들링 할지 여부
- $GITHUB_SHA : Github 자체에서 커밋마다 생성하는 랜덤 한 변숫값으로 파일 업로드 시에 이름 중복으로 충돌날 일이 없게 해 준다
(7) S3 버킷에 있는 파일을 대상으로 CodeDeploy 실행
S3 에 저장한 파일을 EC2에서 당겨온 후 압축을 풀고 스크립트를 실행합니다.
- --application-name: CodeDeploy 애플리케이션 이름
- --deployment-config-name: 배포 방식인데 기본값을 사용
- --deployment-group-name: CodeDeploy 배포 그룹 이름
- --s3-location: 버킷 이름, 키 값, 번들타입
Workflow가 잘 작동하면 다음과 같이 초록색으로 표시된다
무중단 배포 원리
하나의 EC2서버에 하나의 NGINX와 2대의 스프링부트 서브를 이용하는 것
클라이언트의 요청을 NGINX가 받아서 8081 포트로 넘겨준다
새로운 코드가 push 되면 8082 포트로 배포한다.
정상 구동이 확인되면 , NGINX를 reload 한 후 8082 포트를 바라보게 한다
Nginx 설치 및 Symbolic link 연결하기
Nginx가 CodeDeploy Agent에 의해 두 Web Application Server 간의 스위칭 역할을 담당한다
다음 명령어를 통해 Nginx를 설치한다
$ sudo apt install nginx
아래에서 사용할 $service_url을 생성한다
$ sudo vim /home/ubuntu/iluvit/service_url.inc
# /home/ubuntu/iluvit/service_url.inc
set $service_url http://127.0.0.1:8081; # ec2 ip주소 아니라 진짜 127.0.0.1 적어줘야함
/etc/nginx/sites-available 디렉터리는 Nginx의 설정 파일들이 위치한 디렉터리이다.
최초 설치 시에는deafult라는 설정 파일만 존재한다.
Nginx의 설정파일을 다음과 같이 관리자 권한으로 작성한다
$ cd /etc/nginx/sites-available/
$ sudo vim iluvit-prod
# /etc/nginx/sites-available/iluvit-prod
server {
listen 80;
listen [::]:80;
server_name _;
include /home/ubuntu/service_url.inc;
location / {
proxy_pass $service_url;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
}
}
- listen 80 : 웹 서버를 80 포트로 서비스한다는 의미이다. 80은 HTTP의 기본 포트이므로 이제 포트를 생략하여도 웹 브라우저에서 접속할 수 있다.
- location / {... } : /에 해당되는 URL, 즉 모든 요청에 대한 설정을 담당하는 영역이다. 세부 설정은 다음과 같다.
- proxy_pass : Nginx 웹서버의 모든 요청을 $service_url로 리다이렉트 한다.
- proxy_set_header : $service_url로로 실행된 스프링부트 서버에 특정 헤더값을 전달하기 위해서 사용한다. (Nginx를 통해서 스프링부트의 톰캣서버로 요청이 전달되기 때문에 "Remote IP" 헤더값이 실제 값이 아닌 127.0.0.1처럼 잘못 전달되는 것을 방지하기 위해 사용한다.)
sites-enabled 디렉터리는 site-available 디렉터리에 있는 설정 파일 중에서 활성화하고 싶은 것을 링크로 관리하는 디렉터리이다.
default 링크는 삭제하고 sbb 파일을 링크하도록 변경한다
$ cd /etc/nginx/sites-enabled/
$ sudo rm default # default 파일 있을 시
$ sudo ln -s /etc/nginx/sites-available/iluvit-prod
$ sudo systemctl restart nginx
AppSpec 파일 작성
CodeDeploy에서 배포를 위해 참조할 AppSpec 파일을 작성한다.
AppSpec 파일을 사용해서 우리는 프로젝트의 어떤 파일들을 EC2의 어떤 경로에 복사할지 설정 가능하고, 배포 프로세스 이후에 수행할 스크립트를 지정하여 자동으로 서버를 띄울 수도 있습니다.
AppSpec 파일은 기본적으로 루트 디렉터리에 위치해야 합니다.
# appspec.yml
version: 0.0
os: linux
files:
- source: /
destination: /home/ubuntu/iluvit/app
overwrite: yes
file_exists_behavior: OVERWRITE
permissions:
- object: /
pattern: "**"
owner: ubuntu
group: ubuntu
hooks:
ApplicationStart:
- location: scripts/run_new_was.sh
timeout: 60
runas: ubuntu
- location: scripts/health_check.sh
timeout: 60
runas: ubuntu
- location: scripts/switch.sh
timeout: 60
runas: ubuntu
files 섹션
배포 파일에 대한 설정
- source: 인스턴스에 복사할 디렉터리 경로
- destination: 인스턴스에서 파일이 복사되는 위치
- overwrite: 복사할 위치에 파일이 있는 경우 대체
permissions 섹션
files 섹션에서 복사한 파일에 대한 권한 설정
- object: 권한이 지정되는 파일 또는 디렉터리
- pattern (optional): 매칭되는 패턴에만 권한 부여
- owner (optional): object의 소유자
- group (optional): object 의 그룹 이름
hooks 섹션
배포 이후에 수행할 스크립트를 지정
CodeDeploy에는 여러 수명 주기가 있다. 각 수명 주기에 따라 원하는 스크립트를 수행할 수 있다
우리는 여러 수명 주기 중 ApplicationStart에 세 가지 스크립트가 작동하도록 하겠다
- location: hooks에서 실행할 스크립트 위치
- timeout (optional): 스크립트 실행에 허용되는 최대 시간이며, 넘으면 배포 실패로 간주됨
- runas (optional): 스크립트를 실행하는 사용자
배포 스크립트 작성
# scripts/run_new_was.sh
PROJECT_ROOT="/home/ubuntu/iluvit/app" # 프로젝트 루트
# service_url.inc 에서 현재 서버의 포트 번호 가져오기
CURRENT_PORT=$(cat /home/ubuntu/iluvit/service_url.inc | grep -Po '[0-9]+' | tail -1)
TARGET_PORT=0
echo "> Current port of running WAS is ${CURRENT_PORT}."
# 타켓 포트 번호 알아내기
if [ ${CURRENT_PORT} -eq 8081 ]; then
TARGET_PORT=8082 # 현재포트가 8081이면 8082로 배포
elif [ ${CURRENT_PORT} -eq 8082 ]; then
TARGET_PORT=8081 # 현재포트가 8082라면 8081로 배포
else
echo "> Not connected to nginx" # nginx가 실행되고 있지 않다면 에러 코드
fi
# 타겟 포트의 PID를 불러온다
TARGET_PID=$(lsof -Fp -i TCP:${TARGET_PORT} | grep -Po 'p[0-9]+' | grep -Po '[0-9]+')
# PID를 이용해 해당 포트를 사용하는 서버 Kill
if [ ! -z ${TARGET_PID} ]; then
echo "> Kill ${TARGET_PORT}."
sudo kill -9 ${TARGET_PID}
fi
# 타켓 포트에 jar파일을 이용해 새로운 서버 실행
nohup java -jar -Dserver.port=${TARGET_PORT} /home/ubuntu/iluvit/app/build/libs/iLUVit-0.0.1-SNAPSHOT.jar > /home/ubuntu/iluvit/app/nohup.out 2>&1 &
echo "> Now new WAS runs at ${TARGET_PORT}."
exit 0
참고로 nohup은 세션이 끊어져도 서버가 꺼지지 않게 하는 데 사용된다.
> /home/ubuntu/i luv it/app/nohup.out 2>&1 & 해당 부분을 작성하지 않으면 오류가 발생한다
마지막에 & 역시 꼭 붙여야 하며 이는 백그라운드로 동작함을 의미한다
# scripts/health_check.sh
# service_url.inc 에서 현재 서버의 포트 번호 가져오기
CURRENT_PORT=$(cat /home/ubuntu/iluvit/service_url.inc | grep -Po '[0-9]+' | tail -1)
TARGET_PORT=0
# 현재 타겟포트 가져오기
if [ ${CURRENT_PORT} -eq 8081 ]; then
TARGET_PORT=8082
elif [ ${CURRENT_PORT} -eq 8082 ]; then
TARGET_PORT=8081
else
echo "> No WAS is connected to nginx"
exit 1
fi
# 새로 열린 서버가 정상적으로 작동하는지 확인
echo "> Start health check of WAS at 'http://127.0.0.1:${TARGET_PORT}' ..."
for RETRY_COUNT in 1 2 3 4 5 6 7 8 9 10 # 해당 커맨드들을 10번씩 반복
do
echo "> #${RETRY_COUNT} trying..."
# 테스트할 API 주소를 통해 http 상태 코드 가져오기
RESPONSE_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:${TARGET_PORT}/actuator/health)
echo "> ${RESPONSE_CODE}"
if [ ${RESPONSE_CODE} -eq 200 ]; then # RESPONSE_CODE의 http 상태가 200번인 경우
echo "> New WAS successfully running"
exit 0
elif [ ${RETRY_COUNT} -eq 10 ]; then # RESPONSE_CODE의 http 상태가 10번인 경우
echo "> Health check failed."
exit 1
fi
# 아직 열려있지 않았다면 sleep
sleep 15
done
새로 띄운 서버가 완전히 실행되기까지 health check 하는 스크립트이다
예전에는 따로 health check용 API를 만들었지만 최근에는 아래의 코드를 application.yml에 추가하여 간단하게 health check를 할 수 있다
이를 추가함으로써 http://127.0.0.1:8081/actuator/health라는 url로 health check를 할 수 있다
# application.yml
management:
endpoints:
web:
exposure: # health check 가능한 모든 항목을 웹에서 노출하도록 설정
include: "*"
# scripts/switch.sh
# service_url.inc 에서 현재 서버의 포트 번호 가져오기
CURRENT_PORT=$(cat /home/ubuntu/iluvit/service_url.inc | grep -Po '[0-9]+' | tail -1)
TARGET_PORT=0
echo "> Nginx currently proxies to ${CURRENT_PORT}."
# 현재 타겟포트 가져오기
if [ ${CURRENT_PORT} -eq 8081 ]; then
TARGET_PORT=8082
elif [ ${CURRENT_PORT} -eq 8082 ]; then
TARGET_PORT=8081
else
echo "> No WAS is connected to nginx"
exit 1
fi
# $ service_url.inc 파일을 현재 바뀐 서버 포트로 변경
echo "set \$service_url http://127.0.0.1:${TARGET_PORT};" | tee /home/ubuntu/iluvit/service_url.inc
echo "> Now Nginx proxies to ${TARGET_PORT}."
# nginx를 reload 해준다.
sudo service nginx reload
echo "> Nginx reloaded."
- sudo service nginx reload : sudo service nginx restart와 달리 nginx 서버의 재시작 없이, 설정값만 새로 고침을 하게 된다
- tee : 출력 내용을 파일로 만들어주는 커맨드
build.gradle 파일 수정
Spring Boot 2.5 버전부터는 빌드 시 일반 jar 파일 하나와 -plain.jar 파일 하나가 함께 만들어진다
그래서 빌드 시 plain jar 파일은 만들어지지 않도록 build.gradle 파일에 다음 내용을 추가해야 한다
jar {
enabled = false
}
자동 배포가 성공하면 다음과 같은 화면을 볼 수 있다
만약 ApplicationStop 부터 계속 실패한다면?
아래 명령어로 nginx를 재시작하였더니 해결되었다
$ sudo service nginx restart
github actions, code deploy 모두 성공적으로 잘 되는데 바뀐 코드가 실제 서버에 적용되지 않는다면?
service_url에 접근하지 못하는 문제 일 수 있다
service_url.inc 가 있는 경로로 들어가 아래와 같이 권한을 열어준다
$ sudo chmod 655 service_url.inc
지금까지 Github Actions, CodeDeploy, Nginx를 이용한 무중단 배포하는 법을 알아보았다
다음 편에는 두 개의 workflow를 만들어 dev와 prod 각각을 다른 서버에 자동 배포하는 방법을 써보려고 한다!