배포에 45분 걸리던 PG를 3분 만에 배포하기까지
수동 확인과 순차 작업 때문에 45분 걸리던 PG 배포를 Jenkins 파이프라인으로 자동화해 3분까지 줄인 과정을 적었다.
PG 배포 한 번에 45분이 걸렸다. 실제 빌드 시간보다 사람이 순서를 맞추고 결과를 확인하는 시간이 더 길었다. 단계가 많다 보니 배포 담당자마다 확인 방식이 달랐고, 실패했을 때 어느 단계부터 다시 시작해야 하는지도 명확하지 않았다.
목표는 Jenkins를 도입하는 것이 아니었다. 누가 배포해도 같은 절차를 밟고, 실패 지점을 바로 확인하고, 문제가 생기면 되돌릴 수 있게 만드는 것이었다.
먼저 수동 절차를 그대로 적었다
자동화하기 전에 실제 배포자가 하는 일을 순서대로 적었다. 소스 확인, 테스트, 빌드, 산출물 배치, 애플리케이션 기동, 상태 확인이 서로 다른 문서와 기억에 흩어져 있었다.
checkout
→ test
→ build
→ artifact
→ deploy
→ health check
→ verify
중복되는 확인은 합치고, 사람의 판단이 필요한 단계와 기계가 확인할 단계를 나눴다. 자동화할 수 없는 승인은 파이프라인 밖으로 숨기지 않고 명시적인 입력 단계로 남겼다.
실패를 마지막이 아니라 각 단계에서 멈췄다
기존 절차는 마지막 기동 확인에서 실패를 발견하는 경우가 있었다. Jenkins 파이프라인에서는 테스트, 빌드, 배포, 상태 확인이 각각 실패를 반환하게 했다. 앞 단계가 실패하면 다음 단계로 넘어가지 않았다.
로그도 단계별로 나눴다. 배포 실패라는 한 문장 대신 테스트 실패인지, 산출물 문제인지, 기동 뒤 health check 실패인지 바로 구분할 수 있게 했다.
배포와 설정 변경을 분리했다
같은 코드라도 환경 설정이 달라지면 결과가 바뀌었다. 파이프라인에서 사용할 설정의 출처를 고정하고, 민감한 값은 빌드 산출물에 넣지 않았다. 어떤 버전과 어떤 설정으로 배포됐는지 기록을 남겼다.
새 버전이 정상인지 확인하는 기준도 자동화했다. 프로세스가 떠 있다는 사실만 보지 않고 애플리케이션 상태와 필수 의존성 연결을 확인했다.
Jenkinsfile에 배포 순서를 코드로 남겼다
웹 화면에서 단계를 클릭해 만드는 방식은 설정이 바뀐 이유를 찾기 어려웠다. 파이프라인을 저장소의 코드로 두고 애플리케이션 변경과 함께 리뷰했다. 누가 실행해도 같은 명령과 같은 실패 조건을 사용하게 했다.
pipeline {
stages {
stage('Test') { steps { runTests() } }
stage('Build') { steps { buildArtifact() } }
stage('Verify') { steps { verifyArtifact() } }
stage('Deploy') { steps { deployArtifact() } }
stage('Health') { steps { checkApplication() } }
}
}
예시는 단순화한 형태다. 핵심은 단계 이름보다 각 단계의 입력과 출력이 분명한 것이었다. 테스트 결과가 없으면 빌드로 넘어가지 않고, 검증한 산출물만 배포 단계가 받게 했다.
공통 명령은 파이프라인 안에 길게 복사하지 않고 스크립트나 공통 함수로 분리했다. 대신 파이프라인만 읽어도 배포 순서와 중단 조건은 알 수 있게 유지했다.
한 번 만든 산출물을 끝까지 사용했다
환경마다 다시 빌드하면 같은 커밋이라도 의존성이나 설정에 따라 다른 결과가 나올 수 있었다. 테스트를 통과한 산출물을 보관하고 그 파일을 다음 단계와 배포가 그대로 사용하게 했다.
commit
→ build once
→ artifact + checksum
→ deploy same artifact
→ record deployed version
배포 이력에는 커밋뿐 아니라 산출물 식별자와 실행 번호를 남겼다. 운영 중인 코드가 무엇인지 확인할 때 브랜치 이름이나 담당자의 기억을 사용하지 않았다.
설정은 산출물에서 분리했지만 어떤 설정 버전을 사용했는지는 함께 기록했다. 민감한 값을 로그에 출력하지 않으면서도 설정이 바뀌어 결과가 달라졌는지 추적할 수 있어야 했다.
Health check를 기동 확인과 준비 확인으로 나눴다
프로세스가 실행 중이라는 사실만으로 요청을 받을 준비가 끝났다고 볼 수 없었다. 애플리케이션 기동 여부와 필수 의존성 연결 여부를 나누고, 배포 검증은 실제 요청을 처리할 준비가 된 상태를 기다렸다.
| 확인 | 답하려는 질문 |
|---|---|
| Liveness | 프로세스를 다시 시작해야 하는가 |
| Readiness | 지금 요청을 받아도 되는가 |
| Smoke test | 핵심 경로의 최소 동작이 가능한가 |
| Version | 의도한 산출물이 실행 중인가 |
health endpoint가 항상 성공을 반환하도록 만들지 않았다. 필수 연결이 준비되지 않았으면 배포 단계가 실패해야 했다. 반대로 일시적으로 없어도 되는 부가 기능 때문에 전체 애플리케이션을 준비 실패로 만들지도 않았다.
동시 배포와 중복 실행을 막았다
같은 환경에 두 배포가 겹치면 어느 산출물이 최종 상태인지 알기 어려웠다. 환경 단위로 동시 실행을 제한하고, 새 실행이 들어왔을 때 진행 중인 배포를 임의로 덮어쓰지 않게 했다.
재실행은 처음부터 다시 시작하는 경우와 검증된 산출물로 배포만 다시 하는 경우를 구분했다. 실패한 단계를 무조건 건너뛰는 버튼은 만들지 않았다. 앞 단계의 결과가 여전히 유효한지 확인한 뒤 재사용했다.
롤백도 정상 배포와 같은 검증을 거쳤다
직전 버전으로 파일만 바꾸는 것으로 롤백을 끝내지 않았다. 이전 산출물을 배포하고, 기동과 준비 상태를 확인하고, 현재 버전을 기록하는 같은 절차를 사용했다.
문제 감지
→ 신규 경로 중단
→ 직전 정상 산출물 선택
→ 동일한 배포 절차 실행
→ health·version 확인
→ 데이터 호환성 확인
데이터베이스 변경은 별도 문제였다. 이전 코드가 새 스키마를 읽을 수 있는 기간을 확보했고, 되돌릴 수 없는 정리 작업은 안정화 뒤로 미뤘다. 배포 자동화만 빠르고 데이터가 돌아가지 않으면 복구 시간은 줄지 않았다.
되돌리는 절차도 같은 파이프라인에 넣었다
빠른 배포보다 중요한 것은 빠른 복구였다. 직전 정상 산출물을 보관하고, 문제가 생기면 같은 절차로 되돌릴 수 있게 했다. 롤백 명령을 운영자의 기억에 맡기지 않았다.
45분이 3분이 됐다
수동 대기와 반복 확인을 제거한 뒤 배포 시간은 45분에서 3분으로 줄었다. 시간보다 더 큰 변화는 배포를 특정 사람의 숙련도에 의존하지 않게 된 것이었다.
3분이라는 숫자만 목표로 삼지는 않았다. 테스트를 생략하거나 확인 범위를 줄여 만든 시간이 아니어야 했다. 사람의 대기와 중복 작업을 없애되, 배포 가능 여부를 판단하는 검증은 파이프라인 안에 남겼다.
남은 한계도 있었다. 자동화가 모든 운영 판단을 대신하지는 못했다. 데이터 보정이 필요한 변경과 외부 결제사 일정이 얽힌 배포는 여전히 사람이 영향 범위를 확인해야 했다. 대신 그 판단 지점을 숨기지 않고 파이프라인의 명시적인 단계로 만들었다.
Jenkins 화면을 만든 것만으로 CI/CD가 완성된 것은 아니었다. 어떤 실패를 막고 무엇을 증명해야 다음 단계로 갈 수 있는지 정한 것이 핵심이었다. 이후 배포 절차를 바꿀 때도 시간을 먼저 줄이지 않고 실패와 복구 경로부터 확인했다.