https://whitem4rk.tistory.com/61
우선 저번 포스트에서 배포전략을 알아보았고 제 입맛에 맞는 배포흐름을 대강 짰습니다. 이제 구현을 해볼 차례인데 제가 사용한 툴들은 [github actions, nginx, spring, spring cloud config(아니어도됨)] 요정도 입니다. 구글링 해보니 생각보다 이 방식을 사용한 분들이 많더라구요. 그러다 제 구조랑 가장 비슷했던 블로그 포스트를 찾았습니다.
여기 코드도 대부분을 가져다 썼습니다. 너무 감사합니당. 저랑 조금 달랐던 점은 저는 dev서버와 production 서버 둘다 blue/green으로 운영하려고 했기 때문에 약간의 차이가 있습니다. 코드는 dev(8082, 8083포트)용 입니다.
1. Github actions
name: deploy-dev
on:
push:
branches: [ master ]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Set up JDK 11
uses: actions/setup-java@v4
with:
java-version: '11'
distribution: 'adopt'
- name: Setup Gradle
uses: gradle/gradle-build-action@v3
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew build
- name: copy file via ssh
uses: appleboy/scp-action@master
with:
host: ${{ secrets.IBAS_DEV_HOST }}
username: ${{ secrets.IBAS_DEV_USERNAME }}
key: ${{ secrets.IBAS_DEV_SSH_KEY }}
passphrase: ${{ secrets.IBAS_DEV_PASSWORD }}
# port: ${{ secrets.PORT }} # default : 22
source: "resource-server/build/libs/resource-server-0.0.1-SNAPSHOT.jar"
target: ${{ secrets.IBAS_DEV_DEPLOY_PATH }}
strip_components: 3
- name: execute deploy shell script via ssh
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.IBAS_DEV_HOST }}
username: ${{ secrets.IBAS_DEV_USERNAME }}
key: ${{ secrets.IBAS_DEV_SSH_KEY }}
passphrase: ${{ secrets.IBAS_DEV_PASSWORD }}
# port: ${{ secrets.PORT }} # default : 22
script: |
bash ${{ secrets.IBAS_DEV_DEPLOY_PATH }}/deploy.sh
1. 빌드 책임을 github actions에 부여해서 빈약한 ec2의 부담을 조금이나마 줄였습니다.
2. 그리고 scp를 사용해서 jar파일을 DEV_USER 경로로 전송했습니다.
3. 이후 ssh를 사용해서 deploy.sh를 실행해서 서버 전환이 일어나도록 했습니다.
여기서 좀 헤맸던 점은 scp의 source, target 부분에서 strip_components 속성을 정하지 않으면 source 디렉토리를 통째로 옮깁니다. 저는 jar파일만 원하는 위치로 옮기고 싶었기 때문에 상위경로 3개를 지우기 위해 strip_components: 3을 설정했습니다.
2. deploy.sh, spring
@Slf4j
@RestController
@RequiredArgsConstructor
public class EnvironmentController {
private final Environment environment;
@GetMapping("/prod/profiles")
public String prodProfile() {
final List<String> profiles = Arrays.asList(environment.getActiveProfiles());
final List<String> prodProfiles = Arrays.asList("production1", "production2");
final String defaultProfile = profiles.get(0);
return Arrays.stream(environment.getActiveProfiles())
.filter(prodProfiles::contains)
.findAny()
.orElse(defaultProfile);
}
@GetMapping("/dev/profiles")
public String devProfile() {
final List<String> profiles = Arrays.asList(environment.getActiveProfiles());
final List<String> prodProfiles = Arrays.asList("dev1", "dev2");
final String defaultProfile = profiles.get(0);
return Arrays.stream(environment.getActiveProfiles())
.filter(prodProfiles::contains)
.findAny()
.orElse(defaultProfile);
}
}
#!/bin/bash
BASE_PATH=직접입력
BUILD_PATH=$(ls $BASE_PATH/deploy/*.jar)
JAR_NAME=$(basename $BUILD_PATH)
echo "> build 파일명: $JAR_NAME"
# echo "> build 파일 복사"
DEPLOY_PATH=$BASE_PATH/deploy/temp/
# cp $BUILD_PATH $DEPLOY_PATH
echo "> 현재 구동중인 Set 확인"
RESPONSE=$(curl -s http://localhost:8082/api/dev/profiles)
if [ $? -eq 0 ] && [ -n "$RESPONSE" ]; then
CURRENT_PROFILE=$RESPONSE
else
RESPONSE=$(curl -s http://localhost:8083/api/dev/profiles)
if [ $? -eq 0 ] && [ -n "$RESPONSE" ]; then
CURRENT_PROFILE=$RESPONSE
else
CURRENT_PROFILE="NOT WORKING"
fi
fi
echo "> $CURRENT_PROFILE"
# 쉬고 있는 set 찾기: set1이 사용중이면 set2가 쉬고 있고, 반대면 set1이 쉬고 있음
if [ $CURRENT_PROFILE == dev1 ]; then
IDLE_PROFILE=dev2
IDLE_PORT=8083
elif [ $CURRENT_PROFILE == dev2 ]; then
IDLE_PROFILE=dev1
IDLE_PORT=8082
else
echo "> 일치하는 Profile이 없습니다. Profile: $CURRENT_PROFILE"
echo "> dev1을 할당합니다. IDLE_PROFILE: dev1"
IDLE_PROFILE=dev1
IDLE_PORT=8082
fi
echo "> application.jar 교체"
IDLE_APPLICATION=$IDLE_PROFILE-resource-server-0.0.1-SNAPSHOT.jar
cp $BUILD_PATH $DEPLOY_PATH$IDLE_APPLICATION
IDLE_APPLICATION_PATH=$DEPLOY_PATH$IDLE_APPLICATION
# ln -Tfs $DEPLOY_PATH$JAR_NAME $IDLE_APPLICATION_PATH
echo "> $IDLE_PROFILE 에서 구동중인 애플리케이션 pid 확인"
IDLE_PID=$(pgrep -f $IDLE_APPLICATION)
if [ -z $IDLE_PID ]; then
echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
else
echo "> kill -15 $IDLE_PID"
kill -15 $IDLE_PID
sleep 5
fi
echo "> $IDLE_PROFILE 배포"
nohup java -jar -Dspring.profiles.active=$IDLE_PROFILE $IDLE_APPLICATION_PATH > /home/ibas_dev/deploy/temp/output.log 2>&1 &
echo "> $IDLE_PROFILE 10초 후 Health check 시작"
echo "> curl -s http://localhost:$IDLE_PORT/api/actuator/health "
sleep 10
WEBHOOK_URL=직접입력
SUCCESS_MESSAGE="${IDLE_PROFILE} (PORT:${IDLE_PORT}) DEPLOYMENT SUCCESS!"
FAILURE_MESSAGE="${IDLE_PROFILE} (PORT:${IDLE_PORT}) DEPLOYMENT FAILED..."
for retry_count in {1..10}; do
response=$(curl -s http://localhost:$IDLE_PORT/api/actuator/health)
up_count=$(echo $response | grep 'UP' | wc -l)
if [ $up_count -ge 1 ]; then # $up_count >= 1 ("UP" 문자열이 있는지 검증)
echo "> Health check 성공"
echo "set \$dev_service_url http://127.0.0.1:${IDLE_PORT};" | sudo tee /etc/nginx/conf.d/dev_service_url.inc
PROXY_PORT=$(curl -s http://localhost:8082/api/dev/profiles)
echo "> nginx current proxy port: $PROXY_PORT"
echo "> nginx reload"
sudo service nginx reload
break
else
echo "> Health check의 응답을 알 수 없거나 혹은 status가 UP이 아닙니다."
echo "> Health check: ${response}"
fi
if [ $retry_count -eq 10 ]; then
echo "> Health check 실패."
echo "> Nginx에 연결하지 않고 배포를 종료합니다."
PAYLOAD=$(cat <<EOF
{
"content": "$FAILURE_MESSAGE"
}
EOF
)
curl -H "Content-Type: application/json" -d "$PAYLOAD" "$WEBHOOK_URL"
exit 1
fi
echo "> Health check 연결 실패. 재시도..."
sleep 10
done
echo "> $CURRENT_PROFILE 에서 구동중인 애플리케이션 pid 확인"
CURRENT_APPLICATION=$CURRENT_PROFILE-resource-server-0.0.1-SNAPSHOT.jar
CURRENT_PID=$(pgrep -f $CURRENT_APPLICATION)
if [ -z $CURRENT_PID ]; then
echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
else
echo "> kill -15 $CURRENT_PID"
kill -15 $CURRENT_PID
sleep 5
fi
PAYLOAD=$(cat <<EOF
{
"content": "$SUCCESS_MESSAGE"
}
EOF
)
curl -H "Content-Type: application/json" -d "$PAYLOAD" "$WEBHOOK_URL"
exit 0
경로 관련된 변수는 직접 입력하시면 되고 로직은 대충 이렇습니다.
1. 실행중인 profile을 확인해서 만약 있다면 반대 profile을, 없다면 dev1(default)로 설정합니다.
2. 만약 해당 포트가 사용중일수도 있으니 프로세스를 종료합니다.
3. jar 실행 후 10초마다 health check를 해서 정상적으로 서버가 켜졌는지 확인합니다.
4.1 만약 응답이 계속 없을 시 discord webhook으로 실패 메세지를 보냅니다.
4.2 성공한다면 nginx/conf.d/dev_service_url.inc 을 새로 실행한 서버 주소로 바꾼후 nginx reload를 진행합니다. 여기서 root로 접속하지 않았으면 sudo service nginx reload는 권한부족으로 실행을 못할겁니다. 그래서 약간 설정을 해줘야합니다.
sudo visudo
someuser ALL=(ALL) NOPASSWD: /usr/sbin/service nginx reload
5. 4에서 성공한 경우 기존에 실행되던 서버를 종료한 후 discord에 성공메세지를 보냅니다.
3. nginx 관련 설정
server {
listen 80;
server_name dev.example.com www.dev.example.com;
return 301 https://dev.example.com$request_uri;
}
server {
# SSL configuration
listen 443 ssl;
server_name dev.example.com;
allow x.x.x.x;
deny all; # 그 외의 다른 ip 차단
ssl_certificate #ssl 경로;
ssl_certificate_key #ssl 경로;
include /etc/nginx/conf.d/dev_service_url.inc;
location /api {
proxy_pass $dev_service_url;
proxy_set_header HOST $host;
real_ip_header X-Forwarded-For;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
root /프론트서버/build;
index index.html;
try_files $uri /index.html;
real_ip_header X-Forwarded-For;
}
}
다른 nginx 코드와 다른부분은 별로 없고 include /etc/nginx/conf.d/dev_service_url.inc; 이 부분을 deploy.sh가 계속 변경해주면서 포트가 변경되는 원리입니다.
다음은 Docker와 Docker hub를 한번 활용해서 좀 더 독립적으로 실행되도록 업그레이드 할 예정입니다.
# References
'Back-end > IBAS-spring-project' 카테고리의 다른 글
Blue/Green 배포 (Nginx + github actions + Docker compose) (0) | 2024.06.21 |
---|---|
무중단 배포 전략 (Continuous Deployment) (1) | 2024.06.14 |
JPA 연관관계 (일대일, 일대다, 다대일, 다대다) (0) | 2023.08.25 |
프로세스, 그리고 Docker와 VM (0) | 2023.08.06 |
RESTful API (0) | 2023.07.27 |