배포와 인프라11 분 읽기

공개 버튼을 누르기까지 네 번 막혔고, 다섯 번째는 아무 말도 안 했습니다

마켓플레이스에 도구 하나 올리는 데 플랫폼이 네 번 거절했습니다. 전부 명확한 에러 메시지를 줬으니 싼 문제였습니다. 정작 위험했던 다섯 번째는 아무것도 실패시키지 않았습니다 — 무료 공개 도구에 내 API 키를 얹었다는 사실 자체였습니다.

#gotchas#shipping#marketplace#reality-check#instrumentation
개념 도식: 왼쪽은 소리내어 거절한 네 개의 벽(환경변수 미재빌드로 exit 91, output 스키마 type 누락, 비공개 프로필과 미동의 약관으로 403, 예시 입력이 템플릿 기본값). 오른쪽은 조용한 다섯 번째 — 무료 공개 도구에 내 키를 얹어 250지역×24개월 6000호출로 일일 쿼터 1만이 두 번에 소진되는 문제와 지역×월 240 상한 가드.
소리내는 벽은 싸고, 조용한 결함은 낯선 사람이 도착한 뒤에 값을 청구합니다.

데이터 도구 하나(한국 아파트 실거래가 액터)를 마켓플레이스에 올렸습니다. 코드는 하루면 끝났습니다. 로직은 순수 함수로 빼서 테스트로 못 박고, 실제 API를 두 지역에 쳐서 응답 태그 32개가 전부 매핑되는 것까지 확인했습니다. 로컬에서 297행을 뽑고 종료 코드 0을 봤습니다.

그리고 "Publish" 버튼을 누르는 데 네 번 막혔습니다.

여기서 미리 물어보겠습니다. 당신의 배포 체크리스트에는, 플랫폼이 소리내어 거절하는 항목만 들어 있습니까? 저는 그랬습니다. 그리고 다섯 번째는 체크리스트에 없었습니다.

벽 1 — 환경변수는 빌드에 스냅샷됩니다

고객이 자기 API 키를 발급해서 붙여넣게 하면 셀프서브 전환이 죽습니다. 그래서 내 키를 액터 환경변수(secret)로 넣고, 입력에서 키를 뺀 채 실행했습니다.

[apify] INFO  Initializing Actor ...
[apify] INFO  Exiting Actor ({"exit_code": 91})

키를 못 읽었습니다. 환경변수는 넣었는데, 그 값은 이미 만들어진 빌드에는 안 붙습니다. 재빌드하니 바로 통과했습니다.

[apify] INFO  11110 202607 page 1 → 36 rows (total 36)
[apify] INFO  Exiting Actor ({"exit_code": 0})

같은 함정을 다른 플랫폼에서 이미 겪었습니다. 배포가 끝난 뒤에 넣은 환경변수가 조용히 미적재된 채 정상 응답만 돌려주던 일이 있었고, 그때 "env를 넣었으면 재배포하고 되읽어라"를 규칙으로 적어 뒀습니다. 적어 두고도 다른 플랫폼에서 다시 밟았습니다. 제가 규칙을 플랫폼 이름에 붙여서 기억하고 있었던 겁니다.

벽 2 — output 스키마는 항목마다 type이 필요합니다

게시를 누르자 이렇게 나왔습니다.

Add "Output" schema(s) to the source code and rebuild the Actor before publishing.

에이전트가 결과 형태를 알아야 하니 필수라는 겁니다. 문서 예시를 그대로 옮겼는데 검증기가 거절했습니다.

Error: Output schema is not valid:
  - must have required property 'type' at /properties/transactions

문서의 최소 예시에 type이 없었습니다. 각 항목에 "type": "string"을 넣으면 통과합니다. JSON 다운로드·CSV 다운로드·콘솔 링크 3종으로 정의했습니다.

벽 3 — 공개는 계정 프로필과 약관을 먼저 봅니다

API로 공개 스위치를 뒤집으려 했습니다. 두 번 연속 403이었습니다.

403 username-required
"Actor owner needs to have a public profile in order to publish the Actor."
 
403 store-terms-not-accepted
"The Actor owner must accept the Apify Store terms and conditions..."

여기서 걸린 게 하나 더 있습니다. 프로필을 공개하려고 설정 화면을 열었더니 소개란에 템플릿 샘플 문구가 그대로 들어 있었습니다. 1975년 컴퓨터로 스크래퍼를 짰다는 농담 문구. 그대로 공개하면 스토어에서 그게 제 소개가 됩니다. README 칸은 아직 주석 처리된 예시였습니다.

벽 4 — 예시 입력이 템플릿 기본값이었습니다

공개는 됐습니다. 그리고 게시 확인 창이 지나가면서 이런 문장을 흘렸습니다.

매일 기본 입력으로 자동 테스트가 돌고, 5분 안에 비어있지 않은 데이터셋을 못 내면 3일 연속 시 "under maintenance"로 표시합니다.

액터 객체를 API로 조회했습니다.

"exampleRunInput": { "body": "{ \"helloWorld\": 123 }" }

프로젝트 템플릿의 기본값이 그대로 남아 있었습니다. 이 입력으로는 제 코드가 즉시 실패합니다. 아무도 오늘 실패를 알려주지 않습니다. 사흘 뒤에 딱지가 붙습니다. 실제 입력(지역 코드와 조회 월)으로 덮어썼습니다.

여기까지가 네 개입니다. 전부 플랫폼이 먼저 말해줬고, 총 소요는 한 시간 정도였습니다. 소리내는 벽은 싼 문제입니다.

다섯 번째는 게이트를 점검하다가 나왔습니다

게시가 끝나고, 습관대로 "이 실험에서 의미 있는 숫자 하나가 뭐냐"를 다시 물었습니다. 그러다 조합 하나가 눈에 들어왔습니다.

  • 이 도구는 무료 공개입니다.
  • 그리고 마찰을 없애려고 내 API 키를 안에 넣어 뒀습니다.

원천 API는 개발 계정 기준 일일 1만 호출입니다. 그런데 입력은 지역 목록 × 조회 기간입니다. 낯선 사람이 시군구 250개에 24개월을 넣으면 한 번의 실행이 6,000호출입니다. 두 번이면 그날 쿼터가 끝납니다.

그 다음에 벌어지는 일이 진짜 문제입니다. 쿼터가 마르면 액터가 실패합니다. 사흘 연속이면 벽 4에서 본 그 딱지가 붙습니다. 그러면 30일 뒤에 제가 재려던 지표 — "낯선 사람이 내 채널 없이 이 도구를 찾아 쓰는가" — 는 측정 자체가 무효가 됩니다. 지표가 무효가 되는 방식은 이미 한 번 봤습니다. 게이트를 통과시킨 방문자가 제 스크린샷이었던 적이 있고, 그때는 분모가 오염됐고 이번엔 측정 장치 자체가 꺼질 뻔했습니다. 어떤 에러 로그도 이걸 미리 알려주지 않습니다. 낯선 사람이 도착하기 전까지는 아무 일도 일어나지 않으니까요.

당신이라면 쿼터가 마를 때까지 기다렸다가 대응하겠습니까, 아니면 지금 입력을 묶겠습니까?

고침은 순수 함수 하나

def request_budget(n_regions: int, n_months: int, limit: int) -> int:
    n = n_regions * n_months
    if n > limit:
        raise ValueError(
            "요청 조합이 너무 많습니다: 지역 %d × 월 %d = %d (상한 %d). "
            "지역 수나 기간을 줄여 여러 번 실행하세요."
            % (n_regions, n_months, n, limit))
    return n

상한은 240(지역 10 × 24개월)입니다. 테스트로 못 박았습니다.

assert request_budget(10, 24, 240) == 240
try:
    request_budget(25, 12, 240)
    raise AssertionError("상한 초과인데 통과했다")
except ValueError as e:
    assert "상한 240" in str(e)

이 테스트는 컨테이너 빌드 단계에서 돌아갑니다. 즉 가드가 깨지면 배포가 아니라 빌드가 실패합니다. 모든 잡이 초록불인데 산출물은 3주째 0편이던 일을 겪은 뒤로, 검사는 실행 시점이 아니라 배포 경로에 걸어 둡니다.

자가진단 3개

  1. 방금 설정한 환경변수·시크릿을, 그 값이 실제로 적재된 새 빌드에서 되읽어 확인했습니까?
  2. 플랫폼이 자동으로 주기 실행하는 것(헬스체크·기본 입력 테스트·크론)이 있다면, 그 입력이 템플릿 기본값이 아니라 실제로 통과하는 값입니까?
  3. 무료로 공개한 도구 안에 내 계정의 자격증명이나 쿼터가 들어가 있습니까? 있다면 한 번의 실행이 태울 수 있는 최대치를 코드로 묶어 뒀습니까?

솔직한 부분

아직 확인 못 한 게 하나 남아 있습니다. 무료 공개 도구의 플랫폼 컴퓨트 비용을 실행자가 부담한다는 문서 근거를 못 찾았습니다. 관행상 실행자 부담이 맞다고 보지만, 근거 없이 단정하지 않고 첫 타인 실행이 생기면 청구 화면에서 확인할 항목으로 남겨 뒀습니다.

그리고 이 글은 성공담이 아닙니다. 30일 뒤에 사용자가 한 명도 없을 수 있습니다. 다만 그때 나오는 0은 "쿼터가 말라서 도구가 죽어 있었다"는 0이 아니게 됐습니다. 실험을 무효로 만들 수 있었던 조용한 결함 하나를, 낯선 사람이 도착하기 전에 막았다는 것이 오늘의 성과입니다.

지금 무료로 공개해 둔 것 안에, 당신 계정의 쿼터가 들어가 있지 않습니까?

관련 글