diff --git a/.github/workflows/backend_cd.yml b/.github/workflows/backend_cd.yml index da2e3e8f..aa366b7b 100644 --- a/.github/workflows/backend_cd.yml +++ b/.github/workflows/backend_cd.yml @@ -1,4 +1,4 @@ -name: Backend CD +name: Backend None Stop CD on: workflow_dispatch: @@ -9,6 +9,18 @@ on: - backend/** jobs: + find_previous_image_version: + name: 🔎Find Previous Docker Image Version + runs-on: [ self-hosted, devel-up-prod-a ] + outputs: + previousImageVersion: ${{steps.find_version.outputs.name}} + steps: + - id: find_version + run: | + PREVIOUS_IMAGE_NAME=$(docker ps --format "{{.Image}}") + echo "name=$PREVIOUS_IMAGE_NAME" >> $GITHUB_OUTPUT + echo $PREVIOUS_IMAGE_NAME + build: name: 🏗️ Build Jar and Upload Docker Image runs-on: ubuntu-latest @@ -48,10 +60,10 @@ jobs: tags: ${{ secrets.DOCKER_REPOSITORY_NAME }}:${{ github.sha }} platforms: linux/arm64 - deploy: - name: 🚀 Server Deployment + deploy_to_a: + name: 🚀 Server A Deployment needs: build - runs-on: [ self-hosted, develup ] + runs-on: [ self-hosted, devel-up-prod-a ] defaults: run: working-directory: backend @@ -71,20 +83,100 @@ jobs: - name: 🐳 Docker Compose up run: docker compose -f compose.yml up -d - - name: 🐳 Clean Unused Image - run: docker image prune -af + health_check_a: + name: 🙏 Server A Health Check + needs: deploy_to_a + defaults: + run: + working-directory: backend + runs-on: [ self-hosted, devel-up-prod-a ] + steps: + - name: ♻️ Send Helth Check Request + run: chmod u+x ./scripts/healthcheck.sh && ./scripts/healthcheck.sh + + roll-back_a: + name: 🚀 Server A RollBack + needs: [health_check_a, find_previous_image_version] + if: failure() + runs-on: [ self-hosted, devel-up-prod-a ] + defaults: + run: + working-directory: backend + + env: + BACKEND_APP_IMAGE_NAME: ${{ needs.find_previous_image_version.outputs.previousImageVersion }} + + steps: + - uses: actions/checkout@v4 - slack-notify_success: + - name: 🐳 Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: 🐳 Docker Compose up + run: docker compose -f compose.yml up -d + + health_check_a_after_roll-back: + name: 🙏 Server A Health Check After RollBack + needs: roll-back_a + if : ${{always() && needs.roll-back_a.result != 'skipped'}} + defaults: + run: + working-directory: backend + runs-on: [ self-hosted, devel-up-prod-a ] + steps: + - name: ♻️ Send Helth Check Request + run: chmod u+x ./scripts/healthcheck.sh && ./scripts/healthcheck.sh + + deploy_to_b: + name: 🚀 Server B Deployment + needs: health_check_a + if: ${{needs.health_check_a.result == 'success'}} + runs-on: [ self-hosted, devel-up-prod-b ] + defaults: + run: + working-directory: backend + + env: + BACKEND_APP_IMAGE_NAME: ${{ secrets.DOCKER_REPOSITORY_NAME }}:${{ github.sha }} + + steps: + - uses: actions/checkout@v4 + + - name: 🐳 Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: 🐳 Docker Compose up + run: docker compose -f compose.yml up -d + + health_check_b: + name: 🙏 Server B Health Check + needs: deploy_to_b + defaults: + run: + working-directory: backend + runs-on: [ self-hosted, devel-up-prod-b ] + steps: + - name: ♻️ Send Helth Check Request + run: chmod u+x ./scripts/healthcheck.sh && ./scripts/healthcheck.sh + + deploy_success_notify: + name: 📢Send Deploy Success Notification runs-on: ubuntu-latest needs: - - build - - deploy - if: success() + - health_check_a + - health_check_b + if: ${{needs.health_check_a.result == 'success' && needs.health_check_b.result == 'success'}} steps: - name: Extract Commit Title run: | COMMIT_TITLE=$(echo "${{ github.event.head_commit.message }}" | head -n 1) - echo "COMMIT_TITLE=$COMMIT_TITLE" >> $GITHUB_ENV + echo "COMMIT_TITLE=$COMMIT_TITLE" >> $GITHUB_ENV - name: Build and Deploy Success uses: slackapi/slack-github-action@v1.24.0 @@ -106,18 +198,19 @@ jobs: env: SLACK_BOT_TOKEN: ${{ secrets.BOT_TOKEN }} - slack-notify_build-fail: + deploy_to_b_fail_notify: + name: 📢Send Deploy To Server B Fail Notification runs-on: ubuntu-latest needs: - - build - if: failure() + - health_check_b + if: ${{failure() && needs.health_check_b.result == 'failure'}} steps: - name: Extract Commit Title run: | COMMIT_TITLE=$(echo "${{ github.event.head_commit.message }}" | head -n 1) - echo "COMMIT_TITLE=$COMMIT_TITLE" >> $GITHUB_ENV + echo "COMMIT_TITLE=$COMMIT_TITLE" >> $GITHUB_ENV - - name: Build Fail + - name: Build and Deploy Success uses: slackapi/slack-github-action@v1.24.0 with: channel-id: ${{ secrets.ISSUE_CHANNEL }} @@ -129,7 +222,7 @@ jobs: "type": "section", "text": { "type": "mrkdwn", - "text": " \n 📣 Server Build & Deploy 결과를 안내 드립니다. 📣 \n\t • 🔴 Build Fail \n\t • 🏷️ 관련 Commit: <${{ github.event.head_commit.url }}|${{ env.COMMIT_TITLE }}>" + "text": " \n 📣 Server Build & Deploy 결과를 안내 드립니다. 📣 \n\t • 🚀 Build Success \n\t • 🔴 Server B Deploy Fail \n\t • 🏷️ 관련 Commit: <${{ github.event.head_commit.url }}|${{ env.COMMIT_TITLE }}>" } } ] @@ -137,18 +230,51 @@ jobs: env: SLACK_BOT_TOKEN: ${{ secrets.BOT_TOKEN }} - slack-notify_deploy-fail: + roll-back_a_success_notifiy: + name: 📢Send Server A RollBack Success Notification runs-on: ubuntu-latest needs: - - deploy - if: failure() + - health_check_a_after_roll-back + if: ${{failure() && needs.health_check_a_after_roll-back.result == 'success'}} + steps: + - name: Extract Commit Title + run: | + COMMIT_TITLE=$(echo "${{ github.event.head_commit.message }}" | head -n 1) + echo "COMMIT_TITLE=$COMMIT_TITLE" >> $GITHUB_ENV + + - name: Build and Deploy Success + uses: slackapi/slack-github-action@v1.24.0 + with: + channel-id: ${{ secrets.ISSUE_CHANNEL }} + payload: | + { + "text": "Build and Deploy Status", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": " \n 📣 Server Build & Deploy 결과를 안내 드립니다. 📣 \n\t • 🚀 Build Success \n\t • 🟠 Server A RollBack Success \n\t • 🏷️ 관련 Commit: <${{ github.event.head_commit.url }}|${{ env.COMMIT_TITLE }}>" + } + } + ] + } + env: + SLACK_BOT_TOKEN: ${{ secrets.BOT_TOKEN }} + + roll-back_a_fail_notifiy: + name: 📢Send Server A RollBack Fail Notification + runs-on: ubuntu-latest + needs: + - health_check_a_after_roll-back + if: ${{failure() && needs.health_check_a_after_roll-back.result == 'failure'}} steps: - name: Extract Commit Title run: | COMMIT_TITLE=$(echo "${{ github.event.head_commit.message }}" | head -n 1) - echo "COMMIT_TITLE=$COMMIT_TITLE" >> $GITHUB_ENV + echo "COMMIT_TITLE=$COMMIT_TITLE" >> $GITHUB_ENV - - name: Deploy Fail + - name: Build and Deploy Success uses: slackapi/slack-github-action@v1.24.0 with: channel-id: ${{ secrets.ISSUE_CHANNEL }} @@ -160,7 +286,7 @@ jobs: "type": "section", "text": { "type": "mrkdwn", - "text": " \n 📣 Server Build & Deploy 결과를 안내 드립니다. 📣 \n\t • 🚀Build Success \n\t • 🔴Deploy Fail \n\t • 🏷️ 관련 Commit: <${{ github.event.head_commit.url }}|${{ env.COMMIT_TITLE }}>" + "text": " \n 📣 Server Build & Deploy 결과를 안내 드립니다. 📣 \n\t • 🚀 Build Success \n\t • 🔴Server A RollBack Fail \n\t • 🏷️ 관련 Commit: <${{ github.event.head_commit.url }}|${{ env.COMMIT_TITLE }}>" } } ] diff --git a/backend/compose.yml b/backend/compose.yml index 8ca024d1..46da50de 100644 --- a/backend/compose.yml +++ b/backend/compose.yml @@ -1,30 +1,11 @@ services: - nginx: - image: nginx - depends_on: - - application - networks: - - nginx-app-net - ports: - - "80:80" - - "443:443" - volumes: - - /home/ubuntu/custom.conf:/etc/nginx/conf.d/default.conf - - /etc/letsencrypt/live/api.devel-up.co.kr/fullchain.pem:/etc/letsencrypt/live/api.devel-up.co.kr/fullchain.pem - - /etc/letsencrypt/live/api.devel-up.co.kr/privkey.pem:/etc/letsencrypt/live/api.devel-up.co.kr/privkey.pem - application: image: ${BACKEND_APP_IMAGE_NAME} - networks: - - nginx-app-net ports: - - "8080:8080" + - "80:8080" - "8082:8082" environment: TZ: "Asia/Seoul" - SPRING_PROFILE: prod + SPRING_PROFILE: local restart: always container_name: develup-app - -networks: - nginx-app-net: diff --git a/backend/scripts/healthcheck.sh b/backend/scripts/healthcheck.sh new file mode 100644 index 00000000..6085457c --- /dev/null +++ b/backend/scripts/healthcheck.sh @@ -0,0 +1,12 @@ +for i in {1..5}; do +response=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/health) +echo "Attempt $i: Response Code $response" + +if [ "$i" -eq 5 ] && [ "$response" -ne 200 ]; then +echo "Health check failed after 10 attempts." +exit 1 +fi + +sleep 5 +done +echo "Health check passed." diff --git a/backend/src/main/java/develup/api/HealthApi.java b/backend/src/main/java/develup/api/HealthApi.java new file mode 100644 index 00000000..51468aa3 --- /dev/null +++ b/backend/src/main/java/develup/api/HealthApi.java @@ -0,0 +1,15 @@ +package develup.api; + +import develup.api.common.ApiResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HealthApi { + + @GetMapping("/health") + public ResponseEntity> health() { + return ResponseEntity.ok(new ApiResponse<>("up")); + } +} diff --git a/backend/src/test/java/develup/api/HealthApiTest.java b/backend/src/test/java/develup/api/HealthApiTest.java new file mode 100644 index 00000000..68b1e529 --- /dev/null +++ b/backend/src/test/java/develup/api/HealthApiTest.java @@ -0,0 +1,19 @@ +package develup.api; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class HealthApiTest extends ApiTestSupport { + + @Test + @DisplayName("성공 응답을 반환한다") + void health() throws Exception { + mockMvc.perform(get("/health")) + .andDo(print()) + .andExpect(status().isOk()); + } +}