무료 VM을 CI 러너로 쓰면 생기는 일
GitHub Actions 무료 분량을 아예 안 쓰려고 무료 가상서버를 러너로 붙였습니다. 분량은 0이 됐지만, 램 1GB에서 깨진 것이 일곱 가지였습니다.
사이트 두 개에 워크플로가 열네 개 있습니다. 배포, 데이터 수집, 알림, 백업, 점검. 대부분 하루 한 번 이상 돕니다.
GitHub Actions 무료 계정은 월 2,000분을 줍니다. 처음에는 넉넉해 보였는데, 워크플로가 늘고 브라우저 테스트가 붙으면서 한 번 돌 때 10분 넘게 쓰는 것이 생겼습니다. 남은 분량을 신경 쓰면서 크론 주기를 정하게 되더군요. 그건 이미 제약이 붙은 상태입니다.
그래서 셀프호스티드 러너로 옮겼습니다. 자기 서버에서 도는 워크플로는 무료 분량을 한 푼도 쓰지 않습니다. 서버는 클라우드 무료 티어를 썼습니다.
결과부터 말하면 분량은 0이 됐고, 대신 작은 서버에서 깨지는 것들을 일곱 개쯤 고쳐야 했습니다. 그 목록이 이 글의 알맹이입니다.
무료 티어는 생각보다 작다
무료로 받은 서버는 이 정도였습니다.
- CPU 2개
- 램 1GB
- 디스크 얼마간
ARM 인스턴스를 노렸지만 용량이 없어서 x86으로 내려갔습니다. 무료 티어는 인기가 많아 원하는 사양이 늘 비어 있지는 않습니다.
램 1GB. 이 숫자가 이후 벌어질 일의 대부분을 설명합니다. GitHub이 주는 러너는 보통 이보다 훨씬 넉넉하고, 우리가 짠 워크플로는 그 넉넉함을 전제로 하고 있었습니다.
가장 먼저 한 일은 스왑 4GB를 만든 것입니다. 이게 없으면 빌드 중간에 그냥 죽습니다.
옮기자마자 깨진 것들
runs-on을 한 줄 바꿨더니 워크플로가 줄줄이 실패했습니다. 하나씩 적습니다.
1. 브라우저 캐시가 거짓말을 했다. 테스트용 브라우저를 캐시해 두는 단계가 있었는데, 셀프호스티드에서는 브라우저가 애초에 디스크에 남아 있습니다. 그런데 캐시 단계는 "적중"이라고 판정하고 넘어가는데 실제 바이너리가 없는 경우가 생겼습니다. 테스트가 통째로 죽었습니다. 캐시 단계를 지우고 매번 설치하도록 바꿨습니다. 이미 있으면 몇 초면 끝납니다.
2. 기다리는 시간이 전부 짧았다. 화면이 그려지길 15초 기다리는 코드가 있었는데, 느린 서버에서는 15초를 넘겼습니다. 그 한 줄 때문에 배포 전체가 실패했습니다. 60초로 늘렸습니다. 비슷한 대기 시간이 여기저기 있어서 전부 훑었습니다.
3. 동시 실행 개수가 램을 넘겼다. 테스트를 4개씩 병렬로 돌리고 있었는데 램 1GB에서는 메모리 부족으로 죽습니다. 2개로 줄였습니다. 느려지지만 죽는 것보다는 낫습니다.
4. 빌드가 제한 시간을 넘겼다. 로컬에서 40초 걸리던 빌드가 2분을 넘겼습니다. 제한 시간을 넉넉하게 다시 잡았습니다.
5. 페이지 로딩 검사가 영영 안 끝났다. 이게 제일 오래 헤맸습니다. "네트워크가 조용해지면 다음으로"라는 조건으로 페이지를 여는 검사가 있었는데, 서버에서는 이 조건이 영영 만족되지 않았습니다. 시간을 90초로 늘려도 마찬가지였습니다.
원인은 광고와 분석 스크립트였습니다. 페이지가 광고 요청을 보내는데 그 요청들이 느린 서버에서 매달려 있으니 "네트워크가 조용해지는" 순간이 오지 않았던 것입니다. 검사 중에는 그 요청들을 아예 차단하도록 바꿨더니 바로 통과했습니다.
6. 동시에 여는 페이지 수도 문제였다. 6개를 한 번에 열면 서버 부하가 10까지 올라가면서 스와핑으로 기어갑니다. 2개로 줄였습니다.
7. 데이터 수집도 같은 병을 앓았다. 외부 사이트에서 자료를 긁어 오는 작업들이 있는데, 대기 시간이 짧아서 느린 서버에서는 절반쯤 실패했습니다. 이건 사실 원래 러너에서도 가끔 터지던 것이라, 옮기면서 드러난 셈입니다.
러너 하나로는 부족했다
고치고 나서 며칠 돌렸더니 새로운 문제가 생겼습니다. 작업이 줄을 섰습니다.
러너가 하나면 한 번에 한 작업만 돕니다. 아침에 크론 다섯 개가 몰리면 마지막 것은 몇 시간 뒤에 실행됩니다. "매일 아침 알림"이 점심에 오는 상황입니다.
게다가 무료 티어 CPU는 버스트 방식입니다. 평소 적게 쓰면 크레딧이 쌓이고, 몰아 쓰면 크레딧을 태웁니다. 하루 종일 굴렸더니 크레딧이 바닥나서 CPU 사용 가능량이 5%까지 떨어졌습니다. 나머지 95%는 남에게 양보하는 상태입니다. 이러면 뭘 해도 느립니다.
그래서 무료 티어 한도 안에서 서버를 하나 더 만들어 작업을 나눴습니다. 사이트별로 러너를 분리하니 서로 막지 않고, 각 서버의 CPU 크레딧도 여유가 생겼습니다.
접속이 막혔을 때를 대비해야 한다
한 번은 서버에 SSH가 안 됐습니다.
원인은 우분투 자동 보안 업데이트였습니다. SSH 패키지를 갱신하면서 접속 설정을 기본값으로 되살렸고, 제가 열어 둔 포트가 닫혔습니다. 서버는 멀쩡히 돌고 있는데 들어갈 수가 없는 상태입니다.
여기서 배운 것은 "서버에 못 들어가는 상황"을 미리 가정해야 한다는 것입니다. 대비는 두 겹으로 했습니다.
- 설정이 되살아나도 살아남게 접속 설정을 별도 파일로 고정
- SSH 없이 서버를 고칠 수 있는 비상구 — 러너가 살아 있다면 워크플로를 통해 명령을 보낼 수 있습니다. 상태 확인, 접속 복구, 패키지 갱신을 하는 워크플로를 만들어 뒀습니다
두 번째가 실제로 저를 구했습니다. 러너는 살아 있으니 GitHub에서 작업을 던지면 서버가 그걸 받아 실행합니다. 그 경로로 접속 설정을 되돌렸습니다.
그래서 할 만한가
정리하면 이렇습니다.
얻은 것
- CI 사용 분량 0 — 크론 주기를 마음대로 정할 수 있게 됨
- 백업·수집 같은 작업이 내 컴퓨터 상태와 무관하게 돎
- 서버 비용도 0
치른 값
- 워크플로 일곱 군데를 작은 서버에 맞게 고침
- 서버가 죽거나 접속이 막히는 상황을 스스로 책임져야 함
- 무료 티어 CPU 특성(버스트 크레딧)을 이해해야 함
추천하는 경우: 워크플로가 여러 개고, 브라우저 테스트처럼 시간을 많이 먹는 작업이 있고, 무료 분량을 신경 쓰느라 크론 주기를 타협하고 있다면 옮길 값어치가 있습니다.
말리는 경우: 워크플로가 한두 개면 무료 분량으로 충분합니다. 서버를 하나 더 관리하는 부담이 이득보다 큽니다.
옮길 거라면 순서
제가 겪은 순서를 그대로 권합니다.
- 스왑을 먼저 만드세요. 램이 작은 서버는 이게 없으면 아무것도 안 됩니다.
- 워크플로를 하나만 옮겨 보세요. 전부 한꺼번에 바꾸면 뭐가 왜 깨졌는지 알 수 없습니다.
- 실패한 로그에서 "시간 초과"를 찾으세요. 처음 깨지는 것 대부분은 대기 시간이 짧아서입니다.
- 접속이 막혔을 때의 비상구를 만들어 두세요. 필요해진 다음에 만들면 늦습니다.