Troubleshooting 2026.11.07 7 min read

테스트한 소스와 운영 이미지를 digest로 연결한 Jenkins CI

후속 CI에서는 테스트한 소스, 스캔한 로컬 이미지, Registry 이미지, GitOps 배포 참조, 실제 Pod의 imageID를 연결하려 했다. 테스트 뒤에도 빌드와 검증 로직에서 실패가 나면서 각 단계의 성공을 구분해야 했다.

첫 RCA 배포에서는 Registry manifest와 JAR 리소스, SQL, React 파일을 비교했다. 하지만 전체 Git revision과 Java 빌드 정보가 부족해 컴파일된 Java 클래스 전체를 해당 commit의 산출물로 증명할 수는 없었다.

후속 CI에서는 테스트한 소스, 스캔한 로컬 이미지, Registry 이미지, GitOps 배포 참조, 실제 Pod의 imageID를 연결하려 했다. 테스트 뒤에도 빌드와 검증 로직에서 실패가 나면서 각 단계의 성공을 구분해야 했다.

테스트 다음에 두 번 더 실패했다

중간 verification job은 622개 테스트와 skip 0을 기록했지만 이미지 빌드 전에 실패했다. 당시 Podman 4.9.3이 Pipeline에서 전달한 podman build --cpus 옵션을 지원하지 않았다. 애플리케이션 테스트 결과를 빌드 전체의 성공으로 바꿔 적을 수 없었다. 이후 해당 환경에서 사용할 수 있는 CPU period와 quota 방식으로 수정했다.

feature 첫 빌드에서는 638개 테스트가 통과하고 Push까지 진행됐지만 최종 상태는 FAILURE였다. 단일 OCI manifest를 manifest-list처럼 취급한 중복 Podman 조회가 문제였다. 이미지가 올라간 결과와 게시 후 검증이 끝난 결과는 달랐다.

Registry의 실제 manifest와 config를 읽어 각각의 식별자를 대조하도록 검증을 바꿨다. Push 뒤 실패한 실행에는 게시된 이미지가 남아 있었다. 어느 단계에서 멈췄는지에 따라 산출물도 달랐다.

manifest digest와 config ID의 역할을 나누었다

이미지 태그에는 전체 소스 SHA와 Jenkins 빌드 번호를 넣었다. 태그로 실행 이력을 읽고, 실제 배포 참조는 manifest digest에 연결했다. 소스 commit은 OCI full revision과 비교하고 로컬에서 스캔한 이미지 ID는 Registry의 config와 대조했다.

Podman OCI source·revision과 UID 65532 실행 계정
Podman OCI source·revision과 UID 65532 실행 계정.

최종 main build 1의 주요 연결은 다음과 같았다. 긴 식별자는 비교하기 쉽게 앞부분만 표시했다.

연결 대상 확인한 값의 앞부분
Source / OCI full revision 76b913c133a4…
Platform manifest digest sha256:00726f98ec4f…
Platform config / scanned local ID sha256:d3d71234b88d…
Agent manifest digest sha256:d6fba947dc6f…
Agent config / scanned local ID sha256:617243920861…

실제 검증은 전체 값으로 진행했다. 사설 CA HTTPS 경로에서 Registry manifest와 config의 SHA, 스캔 대상 ID, OCI revision, UID 65532 실행 조건을 대조하고 JAR의 SHA-256도 남겼다.

후속 운영 검증에서는 Platform Pod의 spec image와 status imageID를 새 manifest digest와 대조하고 CI의 Registry 증거로 source revision에 연결했다. 운영 Pod의 OCI label과 config를 모두 다시 읽은 검사는 아니었다.

새 Agent 이미지도 같은 CI에서 검증하고 게시했지만 운영 Agent는 기존 safe canary 이미지를 유지했다. Registry에 새 이미지가 있다는 사실만으로 운영 교체까지 끝난 것은 아니었다. 재빌드할 때 byte 단위로 같은 이미지가 나온다는 재현성도 이 작업의 결론에 포함하지 않았다.

SCM에 의해 시작된 main build #1과 source revision
main build #1의 SUCCESS·SCM 시작 정보·source revision.

스캔 도구가 다른 결과를 내자 패키지 범위를 봤다

보안 검사에서는 Jenkins의 Trivy 0.76과 GitHub의 0.70 결과가 달랐다. Jenkins 쪽이 다른 coverage로 놓친 pip vendored package의 HIGH 네 개가 GitHub 검사에서 드러났다. 버전 숫자가 더 크다는 이유로 한쪽 결과를 우선할 수 없었다.

Agent는 표준 라이브러리만 사용하는 구성이었다. severity나 ignore를 완화하지 않고 불필요한 pip와 ensurepip를 제거한 뒤 정확한 최종 main 소스를 다시 빌드하고 스캔했다. Agent 이미지에 pip, ensurepip, bundled installer가 없는지도 별도로 확인했다.

최종 Trivy 0.76의 OS와 library 검사에서는 Platform과 Agent가 각각 reported 0, blocking 0이었고 수정 가능한 HIGH·CRITICAL 차단 게이트가 exit 0으로 끝났다. 해당 이미지의 SBOM도 보관했다. 이 숫자는 당시 도구와 데이터베이스, 검사 범위의 결과였다. 알려지지 않은 취약점까지 없다는 의미로 쓰지 않았다.

Jenkins Trivy 검사 결과와 SBOM 해시
Jenkins Trivy 검사 결과와 SBOM 해시.

이미지는 rootless로 빌드하고 비root 런타임을 검사했다. Agent safe fixture에서는 read-only와 capability 제한, 외부 네트워크가 없는 조건을 사용했다. 자기 Node GET 하나를 허용하고 광범위 조회 아홉 개를 거부하는 동작을 확인했지만, 이는 운영 backend나 클러스터 접근 시험과는 별개의 로컬 검사였다.

자연 polling도 최초 실행과 실제 변경을 구분했다

최종 Pipeline은 main의 ci/Jenkinsfile을 사용하고 linux-ci Agent의 executor 하나에서 실행했다. 동시 빌드는 막았고 SCM polling은 H/10으로 두었다. Controller를 인터넷에 직접 열지 않은 환경에서 소스 변경을 감지할 경로였다.

feature에서는 감시 중인 같은 branch가 변경된 뒤 동일 job의 build 2가 자연스럽게 시작됐다. 실행에는 SCMTriggerCause=true, manual=false가 남았다. SCM 정의를 바꾸거나 수동 Build, 강제 poll로 만든 결과와 구분되는 실제 Git delta 검증이었다.

main build 1도 자연 polling으로 시작됐지만 최초 baseline 실행이었다. 이후 main의 새 Git 변경으로 다시 실행되는 사례까지 입증한 것은 아니었다. 변경 없는 quiet poll 뒤에도 build 1만 남아 있었으므로 그 시점에 불필요한 중복 빌드가 없었다는 결과로 기록했다.

CI가 만든 것은 검토할 GitOps PR이었다

최종 main 실행은 Java 395개, Frontend 24개, Python 219개로 총 638개를 실패·오류·skips 0으로 마쳤다. 이어 이미지 빌드, 런타임과 보안 검사, Registry Login과 Push, digest 검증을 거쳐 GitOps PR을 자동 생성했다. 브라우저와 실제 외부 LLM 호출은 이 성공 범위에 포함하지 않았다.

인증은 폴더 Credentials의 Registry 빌드용·GitOps용 항목으로 주입했다. GitOps PR에는 Platform digest 한 줄의 변경과 소스·빌드·검증 결과를 남겼다.

Jenkins의 GitOps PR #3 생성 로그
Jenkins의 GitOps PR #3 생성 로그.
Jenkins Pipeline의 Finished: SUCCESS
Jenkins Pipeline의 Finished: SUCCESS.
GitOps 이미지 변경 PR #3의 병합과 수동 Sync 승인 범위
GitOps 이미지 변경 PR #3 병합 완료.
GitOps PR 생성 후 SUCCESS로 끝난 Jenkins 콘솔
GitOps PR #3 생성 및 Finished: SUCCESS 확인.

PR 병합과 Argo CD Sync는 수동으로 진행했다. 승인한 PR의 OutOfSync를 확인하고 정확한 revision으로 Sync했다. 테스트 성공, 이미지 게시, 운영 적용을 각각 확인하는 경로였다.

연결에 사용한 공개 이력은 source PR #75와 GitOps PR #3이다. 최종 source main은 76b913c133a47291d68a1922e9c15384c7dacd9a, Platform manifest는 sha256:00726f98ec4f9c1db0bb7d0d91074072ab850dbc9590e3d3243e2d260756ecfa로 대조했다.

SEARCH JOURNAL

기록 검색

글 제목, 요약, 태그에서 찾습니다.

검색을 준비하고 있어요.

검색어를 입력해 주세요.

    Ctrl/⌘ K 검색 Esc 닫기 ↑ ↓ 결과 이동