Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .github/workflows/prod-cicd.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
name: prod

on:
workflow_dispatch:
pull_request:
branches:
- main
push:
branches:
- main
Comment on lines +4 to +9
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for concurrency settings and triggers in the prod-cicd workflow
echo "=== Checking for concurrency settings ==="
rg -nP '^\s*concurrency\s*:' .github/workflows/prod-cicd.yaml || echo "No concurrency block found"

echo -e "\n=== Checking for trigger configuration ==="
rg -nP '^\s*(pull_request|push|on):' .github/workflows/prod-cicd.yaml

echo -e "\n=== Full workflow file content ==="
cat -n .github/workflows/prod-cicd.yaml

Repository: souzip/souzip-api

Length of output: 5432


동시 배포 실행 제어가 누락되었습니다.

push 트리거로 연속 발생 시 deploy 작업이 동시에 실행되어 blue/green 배포와 nginx 설정에서 경합이 발생할 수 있습니다. 배포를 직렬화하기 위해 워크플로우 레벨에 concurrency 설정이 필요합니다.

🔧 제안 수정안
on:
  pull_request:
    branches:
      - main
  push:
    branches:
      - main

+concurrency:
+  group: prod-deploy
+  cancel-in-progress: false
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/prod-cicd.yaml around lines 4 - 9, The workflow is missing
a workflow-level concurrency setting, so multiple pushes can trigger overlapping
runs and cause concurrent execution of the deploy job (risking blue/green and
nginx race conditions); add a top-level concurrency stanza to the workflow
(e.g., concurrency: { group: "deploy-${{ github.ref }}", cancel-in-progress:
true }) to serialize runs for the same branch/ref and ensure the existing deploy
job is executed one at a time.


env:
IMAGE_NAME: asia-northeast3-docker.pkg.dev/souzip-488211/souzip-prod-repo/souzip-api:latest
Expand Down Expand Up @@ -119,6 +124,11 @@ jobs:
PROD_API_DOCS_URL=${{ secrets.PROD_API_DOCS_URL }}
ADMIN_INITIAL_USERNAME=${{ secrets.ADMIN_INITIAL_USERNAME }}
ADMIN_INITIAL_PASSWORD=${{ secrets.ADMIN_INITIAL_PASSWORD }}
NCP_ENDPOINT=${{ secrets.NCP_ENDPOINT }}
NCP_REGION=${{ secrets.NCP_REGION }}
NCP_BUCKET=${{ secrets.NCP_BUCKET }}
NCP_ACCESS_KEY=${{ secrets.NCP_ACCESS_KEY }}
NCP_SECRET_KEY=${{ secrets.NCP_SECRET_KEY }}
EOF

echo "DEPLOYER=${{ github.actor }}" >> deploy/prod/.env
Expand Down
131 changes: 131 additions & 0 deletions deploy/prod/scripts/deploy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#!/bin/bash
set -e

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

REGISTRY="asia-northeast3-docker.pkg.dev/souzip-488211/souzip-prod-repo/souzip-api"

WORK_DIR="/home/souzip-prod/souzip"
DEPLOY_DIR="$WORK_DIR/deploy/prod"

Comment on lines +11 to +13
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, verify the hardcoded WORK_DIR in the deploy script
cat -n deploy/prod/scripts/deploy.sh | head -20

Repository: souzip/souzip-api

Length of output: 648


🏁 Script executed:

# Search for workflow files that might use GCP_PROD_USER
find .github -name "*.yaml" -o -name "*.yml" | head -10

Repository: souzip/souzip-api

Length of output: 125


🏁 Script executed:

# Look for references to the hardcoded path in other files
rg "souzip-prod" --type sh --type yaml

Repository: souzip/souzip-api

Length of output: 497


🏁 Script executed:

cat -n .github/workflows/prod-cicd.yaml | grep -A 5 -B 5 "GCP_PROD_USER\|WORK_DIR\|/home"

Repository: souzip/souzip-api

Length of output: 1121


🏁 Script executed:

# Check for any secrets usage in the workflow
cat .github/workflows/prod-cicd.yaml | head -100

Repository: souzip/souzip-api

Length of output: 2442


워크플로우 경로와 스크립트 경로 불일치로 배포 실패 위험이 있습니다.

워크플로우는 /home/${{ secrets.GCP_PROD_USER }}/souzip으로 파일을 배포(line 97, 109)하지만, deploy.shWORK_DIR/home/souzip-prod/souzip로 고정되어 있습니다(line 11). GCP_PROD_USER 값이 souzip-prod와 다르면 스크립트가 파일을 찾지 못해 배포가 실패합니다.

상대 경로 기반으로 변경하여 스크립트 위치에 따라 동적으로 경로를 결정하는 것을 권장합니다.

🔧 제안 수정안
--- a/deploy/prod/scripts/deploy.sh
+++ b/deploy/prod/scripts/deploy.sh
@@
-WORK_DIR="/home/souzip-prod/souzip"
-DEPLOY_DIR="$WORK_DIR/deploy/prod"
+SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
+DEPLOY_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+WORK_DIR="$(cd "$DEPLOY_DIR/../.." && pwd)"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@deploy/prod/scripts/deploy.sh` around lines 11 - 13, The hardcoded WORK_DIR
(and DEPLOY_DIR) in deploy.sh causes mismatches with the workflow's /home/${{
secrets.GCP_PROD_USER }}/souzip path; change deploy.sh to compute paths relative
to the script location instead of a fixed user folder by deriving the script
directory (e.g., from $0) and setting WORK_DIR based on that directory, then set
DEPLOY_DIR relative to WORK_DIR so the script works regardless of GCP_PROD_USER
or the home path.

BLUE_COMPOSE="docker-compose.blue.yaml"
GREEN_COMPOSE="docker-compose.green.yaml"

BLUE_PROJECT="souzip-blue"
GREEN_PROJECT="souzip-green"

NGINX_UPSTREAM_FILE="/etc/nginx/conf.d/upstream-souzip.conf"

BLUE_PORT=8081
GREEN_PORT=8082

MAX_RETRY=6
RETRY_INTERVAL=10

cd "$WORK_DIR" || exit 1

echo -e "${YELLOW}[1/7] 최신 이미지 다운로드${NC}"

docker pull ${REGISTRY}:latest

NEW_IMAGE=$(docker images ${REGISTRY}:latest -q)
Comment on lines +9 to +34
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# latest 태그 의존 지점 확인
echo "=== Checking deploy.sh ==="
cat -n deploy/prod/scripts/deploy.sh | sed -n '1,50p'

echo -e "\n=== Searching for :latest tags in deployment files ==="
rg -n ':latest' deploy/prod/ .github/workflows/ 2>/dev/null | head -30

Repository: souzip/souzip-api

Length of output: 2102


🏁 Script executed:

#!/bin/bash
# Check how docker-compose files are used and if they reference the pulled image
echo "=== Checking docker-compose usage in deploy.sh ==="
cat -n deploy/prod/scripts/deploy.sh | grep -A 20 -B 5 "docker-compose"

echo -e "\n=== Checking docker-compose.blue.yaml content ==="
head -20 deploy/prod/docker-compose.blue.yaml

echo -e "\n=== Checking how NEW_IMAGE is used after being set ==="
cat -n deploy/prod/scripts/deploy.sh | grep -A 15 "NEW_IMAGE="

Repository: souzip/souzip-api

Length of output: 1788


🏁 Script executed:

#!/bin/bash
# Check how docker-compose files are invoked and if NEW_IMAGE is used
echo "=== Searching for docker-compose invocations in deploy.sh ==="
rg -n "docker-compose" deploy/prod/scripts/deploy.sh

echo -e "\n=== Full deploy.sh to see complete flow ==="
wc -l deploy/prod/scripts/deploy.sh

Repository: souzip/souzip-api

Length of output: 289


🏁 Script executed:

#!/bin/bash
# Get the full deploy.sh to understand the complete deployment flow
cat -n deploy/prod/scripts/deploy.sh

Repository: souzip/souzip-api

Length of output: 3856


프로덕션 배포에서 :latest 태그 사용으로 인한 재현성 및 롤백 추적성 저하

배포 스크립트(32, 34번 줄)뿐만 아니라 docker-compose 파일들(blue.yaml, green.yaml 5번 줄)에서도 :latest 태그가 하드코딩되어 있어, 동일한 이미지 버전을 보장하지 못합니다. 특히 82-83번 줄의 docker compose pull은 compose 파일의 :latest 정의를 따릅니다.

commit SHA 또는 semantic version 같은 immutable 태그로 고정하되, 다음을 함께 변경해야 합니다:

  • deploy.sh에서 IMAGE_TAG 변수화
  • docker-compose.blue.yaml, docker-compose.green.yaml에서 image 필드 수정
  • 필요시 CI/CD 워크플로우에서 태그를 동적으로 주입
🔧 제안 수정안
--- a/deploy/prod/scripts/deploy.sh
+++ b/deploy/prod/scripts/deploy.sh
@@ -1,6 +1,7 @@
 #!/bin/bash
 set -e
 
+IMAGE_TAG="${IMAGE_TAG:-latest}"
 RED='\033[0;31m'
 GREEN='\033[0;32m'
 YELLOW='\033[1;33m'
@@ -29,11 +30,11 @@ cd "$WORK_DIR" || exit 1
 
 echo -e "${YELLOW}[1/7] 최신 이미지 다운로드${NC}"
 
-docker pull ${REGISTRY}:latest
+docker pull "${REGISTRY}:${IMAGE_TAG}"
 
-NEW_IMAGE=$(docker images ${REGISTRY}:latest -q)
+NEW_IMAGE=$(docker images "${REGISTRY}:${IMAGE_TAG}" -q)
--- a/deploy/prod/docker-compose.blue.yaml
+++ b/deploy/prod/docker-compose.blue.yaml
@@ -2,7 +2,7 @@ version: "3.8"
 
 services:
   backend:
-    image: asia-northeast3-docker.pkg.dev/souzip-488211/souzip-prod-repo/souzip-api:latest
+    image: asia-northeast3-docker.pkg.dev/souzip-488211/souzip-prod-repo/souzip-api:${IMAGE_TAG:-latest}
     container_name: souzip-api-blue
--- a/deploy/prod/docker-compose.green.yaml
+++ b/deploy/prod/docker-compose.green.yaml
@@ -2,7 +2,7 @@ version: "3.8"
 
 services:
   backend:
-    image: asia-northeast3-docker.pkg.dev/souzip-488211/souzip-prod-repo/souzip-api:latest
+    image: asia-northeast3-docker.pkg.dev/souzip-488211/souzip-prod-repo/souzip-api:${IMAGE_TAG:-latest}
     container_name: souzip-api-green
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@deploy/prod/scripts/deploy.sh` around lines 9 - 34, The script currently
hardcodes :latest which breaks reproducible deploys; introduce an IMAGE_TAG
variable (default from env or CI) and replace all uses of :latest so docker pull
${REGISTRY}:${IMAGE_TAG} is used, update NEW_IMAGE to query
${REGISTRY}:${IMAGE_TAG}, and change docker-compose.blue.yaml and
docker-compose.green.yaml image fields to reference the IMAGE_TAG (e.g., use
environment substitution or a template placeholder) so docker compose pull /
docker compose up will use the immutable tag; ensure CI injects the IMAGE_TAG
(commit SHA or semver) and the deploy.sh and any docker compose pull commands
consume that variable (symbols to change: IMAGE_TAG, REGISTRY, NEW_IMAGE, docker
pull invocation, docker-compose.blue.yaml and docker-compose.green.yaml image
fields, and any docker compose pull calls).


if [ -z "${NEW_IMAGE:-}" ]; then
echo -e "${RED}[ERROR] 이미지 다운로드 실패${NC}"
exit 1
fi

echo -e "${GREEN}[SUCCESS] 새 이미지: ${NEW_IMAGE}${NC}"

cd "$DEPLOY_DIR" || exit 1

[ -f "$NGINX_UPSTREAM_FILE" ] || {
echo -e "${RED}[ERROR] upstream 파일 없음${NC}"
exit 1
Comment on lines +45 to +47

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 최초 배포에서 upstream 파일 부재를 허용하라

현재 스크립트는 upstream-souzip.conf가 없으면 즉시 종료하기 때문에, 바로 아래에 있는 "최초 배포(upstream 없음)" 분기로 도달하지 못합니다. 신규 서버 프로비저닝 직후나 nginx 설정 파일이 아직 생성되지 않은 환경에서는 blue/green 첫 배포가 항상 실패해 배포 파이프라인이 막히므로, 파일이 없을 때 기본 upstream 파일을 생성하거나 초기값으로 진행할 수 있게 처리해야 합니다.

Useful? React with 👍 / 👎.

}

echo -e "${YELLOW}[2/7] 현재 active 포트 확인${NC}"

CURRENT_PORT=$(grep -oE '127\.0\.0\.1:[0-9]+' "$NGINX_UPSTREAM_FILE" | cut -d: -f2 | head -n 1 || true)

if [ -z "$CURRENT_PORT" ]; then
echo -e "${YELLOW}[WARN] upstream 없음 → 최초 배포 (blue=8081)${NC}"
CURRENT_PORT="none"
fi

if [ "$CURRENT_PORT" == "$BLUE_PORT" ]; then
TARGET="green"
TARGET_PORT=$GREEN_PORT
TARGET_PROJECT=$GREEN_PROJECT
TARGET_COMPOSE=$GREEN_COMPOSE

STOP_PROJECT=$BLUE_PROJECT
STOP_COMPOSE=$BLUE_COMPOSE

else
TARGET="blue"
TARGET_PORT=$BLUE_PORT
TARGET_PROJECT=$BLUE_PROJECT
TARGET_COMPOSE=$BLUE_COMPOSE

STOP_PROJECT=$GREEN_PROJECT
STOP_COMPOSE=$GREEN_COMPOSE
fi

echo -e "${GREEN}[INFO] 현재:$CURRENT_PORT → 배포:$TARGET($TARGET_PORT)${NC}"

echo -e "${YELLOW}[3/7] $TARGET 컨테이너 실행${NC}"

docker compose -p "$TARGET_PROJECT" -f "$TARGET_COMPOSE" pull
docker compose -p "$TARGET_PROJECT" -f "$TARGET_COMPOSE" up -d

echo -e "${YELLOW}[4/7] 헬스체크 시작${NC}"

RETRY_COUNT=0
HEALTH_OK=false

while [ $RETRY_COUNT -lt $MAX_RETRY ]; do

if curl -f -s --max-time 5 "http://localhost:${TARGET_PORT}/actuator/health" > /dev/null; then
echo -e "${GREEN}[SUCCESS] 헬스체크 성공${NC}"
HEALTH_OK=true
break
else
RETRY_COUNT=$((RETRY_COUNT + 1))
echo -e "${YELLOW}[RETRY] ${RETRY_COUNT}/${MAX_RETRY}${NC}"
sleep $RETRY_INTERVAL
fi

done

if [ "$HEALTH_OK" = false ]; then
echo -e "${RED}[ERROR] 헬스체크 실패${NC}"
docker compose -p "$TARGET_PROJECT" -f "$TARGET_COMPOSE" down
exit 1
fi

echo -e "${YELLOW}[5/7] nginx upstream 전환${NC}"

sudo tee "$NGINX_UPSTREAM_FILE" > /dev/null <<EOF
upstream souzip {
server 127.0.0.1:${TARGET_PORT};
}
EOF

sudo nginx -t
sudo nginx -s reload

echo -e "${GREEN}[SUCCESS] nginx 전환 완료${NC}"

echo -e "${YELLOW}[6/7] 이전 컨테이너 종료${NC}"

docker compose -p "$STOP_PROJECT" -f "$STOP_COMPOSE" down || true

echo -e "${YELLOW}[7/7] 이미지 정리${NC}"

docker image prune -f || true

echo -e "${GREEN}[DEPLOY SUCCESS] 완료${NC}"
4 changes: 4 additions & 0 deletions src/main/resources/application-prod.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
spring:
docker:
compose:
enabled: false

datasource:
url: ${PROD_DB_URL}
username: ${PROD_POSTGRES_USER}
Expand Down
10 changes: 5 additions & 5 deletions src/main/resources/db/migration/V1__init_schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
-- Name: cdb_admin; Type: SCHEMA; Schema: -; Owner: postgres
--

CREATE SCHEMA cdb_admin;
CREATE SCHEMA IF NOT EXISTS cdb_admin;


ALTER SCHEMA cdb_admin OWNER TO postgres;
-- ALTER SCHEMA cdb_admin OWNER TO postgres;

--
-- Name: public; Type: SCHEMA; Schema: -; Owner: postgres
Expand All @@ -23,20 +23,20 @@ ALTER SCHEMA cdb_admin OWNER TO postgres;
-- *not* creating schema, since initdb creates it


ALTER SCHEMA public OWNER TO postgres;
-- ALTER SCHEMA public OWNER TO postgres;

--
-- Name: postgis; Type: EXTENSION; Schema: -; Owner: -
--

CREATE EXTENSION IF NOT EXISTS postgis WITH SCHEMA cdb_admin;
-- CREATE EXTENSION IF NOT EXISTS postgis WITH SCHEMA cdb_admin;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Flyway에서 PostGIS extension 생성을 보장하라

이 변경으로 V1 마이그레이션이 postgis extension을 생성하지 않게 되었는데, 애플리케이션은 거리 검색에서 ST_DWithin/ST_Distance 같은 PostGIS 함수를 사용합니다(SouvenirRepository native query). 따라서 새 DB를 V1부터 올리는 환경(신규 prod/stage 또는 ephemeral test DB)에서는 extension이 사전 설치되지 않으면 해당 기능이 function does not exist로 런타임 실패할 수 있어, 마이그레이션에서 extension 보장을 유지하거나 별도 선행 마이그레이션으로 명확히 분리해야 합니다.

Useful? React with 👍 / 👎.



--
-- Name: EXTENSION postgis; Type: COMMENT; Schema: -; Owner:
--

COMMENT ON EXTENSION postgis IS 'PostGIS geometry and geography spatial types and functions';
-- COMMENT ON EXTENSION postgis IS 'PostGIS geometry and geography spatial types and functions';
Comment on lines +32 to +39
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the migration file content around lines 32-39
echo "=== Migration file content (lines 25-45) ==="
sed -n '25,45p' src/main/resources/db/migration/V1__init_schema.sql

echo -e "\n=== Check SouvenirRepository for ST_DWithin/ST_Distance usage ==="
rg -n 'ST_DWithin|ST_Distance|ST_SetSRID|ST_MakePoint' src/main/java/com/souzip/domain/souvenir/repository/SouvenirRepository.java

echo -e "\n=== Check all PostGIS extension references in migration files ==="
find src/main/resources/db/migration -name "*.sql" -exec grep -n 'postgis\|PostGIS' {} +

echo -e "\n=== Full SouvenirRepository.java file (checking lines around 21-46) ==="
wc -l src/main/java/com/souzip/domain/souvenir/repository/SouvenirRepository.java

Repository: souzip/souzip-api

Length of output: 1686


🏁 Script executed:

#!/bin/bash
# View the full SouvenirRepository.java to understand the query context
echo "=== Full SouvenirRepository.java content ==="
cat -n src/main/java/com/souzip/domain/souvenir/repository/SouvenirRepository.java

echo -e "\n=== Check if other Java files use PostGIS functions ==="
rg -l 'ST_DWithin|ST_Distance|ST_SetSRID|ST_MakePoint' src/main/java/

echo -e "\n=== Check for any PostGIS setup in other migration files ==="
find src/main/resources/db/migration -name "*.sql" | sort

Repository: souzip/souzip-api

Length of output: 3282


PostGIS 확장 생성이 주석 처리되어 있어 런타임 쿼리 실패를 초래합니다.

SouvenirRepository.javafindNearbySouvenirs() 메서드(37-45줄)가 ST_DWithin, ST_Distance, ST_SetSRID, ST_MakePoint 등 PostGIS 공간 함수를 직접 사용하고 있습니다. 현재 V1__init_schema.sql 32줄과 39줄의 PostGIS 확장 생성 구문이 주석 처리되어 있어, 이 쿼리는 데이터베이스에 해당 확장이 설치되지 않으면 실행 시 실패합니다.

확장 생성 구문을 활성화하거나, 외부 프로비저닝을 사용한다면 마이그레이션에서 존재 여부를 검증해야 합니다.

제안 수정안
--- a/src/main/resources/db/migration/V1__init_schema.sql
+++ b/src/main/resources/db/migration/V1__init_schema.sql
@@ -29,6 +29,6 @@
 -- Name: postgis; Type: EXTENSION; Schema: -; Owner: -
 --
 
--- CREATE EXTENSION IF NOT EXISTS postgis WITH SCHEMA cdb_admin;
+CREATE EXTENSION IF NOT EXISTS postgis;

@@ -36,4 +36,4 @@
 -- Name: EXTENSION postgis; Type: COMMENT; Schema: -; Owner: 
 --
 
--- COMMENT ON EXTENSION postgis IS 'PostGIS geometry and geography spatial types and functions';
+COMMENT ON EXTENSION postgis IS 'PostGIS geometry and geography spatial types and functions';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/db/migration/V1__init_schema.sql` around lines 32 - 39,
The PostGIS extension creation is commented out in V1__init_schema.sql which
breaks runtime queries used by SouvenirRepository.findNearbySouvenirs (uses
ST_DWithin, ST_Distance, ST_SetSRID, ST_MakePoint). Re-enable the extension
creation (uncomment or add CREATE EXTENSION IF NOT EXISTS postgis) in
V1__init_schema.sql, or alternatively add a migration check that
verifies/creates the extension before any spatial queries run; ensure the
migration uses "CREATE EXTENSION IF NOT EXISTS postgis" so the
findNearbySouvenirs query will succeed.




Expand Down