Spring_Security_기본_설정 : feat : [redis모듈] Redis 관련 환경설정 및 Properties 추… #16
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 "배포가 성공적으로 완료되었습니다." |