Skip to content

Spring_Security_기본_설정 : feat : [redis모듈] Redis 관련 환경설정 및 Properties 추… #16

Spring_Security_기본_설정 : feat : [redis모듈] Redis 관련 환경설정 및 Properties 추…

Spring_Security_기본_설정 : feat : [redis모듈] Redis 관련 환경설정 및 Properties 추… #16

name: spring-boot-cicd
on:
push:
branches:
# - main
env:
PROJECT_NAME: campus-table
DOCKERHUB_REPO: ${{ secrets.DOCKERHUB_USERNAME }}
APP_IMAGE_SUFFIX: "-server"
MAIN_PORT: "8083"
TEST_PORT: "8084"
APP_EXPOSE_PORT: "8080"
MOUNT_DIR: "/volume1/project/campus-table/server"
SPRING_PROFILE_MAIN: "prod"
SPRING_PROFILE_TEST: "prod"
TIME_ZONE: "Asia/Seoul"
REPLICAS: 1 # 동시에 실행할 컨테이너 수
UPDATE_PARALLELISM: 1 # 동시 업데이트 수
UPDATE_DELAY: 10s # 업데이트 대기시간
UPDATE_FAILURE_ACTION: "rollback" # 업데이트 실패 시
RESTART_CONDITION: "on-failure" # 재기동 조건
RESTART_MAX_ATTEMPTS: 3 # 재기동 시도 횟수
jobs:
build:
if: ${{ !startsWith(github.event.head_commit.message, 'version(') }} # 특정 커밋 메시지 워크플로 실행 제외
runs-on: ubuntu-latest
steps:
- name: 코드 체크아웃
uses: actions/checkout@v4
- name: JDK 설정
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: 'gradle'
- name: Gradle Wrapper 실행권한 부여
run: chmod +x gradlew
- name: application-prod.yml 파일 생성
run: |
echo "${{ secrets.APPLICATION_PROD_YML }}" > CT-web/src/main/resources/application-prod.yml
- name: config-imports.yml 파일 생성
run: |
echo "${{ secrets.CONFIG_IMPORTS_YML }}" > CT-web/src/main/resources/config-imports.yml
- name: springdoc.yml 파일 생성
run: |
echo "${{ secrets.SPRINGDOC_YML }}" > CT-web/src/main/resources/springdoc.yml
# 브랜치 별 active profile 설정
- name: Decide active profile
id: profile
run: |
if [ "${GITHUB_REF_NAME}" = "deploy" ]; then
echo "PROFILE=deploy" >> $GITHUB_OUTPUT
else
echo "PROFILE=prod" >> $GITHUB_OUTPUT
fi
- name: Build with Gradle
run: ./gradlew clean build -x test -Dspring.profiles.active=${{ steps.profile.outputs.PROFILE }}
- name: Docker 빌드환경 설정
uses: docker/setup-buildx-action@v3
- name: DockerHub 로그인
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Cache Docker layers
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ hashFiles('Dockerfile') }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Docker 이미지 빌드 및 푸시
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.PROJECT_NAME }}${{ env.APP_IMAGE_SUFFIX }}:${{ github.ref_name }}
cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/${{ env.PROJECT_NAME }}${{ env.APP_IMAGE_SUFFIX }}:cache
cache-to: type=inline
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Deploy
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
password: ${{ secrets.SERVER_PASSWORD }}
port: ${{ secrets.SERVER_PORT }}
script: |
set -e
export PATH=$PATH:/usr/local/bin
echo "환경변수 설정.."
PW="${{ secrets.SERVER_PASSWORD }}"
BRANCH="${{ github.ref_name }}"
REPO="${{ env.DOCKERHUB_REPO }}"
PROJECT="${{ env.PROJECT_NAME }}"
IMG_SUFFIX="${{ env.APP_IMAGE_SUFFIX }}"
IMAGE="${REPO}/${PROJECT}${IMG_SUFFIX}:${BRANCH}"
CONTAINER_NAME="${PROJECT}${IMG_SUFFIX}"
SERVICE_NAME="${PROJECT}${IMG_SUFFIX}"
APP_PORT="${{ env.APP_EXPOSE_PORT }}"
MOUNT_DIR="${{ env.MOUNT_DIR }}"
MAIN_PORT="${{ env.MAIN_PORT }}"
TEST_PORT="${{ env.TEST_PORT }}"
PROFILE_MAIN="${{ env.SPRING_PROFILE_MAIN }}"
PROFILE_TEST="${{ env.SPRING_PROFILE_TEST }}"
TIME_ZONE="${{ env.TIME_ZONE }}"
REPLICAS="${{ env.REPLICAS }}"
UPDATE_PARALLELISM="${{ env.UPDATE_PARALLELISM }}"
UPDATE_DELAY="${{ env.UPDATE_DELAY }}"
UPDATE_FAILURE_ACTION="${{ env.UPDATE_FAILURE_ACTION }}"
RESTART_CONDITION="${{ env.RESTART_CONDITION }}"
RESTART_MAX_ATTEMPTS="${{ env.RESTART_MAX_ATTEMPTS }}"
echo "브랜치=${BRANCH}"
echo "이미지=${IMAGE}"
echo "MAIN_PORT=${MAIN_PORT} | TEST_PORT=${TEST_PORT}"
if [ "${BRANCH}" = "main" ]; then
PORT="${MAIN_PORT}"
PROFILE="${PROFILE_MAIN}"
elif [ "${BRANCH}" = "test" ]; then
CONTAINER_NAME="${CONTAINER_NAME}"-test
SERVICE_NAME="${SERVICE_NAME}-test"
PORT=${TEST_PORT}
PROFILE=${PROFILE_TEST}
else
echo "지원하지 않는 브랜치: ${BRANCH}"
exit 1
fi
echo "브랜치: ${BRANCH}"
echo "컨테이너 이름: ${CONTAINER_NAME}"
echo "포트: ${PORT}"
echo "활성 프로필: ${PROFILE}"
echo "도커 이미지 풀 : ${IMAGE}"
echo $PW | sudo -S docker pull "${IMAGE}"
# Docker Swarm 초기화 확인 (최초 1회 실행)
echo "Docker Swarm 초기화 확인 (최초 1회 실행)"
if ! sudo docker info | grep -q "Swarm: active"; then
echo "Docker Swarm이 비활성화 상태입니다. docker swarm init 진행"
echo $PW | sudo -S docker swarm init || true
echo "Docker Swarm 초기화 완료"
else
echo "Docker Swarm이 이미 활성화되어 있습니다."
fi
echo "서비스 존재 여부 확인"
if sudo docker service ls --format '{{.Name}}' | grep -Eq "^${SERVICE_NAME}\$"; then
echo "서비스 ${SERVICE_NAME}이(가) 존재합니다. 업데이트를 진행합니다."
echo "Rolling Update 실행 (무중단 배포) - 시간이 소요됩니다"
echo $PW | sudo -S docker service update \
--image "${IMAGE}" \
--update-parallelism "${UPDATE_PARALLELISM}" \
--update-delay "${UPDATE_DELAY}" \
--update-failure-action "${UPDATE_FAILURE_ACTION}" \
--env-add "SPRING_PROFILES_ACTIVE=${PROFILE}" \
--env-add "TZ=${TIME_ZONE}" \
"${SERVICE_NAME}"
echo "Rolling Update 시작됨. 진행 상황 모니터링 중..."
else
echo "서비스 ${SERVICE_NAME}이 존재하지 않습니다. 새로운 서비스를 실행합니다."
echo $PW | sudo -S docker service create \
--name "${SERVICE_NAME}" \
--replicas "${REPLICAS}" \
--publish published=${PORT},target=${APP_PORT} \
--update-parallelism "${UPDATE_PARALLELISM}" \
--update-delay "${UPDATE_DELAY}" \
--update-failure-action "${UPDATE_FAILURE_ACTION}" \
--restart-condition "${RESTART_CONDITION}" \
--restart-max-attempts "${RESTART_MAX_ATTEMPTS}" \
--env "TZ=${TIME_ZONE}" \
--env "SPRING_PROFILES_ACTIVE=${PROFILE}" \
--mount type=bind,source=/etc/localtime,target=/etc/localtime,readonly \
--mount type=bind,source="${MOUNT_DIR}",target=/app \
"${IMAGE}"
echo "서비스 ${SERVICE_NAME} 생성 완료"
fi
echo "배포 상태 확인 중 (최대 120초 대기)..."
for i in $(seq 1 120); do
REPLICAS=$(echo $PW | sudo -S docker service ls --filter name=${SERVICE_NAME} --format "{{.Replicas}}")
echo "현재 상태: ${REPLICAS} (시도 ${i}/120)"
# replicas 가 1/1 이면 성공
if echo "$REPLICAS" | grep -q "1/1"; then
echo "서비스가 정상적으로 실행 중 입니다."
DEPLOY_SUCCESS=1
break
fi
if [ "$i" -gt 30 ] && echo "$REPLICAS" | grep -q "0/1"; then
echo "[경고] 서비스 시작에 실패했을 가능성이 있습니다."
fi
sleep 1
done
echo "배포 결과 확인중..."
if [ "${DEPLOY_SUCCESS:-0}" != "1" ]; then
echo "[오류] 서비스 배포 실패. 서비스 로그를 확인합니다."
# 서비스와 태스크 목록 확인
echo "=== 태스크 목록 ==="
echo $PW | sudo -S docker service ps ${SERVICE_NAME} --no-trunc
# 가장 최근 태스크의 로그 출력
TASK_ID=$(echo $PW | sudo -S docker service ps ${SERVICE_NAME} -q | head -n 1)
if [ -n "$TASK_ID" ]; then
echo "=== 태스크 로그 (최근 200줄) ==="
echo $PW | sudo -S docker logs $(sudo docker inspect --format '{{.Status.ContainerStatus.ContainerID}}' $TASK_ID) --tail=200 || true
fi
exit 1
fi
echo "헬스체크 (최대 120회, 1초간격)"
for i in $(seq 1 120); do
if curl -fsS "http://127.0.0.1:${PORT}/actuator/health" >/dev/null 2>&1 || \
curl -fsS "http://127.0.0.1:${PORT}/healthz" >/dev/null 2>&1 || \
curl -fsS "http://127.0.0.1:${PORT}/" >/dev/null 2>&1 || \
curl -fsS "http://127.0.0.1:${PORT}/docs/swagger-ui/index.html" >/dev/null 2>&1 ; then
echo "헬스체크 성공 (시도 ${i})"
HEALTH_OK=1
break
fi
echo "헬스체크 진행중... (시도 ${i}/120)"
sleep 1
done
if [ "${HEALTH_OK:-0}" != "1" ]; then
echo "[경고] 컨테이너 헬스체크 실패. 서비스는 실행 중."
echo "수동으로 확인이 필요합니다."
fi
# <none> 태그로 남은 이미지 정리
echo "불필요한 dangling(<none>) 이미지 정리..."
echo $PW | sudo -S docker image prune -af
echo "배포가 성공적으로 완료되었습니다."