홈시리즈멘토링

© 2026 정기창. All rights reserved.

본 블로그의 콘텐츠는 CC BY-NC-SA 4.0 라이선스를 따릅니다.

☕후원하기소개JSON Formatter러닝 대기질개인정보처리방침이용약관

© 2026 정기창. All rights reserved.

콘텐츠: CC BY-NC-SA 4.0

☕후원하기
소개|JSON Formatter|러닝 대기질|개인정보처리방침|이용약관

집에서 놀던 노트북 두 대를 CI 러너로 만들었습니다 — 라벨로 폴백을 만드는 법

정기창·2026년 8월 11일

CI를 개발용 맥북으로 옮긴 뒤 며칠은 잘 돌았습니다. 문제는 그다음이었습니다. PR을 서너 개 동시에 열어두는 제 작업 방식에서 러너 한 대는 곧 병목이었습니다. 작업들이 줄을 서기 시작했고, 그 줄은 제가 코드를 쓰는 바로 그 노트북 위에서 만들어지고 있었습니다.

CI가 도는 동안 제 맥북은 팬을 돌렸고, 저는 그 옆에서 같은 CPU로 편집기를 굴렸습니다. 러너를 늘려야 했고, 무료로 늘려야 했습니다. 집을 둘러보니 안 쓰는 노트북이 두 대 있었습니다.

1년 넘게 꺼져 있던 홈랩 서버

한 대는 오래된 씽크패드였습니다. 2015년쯤 나온 2코어 4스레드 모델에 메모리는 7.5GB. 여기에 언젠가 Proxmox를 깔아두고 홈 서버라고 부르다가, 1년 넘게 꺼둔 채로 잊고 있었습니다.

켜보니 상태가 흥미로웠습니다. 커널이 21개월 전에 멈춰 있었는데, 단순히 업데이트를 안 한 게 아니라 업데이트 경로 자체가 끊겨 있었습니다. 구독형 저장소가 활성화된 채 구독은 없는 상태라, 패키지 목록을 갱신하려 할 때마다 인증 오류가 나고 있었습니다. 무료 저장소로 바꿔주자 밀려 있던 업데이트가 103개에서 240개로 오히려 늘었습니다. 막혀 있던 것들이 그제야 보이기 시작한 것입니다.

이 대목이 인상적이었습니다. 시스템이 조용한 것과 정상인 것은 다릅니다. 이 서버는 1년 동안 아무 불평도 하지 않았지만, 그 침묵은 건강해서가 아니라 갱신 자체를 포기했기 때문이었습니다.

컨테이너가 아니라 가상 머신으로

Proxmox에서는 가벼운 리눅스 컨테이너와 완전한 가상 머신 중에 고를 수 있습니다. 자원이 빠듯하니 컨테이너가 자연스러워 보였지만 가상 머신을 골랐습니다. CI 작업이 도커를 쓸 수 있어야 했고, 원격 접속용 네트워크 도구도 얹어야 했는데, 이런 것들은 컨테이너 안에서 권한 문제로 번거로워지는 경우가 많습니다. 자원을 조금 더 쓰더라도 이상한 곳에서 막히지 않는 쪽을 택했습니다.

안 쓰던 가상 머신 하나를 지워 공간을 확보하고, 우분투 가상 머신을 만들어 러너를 등록했습니다. 노트북이라 뚜껑을 닫으면 잠들 위험이 있어서 뚜껑 감지를 무시하도록 되어 있는지도 확인했습니다.

메모리가 설계를 결정했습니다

가상 머신에 실제로 잡힌 메모리는 4.8GB였습니다. 그런데 제 백엔드 테스트는 6GB 힙을 요구합니다. 이 두 숫자를 나란히 놓으면 결론은 하나입니다. 이 머신에는 무거운 작업을 보내면 안 됩니다. 보내는 순간 메모리 부족으로 죽고, CI는 빨간불이 됩니다.

문제는 GitHub Actions에 "이 작업은 메모리가 8GB 필요합니다"라고 선언하는 방법이 없다는 것입니다. 러너를 고르는 기준은 오직 라벨, 그러니까 러너에 붙여둔 이름표뿐입니다.

그래서 능력을 라벨로 표현하기로 했습니다. 모든 자가 러너에 light를 붙이고, 메모리가 넉넉한 맥북에만 heavy를 추가로 붙였습니다. 가벼운 작업은 light를, 무거운 작업은 heavy를 요구하게 해두면, 씽크패드에는 무거운 작업이 배정될 경로 자체가 없어집니다. 실행 중에 판단해서 거르는 게 아니라 구조적으로 도달할 수 없게 만드는 쪽이라, 실수할 여지가 없습니다.

라벨 하나로 폴백이 생깁니다

앞 편에서 GitHub Actions에는 자동 폴백이 없다고 적었습니다. 그런데 러너를 여러 대 붙이고 나니, 폴백처럼 동작하는 성질이 공짜로 따라왔습니다.

핵심은 꺼진 러너를 GitHub이 알아서 배정 대상에서 뺀다는 사실입니다. 그러니 여러 머신에 같은 라벨을 붙여두기만 하면, 켜져 있는 머신들끼리 먼저 잡는 쪽이 가져가고 꺼진 머신은 조용히 빠집니다. "윈도우가 안 되면 씽크패드, 그것도 안 되면 맥북"이라는 순서를 제가 코드로 쓸 필요가 없었습니다. 라벨을 공유하는 것만으로 그 의미가 성립했습니다.

대신 포기한 것이 있습니다. 엄격한 우선순위는 만들 수 없습니다. "항상 씽크패드를 먼저 쓰고 걔가 바쁠 때만 맥북"같은 것은 이 방식으로 표현되지 않습니다. 순수한 선착순입니다. 다만 곰곰이 생각해보니 이게 나쁘지 않았습니다. 어차피 제 목적은 여러 PR을 동시에 처리하는 것이었고, 그렇다면 가용한 머신 전부가 동시에 일을 받는 편이 오히려 맞습니다. 못 하는 것을 아쉬워하다가, 안 해도 되는 것이었음을 깨달은 경우였습니다.

윈도우 노트북은 리눅스를 얹지 않았습니다

두 번째 노트북은 윈도우 기기였습니다. 16GB 메모리에 4코어 8스레드로, 하드웨어만 보면 씽크패드보다 훨씬 좋았습니다.

여기서는 리눅스 호환 계층을 얹을지 윈도우에 그대로 설치할지를 골라야 했습니다. 확인해보니 이 머신에 보낼 가벼운 작업 세 개는 셸 스크립트에 의존하지 않고 단순한 패키지 명령만 쓰고 있었습니다. 그렇다면 계층을 하나 더 쌓을 이유가 없었습니다. 윈도우에 그대로 설치하는 것이 가장 짧은 경로였습니다.

맥북을 지키는 방법 — 순서가 아니라 감시

남은 걱정은 맥북이었습니다. 개발 본체이자 가장 강력한 러너라, 제가 한창 작업 중일 때 무거운 CI가 배정되면 곤란합니다.

앞서 봤듯 배정 순서로는 이걸 제어할 수 없습니다. 그래서 방향을 바꿨습니다. 2분마다 시스템 메모리 압력을 확인해서, 여유가 없어지면 맥북의 러너 슬롯을 스스로 내리는 감시 스크립트를 붙였습니다. 슬롯이 내려가면 GitHub 눈에는 꺼진 러너로 보이고, 그러면 앞서 말한 성질에 따라 다른 머신이 그 일을 가져갑니다. 배정 규칙을 건드리지 않고 가용성만 조절해서 같은 목적을 달성한 셈입니다.

한 가지 안전장치를 뒀습니다. 작업을 실행 중인 슬롯은 내리지 않습니다. 메모리가 급하다고 돌던 CI를 죽이면 그건 빨간불이 되고, 저는 멀쩡한 코드를 의심하게 됩니다.

여기서 맥 특유의 함정을 하나 만났습니다. 서비스를 내리는 명령을 실행했는데 잠시 뒤 다시 살아나 있었습니다. 이 명령만으로는 영구 비활성 플래그가 꺼지지 않아 시스템이 곧 되살려버리는 것이었습니다. 비활성으로 표시한 뒤에 내려야 실제로 내려갑니다. 이걸 모르면 "분명히 껐는데 계속 일을 받아 간다"는 유령 현상을 보게 됩니다.

그리고 자동 전환기

마지막으로 언제 자가 러너로 넘길지를 판단하는 장치를 붙였습니다. 무료 사용량을 주기적으로 확인해서 90%에 닿으면 자가 러너 쪽으로 변수를 넘기고, 사용량이 80% 아래로 떨어지면 다시 GitHub 쪽으로 되돌리는 방식입니다. 두 기준을 다르게 둔 이유는 경계선 근처에서 왔다 갔다 하는 것을 막기 위해서입니다.

이걸 만들고 나서 바로 실수를 하나 했습니다. 검증을 하려고 손으로 자가 러너 모드로 바꿔뒀는데, 5분 뒤에 보니 원래대로 돌아가 있었습니다. 자동 전환기가 "사용량이 낮으니 되돌려야지"라고 판단한 것입니다. 자동화가 제 손을 덮어쓰고 있었습니다. 결국 사람이 개입한 동안에는 자동 판단을 멈추는 잠금장치를 따로 만들어야 했습니다.

그래서 러너 네 대가 됐습니다

GitHub이 관리하는 머신 하나에 제 맥북 두 슬롯, 씽크패드, 윈도우 노트북까지. 무료 사용량이 떨어지면 자동으로 제 기계들로 넘어가고, 메모리가 부족하면 알아서 서로 넘겨주는 함대가 만들어졌습니다.

그런데 이 시점에도 저는 여전히 왜 무료 사용량이 부족했는지를 모르고 있었습니다. 모른 채로 대비만 정교하게 쌓아 올린 것입니다. 게다가 노트북 두 대를 하루 종일 켜두려니 이번에는 전기가 아까워졌습니다. 다음 편에서 저는 이 노트북들을 평소엔 재워두고 필요할 때만 깨우려다가, 결국 밤중에 전원 버튼을 누르러 가게 됩니다.

self-hosted runnerProxmoxGitHub Actions홈랩CI인프라

관련 글

CI가 전부 멈췄고, 저는 원인을 확인하지 않았습니다 — 30분 만에 끝난 대응의 함정

GitHub Actions hosted 러너가 새벽에 전면 차단됐습니다. 변수 한 줄과 self-hosted 러너로 30분 만에 우회했지만, 에러 메시지를 원인으로 착각한 탓에 진짜 이유는 2주 뒤에야 알게 됐습니다.

관련도 97%

다 만들어놓고 전부 껐습니다 — 쿼터를 태운 범인은 macOS 10배 배수였습니다

사용량을 총량으로만 보면 원인이 안 보입니다. 저장소별 SKU로 분해하자 raw 99.4분짜리 macOS 작업이 994분으로 청구돼 쿼터의 절반을 태우고 있었습니다. 2주간 만든 러너 함대는 그래서 전부 껐습니다.

관련도 96%

혼자 개발하는데 CI가 왜 필요했나 — 편의 기능인 줄 알았는데 임계 경로였습니다

1인 개발자에게 CI는 사치일까요. 어느 날 CI가 통째로 멈추고 나서야, 그것이 테스트를 대신 돌려주는 편의 기능이 아니라 머지 자체를 막는 임계 경로였다는 것을 알게 됐습니다.

관련도 94%