BuildFailed로 CI가 전부 터졌는데, 원인은 결제 잠금이었다

 ・ 5

photo by Cristina Gottardi(https://unsplash.com/@cristina_gottardi?utm_source=templater_proxy&utm_medium=referral) on Unsplash

CI가 갑자기 전부 빨간불이 뜨면, 대부분 코드나 설정을 먼저 의심하잖아요. 그런데 이번에는 코드도, YAML도, runner도 아니었어요. 오늘은 개발 문제가 아니라 운영 문제에 가까운 삽질을 했거든요.

시작은 단순했어요. 우리 조직의 여러 private repo에서 GitHub Actions CI가 전부 실패하고 있었어요. 처음에는 self-hosted runner 문제라고 생각했죠. 최근에 로컬 Mac mini를 GitHub Actions runner로 붙였고, 여러 repo의 CI를 self-hosted runner로 돌리도록 바꿨거든요.

그런데 증상이 이상했어요.

보통 runner에 문제가 있으면 job이 만들어진 뒤에 queued 상태로 멈추거나, "matching runner가 없다"는 식으로 실패해요. 그런데 이번에는 job 자체가 없었어요.

대표 run 상태는 이랬어요.

conclusion: startup_failure
workflowName: ""
path: BuildFailed
jobs: []
logs: 404

로그도 없었어요. 정확히는 로그가 생기기 전에 죽은 거였죠.

처음 의심한 것: self-hosted runner 설정#

가장 먼저 의심한 건 로컬 runner였어요.

실제로 과거 설정에는 문제가 있었거든요. runner가 기본 _work 폴더를 쓰고 있었고, 외장 SSD에 작업 디렉터리를 두려면 --work 옵션을 줘야 했어요.

현재는 아래처럼 고쳐져 있었어요.

workFolder: /mnt/ssd/actions-work/my-org

runner도 online 상태였고요.

runner: local-mac-mini
status: online
busy: false
labels:
- self-hosted
- macOS
- ARM64
- local
- mac-mini

그런데도 문제가 계속됐어요.

그래서 self-hosted runner를 아예 쓰지 않는, 아주 단순한 smoke workflow도 만들어봤어요.

name: Actions Smoke Test
 
on:
  push:
    branches:
      - test/actions-smoke
  workflow_dispatch:
 
jobs:
  smoke:
    runs-on: ubuntu-latest
    steps:
      - name: Print smoke marker
        run: echo "actions-smoke-ok"

그런데 이것도 똑같이 실패했어요.

conclusion: startup_failure
workflowName: ""
path: BuildFailed
jobs: []

이 시점에서 runner만의 문제는 아니라고 봐야 했어요. ubuntu-latest도 runner에 도달하기 전에 죽었으니까요.

두 번째 의심: Enterprise / Organization runner group 꼬임#

그다음에는 GitHub Enterprise 설정을 의심했어요.

최근 Enterprise 사용이 끝났는데, Organization 쪽 Actions runner group 화면에 이런 경고가 떠 있었거든요.

Runner group Default already exists at the Enterprise level.
Please consider renaming this group.

이건 충분히 수상했어요.

organization에는 runner group Default가 있고, Enterprise level에도 Default가 남아 있는 것처럼 보였어요. 게다가 API로 보면 Default runner group이 두 개처럼 보였고요.

id 1: Default, visibility=all
id 3: Default, visibility=selected

그래서 한동안은 "Enterprise가 끝났는데 runner group policy가 남아서 org Actions가 꼬인 것 아닌가?"라고 생각했어요.

이 가설도 나쁘진 않았어요. 실제로 Enterprise 종료 후 잔여 설정은 정리하는 게 맞고, runner group 이름도 Default보다는 local-mac-mini 같은 구분되는 이름으로 바꾸는 게 좋거든요.

하지만 이것도 결정적 원인은 아니었어요. 왜냐하면 private repo뿐 아니라, GitHub-hosted runner를 쓰는 smoke workflow까지 BuildFailed로 죽었기 때문이에요. runner group이 원인이라면 적어도 ubuntu-latest workflow는 다르게 실패해야 했거든요.

GitHub Support 답변: billing lock#

결국 GitHub Support에 문의했어요. 답변은 의외로 간단했어요.

The BuildFailed behavior occurs when an account's billing is locked,
blocking access to billable features, like Actions.

즉 원인은 코드도, YAML도, runner도 아니었어요. billing lock 때문에 Actions 자체가 막힌 상태였던 거죠.

이 설명이면 모든 증상이 맞아떨어져요.

  • 여러 private repo에서 동시에 실패
  • self-hosted runner도 실패
  • ubuntu-latest도 실패
  • job이 생성되지 않음
  • logs endpoint가 404
  • workflow name이 비어 있음
  • BuildFailed라는 synthetic workflow가 생김

GitHub Actions는 private repo에서 billable feature(과금 대상 기능)로 취급돼요. billing account가 locked 상태면 runner 종류와 상관없이 workflow startup 단계에서 막힐 수 있는 거예요.

문제가 된 건 organization 계정이었어요#

추가로 Support에 확인한 결과, 문제가 된 billing account는 개인 계정이 아니라 organization 계정이었어요.

이걸로 범위가 확정됐어요.

처음에는 개인 계정 문제일 수도, 종료된 Enterprise 문제일 수도, organization 문제일 수도 있었어요. 그런데 최종적으로는 organization의 billing lock이었죠.

Support는 billing team으로 새 티켓을 열어달라고 안내해줬어요.

이번에 배운 것#

이번 삽질에서 얻은 교훈은 꽤 명확해요.

1. BuildFailed + startup_failure + jobs=[]는 runner 문제가 아닐 수 있어요#

runner 문제라면 보통 job은 생겨요. 예를 들면 이런 형태가 되죠.

queued
waiting for runner
no matching runner

그런데 이번처럼:

workflowName: ""
path: BuildFailed
jobs: []
logs: 404

이렇게 나온다면 workflow가 runner에 도달하기 전에 죽은 거예요. 이 경우 YAML, runner label, runner online 여부만 붙잡고 있으면 시간을 많이 낭비할 수 있어요.

2. private repo Actions는 billing 상태의 영향을 받아요#

self-hosted runner를 써도 Actions 자체는 GitHub의 billable feature 영역에 걸려요. 그래서 "내 runner에서 도는데 왜 billing이 문제지?"라고 생각하면 안 돼요.

Actions orchestration(작업을 큐에 넣고 runner에 배정하는 과정) 자체가 GitHub feature이기 때문이에요.

3. Enterprise 종료 후에는 billing/account scope를 꼭 확인해야 해요#

Enterprise를 종료했다고 해서 모든 잔여 상태가 깔끔히 사라지는 건 아닌 것 같아요. 적어도 이번에는 이 전부가 얽혀 있었거든요.

  • Enterprise 종료
  • Organization billing
  • Actions runner group
  • private repo Actions
  • Support ticket routing

Enterprise가 끝났다면 다음을 확인해보세요.

Personal billing
Organization billing
Enterprise billing
Actions access
Runner groups
Outstanding balance
Seats
Billing cycle

특히 결제정보를 추가하기 전에 "얼마가 즉시 청구되는지" 확인하는 게 중요해요.

최종 정리#

이번 문제의 root cause는 이렇게 정리할 수 있어요.

organization billing lock
→ GitHub Actions billable feature blocked
→ all private repo workflows fail at startup
→ synthetic BuildFailed workflow
→ jobs=[]
→ logs=404

다음 액션은 runner를 다시 설치하는 게 아니라 billing team에 확인하는 거예요. 확인해야 할 핵심은 이 정도예요.

  1. 미납 잔액(outstanding balance)이 있는지, 있다면 정확한 금액은 얼마인지
  2. 결제정보를 추가하면 즉시 얼마가 청구되는지 (월결제 전환이 가능한지 포함)
  3. 종료된 Enterprise billing scope와 아직 연결돼 있는지

오늘의 결론은 이거예요. CI가 터졌다고 항상 코드나 runner 문제는 아니에요. 가끔은 결제 상태가 가장 낮은 레벨의 인프라 장애처럼 나타나거든요. 증상이 아무리 봐도 인프라 같은데 아무것도 안 잡힌다면, billing 상태도 한 번 열어보세요.


When you have eliminated the impossible, whatever remains, however improbable, must be the truth.

— Arthur Conan Doyle


다른 글
사업자 등록 전에 확인하고 넘어가야 할 것들 커버 이미지
 ・ 3

사업자 등록 전에 확인하고 넘어가야 할 것들

새 회사 입사 전, 개발자가 준비하면 좋은 것들 커버 이미지
 ・ 5

새 회사 입사 전, 개발자가 준비하면 좋은 것들

특허, 기업가 정신, 그리고 IP 창업 입문 커버 이미지
 ・ 8

특허, 기업가 정신, 그리고 IP 창업 입문