Sandcorp샌드코프 · 웹·AI 제품 개발

새 프로젝트 받는 중 · 갱신 2026.11sandcorp.co.krGitHub

← 기록

배포CI/CDJenkins무중단 배포운영

운영이 궤도에 오른 뒤 배포 열차를 걷었다 — 병합 하나가 배포 하나가 되기까지

PR 여러 개를 모아 한 번에 병합·배포하던 「열차」가 진행 상황을 밖에서 못 보게 만들었습니다. Codemit 배포를 다른 서비스와 같은 공용 파이프라인으로 옮기고, 서비스만의 무중단 교체는 남긴 과정과 첫날 병합 11번·배포 11번의 결과를 적었습니다.

배포 열차를 걷었다

병합 하나 = 배포 하나, Codemit 배포 구조 정리

배경 — 열차가 있던 이유

Codemit은 강의실과 공모전을 함께 운영하는 코딩 교육 서비스입니다. 백엔드·실시간 이벤트·비동기 워커·프론트 등 이미지 여섯 개로 이루어져 있고, 배포할 때는 바뀐 서비스만 새 컨테이너를 먼저 띄운 뒤 옛 컨테이너를 내리는 겹쳐 바꾸기로 무중단을 지킵니다.

지금까지 Codemit의 병합은 「열차」가 맡았습니다. PR 검사가 초록이 되면 PR은 바로 병합되지 않고 대기열에 줄을 섭니다. 열차가 출발하면 줄 선 PR들을 최신 main 위에 차례로 쌓고, 운영용 이미지 굽기·부팅 확인·e2e 시험을 묶음마다 한 번 돌린 뒤 한꺼번에 병합했습니다. 무거운 검사를 돌릴 수 있는 빌드 기계가 하나뿐이라, PR마다 돌리는 대신 묶어서 한 번 돌리는 편이 쌌습니다. PR끼리 부딪히는 문제도 병합 전에 잡을 수 있었습니다.

초기에는 이 구조가 맞았습니다. 그런데 운영이 궤도에 오르고, 여러 AI 작업자가 PR을 동시에 올리고, 조율자가 그 결과를 기다려 다음 일을 넘기는 방식으로 바뀌면서 열차가 걸림돌이 됐습니다. 2026년 11월 5일, 대표 지시로 Codemit 배포 구조를 샌드코프 홈페이지·Foresight와 같은 공용 파이프라인으로 맞추고 열차를 걷었습니다.

무엇이 문제였나 — 진행 상황이 밖에서 보이지 않았습니다

검사가 끝나도 「대기 중」

작업자는 PR을 올린 뒤 CI 대기 스크립트로 검사 결과를 기다립니다. 다른 서비스에서는 검사가 끝나면 GitHub에 「성공」이 찍히고, 작업자는 그걸 보고 병합합니다. Codemit에서는 검사가 초록이어도 열차가 병합하기 전까지 「대기 중」 상태가 하나 더 붙어 있었습니다. 그래서 대기 스크립트는 성공을 끝내 보지 못했고, 작업자와 조율자는 「검사가 아직 도는 건지, 열차를 기다리는 건지, 열차에서 떨어진 건지」를 구분할 수 없었습니다.

배포는 GitHub에 아무것도 남기지 않음

배포 파이프라인은 446줄짜리 독자 파일이었고, GitHub 커밋 상태도 실패 댓글도 남기지 않았습니다. 배포를 기다리는 스크립트가 GitHub 쪽으로 물으면 「배포 기록 없음」이 돌아왔습니다. 배포가 끝났는지, 실패했는지는 CI 콘솔에 들어갈 수 있는 사람만 알았습니다. AI 작업자에게 CI 콘솔 자격을 나눠 주지 않는다는 원칙과 정면으로 부딪혔습니다.

한 서비스만 다른 길

다른 서비스들은 이미 공용 파이프라인 라이브러리 하나를 불러 같은 모양으로 검사하고 배포하고 있었습니다. 대기 스크립트도, 실패 댓글 형식도, 「같은 커밋은 두 번 배포하지 않는다」는 규칙도 거기에 있었습니다. Codemit만 열차·자체 배포 파일·자체 「배포된 커밋」 기록 브랜치를 따로 가지고 있어서, 공용 쪽을 고쳐도 Codemit에는 들어가지 않았고 Codemit 쪽 지식은 Codemit에만 쌓였습니다.

고른 방법 — 틀은 공용으로, 서비스만의 사정은 스크립트로

목표는 하나였습니다. 병합 하나 = 배포 하나, 그리고 다른 서비스의 대기 도구가 Codemit에서도 그대로 먹을 것. 그렇다고 공용 라이브러리를 그대로 쓸 수는 없었습니다. 라이브러리는 저장소마다 이미지 하나를 굽고, 교체할 때 컨테이너를 통째로 새로 만듭니다. 잠깐이라도 끊깁니다. Codemit은 이미지가 여섯 개이고, 바뀐 것만 굽고, 무중단으로 바꿔야 합니다.

그래서 역할을 나눴습니다. 배포 파이프라인 파일은 라이브러리 호출 하나로 줄이고, 실제 굽기와 교체는 Codemit 스크립트 두 개를 라이브러리 인자로 넘겼습니다. 라이브러리는 「같은 커밋 두 번 배포 막기」, GitHub 커밋 상태, 실패 시 PR 댓글, 메신저 알림, 빌드 기계 순서, 수동 되돌리기를 맡습니다. Codemit 스크립트는 여섯 이미지와 바뀐 것만 굽기, 부팅 확인, 겹쳐 바꾸기, 마이그레이션 전 DB 덤프, 헬스 확인을 맡습니다.

라이브러리에 굽기 스크립트를 넘기는 인자가 없어서 라이브러리도 고쳤습니다. 굽기 스크립트, 자동 롤백 켜고 끄기, 빌드용 자격 넘기기를 새 인자로 더하되, 셋 다 기본값이 이전 동작이 되게 했습니다. 다른 서비스는 아무것도 바꾸지 않아도 똑같이 돕니다. 새 인자에는 시험 16건을 붙였습니다.

PR 검사도 라이브러리의 PR 검사 틀로 옮겼습니다. 열차가 하던 이미지 굽기·부팅 확인·e2e를 PR 검사 안으로 옮겨, 열차가 지키던 품질 문을 잃지 않게 했습니다. 문서만 바꾼 PR은 문서 검사만 돕니다.

검토했지만 버린 것

대안버린 이유
열차는 두고 상태만 GitHub에 더 남기기「대기 중」이 열차 출발까지 계속되는 구조는 그대로입니다. 병합 시각과 배포 시각이 PR마다 다르다는 문제도 남습니다.
라이브러리의 기본 교체 방식 쓰기컨테이너를 통째로 새로 만들어 배포마다 잠깐 끊깁니다. Codemit의 무중단 약속을 깹니다.
라이브러리의 자동 롤백 켜기Codemit은 「마지막으로 배포된 커밋」 표식과 비교해 바꿀 서비스를 정합니다. 실패 직후에는 표식이 아직 옛 커밋이라, 옛 커밋으로 같은 스크립트를 다시 돌려도 아무것도 되돌아가지 않습니다. 거짓 「되돌림」 알림을 내느니 끄고, 수동 되돌리기만 남겼습니다.
라이브러리의 파괴적 마이그레이션 검사 켜기Codemit은 이미 자체 표식과 시험으로 무중단에 위험한 마이그레이션을 막고 있습니다. 둘을 같이 켜면 지금 허용되는 PR까지 막힙니다.
첫 빌드 기계가 바쁘면 바로 다음 기계로 넘기기e2e가 시간 한도를 지키려면 운영과 같은 아키텍처의 기계가 필요합니다. 다른 기계에서 에뮬레이션으로 돌리면 의미 있는 결과가 나오지 않아서, 첫 기계를 PR 검사는 최대 25분, 배포는 20분까지 기다리게 했습니다.
  • 「문서만」 병합은 지금처럼 교체를 건너뜁니다. 기준만 따로 두던 기록 브랜치에서 운영 서버의 배포 표식으로 옮겼습니다.

결과 — 첫날 병합 11번, 배포 11번

구조를 바꾼 PR은 더한 줄이 700줄, 지운 줄이 2,253줄이었습니다. 열차 파일과 열차 전용 스크립트들, 446줄짜리 배포 파일이 빠지고 라이브러리 호출 하나와 스크립트 두 개가 들어갔습니다. 이 PR은 열차가 아니라 작업자가 직접 병합했고, 그 병합 커밋의 배포가 새 파일의 첫 실행이었습니다. 병합이 곧 운영 배포라, 첫 배포에서 맞게 돌아야 했습니다.

첫 배포는 2분 4초 만에 성공했고, 병합 커밋에 「배포 중 → 성공」 상태가 달렸습니다. 9분 뒤에는 흐름을 한 번 더 확인하려고 서비스 이미지에 들지 않는 작은 PR을 병합했습니다. 바꿀 서비스가 없다는 계획이 나와 교체 없이 34초 만에 끝났습니다.

그날 오후까지 기능 PR이 이어서 병합됐습니다. 구조 변경 PR부터 하루 동안 병합 11번에 배포 11번이 돌았고, 11번 모두 병합 커밋에 「배포 성공」 상태가 달렸습니다. 배포 한 번은 34초에서 6분 47초 사이였고, 대부분 2분 안팎이었습니다. 가장 길었던 6분 47초는 배포 스크립트 자체를 고친 PR이었습니다.

새 흐름이 처음 막아 준 것

새 PR 검사가 처음으로 실패를 잡은 것도 같은 날이었습니다. 한 기능 PR이 e2e용 시험 데이터를 만드는 명령을 더했는데, 그 명령이 공모전을 「공개됨」 상태로 바로 만들었습니다. Codemit에는 「상태를 새로 만들 때 쓸 수 있는 값은 정해 둔 목록 안에서만」이라는 규칙 시험이 있고, 그 시험이 떨어졌습니다.

열차 시절이었다면 이 실패는 열차 콘솔에만 남았을 것입니다. 이번에는 PR에 「PR 검사 실패 — 검사 · 커밋 앞 12자리 · 빌드 번호」 댓글이 달리고, 그 아래 실패한 시험 이름과 어설션 줄이 그대로 붙었습니다. PR을 올린 AI 작업자는 CI 콘솔에 들어가지 않고 이 댓글에서 실패 줄을 읽었고, 시험 데이터 명령을 규칙에 맞게 고쳐 다시 올렸습니다. 다시 돈 검사가 성공한 뒤 병합됐고, 병합이 곧 배포로 이어졌습니다. 새 경로가 처음부터 의도대로 쓰인 사례였습니다.

이 댓글이 달리게 하려면 CI 쪽 PR 검사 잡을 「브랜치 발견」에서 「PR 발견」으로 바꿔야 했습니다. 바꾼 뒤 PR 빌드가 뜨지 않은 일이 있었는데, 원인은 GitHub 웹훅이 push 이벤트만 보내고 있던 것이었습니다. 이 요령은 팁 멀티브랜치 잡을 「PR 발견」으로 바꿨는데 PR 빌드가 안 뜨면 웹훅 이벤트부터 본다에 적었습니다.

손으로 다시 띄우다 멈춘 일

같은 날, 설정값 하나만 운영에 반영하려고 새 흐름을 거치지 않고 같은 커밋으로 컨테이너를 손으로 다시 띄운 일도 있었습니다. 교체는 마이그레이션 단계에서 DB 주소가 깨진 것 같은 오류를 내고 멈췄습니다. 실제 원인은 배포 스크립트가 설정값을 읽을 때 쓰는 다른 이미지에 새 이름표가 없어서, 이미지 받기 출력이 값에 섞여 들어간 것이었습니다(팁 같은 커밋으로 컨테이너만 다시 띄울 때는 배포 스크립트가 쓰는 이미지 이름표를 전부 맞춘다).

겹쳐 바꾸기는 새 컨테이너가 준비되기 전에는 옛 컨테이너를 내리지 않습니다. 그래서 교체가 중간에 멈춰도 옛 컨테이너가 그대로 서비스를 이어 갔고, 사용자 쪽 영향은 없었습니다. 이 오류는 오류 추적기를 거쳐 자동 진단으로 넘어갔고, 진단 결과로 배포 스크립트를 고친 PR이 만들어져 병합됐습니다. 그 수정 역시 새 흐름을 따라 병합 하나, 배포 하나로 운영에 들어갔습니다.

배운 것

  • 묶어서 한 번에 처리하는 구조는 자원이 귀할 때 맞는 선택입니다. 하지만 기다리는 쪽이 사람 한 명에서 여러 작업자와 조율자로 바뀌면, 「지금 어디쯤인가」를 밖에서 읽을 수 있는지가 처리량보다 중요해집니다.
  • 열차를 걷으면서 잃는 것을 먼저 적어 두는 것이 도움이 됐습니다. 열차가 하던 이미지·부팅·e2e 검사를 PR 검사로 옮겼기 때문에, 구조를 바꾼 날에도 품질 문이 비지 않았습니다.
  • 병합이 곧 운영 배포인 구조에서 배포 구조 자체를 바꾸는 PR은 「첫 실행이 곧 실전」입니다. 가짜 배포 스크립트로 교체·문서만·받기 실패·교체 실패 네 경로를 미리 돌려 본 것이, 첫 배포가 한 번에 성공한 이유였습니다.
  • 흐름을 확인하는 작은 PR을 곧바로 하나 더 내는 것도 쓸모가 있었습니다. 큰 변경 하나의 성공은 우연일 수 있지만, 서로 다른 경로(교체 있음·교체 없음)가 둘 다 맞게 돌면 믿을 만합니다.
  • 설정값 하나만 바꾸는 일도 정식 흐름에 싣는 편이 안전합니다. 그날 멈춘 것은 새 흐름이 아니라 그 흐름을 비켜 간 손 교체였고, 그것을 고친 것은 다시 새 흐름이었습니다.

남은 한계

  • PR끼리의 충돌을 미리 잡지 못합니다. 열차는 여러 PR을 함께 얹어 검사했지만, 지금은 PR마다 따로 검사합니다. 뒤처진 PR이 병합되면 배포가 바뀐 부분만 다시 굽고 부팅을 확인하지만, e2e는 배포 단계에서 다시 돌지 않습니다.
  • 자동 롤백이 없습니다. 공용 라이브러리의 자동 롤백은 Codemit의 「배포 표식 기준」 교체와 맞지 않아 껐습니다. 배포가 실패하면 사람이(또는 지시받은 작업자가) 되돌릴 커밋을 정해 수동 되돌리기를 돌려야 합니다. 표식 기준에 맞는 되돌리기는 아직 만들지 않았습니다.
  • 무거운 검사를 하는 빌드 기계가 여전히 하나입니다. 이제 PR 검사와 배포가 그 한 대를 나눠 쓰므로, PR이 몰리면 검사 대기가 길어집니다. 첫날은 병합 11번 동안 문제가 없었지만, 더 많은 PR이 한꺼번에 몰리는 날은 아직 겪지 않았습니다.
  • 배포 구조를 바꾸는 PR은 병합 전에 실물로 돌려 볼 길이 없습니다. 병합이 곧 운영 배포라, 가짜 스크립트 시험으로 경로를 미리 확인하는 것이 지금의 최선입니다.

문의 →