OpenClaw 셀프 호스팅: 돈 낭비의 함정과 두 가지 조용한 실패
요약
OpenClaw를 셀프 호스팅하며 겪은 OpenRouter의 비용 관리 함정과 시스템 장애 사례를 다룹니다. OpenRouter의 자동 라우팅 기능이 의도치 않은 비용을 발생시키는 방식과 이를 방지하기 위한 설정법을 설명합니다.
핵심 포인트
- OpenRouter BYOK 설정 시 자동 폴백으로 인해 의도치 않은 크레딧 소모 가능
- 비용 최적화를 위해 'provider.only' 파라미터를 사용하여 특정 제공업체로 고정 권장
- 모델 제공업체별 가격 차이가 매우 크므로 슬러그(slug) 기반의 라우팅 설정 필요
- API 지출 제한 설정 및 시스템 모니터링의 중요성 강조
저는 Hetzner CAX ARM VPS에서 OpenClaw를 실행하고 있습니다. 이 서비스는 Signal을 통해 저에게 아침 언론 리뷰를 전달해 줍니다. 이 환경에서 겪은 세 가지 주의 사항을 기록해 둘 가치가 있습니다. 하나는 모델 라우팅 (model-routing) 레이어에서의 돈 낭비 함정이고, 나머지 둘은 각각 브리핑을 며칠 동안 중단되게 만든 조용한 실패 (silent failures) 사례입니다. 혹시 같은 문제를 겪고 계실 분들을 위해 공유합니다.
OpenRouter는 당신이 선택하지 않은 제공업체에 크레딧을 사용할 수 있습니다
저는 여러 모델로 들어가는 단일 관문으로 OpenRouter를 사용합니다. 이 서비스의 BYOK (bring-your-own-key, 직접 키 가져오기) 기능에는 함정이 있습니다. 특정 모델에 대해 본인의 OpenAI 키를 추가하고 "Always use for this provider (이 제공업체에 항상 사용)"를 활성화하면, 이를 OpenRouter 크레딧을 절대 사용하지 않는다는 의미로 오해하기 쉽습니다. 하지만 실제로는 그렇지 않습니다.
해당 토글은 오직 해당 제공업체에 대해 당신의 키를 사용한다는 것만을 보장합니다. 당신의 키가 실패하거나 사용할 수 없을 때, 동일한 모델을 제공하는 다른 제공업체로 OpenRouter가 라우팅(routing)하는 것을 막지는 않습니다. 그리고 그 폴백 (fallback) 과정에서 당신이 선택하지 않은 제공업체에 대해 토큰당 과금 방식으로 OpenRouter 크레딧을 소모하게 됩니다. 설계된 대로 작동하는 것이지만, 토글을 켰을 때 당신이 머릿속으로 생각했던 설계는 아닐 것입니다.
제가 원했던 대로 작동하는 설정은 provider.only 라우팅 파라미터 (param)입니다. 이는 OpenRouter에게 폴백하는 대신 요청을 실패시키라고 지시합니다:
"provider": { "only": ["your-provider-slug"] }
BYOK를 사용하지 않더라도 저를 놀라게 했던 부분은, 단일 모델이라도 제공업체에 따라 가격 범위가 매우 넓다는 점이었습니다. 제가 실제로 실행 중인 openai/gpt-oss-120b를 GET /api/v1/models/openai/gpt-oss-120b/endpoints를 통해 확인해 보니, 백만 토큰당 약 $0.039에서 $0.95 사이에서 작동했습니다. 기본 라우팅은 가격과 가용성의 자체적인 조합을 최적화하며, 조용히 비싼 쪽으로 당신을 안내할 수 있습니다.
그래서 저는 이를 고정(pinned)했습니다. OpenClaw에서 라우팅 파라미터는 models.providers.openrouter.params.provider 아래에 위치합니다:
"provider": { "only": ["deepinfra", "dekallm", "novita"], "sort": "price" }
이제 OpenRouter는 이 세 가지만 사용하며, 가장 저렴한 순서대로 정렬합니다. 또한 실패가 발생하면 비싼 제공업체로 격상(escalating)되는 대신, 저의 무료 폴백(fallback)으로 전환됩니다. 제가 몇 분을 허비하게 만든 한 가지 주의사항은, only 필드에는 표시 이름(display name)이 아닌 제공업체의 _슬러그(slug)_를 입력해야 한다는 점입니다. 슬러그는 각 엔드포인트의 tag 필드에서 / 앞부분에 해당하는 값입니다.
이와 상관없이 설정해둘 가치가 있는 두 가지 안전장치가 있습니다:
- OpenRouter API 키는 기본적으로 지출 제한(spend limit)이 없습니다 — 제 키는 `
브리핑은 조용해졌다. 게이트웨이는 정상적으로 작동했고, 건강 상태도 좋았지만 signal-cli는 계속 다운되었다. 충돌은 메시지 암호화(encryption) 과정 중 libsignal의 네이티브 JNI 코드 깊은 곳에서 발생한 SIGSEGV를 가리켰다 — 생성은 문제없이 작동했지만, 전송할 때마다 데몬이 종료되었고 워치독(watchdog)이 계속 재시작시키면서 같은 벽에 부딪혔다.
근본 원인은 ARM64 패키징 격차이다. signal-cli의 번들된 libsignal-client jar는 Linux x86 및 macOS ARM용 네이티브 라이브러리를 포함하지만, Linux ARM64용은 아니다. ARM 장치에서는 일치하지 않는 libsignal_jni.so로 폴백(fallback)하여 JNI 핸들을 손상시키고 암호화 경로에서 세그폴트(segfault)가 발생한다. 나는 먼저 JVM 플래그에 시간을 썼다 — 인터프리터 전용(-Xint), 다른 가비지 컬렉터, 압축된 oops 비활성화 등 — 하지만 모든 것이 여전히 충돌했다. 이는 JIT나 GC 문제가 아니다. 네이티브 라이브러리가 단순히 플랫폼에 맞지 않는 것이다.
작동한 방법: 수동으로 네이티브 빌드를 고치려 하지 말고, 올바른 ARM64 바이너리를 제공하는 컨테이너에서 signal-cli를 실행하는 것이다 — bbernhard/signal-cli-rest-api. 기존 계정 데이터를 마운트하여 재페어링이나 안전 번호 변경이 없도록 한다:
docker run -d --name signal-daemon --restart unless-stopped --no-healthcheck \
-p 127.0.0.1:8080:8080 \
-e XDG_DATA_HOME=/data -e HOME=/tmp \
...
그런 다음 channels.signal.autoStart를 false로, 그리고 channels.signal.httpUrl을 http://127.0.0.1:8080으로 설정하여 OpenClaw가 손상된 로컬 바이너리를 생성하는 대신 컨테이너에 연결되도록 한다. 전송이 충돌하는 것을 멈췄고, 직접 전송 테스트는 SUCCESS로 돌아왔다. 업데이트는 이제 docker pull이다.
아직 해결되지 않은 사소한 문제: 컨테이너 내에서는 수신 메시지에 대한 답장이 받는 사람을 잘 해결하지만, 선제적인(proactive) 예약 전송(크론
자동 업데이트가 단 하나의 오류도 없이 예약된 작업(scheduled jobs)을 중단시킬 수 있습니다
브리핑은 다시 조용해졌습니다. 하지만 아무것도 충돌(crash)하지 않았습니다. 게이트웨이는 정상이었고, systemctl status는 active (running)를 나타냈으며, 재시작 횟수도 낮았습니다. 다만 일일 크론(cron) 작업이 실행되지 않을 뿐이었습니다. 오류도 없었고, 실행 로그(run-log) 행도 남지 않았습니다. 이틀이 지나서야 저는 이를 알아차렸습니다.
원인은 다음과 같습니다: OpenClaw가 디스크에서 자동 업데이트를 수행하면서 버전 업데이트(bump)의 일환으로 크론 저장소(cron store)를 마이그레이션(migration)했습니다. 기능별 JSON 파일들이 하나의 ~/.openclaw/state/openclaw.sqlite로 통합되었고, 기존 파일들은 *.migrated로 이름이 변경되었습니다. 하지만 실행 중인 프로세스를 재시작하지는 않았습니다. 메모리 내(in-memory)의 기존 스케줄러는 이제 이름이 바뀐 jobs.json을 여전히 가리키고 있었고, 해당 파일은 더 이상 존재하지 않았습니다. 디스크에는 새로운 코드가, 메모리에는 오래된 코드가 있었으며, 저장소는 두 가지 모두의 발밑에서 옮겨져 버린 상태였습니다.
이 현상은 이름을 붙일 가치가 있습니다: 프로세스를 재시작하지 않고 상태(state)를 마이그레이션하는 자동 업데이트 데몬(daemon)은 조용히 실패합니다. 자동 업데이트 상황에서 "어제는 잘 작동했다"는 말은 아무런 의미가 없습니다. 최신 버전의 OpenClaw는 업데이트 후 재시작 경로를 제공하지만, 폴백(fallback) 메커니즘이 저장소 마이그레이션을 제대로 포착하지 못할 수도 있으며, 저는 브리핑의 운명을 거기에 걸고 싶지 않았습니다.
해결책은 프로세스가 아닌 결과를 감시하는 것입니다. 왜냐하면 프로세스는 내내 '정상(green)' 상태였기 때문입니다. 브리핑 예정 시간으로부터 한 시간 뒤에 실행되도록 설정된, 일일 systemd 타이머(timer) 기반의 작은 Python 표준 라이브러리(stdlib) 스크립트는 단 하나의 질문을 던집니다: "오늘 예상된 출력이 실제로 도착했는가?"
만약 누락되었다면, 스크립트는 동일한 Signal 채널을 통해 문제 내용과 해결 방법(sudo systemctl restart openclaw.service)을 메시지로 보내줍니다. 정상적인 날에는 아무것도 보내지 않습니다. "데몬이 작동 중인가"를 확인하는 것은 이틀 동안 계속 '정상'으로 표시되지만, "내가 원하는 일이 일어났는가"를 확인하는 방식은 문제 발생 당일 아침에 바로 잡아냅니다.
디버깅 중에 주의해야 할 실수(one footgun)가 하나 있습니다: 호스트에서 openclaw CLI를 실행하지 마세요. openclaw cron list, openclaw doctor 등 어떤 것이든 상관없습니다. CLI는 실행 중인 게이트웨이의 PID를 감지하고 자신의 프로세스를 시작하기 전에 해당 프로세스에 SIGTERM을 보내, 서비스를 20~30초 동안 중단시켜 버립니다. 크론 저장소를 직접 편집하거나, 전송을 위해 signal-cli의 JSON-RPC를 호출하며, 해당 서버(box)가 아닌 어디에서든 관리 작업을 수행하세요.
세 가지 사례 모두에서 나타나는 패턴은 다음과 같습니다. 상태가 괜찮다고 알려주는 요소들 — 토글(toggle), systemctl status, 워치독(watchdog) — 이 실제로 당신이 신경 쓰는 대상 바로 옆에 있는 무언가를 측정하고 있다는 점입니다. 매번 해결책은 결과를 관찰하고, 다시 되돌아갈 수 있는 수단을 유지하는 것이었습니다. 만약 당신이 자신의 서버(box)에서 OpenClaw를 실행하다가 네 번째 사례를 겪거나, 선제적 전송(proactive-send)의 공백에 대해 더 깔끔한 해결책을 찾았다면, 제게 알려주시기 바랍니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기