Rust Axum + Cloudflare Tunnel로 policy-api.graxel.ai 운영하기
Oracle ARM 서버의 Rust Axum API를 Cloudflare Tunnel로 외부에 공개하고 무중단 배포까지 안정화한 실전 기록입니다.
policy-api.graxel.ai는 내혜택 서비스의 백엔드 정책 API입니다. Oracle ARM 24GB 서버 위에서 Rust Axum으로 동작하며, 외부 노출은 Cloudflare Tunnel을 통해 이루어집니다. 도메인 하나와 터널 하나로 공인 IP 없이 서비스를 운영한 3개월간의 기록을 공유합니다.
왜 Cloudflare Tunnel인가
Oracle 무료 티어 서버에 공인 IP가 붙어 있긴 하지만, 80/443 포트를 그대로 외부에 여는 순간 봇 스캐너의 먹잇감이 됩니다. NGINX + Let's Encrypt로 방어선을 치려다가, 어차피 Cloudflare를 CDN으로 쓰고 있으니 터널로 묶으면 더 깔끔하겠다는 판단으로 전환했습니다.
Cloudflare Tunnel 공식 문서에 나온 그대로 cloudflared 데몬을 systemd로 등록했고, 한 번 설정한 뒤로 3개월간 수정한 적이 없습니다.
설정 파일 일부
# /etc/cloudflared/config.yml
tunnel: policy-api
credentials-file: /etc/cloudflared/<tunnel-id>.json
ingress:
- hostname: policy-api.graxel.ai
service: http://localhost:8080
originRequest:
connectTimeout: 10s
keepAliveTimeout: 90s
- service: http_status:404
Rust Axum 서버 구조
API 서버는 전형적인 Axum 구조를 따릅니다. axum 0.7, tokio, sqlx, tower-http의 조합입니다. 단순하게 유지하는 것이 장애 원인 파악에 유리하다는 경험상의 원칙입니다.
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let pool = PgPoolOptions::new()
.max_connections(20)
.acquire_timeout(Duration::from_secs(3))
.connect(&env::var("DATABASE_URL")?)
.await?;
let app = Router::new()
.route("/health", get(health))
.route("/v1/policies/search", post(search))
.layer(TraceLayer::new_for_http())
.with_state(AppState { pool });
let listener = TcpListener::bind("127.0.0.1:8080").await?;
axum::serve(listener, app).await?;
Ok(())
}
max_connections=20은 Oracle 무료 티어 PostgreSQL의 연결 수 여유를 고려한 값입니다. 실제 모니터링 결과 피크 시간대에도 12~14개를 넘지 않았습니다.
무중단 배포를 만드는 방법
처음에는 단순히 systemctl restart policy-api로 껐다 켰습니다. 재시작 사이 2초간의 다운타임이 있었는데, Cloudflare Tunnel이 이 사이의 502를 사용자에게 그대로 노출한다는 사실을 알고 나서부터는 blue-green 방식으로 바꿨습니다.
- 포트 8080(blue), 8081(green) 두 개로 서비스를 동시에 올린다.
- 배포 시 새 버전을 idle 쪽에 먼저 띄우고
/health통과를 확인. - cloudflared ingress의 service 대상을 SIGHUP으로 교체.
- 30초 뒤 이전 버전 프로세스 종료.
이 방식으로 3개월간 5xx 비율이 0.01% 미만을 유지하고 있습니다.
SQLx 마이그레이션 — 가장 조심한 부분
정책 데이터는 11,600개가 넘기 때문에 ALTER TABLE 한 번이 수 분씩 걸릴 수 있습니다. 그래서 마이그레이션은 반드시 다음 규칙을 따릅니다.
컬럼은 항상 NULLABLE로 먼저 추가하고, 백필 배치를 따로 돌린 뒤, 마지막에 NOT NULL 제약을 건다.
이 원칙 덕분에 마이그레이션 중 트래픽을 끊지 않아도 되었습니다. SQLx의 sqlx migrate가 기본 제공하는 트랜잭션 래핑도 안전장치로 활용했습니다.
비용과 한계
Oracle ARM은 Always Free이고 Cloudflare Tunnel도 무료 플랜으로 충분합니다. 도메인 비용을 제외하면 사실상 0원에 가까운 운영입니다. 다만 Oracle 무료 티어의 네트워크 아웃바운드 제한이 월 10TB라 큰 응답을 자주 내려주는 API에는 부적합합니다. 다행히 정책 검색 응답은 평균 2KB 수준이라 여유가 충분합니다.
좀 더 상세한 인프라 선택지는 무료 인프라 조합에서 다뤘고, 내혜택 서비스 자체의 RAG 구조는 RAG 운영기에서 이어집니다. 질문은 문의로 보내주시면 답변 드립니다.
공유하기
이어 읽으면 좋은 글
같은 주제와 태그를 기준으로 GRAXEL 운영 맥락을 더 깊게 볼 수 있는 글입니다.
내혜택 RAG 시스템 운영기 — 11,600개 정책을 어떻게 검색 가능하게 만들었나
pgvector 하이브리드 검색과 Ollama 임베딩으로 11,600개 정부 혜택 정책을 검색하는 내혜택 서비스의 실제 운영 경험을 공유합니다.
1인 개발자 SaaS 모노레포 vs 멀티레포 — Graxel 운영 1년 후 다시 보는 결정
pnpm과 Turborepo로 구축한 모노레포 아키텍처가 1인 개발자에게 정말 정답이었을까요? 1년간의 뼈저린 운영 회고와 실패담.
Cloudflare Pages 무료 티어로 SaaS 시작하기 — 진짜 1년 비용 후기
1인 개발자가 Cloudflare Pages 무료 티어로 1년간 portal, myhyetaek 등 5개 서비스를 운영하며 지출한 실제 비용과 뼈아픈 실패담을 공개합니다.