-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/19 zero downtime #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a6794a2
f46f40b
b3e1a50
b7b026c
7062aad
d1e7b86
0b0786f
c737183
873f7fc
a695163
fa14884
658401e
64bc0f6
89d9bb2
7286d84
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| name: Notify Slack on Pull Request Upload | ||
|
|
||
| on: | ||
| pull_request: | ||
| types: [opened] | ||
|
|
||
| jobs: | ||
| slack_notification: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Send Slack notification | ||
| uses: slackapi/slack-github-action@v1.27.0 | ||
| with: | ||
| payload: | | ||
| { | ||
| "text": ":bell: *New Pull Request!*", | ||
| "blocks": [ | ||
| { | ||
| "type": "section", | ||
| "text": { | ||
| "type": "mrkdwn", | ||
| "text": ":sparkles: *${{ github.actor }}* opened a PR" | ||
| } | ||
| }, | ||
| { | ||
| "type": "section", | ||
| "fields": [ | ||
| { | ||
| "type": "mrkdwn", | ||
| "text": "*Title:*\n${{ github.event.pull_request.title }}" | ||
| }, | ||
| { | ||
| "type": "mrkdwn", | ||
| "text": "*Base Branch:*\n${{ github.base_ref }}" | ||
| }, | ||
| { | ||
| "type": "mrkdwn", | ||
| "text": "*From Branch:*\n${{ github.head_ref }}" | ||
| }, | ||
| { | ||
| "type": "mrkdwn", | ||
| "text": "*Link:*\n<${{ github.event.pull_request.html_url }}|View PR>" | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| env: | ||
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| #!/bin/bash | ||
|
|
||
| APP_NAME=growin-api | ||
| BLUE_PORT=8080 | ||
| GREEN_PORT=8081 | ||
| NGINX_CONF=/home/ubuntu/growin/nginx/default.conf | ||
|
|
||
|
|
||
| echo "deploy start" | ||
|
|
||
| if ! docker ps --format '{{.Names}}' | grep -q "${APP_NAME}-blue" && \ | ||
| ! docker ps --format '{{.Names}}' | grep -q "${APP_NAME}-green"; then | ||
| echo "First deployment detected โ starting blue container..." | ||
| docker compose -f docker/docker-compose.blue.yml up -d | ||
| exit 0 | ||
| fi | ||
|
|
||
|
|
||
| if docker ps --format '{{.Names}}' | grep -q "${APP_NAME}-blue"; then | ||
| CURRENT="blue" | ||
| NEXT="green" | ||
| CURRENT_PORT=$BLUE_PORT | ||
| NEXT_PORT=$GREEN_PORT | ||
| else | ||
| CURRENT="green" | ||
| NEXT="blue" | ||
| CURRENT_PORT=$GREEN_PORT | ||
| NEXT_PORT=$BLUE_PORT | ||
| fi | ||
|
|
||
|
|
||
| ehco "Current Container : $CURRENT" | ||
| echo "Next Container : $NEXT" | ||
|
|
||
| echo "deploy $NEXT Container" | ||
| docker compose -f docker/docker-compose.${NEXT}.yml up -d | ||
|
|
||
|
|
||
| echo "running health check" | ||
| success=false | ||
| for i in {1..20}; do | ||
| sleep 3 | ||
| if curl -fs "http://localhost:${NEXT_PORT}/test/health" | grep -q "UP"; then | ||
| ehco "Health Check Passed" | ||
| success=true | ||
| break | ||
| fi | ||
| echo "Waiting for Service to be UP ... (${i}/10)" | ||
| done | ||
|
|
||
|
|
||
| # ์คํ ์คํจ ์ -> ๋กค๋ฐฑ ์งํ ํ ์ข ๋ฃ | ||
| if [ "$success" = false ]; then | ||
| echo "Health check failed! Rolling back..." | ||
| docker compose -f docker/docker-compose.${NEXT}.yml down | ||
| exit 1 | ||
| fi | ||
|
|
||
|
|
||
| # Reload Nginx | ||
| echo "if success, switch nginx conf and stop old container" | ||
| sudo sed -i "s/${APP_NAME}-${CURRENT}/${APP_NAME}-${NEXT}/" $NGINX_CONF | ||
| sudo sed -i "s/${CURRENT_PORT}/${NEXT_PORT}/" $NGINX_CONF | ||
| sudo docker exec nginx nginx -s reload | ||
|
|
||
| # Stop old container | ||
| echo "==> Stopping old container ${APP_NAME}_${CURRENT}" | ||
| docker stop ${APP_NAME}-${CURRENT} || true | ||
| docker rm ${APP_NAME}-${CURRENT} || true | ||
|
|
||
| echo "Cleaning unused images" | ||
| docker image prune -f >/dev/null 2>&1 | ||
|
|
||
| echo "==============================" | ||
| echo "DEPLOYMENT SUCCESSFUL" | ||
| echo "Active container: ${NEXT}" | ||
| echo "==============================" |
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| version: '3.8' | ||
|
|
||
| services: | ||
| growin-api-blue: | ||
| image: ghcr.io/growin-2025/growin-server:latest | ||
| container_name: growin-api-blue | ||
| ports: | ||
| - "8080:8080" | ||
| env_file: | ||
| - .env | ||
| networks: | ||
| - app-network | ||
|
|
||
| networks: | ||
| app-network: | ||
| external: true |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| version: '3.8' | ||
|
|
||
| services: | ||
| nginx: | ||
| container_name: nginx | ||
| image: nginx:latest | ||
| ports: | ||
| - "80:80" | ||
| - "443:443" | ||
| volumes: | ||
| - ./nginx:/etc/nginx/conf.d | ||
| - ./data/certbot/conf:/etc/letsencrypt | ||
| - ./data/certbot/www:/var/www/certbot | ||
| restart: always | ||
| networks: | ||
| - growin-network | ||
|
|
||
| certbot: | ||
| container_name: certbot | ||
| image: certbot/certbot | ||
| volumes: | ||
| - ./data/certbot/conf:/etc/letsencrypt | ||
| - ./data/certbot/www:/var/www/certbot | ||
| entrypoint: /bin/sh -c "trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;" | ||
| networks: | ||
| - growin-network | ||
|
|
||
| networks: | ||
| app-network: | ||
| name: growin-network | ||
| driver: bridge |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| version: '3.8' | ||
|
|
||
| services: | ||
| growin-api-green: | ||
| image: ghcr.io/growin-2025/growin-server:latest | ||
| container_name: growin-api-green | ||
| ports: | ||
| - "8081:8080" | ||
| env_file: | ||
| - .env | ||
| networks: | ||
| - app-network | ||
|
|
||
| networks: | ||
| app-network: | ||
| external: true |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # HTTP โ HTTPS ๋ฆฌ๋ค์ด๋ ํธ | ||
| server { | ||
| listen 80; | ||
| server_name growinserver.shop; | ||
| server_tokens off; | ||
|
|
||
| location /.well-known/acme-challenge/ { | ||
| root /var/www/certbot; | ||
| allow all; | ||
| } | ||
|
|
||
| location / { | ||
| return 301 https://$host$request_uri; | ||
| } | ||
| } | ||
|
|
||
| # HTTPS ์ค์ | ||
| server { | ||
| listen 443 ssl; | ||
| server_name growinserver.shop; | ||
|
|
||
| ssl_certificate /etc/letsencrypt/live/growinserver.shop/fullchain.pem; | ||
| ssl_certificate_key /etc/letsencrypt/live/growinserver.shop/privkey.pem; | ||
| ssl_protocols TLSv1.2 TLSv1.3; | ||
| ssl_ciphers HIGH:!aNULL:!MD5; | ||
|
|
||
| location / { | ||
| proxy_pass http://growin-api-blue:8080; | ||
| proxy_set_header Host $host; | ||
| proxy_set_header X-Real-IP $remote_addr; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| //package ita.growin.domain.task.controller; | ||
| // | ||
| //import io.swagger.v3.oas.annotations.Operation; | ||
| //import io.swagger.v3.oas.annotations.tags.Tag; | ||
| //import ita.growin.domain.task.dto.CreateTaskRequestDto; | ||
| //import ita.growin.domain.task.dto.UpdateTaskRequestDto; | ||
| //import ita.growin.domain.task.service.TaskService; | ||
| //import ita.growin.global.response.APIResponse; | ||
| //import org.springframework.data.domain.Pageable; | ||
| //import org.springframework.data.domain.Slice; | ||
| //import org.springframework.data.web.PageableDefault; | ||
| //import org.springframework.lang.Nullable; | ||
| //import org.springframework.web.bind.annotation.*; | ||
| // | ||
| //@RestController | ||
| //@RequestMapping("/api/v1/tasks") | ||
| //@Tag(name = "ํ ์ผ", description = "ํ ์ผ ๊ด๋ จ API") | ||
| //public class TaskController { | ||
| // | ||
| // private final TaskService taskService; | ||
| // | ||
| // public TaskController(TaskService taskService) { | ||
| // this.taskService = taskService; | ||
| // } | ||
| // | ||
| // @PostMapping | ||
| // @Operation(summary = "ํ ์ผ ์์ฑ API") | ||
| // public APIResponse<?> createTask( | ||
| // @RequestParam("eventId") @Nullable Long eventId, | ||
| // @RequestBody CreateTaskRequestDto req) { | ||
| // taskService.createTask(eventId, req); | ||
| // return APIResponse.success(); | ||
| // } | ||
| // | ||
| // @PatchMapping("/{taskId}") | ||
| // @Operation(summary = "ํ ์ผ ์์ API") | ||
| // public APIResponse<?> updateTask( | ||
| // @PathVariable Long taskId, | ||
| // @RequestBody UpdateTaskRequestDto req) { | ||
| // taskService.updateTask(taskId, req); | ||
| // return APIResponse.success(); | ||
| // } | ||
| // | ||
| // @DeleteMapping("/{taskId}") | ||
| // @Operation(summary = "ํ ์ผ ์ญ์ API") | ||
| // public APIResponse<?> deleteTask( | ||
| // @RequestParam("eventId") @Nullable Long eventId, | ||
| // @PathVariable Long taskId) { | ||
| // taskService.deleteTask(eventId, taskId); | ||
| // return APIResponse.success(); | ||
| // } | ||
| // | ||
| // @GetMapping("/{taskId}") | ||
| // @Operation(summary = "ํ ์ผ ๋จ๊ฑด ์กฐํ") | ||
| // public APIResponse<?> getTask(@PathVariable Long taskId) { | ||
| // return APIResponse.success(taskService.getTask(taskId)); | ||
| // } | ||
| // | ||
| // @GetMapping("/event/{eventId}/tasks") | ||
| // @Operation(summary = "์ผ์ ๋ด ํ ์ผ ์กฐํ") | ||
| // public APIResponse<Slice<?>> getTasks( | ||
| // @PathVariable Long eventId, | ||
| // @PageableDefault(size = 20) Pageable pageable) { | ||
| // return APIResponse.success(taskService.getTasks(eventId, pageable)); | ||
| // } | ||
| // | ||
| // @GetMapping("/tasks/today") | ||
| // @Operation(summary = "์ค๋ ํ ์ผ ์กฐํ") | ||
| // public APIResponse<?> getTodayTasks( | ||
| // @PageableDefault(size = 20) Pageable pageable | ||
| // ) { | ||
| // return APIResponse.success(taskService.getTodayTasks(pageable)); | ||
| // } | ||
| // | ||
| // @GetMapping("/tasks/someday") | ||
| // @Operation(summary = "์ธ์ ๊ฐ ํ ์ผ ์กฐํ") | ||
| // public APIResponse<?> getSomedayTasks( | ||
| // @PageableDefault(size = 20) Pageable pageable | ||
| // ) { | ||
| // return APIResponse.success(taskService.getSomedayTasks(pageable)); | ||
| // } | ||
| //} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package ita.growin.global.config; | ||
|
|
||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.security.config.annotation.web.builders.HttpSecurity; | ||
| import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; | ||
| import org.springframework.security.web.SecurityFilterChain; | ||
|
|
||
| @Configuration | ||
| public class SecurityConfig { | ||
|
|
||
| @Bean | ||
| public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { | ||
| http.csrf(AbstractHttpConfigurer::disable) | ||
| .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) | ||
| .formLogin(AbstractHttpConfigurer::disable) | ||
| .httpBasic(AbstractHttpConfigurer::disable); | ||
| return http.build(); | ||
| } | ||
|
Comment on lines
+12
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐งฉ Analysis chainํ๋ก๋์ ํ๊ฒฝ ๋ณด์ ์ค์ ํ์ ํ์ฌ ๋ชจ๋ ์์ฒญ์ ๋ํด ์ธ์ฆ ์์ด ์ ๊ทผ์ ํ์ฉํ๊ณ ์์ต๋๋ค(
ํ์ฌ ๊ตฌ์ฑ์ด ๊ฐ๋ฐ ๋จ๊ณ์ฉ ์์ ์ค์ ์ธ์ง, ํ๋ก๋์ ๋ฐฐํฌ ์ ๋ณด์ ๊ฐํ ๊ณํ์ด ์๋์ง ํ์ธํ์ธ์. SwaggerConfig์์ BearerAuth๊ฐ ์ ์๋์ด ์์ผ๋ฏ๋ก, ํฅํ JWT ๊ธฐ๋ฐ ์ธ์ฆ ๊ตฌํ์ ๊ณํ ์ค์ธ ๊ฒ์ผ๋ก ๋ณด์ ๋๋ค. Length of output: 60 ๐ Script executed: #!/bin/bash
# Find and examine SwaggerConfig or similar configuration files
find . -name "*Swagger*" -o -name "*swagger*" 2>/dev/null | grep -E "\.(java|yml|yaml)$"Length of output: 59 ๐ Script executed: #!/bin/bash
# Search for TODO/FIXME comments related to security or auth
rg -n -i "(TODO|FIXME).*(security|auth|jwt|bearer)" --type javaLength of output: 0 ๐ Script executed: #!/bin/bash
# Check for application properties/yml files with profiles
find . -name "application*.properties" -o -name "application*.yml" -o -name "application*.yaml" 2>/dev/nullLength of output: 166 ๐ Script executed: #!/bin/bash
# Examine the full SecurityConfig.java file
find . -name "SecurityConfig.java" -exec cat {} \;Length of output: 816 Length of output: 1619 ๐ Script executed: #!/bin/bash
# Examine main application.yaml
cat ./src/main/resources/application.yamlLength of output: 120 ๐ Script executed: #!/bin/bash
# Search for JWT, Bearer, Authentication related code
rg -n -i "(jwt|bearer|authentication|authoriz)" --type java -C 2Length of output: 2955 ๐ Script executed: #!/bin/bash
# Search for @ConditionalOnProfile or profile-conditional configurations
rg -n "@ConditionalOnProfile\|@Profile" --type javaLength of output: 0 ํ๋ก๋์ ํ๊ฒฝ ๋ณด์ ์ค์ ํ์ ๊ตฌํ ํ์ฌ SecurityConfig์์ ๋ชจ๋ ์์ฒญ์ ์ธ์ฆ ์์ด ์ ๊ทผ์ ํ์ฉํ๊ณ ์์ต๋๋ค( ํ์ธ๋ ๋ฌธ์ :
ํ์ ์กฐ์น:
SwaggerConfig์ BearerAuth ์ ์์ CommonErrorCode์ UNAUTHORIZED/FORBIDDEN ์ฝ๋๋ฅผ ๊ณ ๋ คํ๋ฉด JWT ์ธ์ฆ ๊ตฌํ์ด ๊ณํ ์ค์ธ ๊ฒ์ผ๋ก ๋ณด์ ๋๋ค. ๊ตฌํ ์ผ์ ๊ณผ ๊ณํ์ ๋ช ํํ ํ๊ณ , ๋ฐฐํฌ ์ ์ ๋ฐ๋์ ๋ณด์ ๊ฐํ๋ฅผ ์๋ฃํ์ธ์. ๐ค Prompt for AI Agents |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
์ฃผ์ ์ฒ๋ฆฌ๋ ์ฝ๋ ์ ๋ฆฌ ํ์
์ ์ฒด ์ปจํธ๋กค๋ฌ๊ฐ ์ฃผ์ ์ฒ๋ฆฌ๋์ด ์์ต๋๋ค. ์ด๋ ๋ค์ ์ค ํ๋๋ฅผ ์๋ฏธํ ์ ์์ต๋๋ค:
์ด ํ์ผ์ด PR์ ํฌํจ๋ ์๋๋ฅผ ๋ช ํํ ํด์ฃผ์ธ์:
ํ์ํ๋ค๋ฉด ์ด์ ์์ฑ์ ๋์๋๋ฆด ์ ์์ต๋๋ค.
๐ค Prompt for AI Agents