news.hwmoon.com 기사 자동 발행 중단 재발 방지 기록
정시 기사 발행이 멈춘 원인을 Azure DevOps run, Astro build, Rollup dependency, Static Web Apps deploy 경로로 나눠 확인하고 재발 방지 조치를 반영한 운영 기록입니다.
- Node.js
- 22.12.0
- Astro
- 6.3.5
- Rollup
- 4.43.0
- Azure Static Web Apps
- Production deploy
이번 기록은 news.hwmoon.com의 정시 기사 자동 발행이 반복적으로 멈춘 문제를 정리한 incident review다.
겉으로는 “배포 실패” 알림 하나로 보였지만 실제로는 두 층의 문제가 겹쳐 있었다.
| 구분 | 증상 | 확인한 원인 |
|---|---|---|
| 배포 파이프라인 | cd-dev-static-web-app 실패 | Key Vault가 아니라 Astro build 단계의 JavaScript parse 오류 |
| 기사 생성 파이프라인 | 정시 기사 생성 후 preflight 실패 | 최신 Rollup 조합에서 내부 deopt 처리 오류와 SIGSEGV 발생 |
| 운영 감시 | 실패는 알렸지만 맥락이 부족함 | 빌드 전 소스 안전성 검사, 재시도, 실패 후보 격리 기록이 부족함 |
중요한 결론은 하나다. 이번 장애의 직접 원인은 Key Vault secret 조회가 아니었다. Static Web Apps token을 보기 전에 build validation이 먼저 깨졌다.
타임라인
운영 기준 시간은 KST다.
| 시각 | Run | 결과 | 의미 |
|---|---|---|---|
| 2026-06-05 08:01 | cd-dev-static-web-app #1741 | 실패 | 기사 반영 commit에서 Astro build가 Invalid or unexpected token으로 중단 |
| 2026-06-05 09:03 | article-generation #1747 | 실패 | 생성 후보는 만들어졌지만 expressionsToBeDeoptimized is not iterable, 이후 retry에서 SIGSEGV |
| 2026-06-05 09:19 | PR #381 | 병합 | Astro/Rollup pinning과 후보별 preflight 격리 반영 |
| 2026-06-05 09:20-09:25 | #1751, #1752, #1753, #1754 | 성공 | 새 main commit 기준 CI/CD 검증과 배포 성공 |
| 2026-06-05 09:25-09:28 | article-generation #1755 | 성공 | 기사 생성, preflight, Static Web Apps 반영 성공 |
왜 Key Vault로 보였나
배포 실패 알림은 보통 secret, token, Key Vault 문제처럼 보이기 쉽다. Static Web Apps 배포 단계가 secret을 필요로 하기 때문이다.
하지만 이번 run에서는 실패 지점이 달랐다.
content safety
content check
content audit
Astro build
여기까지가 Static Web Apps 업로드보다 앞선 단계다. 실패는 build에서 발생했다. 따라서 Key Vault를 계속 의심하면 복구 방향이 늦어진다.
운영 판단 기준은 이렇게 바꿨다.
| 실패 위치 | 먼저 볼 것 |
|---|---|
| Key Vault task 이전 | content, dependency, build, generated source |
| SWA deploy task 내부 | SWA token, API build, upload path, staticappsclient |
| deploy 성공 후 사이트 미반영 | dist path, self-hosted agent mount, cache, route smoke check |
1차 조치
먼저 배포 전 검증을 더 촘촘하게 만들었다.
- 기사 markdown에 제어 문자, BOM, replacement character, frontmatter fence 문제가 있는지 검사하는
content:safety를 추가했다. build를 직접astro build로 실행하지 않고.astro,dist를 정리한 뒤 retry하는 wrapper로 감쌌다.- CI/CD와 기사 생성에서 같은 preflight script를 쓰도록
validate:app,deploy:preflight를 공통화했다. - self-hosted agent checkout을 clean mode로 바꿔 이전 build artifact가 다음 run에 섞이지 않게 했다.
- article-only 변경은 일반 CD 중복 실행에서 제외하고, 기사 생성 파이프라인이 검증된 산출물을 직접 배포하도록 정리했다.
이 조치 후 일반 CI/CD는 정상화됐다. 실제로 PR #380 반영 후 main 기준 CD와 CI가 성공했고 사이트도 갱신됐다.
2차 조치
다음 정시 run에서 새로운 사실이 드러났다. 기사 파일의 문자 안전성은 통과했지만 Rollup 내부 오류가 build를 깨뜨렸다.
expressionsToBeDeoptimized is not iterable
SIGSEGV
이 문제는 단순 retry만으로는 충분하지 않았다. retry가 같은 dependency 조합과 같은 후보 파일을 다시 밟으면 같은 지점에서 멈출 수 있기 때문이다.
그래서 두 가지를 추가했다.
첫째, build toolchain을 잠갔다.
{
"devDependencies": {
"astro": "6.3.5"
},
"overrides": {
"rollup": "4.43.0"
}
}
package-lock.json까지 함께 반영해 Azure DevOps의 npm ci가 매번 같은 Rollup 버전을 설치하게 했다.
둘째, 기사 후보 단위로 실패를 격리했다.
이전 흐름은 후보 하나가 build를 깨뜨리면 전체 run이 멈췄다.
candidate -> article file -> preflight fail -> pipeline fail
이제는 후보 pool을 조금 더 넓게 만들고, 각 후보를 생성한 직후 preflight를 통과해야만 최종 생성 목록에 넣는다.
candidate A -> preflight fail -> remove article file -> try candidate B
candidate B -> preflight pass -> publish
모든 후보가 실패하면 공개 콘텐츠를 만들지 않고 실패 report만 남긴다. 부분적으로 깨진 기사 파일이 남아 다음 run을 계속 오염시키는 상황을 막기 위한 선택이다.
검증 결과
로컬에서는 아래 순서로 확인했다.
node --check app/scripts/run-article-generation.mjs
node --check app/scripts/check-source-text-safety.mjs
node --check app/scripts/run-build-with-retry.mjs
npm --prefix app ci --ignore-scripts
npm --prefix app run article:check
npm --prefix app run content:safety
npm --prefix app run deploy:preflight
npm ls rollup 기준으로 Astro와 Vite가 모두 rollup@4.43.0을 사용했다.
Azure DevOps에서는 새 main commit 기준으로 다음을 확인했다.
| Run | Pipeline | 결과 |
|---|---|---|
| #1751 | cd-dev-static-web-app | 성공 |
| #1752 | ci-app | 성공 |
| #1753 | ci-app 수동 재검증 | 성공 |
| #1754 | cd-dev-static-web-app 수동 재검증 | 성공 |
| #1755 | article-generation 수동 재검증 | 성공 |
공개 사이트도 HTTP 200을 반환했고, Last-Modified는 2026-06-05 09:28 KST 배포 시각으로 갱신됐다.
운영 기준
앞으로 같은 유형의 장애를 볼 때 판단 순서는 이렇다.
- Slack 알림의 pipeline 이름과 run 번호를 먼저 본다.
- Key Vault를 보기 전에 실패 step이 secret 조회 전인지 후인지 확인한다.
- build 이전 실패면 source safety, content audit, dependency lock, generated article 후보를 본다.
- build 이후 실패면 SWA token, deploy task, API build, upload path를 본다.
- pipeline은 성공했는데 사이트가 낡았으면 self-hosted agent mount와 route smoke check를 본다.
이번 조치의 핵심은 “실패를 숨기지 않는 것”이다. 실패 후보는 제거하고 다음 후보를 시도하되, 모든 후보가 실패하면 run은 실패해야 한다. 그래야 운영자가 조용한 데이터 손상 대신 명확한 실패를 볼 수 있다.
남은 리스크
완전히 사라진 리스크는 아니다.
| 리스크 | 대응 |
|---|---|
| Rollup 또는 Astro의 새 회귀 | lockfile과 override로 즉시 확산 차단 |
| 특정 기사 후보만 build를 깨뜨림 | 후보별 preflight와 산출물 제거로 격리 |
| self-hosted agent 상태 불량 | Slack watchdog과 CI/CD 실패 알림으로 조기 감지 |
| secret 만료 | 실패 step이 secret 이후일 때만 Key Vault와 DevOps variable 확인 |
정리하면 이번 복구는 “한 번 다시 돌려보기”가 아니라, 실패 위치를 더 잘 보이게 하고, 재시도 가능한 후보는 격리하고, dependency drift를 잠그고, 실제 사이트 반영까지 검증한 조치다.
시리즈
Publisher Infrastructure
전체 8편 중 6편입니다. 관련 구축 기록을 순서대로 모아 전체 맥락을 쉽게 확인할 수 있습니다.
- 1. Azure DNS와 Zoho Mail로 도메인 이메일 만들기
- 2. Synology NAS로 Azure DevOps Self-hosted Agent 운영하기
- 3. Pipeline은 성공했는데 Static Web Apps가 404를 냈던 이유
- 4. Azure Repos를 운영 원본으로 두고 GitHub를 백업으로 쓰는 전략
- 5. Slack 기반 AIOps로 NAS Agent 운영 자동화하기
- 6. news.hwmoon.com 기사 자동 발행 중단 재발 방지 기록
- 7. news.hwmoon.com AdSense 승인 전 준비와 운영 전환 기록
- 8. news.hwmoon.com 배포 중단 원인과 Agent 자가복구 체계 구축 기록