Pipeline은 성공했는데 Static Web Apps가 404를 냈던 이유
Synology NAS self-hosted agent에서 Azure Static Web Apps 배포가 성공으로 보였지만 신규 route가 404로 남았던 원인과 /azp/_work bind mount 복구 과정을 정리합니다.
- Azure Pipelines Agent
- 4.273.0
- Static Web Apps Client
- stable
- Docker
- 24.0.2
이 글의 목차
이번 이슈는 겉으로 보면 이상했다. Azure DevOps pipeline은 성공했고, article generation도 성공했고, Astro build도 성공했다. 배포 task도 실패하지 않았다.
그런데 실제 서비스에서는 새 기사 URL이 404였다.
문제는 코드가 아니라 self-hosted agent의 work directory mount 방식이었다. 특히 Azure Static Web Apps 배포 task가 내부적으로 별도 Docker 컨테이너를 실행한다는 점을 놓치면, 이 문제는 꽤 헷갈린다.
증상
문제 상황은 아래와 같았다.
article-generation pipeline: succeeded
PR creation and merge: succeeded
Astro route generation: succeeded
Static Web Apps deploy step: succeeded
Public article URL: 404
빌드 로그에는 신규 route가 생성됐다고 나왔다.
/kr/example-article-slug/index.html
하지만 공개 서비스에서 같은 route를 열면 404가 났다.
curl -I https://news.hwmoon.com/kr/example-article-slug/
처음에는 DNS, CDN cache, Static Web Apps propagation, sitemap 문제를 의심했다. 하지만 이전 기사와 홈 페이지는 정상으로 보였고, pipeline도 실패하지 않았기 때문에 방향을 다시 잡아야 했다.
핵심 원인
원인은 self-hosted agent 컨테이너의 work directory가 NAS host 기준 경로와 일치하지 않았기 때문이다.
문제가 있던 구조는 개념적으로 아래와 같았다.
agent container:
/azp/_work
NAS host:
/volume1/docker/hwmoon-azp-agent/_work
agent 컨테이너 안에서는 /azp/_work가 정상처럼 보인다. 빌드도 된다. app/dist도 만들어진다.
하지만 Azure Static Web Apps 배포 task는 단순히 현재 프로세스에서 파일을 업로드하는 방식이 아니다. self-hosted agent 안에서 Docker socket을 통해 별도의 mcr.microsoft.com/appsvc/staticappsclient 컨테이너를 실행한다.
이때 sibling container가 바라보는 bind mount source는 agent container 내부 경로가 아니라 NAS host 경로다.
즉, agent 컨테이너 안의 /azp/_work와 NAS host의 /azp/_work가 같은 경로로 맞춰져 있지 않으면 배포 컨테이너는 최신 build artifact가 아닌 다른 경로를 보게 된다.
그 결과 이런 모순이 생긴다.
Build container sees latest dist.
Deploy helper container sees another host path.
Pipeline looks green.
Production still serves old artifacts.
왜 pipeline이 실패하지 않았나
배포 task 입장에서는 upload 대상 디렉토리가 존재했고, 업로드 자체도 수행됐다. 그래서 task는 성공으로 끝날 수 있다.
하지만 그 디렉토리가 방금 빌드한 최신 dist가 아니라면, 배포 결과는 오래된 산출물일 수 있다. 이 경우 pipeline success는 실제 서비스 성공을 보장하지 않는다.
이런 유형의 문제는 특히 self-hosted agent에서 Docker socket을 mount해 쓰는 경우 자주 생길 수 있다.
docker socket mounted
-> agent container can start sibling containers
-> sibling containers resolve bind mounts on the host
-> host path and container path must be aligned
복구 방법
복구의 핵심은 단순했다.
NAS host의 /azp/_work를 agent 컨테이너의 /azp/_work에 그대로 bind mount한다.
운영 기준 실행 구조는 아래처럼 정리했다.
sudo mkdir -p /azp/_work
sudo chmod 700 /azp /azp/_work
sudo docker run -d \
--name hwmoon-azp-agent \
--restart unless-stopped \
--env-file /volume1/docker/hwmoon-azp-agent/.env \
-e AGENT_ALLOW_RUNASROOT=1 \
-e DOCKER_API_VERSION=1.43 \
-e AZP_URL="https://dev.azure.com/example-org" \
-e AZP_POOL="Default" \
-e AZP_AGENT_NAME="synology-article-agent" \
-e AZP_WORK="_work" \
-e TARGETARCH="linux-x64" \
-v /azp/_work:/azp/_work \
-v /var/run/docker.sock:/var/run/docker.sock \
hwmoon-azp-agent:local
실제 PAT 값은 .env에만 둔다. 글, repository, pipeline log에 남기지 않는다.
정상 mount는 아래처럼 보여야 한다.
sudo docker inspect hwmoon-azp-agent \
--format '{{range .Mounts}}{{println .Source "->" .Destination "type=" .Type}}{{end}}'
기대값:
/azp/_work -> /azp/_work type= bind
/var/run/docker.sock -> /var/run/docker.sock type= bind
여기서 중요한 점은 named volume을 쓰지 않는 것이다.
# risky for this deployment pattern
volumes:
- hwmoon-azp-work:/azp/_work
이 구조는 일반적인 agent job에는 동작할 수 있다. 하지만 Static Web Apps deploy task처럼 Docker socket을 통해 sibling deployment container를 실행하는 경우에는 host path mismatch를 만들 수 있다.
재발 방지
이번 복구 후에는 NAS에 저장된 compose 파일도 현재 정상 실행값과 맞췄다. 실행 중인 컨테이너만 고치면, 나중에 DSM Container Manager나 compose로 재빌드할 때 같은 문제가 다시 생길 수 있기 때문이다.
재발 방지 체크리스트는 아래다.
| Check | Expected |
|---|---|
| Agent work mount | /azp/_work -> /azp/_work |
| Docker socket mount | /var/run/docker.sock -> /var/run/docker.sock |
| Deploy helper container | exited state may remain, but route must be live |
| Pipeline result | succeeded |
| Public route check | HTTP 200 |
| Sitemap check | new route included |
배포 후에는 pipeline 성공만 보지 않고 실제 공개 URL까지 확인한다.
curl -I https://news.example.com/kr/example-article-slug/
curl -I https://news.example.com/sitemap.xml
정적 사이트 배포에서는 특히 이 검증이 중요하다. 빌드 결과가 맞아도, CDN이나 배포 task가 다른 산출물을 올리면 사용자가 보는 화면은 다를 수 있다.
운영 관점에서 배운 점
이번 이슈의 교훈은 세 가지다.
첫째, self-hosted agent에서 Docker socket을 mount하면 agent 컨테이너 하나만 보는 것으로는 충분하지 않다. 그 agent가 실행하는 다른 컨테이너의 관점까지 봐야 한다.
둘째, pipeline success와 production success는 다르다. 마지막에는 반드시 공개 URL, sitemap, 주요 route를 확인해야 한다.
셋째, 임시 복구와 영구 복구는 다르다. 실행 중인 컨테이너를 고친 뒤에도 compose file, runbook, 운영 문서를 같이 고쳐야 다음 재시작 때 같은 문제가 반복되지 않는다.
이번 장애는 크지는 않았지만 좋은 운영 훈련이었다. NAS self-hosted agent는 비용 측면에서 매력적이고, 초기 자동화에는 충분히 강력하다. 다만 Docker 기반 배포 task를 같이 쓸 때는 host path, container path, sibling container의 관계를 명확히 이해해야 한다.
작은 자동화에서도 결국 운영의 기본은 같다.
빌드가 됐는가?
배포가 됐는가?
사용자가 보는 URL이 열리는가?
다음 재시작 후에도 같은 설정이 유지되는가?
이번에는 마지막 두 질문이 문제를 찾아냈다.
시리즈
Publisher Infrastructure
전체 8편 중 3편입니다. 관련 구축 기록을 순서대로 모아 전체 맥락을 쉽게 확인할 수 있습니다.
- 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 자가복구 체계 구축 기록