LabHub

CI/CD 파이프라인 · 빌드 자동화 · 이론

재현 가능한 빌드가 먼저다

LabHub 에서 이어서 보기

한 줄 요약

CI 는 빌드를 대신 눌러 주는 로봇이 아니라, 같은 입력이면 언제 어디서 돌려도 같은 산출물이 나오도록 강제하는 재현성 장치다.

왜 이게 필요했나

손으로 빌드하던 시절의 사고는 늘 같은 모양이었다. 내 노트북에서는 되는데 서버에서는 안 되고, 어제 만든 이미지와 오늘 만든 이미지가 다른데 무엇이 달라졌는지 아무도 설명하지 못한다. 원인은 대개 "고정하지 않은 것"에 있다. 의존성 버전을 고정하지 않았거나, 액션과 베이스 이미지를 태그로만 참조했거나, 산출물 이름에 무엇으로 만들었는지가 남아 있지 않다.

태그는 사람이 옮길 수 있는 이름표다. tj-actions/changed-files 공급망 공격에서 공격자는 액션의 태그를 악성 커밋으로 재지정했다. 저장소를 뚫은 게 아니라 이름표만 옮겼는데, 그 태그를 참조하던 수많은 파이프라인이 그대로 악성 코드를 실행했다. 그래서 규칙은 짧다. 태그는 악의적으로 변경될 수 있지만 커밋 SHA 는 변경이 불가능하다. 액션과 이미지는 태그가 아니라 커밋 SHA 또는 다이제스트로 고정한다.

프로덕션에 latest 를 쓰면 두 가지를 동시에 잃는다. 어떤 버전이 배포됐는지 추적이 불가능해지고, 사고가 났을 때 되돌릴 대상 자체가 없다. 그래서 배포에 쓰는 태그는 반드시 불변이어야 한다. 커밋 SHA 나 시맨틱 버전처럼 한 번 정해지면 다른 것을 가리키지 않는 값이어야 한다.

어떻게 동작하나

재현 가능한 빌드는 네 축으로 만든다.

1. 입력 고정. 락 파일은 반드시 커밋하고, CI 는 npm ci, --frozen-lockfile, -lockfile=readonly 처럼 락 파일을 갱신하지 않고 적힌 그대로 설치하는 명령을 쓴다. 평범한 install 계열은 락 파일을 슬쩍 갱신해 실행마다 다른 의존성 트리를 만든다.
2. 출력 식별. 산출물 이름과 이미지 태그에 커밋마다 바뀌는 불변 값을 넣는다. latest 는 그 위에 덧붙이는 별칭일 뿐, 식별자가 아니다.
3. 캐시. 캐시 키에 락 파일 해시를 넣어야 의존성이 바뀔 때 키가 저절로 달라져 자동 무효화된다. ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 에 restore-keys 접두 체인을 붙이는 형태가 표준이다. 효과적인 캐시 키 전략은 빌드 시간을 50~70% 단축한다. 한 팀의 실측은 평균 CI 빌드가 28분에서 8분으로 71% 줄었다. 다만 캐시 저장소당 한도는 10GB 이고 넘으면 오래된 것부터 축출되므로, 스코프를 커밋 단위로 잘게 쪼개면 캐시끼리 서로를 밀어내 히트율이 오히려 떨어진다. 캐시는 신뢰 경계이기도 하다. Fork PR 이 악성 의존성을 캐시에 주입하면 이후 빌드가 그 캐시를 그대로 쓰는 캐시 포이즈닝이 성립한다.
4. 실패 처리. 매트릭스 빌드의 fail-fast 기본값은 true 라서 하나가 실패하면 나머지가 취소된다. 빠른 피드백이 중요하면 true, 전체 호환성 확인이 중요하면 false 로 바꾼다. 동시성 제어를 걸지 않으면 연속 Push 시 여러 배포 Job 이 동시에 실행되어 롤백이 복잡해지는 사고가 난다.

현장에서 만나는 모습

장애 회고에서 가장 자주 나오는 문장이 "그때 배포된 게 정확히 뭐였죠"다. 이미지 태그가 latest 하나뿐이면 이 질문에 아무도 답하지 못한다. 반대로 태그에 커밋 해시가 박혀 있으면 레지스트리와 Git 로그만으로 5분 안에 답이 나온다.

숫자로 관리하는 팀은 목표를 이렇게 잡는다. Lead Time 1시간 이하, 배포 빈도 일 10회 이상, 변경 실패율 5% 이하, MTTR 30분 이하, 파이프라인 실행 15분 이하, 커버리지 80% 이상. 이 중 파이프라인 실행 시간이 15분을 넘기 시작하면 사람들이 CI 결과를 기다리지 않고 다른 일을 하러 가면서 피드백 루프가 통째로 무너진다.

다음 실습에서 할 것

이 파드에는 Jenkins 도 GitHub Actions 러너도 없다. 그래서 build → test → package 3단계를 셸 스크립트로 직접 만든다. 중요한 것은 벤더가 아니라 메커니즘이기 때문이다. 소스 해시로 이름 붙은 아티팩트를 한 개만 만들고, 같은 입력으로 다시 돌리면 재사용하고, 마지막에는 해시 태그와 latest 를 같은 이미지에 붙여 무엇이 불변 식별자이고 무엇이 움직이는 별칭인지 눈으로 확인한다.