Skip to content
Merged
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
81 changes: 25 additions & 56 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ jobs:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_SSH_KEY }}
source: "shared-infra/blocks/run_container.sh,shared-infra/blocks/healthcheck.sh,infra/nginx/extractor.conf"
source: "shared-infra/blocks/run_container.sh,shared-infra/blocks/healthcheck.sh,shared-infra/blocks/slot_decide.sh,shared-infra/blocks/slot_switch.sh,infra/nginx/extractor.conf"
target: "/tmp/piki-blocks"
strip_components: 2

Expand Down Expand Up @@ -146,11 +146,11 @@ jobs:
echo "LOGGING_STRUCTURED_FORMAT_CONSOLE=ecs" >> "$ENV_FILE"
echo "SSM env $(grep -c '' "$ENV_FILE") 라인 로드 (/piki-extractor/app/)"

chmod +x /tmp/piki-blocks/run_container.sh /tmp/piki-blocks/healthcheck.sh
chmod +x /tmp/piki-blocks/run_container.sh /tmp/piki-blocks/healthcheck.sh \
/tmp/piki-blocks/slot_decide.sh /tmp/piki-blocks/slot_switch.sh

# ── nginx 프로비저닝(멱등) - 무중단 전환의 고정 프론트(:8090) ──
# 설치·conf 반영은 매 배포 수행하되, (재)시작·reload 는 아래 전환 단계가 수행한다
# (레거시 단일 컨테이너가 :8090 을 점유한 이행 1회차엔 그 전까지 nginx 가 8090 을 못 문다).
# 설치·conf 반영은 매 배포 수행하고, (재)시작·reload 는 아래 전환 블록(slot_switch)이 수행한다.
command -v nginx >/dev/null || {
sudo DEBIAN_FRONTEND=noninteractive apt-get update -y
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y nginx
Expand All @@ -159,20 +159,13 @@ jobs:
sudo cp /tmp/piki-blocks/extractor.conf /etc/nginx/sites-available/piki-extractor
sudo ln -sf /etc/nginx/sites-available/piki-extractor /etc/nginx/sites-enabled/piki-extractor

# ── 슬롯 결정: upstream 상태 파일(server 127.0.0.1:1809X;)이 source of truth ──
# ── 슬롯 결정: 공용 블록(slot_decide)에 위임 - 상태 파일이 source of truth, 값(슬롯 이름·포트)은
# 이 호출부가 소유한다. 할당 후 eval 2단계는 블록 헤더가 정한 소비 계약(치환 실패 은폐 방지).
UPSTREAM_CONF=/etc/nginx/piki-extractor-upstream.conf
ACTIVE_PORT=""
if [ -f "$UPSTREAM_CONF" ]; then
# 콜론 뒤 숫자만 추출한다 - 라인이 127.0.0.1 을 담고 있어 무차별 숫자 grep 은 "127" 을 먼저 문다.
ACTIVE_PORT=$(grep -oE ':[0-9]+' "$UPSTREAM_CONF" | head -1 | tr -d ':' || true)
fi
if [ "$ACTIVE_PORT" = "18090" ]; then
ACTIVE=blue INACTIVE=green INACTIVE_PORT=18091
else
# 18091 활성이거나, 상태 파일이 없는 부트스트랩(레거시 단일 컨테이너가 8090 서빙 중) - blue 로 간다.
ACTIVE=green INACTIVE=blue INACTIVE_PORT=18090
fi
echo "DEPLOY slot=$INACTIVE port=$INACTIVE_PORT (active=${ACTIVE_PORT:-legacy/none})"
DECIDED=$(bash /tmp/piki-blocks/slot_decide.sh --state-file "$UPSTREAM_CONF" \
--slot-a blue:18090 --slot-b green:18091)
eval "$DECIDED"
echo "DEPLOY slot=$INACTIVE port=$INACTIVE_PORT (active=${ACTIVE_PORT:-none})"

# 직전 배포의 teardown 이 끊겼으면 이 슬롯 컨테이너가 남아있을 수 있다 - run 의 이름·포트 충돌 전에
# 정리하고, 데몬 지연에 대비해 timeout 상한을 건다 (core deploy 와 동일 가드).
Expand Down Expand Up @@ -203,51 +196,27 @@ jobs:
exit 1
fi

# ── 전환: 이전 upstream 을 보존한 채 갱신 → 검증·reload·최종 헬스 중 무엇이 실패해도 원복 ──
# 원복이 없으면 상태 파일(=슬롯 판정의 source of truth)만 새 슬롯을 가리킨 채 남아,
# 다음 배포가 "실제 서빙 중인 슬롯"을 비활성으로 오판해 제거한다 (상태·현실 괴리 사고).
PREV_UPSTREAM=""
[ -f "$UPSTREAM_CONF" ] && PREV_UPSTREAM=$(cat "$UPSTREAM_CONF")
restore_upstream() {
# 부트스트랩(이전 상태 없음)은 원복 대상도 없다 - 레거시는 이미 제거됐을 수 있어 수동 개입 영역.
[ -n "$PREV_UPSTREAM" ] || return 0
printf '%s\n' "$PREV_UPSTREAM" | sudo tee "$UPSTREAM_CONF" >/dev/null
sudo systemctl reload nginx || true
}
echo "server 127.0.0.1:$INACTIVE_PORT;" | sudo tee "$UPSTREAM_CONF" >/dev/null
if ! sudo nginx -t; then
echo "::error::nginx -t 실패 - upstream 원복 후 중단 (트래픽은 기존 경로 유지)"
restore_upstream
exit 1
fi
if docker ps -a --format '{{.Names}}' | grep -qx piki-extractor; then
# 블루그린 이행 1회차: 레거시 단일 컨테이너가 :8090 을 점유해 nginx 가 그 포트를 못 문다.
# 제거→nginx 기동 사이 수초 공백은 소비자(core 워커) 재시도가 흡수한다. 이행 후엔 이 분기가 no-op.
timeout 40 docker stop -t 30 piki-extractor 2>/dev/null || true
timeout 30 docker rm -f piki-extractor 2>/dev/null || true
fi
# ── 전환: 공용 블록(slot_switch)에 위임 ──
# 갱신 → nginx -t → reload(정지 시 restart 폴백) → 프론트 경유 최종 검증까지 블록의 원복 경계
# 안에서 돈다: 어느 단계가 실패해도 이전 upstream 이 원복되고 exit 1 이 온다. 검증 실패 시
# 새 슬롯 정리는 호출자 몫(블록은 컨테이너를 모른다). 이행 1회차의 레거시 단일 컨테이너 분기는
# 이행 완료(2026-08-10)로 제거됐다.
sudo systemctl enable nginx >/dev/null 2>&1 || true
if ! (sudo systemctl reload nginx || sudo systemctl restart nginx); then
echo "::error::nginx reload/restart 실패 - upstream 원복 후 중단"
restore_upstream
exit 1
fi

# 전환 검증: 소비자와 같은 경로(:8090 → nginx → 새 슬롯)로 최종 확인. 실패 시 이전 슬롯으로 원복하고
# 새 슬롯을 정리한다 - 구 슬롯 제거는 이 검증을 통과한 뒤에만 일어난다.
if ! bash /tmp/piki-blocks/healthcheck.sh \
--url http://localhost:8090/actuator/health --interval 2 --attempts 10 \
--expect-body '"status":"UP"'; then
echo "::error::전환 후 최종 헬스체크 실패 - 이전 슬롯으로 원복"
restore_upstream
if ! bash /tmp/piki-blocks/slot_switch.sh \
--state-file "$UPSTREAM_CONF" --server "127.0.0.1:$INACTIVE_PORT" \
--verify-cmd "bash /tmp/piki-blocks/healthcheck.sh --url http://localhost:8090/actuator/health --interval 2 --attempts 10 --expect-body '\"status\":\"UP\"'"; then
Comment thread
m-a-king marked this conversation as resolved.
echo "::error::슬롯 전환 실패(이전 upstream 원복됨) - 새 슬롯 정리 후 중단"
timeout 40 docker stop -t 30 "piki-extractor-$INACTIVE" 2>/dev/null || true
timeout 30 docker rm -f "piki-extractor-$INACTIVE" 2>/dev/null || true
exit 1
fi

# 구 슬롯 종료 - 전환은 이미 끝났으므로 best-effort (데몬 지연이 성공한 배포를 못 막게 timeout 상한)
timeout 60 docker stop -t 30 "piki-extractor-$ACTIVE" 2>/dev/null || true
timeout 30 docker rm -f "piki-extractor-$ACTIVE" 2>/dev/null || true
# 구 슬롯 종료 - 전환은 이미 끝났으므로 best-effort (데몬 지연이 성공한 배포를 못 막게 timeout 상한).
# 부트스트랩(ACTIVE 빈 값)이면 종료할 구 슬롯이 없다.
if [ -n "$ACTIVE" ]; then
timeout 60 docker stop -t 30 "piki-extractor-$ACTIVE" 2>/dev/null || true
timeout 30 docker rm -f "piki-extractor-$ACTIVE" 2>/dev/null || true
fi

# 이전 이미지 정리 (dangling 만 - 직전 sha 태그는 남겨 수동 롤백 여지 유지)
docker image prune -f >/dev/null
Loading