이전 원본이 아니라 이전 이미지로 돌아간다
새 릴리스에 문제가 생겼을 때 이전 원본을 다시 빌드하면 과거에 성공한 이미지 자체를 실행하는 것이 아니다. 기본 이미지와 도구 체인, 외부 의존성이 달라질 수 있고 운영 서버도 다시 빌드 장비가 된다.
latest, stable, previous 같은 tag도 같은 이름이 다른 manifest를 가리킬 수 있다. 롤백 대상은
사람의 기억이나 바뀔 수 있는 별칭이 아니라 이미 검증한 콘텐츠 digest여야 했다.
Rollback = 이전에 성공했던 exact digest를 다시 실행Full SHA tag는 원본에서 이미지를 찾는 데 사용하고, 실제 실행에는
repository@sha256:<digest>를 사용했다.
current와 previous에 세 값만 저장했다
OCI에는 root 소유의 릴리스 env 두 개를 둔다.
current: 현재 성공적으로 서비스 중인 릴리스previous:current바로 전에 성공했던 릴리스
각 파일에는 다음 값만 저장한다.
HOUKAGO_SERVER_IMAGE=<repository>@sha256:<digest>
HOUKAGO_SERVER_REVISION=<full Git SHA>
HOUKAGO_SERVER_SCHEMA_COMPATIBILITY=unchangedTimestamp와 delivery ID는 identity에서 제외했다. Worker는 key가 정확히 세 개인지, image와 revision 형식,
compatibility가 unchanged인지 읽을 때마다 확인한다.
데이터베이스 자격 증명이 있는 server.env와 릴리스 env도 분리했다. 비밀값 교체와 애플리케이션 이미지
전환은 다른 생명주기이기 때문이다. Compose 명령은 두 파일을 명시하고 shell에 남은 image 값은 제거한다.
검증을 통과한 뒤에만 current가 된다
이미지를 가져오거나 컨테이너가 실행 상태가 됐다는 이유만으로 릴리스를 승격하지 않는다.
- 마이그레이션과 이미지 식별자, Compose 해석 결과를 검사한다.
- Candidate digest로 app만 다시 만든다.
- MySQL 상태와 컨테이너 식별자를 확인한다.
- Spring 상태와 후보 이미지 ID를 확인한다.
- 공개 list, featured, detail API를 확인한다.
- 기존 current를 previous로, candidate를 current로 atomic하게 게시한다.
- 작업을
succeeded로 옮긴다.
Health나 API 확인에 실패한 candidate는 current에 기록하지 않는다. 같은 digest와 revision이 이미 current라면
already_current로 끝내고 app 교체와 상태 회전을 모두 생략한다.
배포와 rollback은 두 상태를 교환한다
초기 current가 A일 때 candidate B가 모든 검증을 통과하면 다음처럼 바뀐다.
배포 전: current=A
배포 후: current=B, previous=A상태 파일은 같은 디렉터리에 임시 파일을 만든 뒤 권한을 적용하고 이름을 바꾼다. 게시 전에 기존
current와 previous도 임시 백업으로 보존해 중간 실패에서 복구할 입력을 남긴다.
rollback 명령은 임의 tag가 아니라 검증된 previous env를 대상으로 한다.
Rollback 전: current=B, previous=A
Rollback 후: current=A, previous=B교환 뒤에도 B를 잃지 않으므로 다시 배포하거나 상태를 비교할 수 있다. Previous가 없는 최초 전환에서는 rollback을 허용하지 않고 별도 수동 복구 경계로 남긴다.
Rollback도 같은 기준으로 검사했다
이전 image라는 이유로 검증을 줄이지 않는다.
- Exact digest와
linux/arm64, RepoDigest, revision·source label app.image와sync.image가 같은 digest인지- MySQL ID를 유지한 app-only recreate
- MySQL과 Spring 상태
- Candidate image ID와 공개 list·featured·detail API
운영에서는 A에서 B를 배포한 뒤 A로 rollback하고 같은 B를 다시 복원했다.
A -> B: current=B, previous=A
B -> A: current=A, previous=B
A -> B: current=B, previous=A세 전환 모두 같은 변경 불가능한 digest를 사용했고 MySQL 컨테이너 ID와 재시작 횟수는 유지됐다. App이 하나라
각 rollback과 복원에서 약 11~13초의 외부 HTTP 502가 관찰됐다.
이미지 롤백은 스키마를 되돌리지 않는다
Flyway 마이그레이션이 적용된 뒤 이전 이미지를 실행해도 데이터베이스 스키마는 이전 상태로 돌아가지 않는다. 이전 애플리케이션이 새 스키마와 호환되지 않으면 정확한 digest 롤백도 장애를 만들 수 있다.
Docker image rollback != Database schema rollback자동 배포는 current와 target 사이에서 migration 파일이 하나라도 바뀌면 중단한다. SQL을 분석해 안전성을
추측하지 않고 수동 검토로 전환한다. Release env도 현재는 schemaCompatibility=unchanged만 허용한다.
일반 코드 릴리스는 스키마가 같다는 전제에서 이미지만 되돌릴 수 있다. 스키마가 바뀌는 릴리스는 이미지와 DB 상태, 하위 호환성과 데이터 복구를 함께 검토해야 한다.
상태 기록 실패도 배포 실패다
후보 app과 API가 성공해도 릴리스 파일 게시가 실패할 수 있다. 실제 운영 활성화에서도 원자적 이름 변경에 필요한 상위 디렉터리 쓰기 권한이 없어 이 단계가 중단된 적이 있었다.
Worker는 기존 상태를 먼저 백업한다. 승격이나 succeeded 이동이 실패하면 이전 current 이미지로 app을
복구하고 상태도 되돌린다. 이 복구까지 실패하면 성공으로 숨기지 않고 manual recovery required로 남긴다.
App 상태와 릴리스 상태 기록은 하나의 트랜잭션이 아니다. 실행 중인 이미지와 기록된 current가
다른 상태를 성공으로 취급하지 않는 것이 중요했다.
두 세대만 보존하는 한계가 있다
current와 previous 두 파일은 단일 VM에서 읽고 복구하기 쉽지만 한 세대 이전만 제공한다. 여러 릴리스를
건너뛰는 이력은 없고, 파일 시스템 권한과 원자적 게시를 운영해야 한다. 롤백에도 짧은
중단 시간이 있으며 GHCR 가용성과 이전 digest 보존에 의존한다. DB 롤백도 제공하지 않는다.
릴리스와 감사 요구가 늘면 덧붙이기 전용 배포 이력이나 별도 메타데이터 저장소가 필요할 수 있다. 당시에는 현재와 직전의 성공 이미지를 정확히 교환하는 것이 가장 작은 복구 단위였다.