테스트 통과만으로는 배포 후보가 되지 않았다
Parser와 Webhook, Host Worker나 운영 Compose가 바뀌면 새 서버 이미지가 필요하다. Spring 테스트가 통과하거나 Docker image가 만들어진다는 사실 하나만으로 운영에 배포할 수는 없었다.
CI는 원본 리비전이 배포 후보가 될 수 있는지 검증하고, CD는 검증된 리비전의 변경 불가능한 배포 산출물을 운영에 전달하도록 책임을 나눴다. 둘을 한 workflow에 섞으면 테스트와 이미지 게시, OCI 배포 중 어디에서 실패했는지 구분하기 어렵기 때문이다.
Backend CI는 main 대상 pull request와 main push, 수동 실행에서 동작한다. Repository는 읽기 권한만
가지며 같은 PR이나 ref의 오래된 실행은 취소한다. 끝나지 않는 실행을 막기 위해 job 제한은 30분으로 뒀다.
네 단계가 서로 다른 실패를 찾는다
Verify job은 Java 21 Temurin과 Gradle 환경을 준비한 뒤 다음 순서로 실행된다.
Gradle test
-> bootJar
-> 운영 Compose 계약 검사
-> Docker image build
-> 검증된 revision핵심 명령은 다음과 같다.
./gradlew test --no-daemon --stacktrace
./gradlew bootJar --no-daemon --stacktrace
docker compose --env-file "$server_env" --env-file "$release_env" \
--profile sync -f compose.prod.yml config --quiet
docker build --tag houkago-server:ci .앞 단계가 성공해도 뒤 단계의 조건까지 보장하지는 않는다.
| 경계 | 확인하는 질문 |
|---|---|
| 애플리케이션 | Java와 Host Worker가 기존 계약을 지키는가 |
| 실행 파일 | 실행 가능한 Spring Boot jar를 만들 수 있는가 |
| 운영 설정 | Compose와 app·sync image 관계가 올바른가 |
| 컨테이너 | 실제 Dockerfile로 실행 이미지를 만들 수 있는가 |
애플리케이션과 Worker를 함께 검사했다
./gradlew test에는 Java 단위·통합 테스트뿐 아니라 Content Sync Worker와 Backend Deploy Worker의 shell
회귀 테스트도 연결했다. 표준 Gradle 명령 하나로 애플리케이션과 호스트 자동화를 함께 확인한다. Worker
검사를 선택 사항으로 두면 Java만 통과한 revision이 운영 script의 회귀를 포함할 수 있기 때문이다.
MySQL이 필요한 repository와 공개 API 통합 테스트에는 Testcontainers를 사용한다. GitHub runner의 Docker 위에서 테스트가 MySQL container의 시작과 종료를 직접 관리한다. 별도 MySQL service를 workflow에 중복으로 정의하지 않고 같은 테스트를 로컬과 CI에서 실행할 수 있다.
실행 가능한 jar를 별도로 만들었다
테스트 JVM에서 클래스가 동작해도 운영 이미지에 넣을 jar가 만들어진다는 보장은 없다.
./gradlew bootJar로 classpath와 plugin 설정, main application 구성이 실제 실행 산출물로 조립되는지
확인했다. 이 단계를 생략하면 패키징 문제를 컨테이너 빌드에서야 발견할 수 있다.
실제 운영 Compose를 해석했다
Java artifact가 정상이어도 compose.prod.yml을 해석하지 못하면 배포할 수 없다. CI는 sync profile과
운영에서 사용하는 환경 파일 조합으로 실제 Compose 설정을 검사한다.
운영 secret은 복사하지 않고 필요한 key에 임시 값을 넣는다. Shell에 남아 있을 수 있는
HOUKAGO_SERVER_IMAGE도 제거한 뒤 interpolation을 실행한다. 이어서 해석된 JSON에서 app.image와
sync.image가 같은 immutable digest placeholder를 가리키는지 확인한다.
이 검사는 다음 계약을 지킨다.
app,mysql,sync가 필요한 환경과 profile로 해석되는가- App과 일회성 sync가 같은 release image를 보는가
- 운영 release가 tag가 아니라 digest로 고정되는가
마지막에는 실제 이미지를 만들었다
저장소의 Dockerfile로 houkago-server:ci를 build한다. Multi-stage build와 jar 복사 경로, 실행용 기본
이미지, 컨테이너 진입점이 함께 성립하는지 확인하는 단계다.
이 이미지는 registry에 게시하지 않는다. CI는 리비전을 컨테이너로 조립할 수 있다는 사실까지만 검증한다. 운영 아키텍처와 이미지 이름, digest는 이미지 게시와 배포 단계의 책임이다.
실행 시간과 검증 범위를 구분했다
첫 성공 실행은 약 4분이었다. 이후 성공 run 31769101818은 commit 48a8904에서 2026년 8월 14일
04:11:38 UTC에 시작해 04:14:35 UTC에 끝났고 2분 57초가 걸렸다. 당시 테스트 수와 실행기, 의존성
캐시와 네트워크 상태에 따른 관찰값이며 SLA가 아니다. 30분 제한도 목표 시간이 아니라 비정상 실행을
끝내는 안전장치다.
CI를 통과해도 운영 배포까지 증명되는 것은 아니다. ARM64 이미지를 어디서 만들지, 검증한 commit과 image가 같다는 것을 어떻게 확인할지, 어떤 digest를 배포할지와 실패 시 rollback은 남아 있었다. 실제 운영 검증에서도 systemd namespace의 checkout 접근과 환경 전달처럼 CI에서 보이지 않던 문제가 발견됐다.
CI의 역할은 운영을 흉내 내는 것이 아니었다. 코드와 실행 파일, 운영 설정, 컨테이너 조립이라는 네 경계를 통과한 revision만 다음 단계로 보내는 것이었다.