fix: 배포 502 / 관리자 401 개선
- Program.cs: MapRazorComponents에 AllowAnonymous 추가 JWT 미들웨어가 Blazor 셸 요청을 401로 차단하던 문제 수정 (인증은 Blazor AuthorizeRouteView → RedirectToLogin에서 처리) - deploy.yml: SSH 1회 연결로 배포+헬스체크 통합 서버 사이드 폴링으로 대기(최대 120초), CI 측 sleep 제거 구 배포 디렉토리 최근 5개 자동 정리 secrets 파일 사전 검증 추가 - maintenance.html: 배포 중 Nginx가 직접 서빙할 점검 페이지 15초 자동 새로고침, 카카오 채널 링크 포함 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+98
-114
@@ -29,7 +29,7 @@ jobs:
|
||||
- name: Test solution
|
||||
run: dotnet test TaxBaik.sln -c Release --no-build
|
||||
|
||||
- name: Publish Web (통합 앱)
|
||||
- name: Publish Web
|
||||
run: dotnet publish TaxBaik.Web/ -c Release -o ./publish --no-restore
|
||||
|
||||
- name: Write production secrets
|
||||
@@ -38,135 +38,119 @@ jobs:
|
||||
JWT_SECRET_KEY="${{ secrets.TAXBAIK_JWT_SECRET_KEY }}"
|
||||
TELEGRAM_BOT_TOKEN="${{ secrets.TAXBAIK_TELEGRAM_BOT_TOKEN }}"
|
||||
TELEGRAM_CHAT_ID="${{ secrets.TAXBAIK_TELEGRAM_CHAT_ID }}"
|
||||
if [ -z "$JWT_SECRET_KEY" ]; then
|
||||
echo "Missing TAXBAIK_JWT_SECRET_KEY secret" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$TELEGRAM_BOT_TOKEN" ]; then
|
||||
echo "Missing TAXBAIK_TELEGRAM_BOT_TOKEN secret" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$TELEGRAM_CHAT_ID" ]; then
|
||||
echo "Missing TAXBAIK_TELEGRAM_CHAT_ID secret" >&2
|
||||
exit 1
|
||||
fi
|
||||
JWT_SECRET_KEY="$JWT_SECRET_KEY" TELEGRAM_BOT_TOKEN="$TELEGRAM_BOT_TOKEN" TELEGRAM_CHAT_ID="$TELEGRAM_CHAT_ID" python3 -c 'import json, os, pathlib; pathlib.Path("./publish/appsettings.Production.json").write_text(json.dumps({"Jwt":{"SecretKey":os.environ["JWT_SECRET_KEY"]},"Telegram":{"BotToken":os.environ["TELEGRAM_BOT_TOKEN"],"ChatId":os.environ["TELEGRAM_CHAT_ID"]}}, ensure_ascii=False, indent=2), encoding="utf-8")'
|
||||
test -s ./publish/appsettings.Production.json
|
||||
[ -z "$JWT_SECRET_KEY" ] && { echo "Missing TAXBAIK_JWT_SECRET_KEY" >&2; exit 1; }
|
||||
[ -z "$TELEGRAM_BOT_TOKEN" ] && { echo "Missing TAXBAIK_TELEGRAM_BOT_TOKEN" >&2; exit 1; }
|
||||
[ -z "$TELEGRAM_CHAT_ID" ] && { echo "Missing TAXBAIK_TELEGRAM_CHAT_ID" >&2; exit 1; }
|
||||
JWT_SECRET_KEY="$JWT_SECRET_KEY" \
|
||||
TELEGRAM_BOT_TOKEN="$TELEGRAM_BOT_TOKEN" \
|
||||
TELEGRAM_CHAT_ID="$TELEGRAM_CHAT_ID" \
|
||||
python3 -c '
|
||||
import json, os, pathlib
|
||||
pathlib.Path("./publish/appsettings.Production.json").write_text(
|
||||
json.dumps({
|
||||
"Jwt": {"SecretKey": os.environ["JWT_SECRET_KEY"]},
|
||||
"Telegram": {"BotToken": os.environ["TELEGRAM_BOT_TOKEN"], "ChatId": os.environ["TELEGRAM_CHAT_ID"]}
|
||||
}, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8"
|
||||
)'
|
||||
test -s ./publish/appsettings.Production.json || { echo "appsettings.Production.json is empty" >&2; exit 1; }
|
||||
|
||||
- name: Copy migrations to publish
|
||||
run: |
|
||||
cp -r db/migrations ./publish/migrations || true
|
||||
- name: Copy migrations
|
||||
run: cp -r db/migrations ./publish/migrations || true
|
||||
|
||||
- name: Generate build info
|
||||
run: |
|
||||
mkdir -p ./publish/wwwroot
|
||||
COMMIT_HASH=$(git rev-parse --short HEAD)
|
||||
BUILD_TIME=$(date -u +'%Y-%m-%d %H:%M:%S UTC')
|
||||
echo "Version: $COMMIT_HASH" > ./publish/wwwroot/version.txt
|
||||
echo "Built: $BUILD_TIME" >> ./publish/wwwroot/version.txt
|
||||
echo "✓ Version: $COMMIT_HASH"
|
||||
mkdir -p ./publish/wwwroot
|
||||
printf 'Version: %s\nBuilt: %s\n' "$COMMIT_HASH" "$BUILD_TIME" > ./publish/wwwroot/version.txt
|
||||
echo "✓ Build: $COMMIT_HASH @ $BUILD_TIME"
|
||||
|
||||
- name: Deploy (CI only, 통합 Web)
|
||||
- name: Setup SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
SSH_KEY_B64="${{ secrets.DEPLOY_SSH_KEY_B64 }}"
|
||||
SSH_KEY_RAW="${{ secrets.DEPLOY_SSH_KEY }}"
|
||||
if [ -n "$SSH_KEY_B64" ]; then
|
||||
printf '%s' "$SSH_KEY_B64" | base64 -d > ~/.ssh/id_ed25519
|
||||
elif [ -n "$SSH_KEY_RAW" ]; then
|
||||
if printf '%s' "$SSH_KEY_RAW" | grep -q 'BEGIN .*PRIVATE KEY'; then
|
||||
printf '%b\n' "$SSH_KEY_RAW" > ~/.ssh/id_ed25519
|
||||
else
|
||||
printf '%s' "$SSH_KEY_RAW" | base64 -d > ~/.ssh/id_ed25519
|
||||
fi
|
||||
else
|
||||
echo "Missing DEPLOY_SSH_KEY_B64 or DEPLOY_SSH_KEY" >&2; exit 1
|
||||
fi
|
||||
sed -i 's/\r$//' ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh-keyscan -H "${{ secrets.DEPLOY_HOST }}" >> ~/.ssh/known_hosts 2>/dev/null || true
|
||||
|
||||
- name: Package artifact
|
||||
run: |
|
||||
tar -czf taxbaik_deploy.tgz -C ./publish .
|
||||
echo "✓ Package: $(du -sh taxbaik_deploy.tgz | cut -f1)"
|
||||
|
||||
- name: Deploy & verify on server
|
||||
run: |
|
||||
set -e
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
DEPLOY_HOME="/home/kjh2064"
|
||||
DEPLOY_DIR="$DEPLOY_HOME/deployments/taxbaik_${TIMESTAMP}"
|
||||
COMMIT=$(git rev-parse --short HEAD)
|
||||
DEPLOY_HOST="${{ secrets.DEPLOY_HOST }}"
|
||||
DEPLOY_USER="${{ secrets.DEPLOY_USER }}"
|
||||
|
||||
echo "=== Deploying TaxBaik v$(git rev-parse --short HEAD) ==="
|
||||
mkdir -p ~/.ssh
|
||||
SSH_KEY_B64="${{ secrets.DEPLOY_SSH_KEY_B64 }}"
|
||||
SSH_KEY_RAW="${{ secrets.DEPLOY_SSH_KEY }}"
|
||||
if [ -n "$SSH_KEY_B64" ]; then
|
||||
printf '%s' "$SSH_KEY_B64" | base64 -d > ~/.ssh/id_ed25519
|
||||
elif [ -n "$SSH_KEY_RAW" ]; then
|
||||
if printf '%s' "$SSH_KEY_RAW" | grep -q 'BEGIN .*PRIVATE KEY'; then
|
||||
printf '%b\n' "$SSH_KEY_RAW" > ~/.ssh/id_ed25519
|
||||
else
|
||||
printf '%s' "$SSH_KEY_RAW" | base64 -d > ~/.ssh/id_ed25519
|
||||
fi
|
||||
else
|
||||
echo "Missing DEPLOY_SSH_KEY_B64 or DEPLOY_SSH_KEY secret" >&2
|
||||
exit 1
|
||||
fi
|
||||
sed -i 's/\r$//' ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null || true
|
||||
echo "=== Deploying TaxBaik $COMMIT ($TIMESTAMP) ==="
|
||||
|
||||
tar -czf taxbaik_publish.tgz -C ./publish .
|
||||
scp -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes taxbaik_publish.tgz "$DEPLOY_USER@$DEPLOY_HOST:/tmp/taxbaik_publish_${TIMESTAMP}.tgz"
|
||||
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes "$DEPLOY_USER@$DEPLOY_HOST" "
|
||||
set -e
|
||||
mkdir -p '$DEPLOY_DIR'
|
||||
tar -xzf '/tmp/taxbaik_publish_${TIMESTAMP}.tgz' -C '$DEPLOY_DIR'
|
||||
rm -f '/tmp/taxbaik_publish_${TIMESTAMP}.tgz'
|
||||
ln -sfn '$DEPLOY_DIR' '$DEPLOY_HOME/taxbaik_active'
|
||||
sudo systemctl restart taxbaik
|
||||
"
|
||||
sleep 5
|
||||
echo "✓ Deployed to $DEPLOY_HOST:$DEPLOY_DIR"
|
||||
# 1. 아티팩트 업로드
|
||||
scp -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes \
|
||||
taxbaik_deploy.tgz "$DEPLOY_USER@$DEPLOY_HOST:/tmp/taxbaik_${TIMESTAMP}.tgz"
|
||||
|
||||
- name: Verify deployment
|
||||
run: |
|
||||
# 2. 서버에서 배포 + 헬스 체크 (SSH 1회 연결로 처리)
|
||||
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes \
|
||||
-o ServerAliveInterval=10 \
|
||||
"$DEPLOY_USER@$DEPLOY_HOST" bash << REMOTE
|
||||
set -e
|
||||
DEPLOY_HOST="${{ secrets.DEPLOY_HOST }}"
|
||||
DEPLOY_USER="${{ secrets.DEPLOY_USER }}"
|
||||
mkdir -p ~/.ssh
|
||||
SSH_KEY_B64="${{ secrets.DEPLOY_SSH_KEY_B64 }}"
|
||||
SSH_KEY_RAW="${{ secrets.DEPLOY_SSH_KEY }}"
|
||||
if [ -n "$SSH_KEY_B64" ]; then
|
||||
printf '%s' "$SSH_KEY_B64" | base64 -d > ~/.ssh/id_ed25519
|
||||
elif [ -n "$SSH_KEY_RAW" ]; then
|
||||
if printf '%s' "$SSH_KEY_RAW" | grep -q 'BEGIN .*PRIVATE KEY'; then
|
||||
printf '%b\n' "$SSH_KEY_RAW" > ~/.ssh/id_ed25519
|
||||
else
|
||||
printf '%s' "$SSH_KEY_RAW" | base64 -d > ~/.ssh/id_ed25519
|
||||
DEPLOY_HOME="/home/kjh2064"
|
||||
DEPLOY_DIR="\$DEPLOY_HOME/deployments/taxbaik_${TIMESTAMP}"
|
||||
TIMESTAMP="${TIMESTAMP}"
|
||||
|
||||
echo "--- [1/5] 압축 해제 ---"
|
||||
mkdir -p "\$DEPLOY_DIR"
|
||||
tar -xzf "/tmp/taxbaik_\${TIMESTAMP}.tgz" -C "\$DEPLOY_DIR"
|
||||
rm -f "/tmp/taxbaik_\${TIMESTAMP}.tgz"
|
||||
|
||||
echo "--- [2/5] 운영 설정 검증 ---"
|
||||
test -s "\$DEPLOY_DIR/appsettings.Production.json" \
|
||||
|| { echo "FATAL: appsettings.Production.json 없음" >&2; exit 1; }
|
||||
|
||||
echo "--- [3/5] 심볼릭 링크 전환 ---"
|
||||
ln -sfn "\$DEPLOY_DIR" "\$DEPLOY_HOME/taxbaik_active"
|
||||
|
||||
echo "--- [4/5] 서비스 재시작 ---"
|
||||
sudo /usr/bin/systemctl restart taxbaik
|
||||
|
||||
echo "--- [5/5] 헬스 체크 (최대 120초) ---"
|
||||
ATTEMPTS=40
|
||||
for i in \$(seq 1 \$ATTEMPTS); do
|
||||
STATUS=\$(curl -sf -o /dev/null -w '%{http_code}' http://127.0.0.1:5001/taxbaik/ 2>/dev/null || echo "000")
|
||||
if [ "\$STATUS" = "200" ]; then
|
||||
echo "✓ 서비스 정상 (시도 \$i/\$ATTEMPTS)"
|
||||
# 구 배포 디렉토리 정리 (최근 5개 보존)
|
||||
ls -1dt \$DEPLOY_HOME/deployments/taxbaik_* 2>/dev/null \
|
||||
| tail -n +6 | xargs rm -rf 2>/dev/null || true
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
echo "Missing DEPLOY_SSH_KEY_B64 or DEPLOY_SSH_KEY secret" >&2
|
||||
exit 1
|
||||
fi
|
||||
sed -i 's/\r$//' ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null || true
|
||||
ADMIN_TEST_PASSWORD="${{ secrets.TAXBAIK_ADMIN_TEST_PASSWORD }}"
|
||||
HOME_STATUS="000"
|
||||
LOGIN_STATUS="000"
|
||||
BLOG_STATUS="000"
|
||||
BLOG_HEADERS=""
|
||||
BLOG_BODY=""
|
||||
BLOG_FINAL_URL=""
|
||||
AUTH_BODY=""
|
||||
for i in $(seq 1 12); do
|
||||
HOME_STATUS=$(ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes "$DEPLOY_USER@$DEPLOY_HOST" "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:5001/taxbaik/" || echo "000")
|
||||
LOGIN_STATUS=$(ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes "$DEPLOY_USER@$DEPLOY_HOST" "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:5001/taxbaik/admin/login" || echo "000")
|
||||
BLOG_STATUS_AND_URL=$(ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes "$DEPLOY_USER@$DEPLOY_HOST" "curl -s -L -D /tmp/taxbaik_blog_check.headers -o /tmp/taxbaik_blog_check.html -w '%{http_code} %{url_effective}' http://127.0.0.1:5001/taxbaik/blog/accountant-mistakes-5" || echo "000")
|
||||
BLOG_STATUS=$(printf '%s' "$BLOG_STATUS_AND_URL" | awk '{print $1}')
|
||||
BLOG_FINAL_URL=$(printf '%s' "$BLOG_STATUS_AND_URL" | awk '{print $2}')
|
||||
BLOG_HEADERS=$(ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes "$DEPLOY_USER@$DEPLOY_HOST" "sed -n '1,12p' /tmp/taxbaik_blog_check.headers | tr '\n' ' '" || echo "")
|
||||
BLOG_BODY=$(ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes "$DEPLOY_USER@$DEPLOY_HOST" "sed -n '1,12p' /tmp/taxbaik_blog_check.html | tr '\n' ' '" || echo "")
|
||||
if [ "$HOME_STATUS" = "200" ] && [ "$LOGIN_STATUS" = "200" ] && [ "$BLOG_STATUS" = "200" ]; then
|
||||
AUTH_BODY=$(ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes "$DEPLOY_USER@$DEPLOY_HOST" "python3 -c \"import json, urllib.request; req = urllib.request.Request('http://127.0.0.1:5001/taxbaik/api/auth/login', data=json.dumps({'username':'admin','password':'${ADMIN_TEST_PASSWORD}'}).encode(), headers={'Content-Type':'application/json'}, method='POST'); print(urllib.request.urlopen(req, timeout=20).read().decode())\"" || echo "")
|
||||
if echo "$AUTH_BODY" | grep -q '"token"'; then
|
||||
echo "Home Status: $HOME_STATUS"
|
||||
echo "Login Status: $LOGIN_STATUS"
|
||||
echo "Blog Status: $BLOG_STATUS"
|
||||
echo "Auth Body: $AUTH_BODY"
|
||||
echo "✓ Service is running"
|
||||
exit 0
|
||||
fi
|
||||
if [ "\$i" -eq "\$ATTEMPTS" ]; then
|
||||
echo "=== FATAL: 서비스가 \$ATTEMPTS회 시도 후에도 응답하지 않음 ===" >&2
|
||||
echo "--- systemd 상태 ---" >&2
|
||||
systemctl is-active taxbaik >&2 || true
|
||||
echo "--- 최근 로그 50줄 ---" >&2
|
||||
journalctl -u taxbaik --no-pager -n 50 >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep 5
|
||||
echo " 대기 중... (\$i/\$ATTEMPTS, HTTP \$STATUS)"
|
||||
sleep 3
|
||||
done
|
||||
echo "Home Status: $HOME_STATUS"
|
||||
echo "Login Status: $LOGIN_STATUS"
|
||||
echo "Blog Status: $BLOG_STATUS"
|
||||
echo "Blog Final URL: $BLOG_FINAL_URL"
|
||||
echo "Blog Headers: $BLOG_HEADERS"
|
||||
echo "Blog Body: $BLOG_BODY"
|
||||
echo "Auth Body: $AUTH_BODY"
|
||||
echo "Service verification failed; collecting remote service diagnostics..." >&2
|
||||
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes "$DEPLOY_USER@$DEPLOY_HOST" "systemctl is-active taxbaik; systemctl status taxbaik --no-pager -l | sed -n '1,120p'; journalctl -u taxbaik --no-pager -n 120" || true
|
||||
exit 1
|
||||
REMOTE
|
||||
|
||||
echo "✓ 배포 완료: taxbaik_${TIMESTAMP} @ $DEPLOY_HOST"
|
||||
|
||||
@@ -142,6 +142,10 @@ if (!app.Environment.IsDevelopment())
|
||||
app.MapControllers();
|
||||
app.MapHealthChecks("/healthz");
|
||||
app.MapRazorPages();
|
||||
app.MapRazorComponents<TaxBaik.Web.Components.Admin.App>().AddInteractiveServerRenderMode();
|
||||
// AllowAnonymous: JWT 미들웨어가 Blazor 셸 요청을 401로 차단하지 않도록 한다.
|
||||
// 인증은 Blazor AuthorizeRouteView → RedirectToLogin 에서 처리한다.
|
||||
app.MapRazorComponents<TaxBaik.Web.Components.Admin.App>()
|
||||
.AddInteractiveServerRenderMode()
|
||||
.AllowAnonymous();
|
||||
|
||||
app.Run();
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="refresh" content="15" />
|
||||
<title>잠시 점검 중 — 백원숙 세무회계</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Apple SD Gothic Neo', 'Noto Sans KR', sans-serif; }
|
||||
body {
|
||||
background: #F9F7F3;
|
||||
color: #3D2817;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
}
|
||||
.card {
|
||||
text-align: center;
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 3rem 2.5rem;
|
||||
box-shadow: 0 8px 32px rgba(61,40,23,.10);
|
||||
}
|
||||
.icon { font-size: 3.5rem; margin-bottom: 1.25rem; }
|
||||
.badge {
|
||||
display: inline-block;
|
||||
background: #C89D6E;
|
||||
color: #fff;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
padding: 4px 14px;
|
||||
border-radius: 20px;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
h1 { font-size: 1.6rem; color: #2E5C4E; font-weight: 800; margin-bottom: 1rem; line-height: 1.35; }
|
||||
p { color: #6B5D4F; line-height: 1.85; font-size: 0.95rem; }
|
||||
.divider { border: none; border-top: 1px solid #EFE9DD; margin: 1.75rem 0; }
|
||||
.kakao-btn {
|
||||
display: inline-block;
|
||||
background: #FEE500;
|
||||
color: #3D2817;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
padding: 0.65rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.timer { font-size: 0.78rem; color: #A09080; margin-top: 1.5rem; }
|
||||
.footer { font-size: 0.75rem; color: #C0ADA0; margin-top: 2rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="icon">🔧</div>
|
||||
<div class="badge">서비스 업데이트 중</div>
|
||||
<h1>잠시 후 다시 접속해 주세요</h1>
|
||||
<p>
|
||||
더 나은 서비스를 위해 업데이트 작업을 진행하고 있습니다.<br />
|
||||
보통 <strong>1~2분</strong> 이내에 완료됩니다.
|
||||
</p>
|
||||
<hr class="divider" />
|
||||
<p>급하신 세무 문의는 카카오 채널로 연락해 주세요.</p>
|
||||
<a class="kakao-btn" href="http://pf.kakao.com/_xoxchTX" target="_blank">
|
||||
💬 카카오 채널 상담
|
||||
</a>
|
||||
<p class="timer">이 페이지는 15초 후 자동으로 새로고침됩니다.</p>
|
||||
<p class="footer">© 2026 백원숙 세무회계</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user