Files
llm-wiki/raw/branch-notes/feature-container-runtime-contract.md

51 KiB
Raw Permalink Blame History

title, source_type, status, branch, parent_branch, related_projects, governing_docs, tags, created, target_merge, status_label, id, kind, project, work_item, inherits, refines, overrides, depends_on, contract_packet, contract_packet_sha256
title source_type status branch parent_branch related_projects governing_docs tags created target_merge status_label id kind project work_item inherits refines overrides depends_on contract_packet contract_packet_sha256
branch / feature-container-runtime-contract branch-note raw feature-container-runtime-contract
ca-skeleton
wiki/projects/ca-tmpl/runtime-container-health-migration
branch
ca-skeleton
container
runtime
jvm
2026-05-22 in-progress BR-CA-SKELETON-OPERATIONAL-CONTRACT-030 project-work-item ca-skeleton-operational-contract WI-CA-SKELETON-OPERATIONAL-CONTRACT-030
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-CONTAINER-001@1
1 4ea821a22a546fda0f4407358f63672198be276985d938779551dfde818ab5e8

branch: feature-container-runtime-contract

Layer: raw/branch-notes/ — JVM application이 container 환경에서 예측 가능하게 동작하기 위한 runtime 기준을 정의합니다.

부모 (필수)

ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 해당 영역 (governing: wiki/projects/ca-tmpl/runtime-container-health-migration §Container 슬라이스) 의 결정/근거/금지 사항을 정제한다.

브랜치 계약 패킷

  • 생성 시 프로젝트 개정: 1
  • 패킷 스키마: contract_packet: 1
  • 완료 조건: non-root·memory·health container contract test가 통과한다

상속한 프로젝트 결정

Decision Ref Project Summary Branch Application Source
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-CONTAINER-001@1 Temurin JRE slim이 default이며 distroless는 debug/runbook 보강 후 허용한다 Work Item 완료 조건에 적용 raw/project-notes/ca-skeleton-operational-contract

브랜치 지역 결정

기존 branch-local 결정은 아래 ## Decision Evidence Map / 결정-근거 매핑의 D-row가 소유하며 이 packet에서 복제하지 않는다.

Decision ID Decision Relation Supporting Claims Status

선언한 예외

Override ID Overrides Reason Approval Status

목표

같은 Spring application이라도 container memory, timezone, signal, filesystem, healthcheck 기준이 없으면 서버별로 다르게 실패합니다. 이 branch는 skeleton의 runtime contract를 고정합니다.

  • 이슈:
  • PR:

범위

포함 범위

  • JVM memory/container limit 기준.
  • timezone/locale 기준.
  • graceful shutdown signal 기준.
  • healthcheck command 기준.
  • writable filesystem 최소화 기준.
  • temp directory/resource exhaustion 기준.

제외 범위

  • Kubernetes manifest 작성.
  • Helm chart 작성.
  • cloud-specific autoscaling.
  • health probe endpoint shape / group membership (→ raw/branch-notes/feature-runtime-health-lifecycle-contract owner; 본 branch 는 manifest-side probe timing field 만).
  • app-side graceful shutdown ordering invariant (→ sibling D4 owner; 본 branch 는 manifest-side budget 값 owner).

근거 (필수, 최소 1개+)

본 branch의 결정 근거. 상세 비교는 §외부 근거 / 대안 조사 (있다면) 참조.

Source 정당화하는 결정
raw/official-docs/container-distroless-google-github 보안 surface 축소 vs in-container 디버깅 손실
raw/official-docs/container-alpine-java-musl-tradeoffs image 크기 작음 vs native lib/DNS resolver 호환성 risk
raw/official-docs/container-graalvm-native-image-spring-boot cold start/메모리 우위 vs reflection 메타데이터 + 빌드 시간 + peak throughput 손실
raw/company-tech-blogs/container-woowahan-spring-native-tradeoffs 우아한형제들 Spring Native 도입기, hybrid 채택 결론
raw/official-docs/k8s-application-security-checklist-readonly-fs D2 — Kubernetes 공식 checklist 가 readOnlyRootFilesystem: true 를 "most applications" 에 적용되는 base security hardening 항목으로 명시 (K8S-ASC-C1, K8S-ASC-C2)
raw/official-docs/k8s-pod-security-standards-restricted D2 — Restricted profile 이 emptyDir 을 허용 볼륨으로 명시 (K8S-PSS-C2); readOnlyRootFilesystem 이 현행 Restricted admission field 목록에 없음 확인 (K8S-PSS-C3)
raw/official-docs/redhat-openjdk-container-awareness-java17 D4 — -XX:MaxRAMPercentage=75 rationale: MaxRAMPercentage 기본값 25%, cgroup v1/v2 지원 JDK 버전 경계, container limit → GC/heap/thread-pool ergonomics 영향 (RHAT-JCONT-C1~C4)
raw/official-docs/openjdk-jdk-8196595-container-support D4 — UseContainerSupport 기본 활성(default true) + -XX:{Initial,Max,Min}RAMPercentage 플래그가 container/system 메모리 대비 비율로 Java heap 크기를 제어함을 Oracle 공식 JDK 문서로 증명 (JDK-8196595-C1~C5)
raw/official-docs/kubernetes-pod-lifecycle-termination D5 — terminationGracePeriodSeconds(default 30s) + preStop → SIGTERM → grace 만료 시 SIGKILL 순서 (K8S-POD-LC-C1~C5)
raw/official-docs/spring-boot-graceful-shutdown-reference D5 — Spring graceful shutdown 기본 활성 + 기존 요청 완료/신규 거부 + spring.lifecycle.timeout-per-shutdown-phase (SB-GS-C1~C5)
raw/official-docs/config-12-factor-app-config D1 — config 는 deploy 마다 가변·code 는 불변(deploy 간 가변성 분리) + config 는 env vars 에 저장하는 12-factor Factor III 원칙 (TWELVE-FACTOR-CONFIG-C1, C2)
raw/official-docs/container-stdout-logging-12factor-official D1 — 실행 환경(=deployment manifest)이 runtime 관심사를 소유하고 앱은 설정 불가하다는 12-factor Logs(XI) 책임 분리 원칙 보강 (LOG-12F-C4)
raw/company-tech-blogs/runtime-health-datadog-engineering-graceful-shutdown D5 — preStop 5s + drain + grace 비율 권장치 (RH-DD-C1~C4, needs-confirmation 강도)

외부 근거 / 대안 조사 (2026-05-22 — Group G-D: Container Runtime)

본 branch의 Temurin JRE slim + -XX:MaxRAMPercentage=75 + UTC/UTF-8 + graceful shutdown(app 20s + preStop 5s + grace 35s) 결정에 대한 외부 source 조사. 비교 분석은 (예정) wiki/concepts/runtime-container-health-migration.md 참조.

  • 채택 결정 (Temurin JRE slim baseline):
    • (Spring Boot 3 JVM 기본 정책 정합)
  • 검토한 대안:
  • 비교 핵심: Temurin JRE slim은 운영 친숙도/디버깅 우선. Distroless는 보안↑/디버깅↓. GraalVM native-image는 startup·메모리 우위지만 reflection 비용 + peak throughput 손실 — 우아한형제들 사례도 hybrid 채택. ca-tmpl baseline은 skeleton 단계에 적합, native-image는 cold start 민감 service 진입점.
  • 2026-06-14 보강 (D2/D4 자동조사 — /branch-spec):
    • D2 (read-only root fs): K8s 공식 Application Security Checklist + NSA/CISA Hardening Guide 가 read-only root fs + tmpfs/emptyDir writable mount 패턴을 권고. 단 PSS Restricted admission 은 readOnlyRootFilesystem 을 자동 강제하지 않음(K8S-PSS-C3) → 명시 securityContext 또는 별도 policy engine 필요. 대안: writable root fs(레거시 path 조사 임시), 완전 read-only no-mount(non-JVM static binary 한정 — JVM 은 startup write 로 broken).
    • D4 (container-aware JVM): MaxRAMPercentage 비율(cgroup limit 추적) vs 절대 -Xmx(고정·재조정 필요) vs JVM 기본 25%(Spring Boot 단일 프로세스 과소배정 — 금지). 75% 는 vendor 범위(Red Hat 50→80%) 안의 팀 관행. locale 은 C.UTF-8 권고(Debian slim 내장).

TODO

TODO drained — 결정은 error.category enum 표 / MDC Key Standard 표 / error.details JSON shape 참조

진행 중 메모

  • 2026-06-14 (/branch-spec): D2(read-only root fs)·D4(container-aware JVM) 외부 근거 자동조사 + 아카이브(K8s checklist/PSS, OpenJDK, Red Hat). D1(12-factor)·D5(K8s pod lifecycle + Spring graceful shutdown) 기존 raw source wire. ## 구현 가이드·## 엣지·실패·의존·## Audit & Findings 신설. ca-tmpl ground-truth 대조에서 발견한 drift(§Audit) 는 사용자 결정 영역이라 자동 rewrite 하지 않고 권고만.
  • 2026-06-15 (ca-implementer): 구현 완료 (locally-verified)src/Dockerfile (multi-stage, Temurin JRE jammy, non-root app user, JAVA_TOOL_OPTIONS 전체 셋, C.UTF-8, EXPOSE 8080/9001), src/.dockerignore (신규 생성), docker-compose.yml (read_only+tmpfs+mem_limit 512m+stop_grace_period 35s), docker-compose.dev.yml, docker-compose.local.yml 작성 완료. OperationalError.JVM_OOM (INTERNAL/500/false) 신규 추가 — actually-implemented. ContainerRuntimeOomContractTest (app-bootstrap) 신규 — 소프트 runbook 파일 체크 패턴. ./gradlew :shared-contract:test + ./gradlew :app-bootstrap:test PASS. SHUTDOWN_BUDGET_DRIFT 주의: compose stop_grace_period=35s, APP_SERVER_SHUTDOWN_TIMEOUT=20s 를 canonical 값으로 사용; src/.env 의 30s 값(env-driven-config 브랜치 소유)과 drift 존재 — compose 파일에 주석으로 명시. LOCALE_DRIFT 해소: C.UTF-8 로 구현. Dockerfile HEALTHCHECK 교차 기능 커플링(actuator 브랜치 소유) — 주석으로 명시.
  • 2026-06-15 (/verify): docker 라이브 표면 검증 — 이미지 빌드 + JVM ergonomics(cgroup heap 추적) + non-root + C.UTF-8/UTC + read-only fs/tmpfs 모두 PASS(§Audit DOCKERFILE_IMPLEMENTED). 2건 보정: ① JVM_OOM 런타임 emit 은 앱이 하지 않고 observability 로 위임(문구 정합, §구현 가이드 §5 + §Audit OOM_LOG_LOSS), ② $HOME read-only fs write 위험 발견 → Dockerfile ENV HOME=/tmp 추가.
  • 2026-06-15 (ca-quality-reviewer advisory fixes): 2건 보안·신뢰성 픽스 적용. ① docker-compose.local.yml${POSTGRES_PASSWORD:-changeme} 2곳(db 서비스 + app 서비스 datasource) 을 required-variable form ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in your local .env — no default provided} 로 교체(no-default-ships) — AGENTS.md 하드코딩 비밀 금지 준수. ② src/Dockerfile 의존성 warm-up 라인 ./gradlew dependencies ... 2>/dev/null || true2>/dev/null || true 제거(fail-fast) — CI network-restricted 환경에서 dependency resolution 실패를 조용히 삼키지 않도록 수정. ./gradlew :shared-contract:test :app-bootstrap:test --tests '*ContainerRuntimeOomContractTest' PASS 확인.

Work Item Contract

각 TODO는 아래 판정 단위로 재작성되어야 canonical 승급 가능합니다. TODO가 단순히 기준 작성으로 남아 있으면 이 branch는 완료로 보지 않습니다.

field required rule
Decision yes 구현자가 선택해야 하는 기본값
Allowed yes 허용되는 예외와 조건
Forbidden yes 절대 금지되는 구현/문서 상태
Required registry update conditional error/env/header/log/metric/capability 변경 시 필수
Required contract test yes 계약 위반 시 실패해야 하는 테스트
Failure condition yes review/build에서 실패로 판정할 상태
Canonical extraction target yes wiki/projects 승급 위치

결정 사항

  • 2026-05-22: runtime 기준은 application code와 deployment manifest 사이의 계약으로 둠.
  • 2026-05-22: prod container는 writable path를 최소화하고 temp directory를 명시해야 함.
  • 2026-05-22: base image 기본값은 Temurin JRE slim. distroless는 debug/runbook 보강 후 허용.
  • 2026-05-22: JVM 기본값은 -XX:MaxRAMPercentage=75, timezone UTC, locale en_US.UTF-8.
  • 2026-05-22: deployment manifest sync는 이 branch가 owner이며 terminationGracePeriodSeconds, preStop, app shutdown timeout, health probes를 한 표로 관리.

결정-근거 매핑

각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시. Decision ID 는 이 branch-note 안에서 안정적으로 유지. 선택 조건 열(R2): "이 조건일 때 이 결정, 다른 조건이면 어떤 대안". 분기 없으면 N/A.

Decision ID Decision 선택 조건 (언제 이 결정 / 언제 대안) Supporting Claims Evidence Strength Open Risk
D1 runtime 기준은 application code 와 deployment manifest 사이의 계약 runtime tunable(memory/timezone/shutdown/probe)이 환경마다 달라질 수 있으면 → 이미지가 아니라 manifest/env 로 외부화. 빌드 시 고정 + 환경 불변 값이면 → 이미지에 baked 허용(예외) raw/official-docs/config-12-factor-app-config.md#TWELVE-FACTOR-CONFIG-C1 (config 는 deploy 마다 가변, code 는 불변 — deploy 간 가변성 분리), #TWELVE-FACTOR-CONFIG-C2 (config 를 env vars 에 저장); raw/official-docs/container-stdout-logging-12factor-official.md#LOG-12F-C4 (실행 환경이 runtime 관심사를 완전 관리, 앱 설정 불가 — 동일 방법론 보강) official-reference (12-factor Config/Logs 책임 분리 원칙) + team-policy (구체적 owner 분배) 12-factor 는 config↔code, app↔실행환경 분리 원칙 만 지지 — "이 branch 가 manifest sync owner" 라는 구체적 책임 분배는 외부 표준 부재(team-policy)
D2 prod container 는 writable path 최소화 + read-only root filesystem 의무화 + temp directory 명시 prod K8s JVM 컨테이너 → read-only root fs + tmpfs/emptyDir writable mount. 레거시 앱이 다수 path 에 write 하고 path 매핑 미완료면 → writable root fs 임시(배포 전 path 조사 단계, prod 금지). non-JVM static binary 면 → no-mount 완전 read-only 가능(JVM 은 startup write 로 broken) raw/official-docs/k8s-application-security-checklist-readonly-fs.md#K8S-ASC-C1 (readOnlyRootFilesystem: true 명시적 권고), #K8S-ASC-C2 (base security hardening — most applications), raw/official-docs/k8s-pod-security-standards-restricted.md#K8S-PSS-C2 (emptyDir = Restricted 허용 볼륨) official-vendor-doc K8S-PSS-C3: readOnlyRootFilesystem 은 PSS Restricted admission 이 자동 강제하지 않음 — securityContext 명시 또는 별도 policy engine 필요. ca-tmpl 의 실제 write-path 전부 emptyDir/tmpfs redirect 됨은 구현 검증 필요(Claims To Verify)
D3 base image default = Temurin JRE slim, distroless 는 debug runbook 보강 후 허용 운영/디버깅 친숙도 우선 → Temurin JRE slim. 보안 surface 최소화 + 디버깅 runbook 보강 완료 → distroless. cold-start/메모리 민감 + reflection 적은 service → GraalVM native 검토 raw/official-docs/container-distroless-google-github.md#CDG-C1 (distroless = app + runtime only, no shell), #CDG-C5 (:debug variant 는 busybox shell 제공) official-vendor-doc (Google distroless 의 공식 trade-off) Google 의 CDG-C2 "best practice" 는 self-claim — industry-wide consensus 아님
D4 JVM 기본값 = -XX:MaxRAMPercentage=75, timezone UTC, locale en_US.UTF-8 container memory limit 이 환경마다 다르거나 변동 → MaxRAMPercentage(비율, cgroup 추적). 메모리 프로파일 고정 + 절대값 고정 규정 → -Xmx. (무설정 기본 25% 는 Spring Boot 단일 프로세스 과소배정 → 금지) raw/official-docs/openjdk-jdk-8196595-container-support.md#JDK-8196595-C1 (UseContainerSupport 기본 활성), #JDK-8196595-C3 (MaxRAMPercentage = heap 최대 % of memory, 기본 25%), raw/official-docs/redhat-openjdk-container-awareness-java17.md#RHAT-JCONT-C4 (container limit → GC/heap/thread-pool ergonomics) official-vendor-doc (C1+C3) + team-convention (75% 수치) 75% 를 official best practice 로 표현 금지 — vendor 범위 70~80% 내 팀 관행. locale en_US.UTF-8C.UTF-8 수정 권고 (§Audit LOCALE_DRIFT)
D5 deployment manifest sync owner = 본 branch (terminationGracePeriodSeconds, preStop, app shutdown timeout, health probe timing 일원화) app-side(Spring graceful) 와 manifest-side(K8s grace/preStop) 가 양쪽에 걸칠 때 → 한 표로 일원화 owner 필요. 단일 비-K8s 배포면 manifest sync 표 불필요(예외) raw/official-docs/kubernetes-pod-lifecycle-termination.md#K8S-POD-LC-C1 (terminationGracePeriodSeconds default 30s + preStop→SIGTERM), #K8S-POD-LC-C3 (kubelet→SIGTERM to PID1), #K8S-POD-LC-C2 (grace 만료 시 SIGKILL), raw/official-docs/spring-boot-graceful-shutdown-reference.md#SB-GS-C3 (기존 요청 완료/신규 거부), #SB-GS-C4 (timeout-per-shutdown-phase) official-vendor-doc (K8s + Spring 공식) budget 수치(35s/20s/5s)는 Datadog 사례(RH-DD-C2 needs-confirmation) 기반 권장치 — 실측 없음. env-key APP_SERVER_SHUTDOWN_TIMEOUT default 30s 와 본 표 20s drift (§Audit SHUTDOWN_BUDGET_DRIFT)

Note: Alpine + musl 대안의 risk 는 raw/official-docs/container-alpine-java-musl-tradeoffs.md#CAJM-C1~CAJM-C5 가 직접 지지하며, ca-tmpl Temurin JRE slim 채택의 negative-evidence 역할. Distroless 의 image size 이점 (CDG-C4) 은 static-debian13 기준이며 Java distroless variant 는 더 큼 — 본 branch 의 baseline 비교 시 주의. container-woowahan-spring-native-tradeoffscompany-tech-blog 카테고리이므로 GraalVM hybrid 결론은 company-case-study 강도만 가지며 official best practice 로 표현 금지.

구현 가이드

결정 (Decisions) 이 "무엇" 이라면, 본 §는 "어디에 어떻게" 의 사전 명세. 3-rule(CLAUDE.md §15.5): R1 각 cell 은 Decision ID + Supporting Claim reference, R2 근거 없는 detail 은 UNSUPPORTED_IMPL_DECISION + trade-off 한 줄, R3 본 branch 범위 밖 detail 은 위임(§Audit). 코드 상태 주의: ca-tmpl src/Dockerfile 은 빈 파일 → 본 § 의 base image/JVM/securityContext detail 은 전부 planned. server.shutdown/timeout-per-shutdown-phase config 만 actually-implemented(env-key 배선).

1. Base image + Dockerfile (Trace: D3 · CDG-C1/C5 · CAJM-C1~C5)

Trace: D3. Temurin JRE slim 채택 = distroless/alpine-musl/GraalVM 대안 검토 후 baseline.

  • UNSUPPORTED_IMPL_DECISION: multi-stage 구조, JRE 버전 핀(예: eclipse-temurin:21-jre-jammy), non-root UID 값은 무출처 팀 선택 — Dockerfile 미작성이라 전부 planned.
항목 명세 상태 Anchor
base image Temurin JRE slim (distroless = debug runbook 보강 후 허용) planned (src/Dockerfile empty) D3 / CDG-C1
USER non-root (K8S-ASC-C3: privileged:false + drop ALL caps 와 정합) planned K8S-ASC-C3
forbidden prod 에서 root full JDK image D3

2. JVM ergonomics + locale (Trace: D4 · JDK-8196595-C1/C3 · RHAT-JCONT-C1/C4)

Trace: D4. UseContainerSupport(default-on) + MaxRAMPercentage(cgroup 비율) 채택.

  • UNSUPPORTED_IMPL_DECISION: 75% 수치는 vendor 범위(70~80%) 내 팀 관행 — non-heap(metaspace/code cache/thread stacks/direct buffer, RHAT-JCONT-C4)이 25% 안이라는 가정. HeapDumpPath naming <pod>-<ts> 패턴, emptyDir.sizeLimit(heap dump 누적 eviction 방지) 값 미정.
-XX:MaxRAMPercentage=75
-XX:+UseContainerSupport       # JDK 10+ default (JDK-8196595-C1), 명시 권장
-XX:HeapDumpPath=/var/tmp/heap/<pod>-<ts>.hprof   # emptyDir mount 의무 (§3)
-XX:+ExitOnOutOfMemoryError    # → §5 OOM_LOG_LOSS 주의
TZ=UTC
LANG=C.UTF-8                   # ⚠️ 현행 결정문은 en_US.UTF-8 — §Audit LOCALE_DRIFT, C.UTF-8 권고
  • 컨테이너 memory limit 반드시 설정 — 미설정 시 MaxRAMPercentage 가 host RAM 기준(RHAT-JCONT-C4) → 과대/과소 할당.

3. Filesystem policy: read-only root fs + writable mounts (Trace: D2 · K8S-ASC-C1 · K8S-PSS-C2/C3)

Trace: D2. read-only root fs + 필수 경로만 tmpfs/emptyDir.

  • UNSUPPORTED_IMPL_DECISION: 경로별 emptyDir vs tmpfs(medium: Memory) 선택은 trade-off(tmpfs=pod 메모리 소비/≈0 latency vs emptyDir=node disk I/O). server.tomcat.basedir=/tmp redirect 는 D2 도출 필수 수반결정(미설정 시 read-only root fs 에서 Tomcat work dir write fail → startup CrashLoop — D2 자동조사 finding).
  • OUT_OF_BRANCH_SCOPE: PSS Restricted admission enforcement 설정 자체(policy engine 배선)는 security baseline branch 영역 — 본 branch 는 securityContext 필드 값만.
항목 명세 상태 Anchor
root fs securityContext.readOnlyRootFilesystem: true planned K8S-ASC-C1
heap dump path /var/tmp/heap emptyDir mount planned K8S-PSS-C2 (emptyDir 허용)
temp/upload /tmp tmpfs mount planned K8S-PSS-C2
Tomcat work dir server.tomcat.basedir=/tmp (또는 java.io.tmpdir=/tmp) planned (수반결정) D2 자동조사 finding
enforcement 주의 readOnlyRootFilesystem 은 PSS Restricted 가 자동 강제 안 함 → 명시 securityContext 필수 K8S-PSS-C3

4. Deployment manifest sync (Trace: D5 · K8S-POD-LC-C1/C2/C3 · SB-GS-C3/C4)

Trace: D5. app-side(Spring) ↔ manifest-side(K8s) timeout/probe 일원화. 본 branch = manifest-side 값 owner.

field default 위임/상태 Anchor
app shutdown timeout 20s (server.shutdown=graceful + spring.lifecycle.timeout-per-shutdown-phase) config actually-implemented(env-key 배선); 값 drift — env default 30s (§Audit) SB-GS-C3/C4 / app-bootstrap/.../application.yml:205,211
preStop hook sleep 5s 본 branch owner K8S-POD-LC-C1
terminationGracePeriodSeconds 35s 본 branch owner K8S-POD-LC-C1 (default 30s 를 override)
safety margin 10s (drain late completion 흡수) 본 branch — env default 30s 적용 시 0 으로 붕괴(§Audit)
readiness failure before drain required 위임 sibling(ordering) K8S-POD-LC-C3
startup probe required when migration/startup validation enabled 위임 sibling(endpoint shape)

Runtime Defaults 요약표 (위 표의 정책 한 줄 view):

item default allowed forbidden
base image Temurin JRE slim distroless with debug runbook root full JDK image in prod
JVM memory -XX:MaxRAMPercentage=75 workload-specific override container limit ignored
timezone UTC none server default timezone
shutdown SIGTERM -> readiness down -> drain -> exit forced kill after grace SIGKILL before app timeout
manifest sync one table for app timeout/probes/preStop platform-specific overlay app/manifest timeout mismatch

5. OOM 분류 + JVM_OOM error code (Trace: D4 · registry error-codes.yaml · K8S-POD-LC-C2)

Trace: D4(ExitOnOutOfMemoryError) + registry JVM_OOM. JVM_OOM 은 본 branch 가 registry ownerdocs/registries/error-codes.yaml L213: code: JVM_OOM, category: INTERNAL, http_status: 500, retryable: false, owner_branch: feature-container-runtime-contract, owner_layer: infrastructure, runbook_link: runbook://runtime/jvm-oom, required_test: contract-verification:container-runtime-oom. enum/registry 분류 = actually-implemented (OperationalError.JVM_OOM = INTERNAL/500/false + parity test, 2026-06-15 GREEN). 런타임 구조화 emit(error.code=JVM_OOM 로그)은 미배선 — 설계상 위임 (아래 결정 + §Audit OOM_LOG_LOSS).

  • 결정 (2026-06-15): JVM_OOM 구조화 로그는 앱이 emit 하지 않음 — -XX:+ExitOnOutOfMemoryError 가 OOM 즉시 abort 하여 앱 핸들러(shutdown hook/UncaughtExceptionHandler)로 안정 emit 불가. 런타임 구분 신호 = JVM 네이티브 OOM stderr + exit 137 + heap dump. 구조화 error.code=JVM_OOM 로그-기반 alert 는 observability 브랜치로 위임. enum 은 분류 SSOT 로만 유지.
  • container exit 137 (SIGKILL) → OOMKilled (container OOM, kubelet 결정, K8S-POD-LC-C2).
  • JVM OutOfMemoryError-XX:+ExitOnOutOfMemoryError 로 137 exit + -XX:+HeapDumpOnOutOfMemoryError/var/tmp/heap 에 heap dump.
  • 두 케이스 모두 exit 137 → 구분 신호 = heap dump 유무 + JVM 네이티브 OOM stderr ("Terminating due to java.lang.OutOfMemoryError"). kubelet OOMKill 은 둘 다 없음.
  • ⚠️ OOM_LOG_LOSS (해소 — 위임 결정): 앱이 error.code=JVM_OOM 을 직접 emit 하지 않음(ExitOnOutOfMemoryError 가 핸들러보다 먼저 abort — 의도된 설계). 구조화 alert 는 observability log-pattern(네이티브 OOM msg + exit 137)으로 위임. enum 은 분류 코드로 유지. §Audit 참조.

6. (위임) Health probe endpoint standard — OUT_OF_BRANCH_SCOPE

endpoint shape / group membership 은 raw/branch-notes/feature-runtime-health-lifecycle-contract §1 SSOT. 아래는 manifest-side 참조용 mirror — 값 변경 시 sibling 이 authoritative. 본 branch 는 manifest 의 probe timing field 만 owns.

  • liveness: GET /actuator/health/liveness (deadlock/메모리 한정 검사)
  • readiness: GET /actuator/health/readiness (dependency status)
  • startup: GET /actuator/health/startup (migration/validation 진행 중)
  • single-probe timeout: liveness 1s / readiness 2s / startup 30s.
  • startup probe total budget(failureThreshold × periodSeconds = 150s)는 raw/branch-notes/feature-runtime-health-lifecycle-contract SSOT.

엣지·실패·의존

R4(깊이 게이트) 캡처용. 정상 경로 외 구현 중 부딪힐 실패/엣지/타 계약 의존.

  • 실패·엣지 경로:
    • read-only root fs + unmounted write path: /tmp mount 누락 시 Spring Boot embedded Tomcat startup write → Permission denied → startup probe failureThreshold → CrashLoopBackOff. 포착: dev/staging 에서 readOnlyRootFilesystem: true + smoke test (Claims To Verify). 사전 식별: strace -e trace=open,openat,creat 로 write syscall 추적.
    • shutdown budget > terminationGracePeriodSeconds: grace 만료 시 SIGKILL → inflight 유실(K8S-POD-LC-C2). 본 표 app 20s + preStop 5s = 25s ≤ grace 35s 이나, env default 30s 적용 시 30+5=35=grace → margin 0(§Audit SHUTDOWN_BUDGET_DRIFT).
    • ExitOnOutOfMemoryError 즉시 exit → JVM_OOM log flush 손실 가능(audit 2026-05-25 #4.28).
    • exit 137 모호성: kubelet OOMKill(SIGKILL) vs JVM OOM(ExitOnOutOfMemoryError 137) 둘 다 137 → log error.code=JVM_OOM 유무로만 구분.
    • MaxRAMPercentage non-heap spike: metaspace/direct buffer 급증 → 75% heap + 25% non-heap 가정 초과 → cgroup limit 초과 → OOMKill(RHAT-JCONT-C4).
    • container memory limit 미설정: MaxRAMPercentage 가 host RAM 기준 → 과대/과소 할당.
    • heap dump 누적: /var/tmp/heap emptyDir 이 node ephemeral storage quota 초과 → pod eviction. emptyDir.sizeLimit 미설정 risk.
  • 다른 계약 의존:
    • raw/branch-notes/feature-env-driven-runtime-configuration D2APP_SERVER_SHUTDOWN(default graceful) / APP_SERVER_SHUTDOWN_TIMEOUT(default 30s, validation spring_duration_shorthand_le_termination_grace) env-key consume. 본 branch 의 terminationGracePeriodSeconds 가 이 값 ≥ 여야 함.
    • raw/branch-notes/feature-runtime-health-lifecycle-contract D4/§4 — graceful shutdown ordering invariant(app-side) owner; §6 에서 TZ=UTC 를 본 branch 로 위임. 본 branch = manifest-side 값 + container env owner.
    • raw/branch-notes/feature-migration-startup-contract — startup probe budget(150s) 및 startup validation 은 그쪽 owner; 본 branch 는 manifest 의 startup probe 존재 만.
    • registry error-codes.yaml JVM_OOM본 branch owner(INTERNAL/500/retryable=false/owner_layer=infrastructure/required_test=contract-verification:container-runtime-oom).
    • registry metrics.yaml jvm.memory.used/jvm.gc.pause (owner feature-metrics-alerting-contract) — OOM/heap alert 연계(heap used/max > 0.85 for 10m).

테스트 계약

  • inflight request 처리 검사: server.shutdown=graceful + spring.lifecycle.timeout-per-shutdown-phase property가 명시되어 있어야 함. 측정 방법: ./gradlew bootRuncurl localhost:8080/long-running 호출 + SIGTERM 보내고 응답 도착 timeout < 25s 이내 verify. property 누락 또는 25s 초과 시 fail. (값 drift 주의 — §Audit SHUTDOWN_BUDGET_DRIFT)
  • timezone이 서버 default에 암묵 의존하면 실패.
  • temp cleanup 검사: APP_FILE_UPLOAD_ENABLED=true이면 다음 3가지 cleanup 메커니즘이 모두 활성: (a) try-with-resources via MultipartFile.transferTo cleanup (b) startup sweeper bean (OrphanTempFileSweeper) 등록 — /var/tmp/upload/* 1시간 초과 파일 삭제 (c) JVM shutdown hook. 측정 방법: 1h-old file을 /var/tmp/upload/에 두고 application 재시작 → 5분 이내 파일 삭제 verify. (주의: OrphanTempFileSweeper 는 ca-tmpl src/ 에 미존재 → planned)
  • app shutdown timeout이 manifest termination grace보다 길면 실패.
  • OOM 분류 검사: container exit code 137(SIGKILL) → OOMKilled (container OOM, kubelet); JVM OutOfMemoryError-XX:+ExitOnOutOfMemoryError로 137 exit + /var/tmp/heap heap dump. 측정 방법: -Xmx16m로 강제 JVM OOM 트리거 → exit code 137 + heap dump 파일 생성 verify (앱은 error.code=JVM_OOM 을 직접 emit 하지 않음 — 구조화 alert 는 observability log-pattern). required_test contract-verification:container-runtime-oom 은 enum↔registry parity 를 검증(2026-06-15 GREEN).

검증해야 할 주장

공식 문서나 사례는 근거지만, 내 프로젝트에서의 동작을 자동으로 보장하지 않는다. 구현 전/중/후에 실제로 검증해야 하는 주장을 분리.

Claim Why uncertain How to verify Status
Spring Boot graceful shutdown 이 20s 내 inflight request 처리 완료 server.shutdown=graceful + timeout-per-shutdown-phase 의 실제 동작은 endpoint 로직에 따라 달라짐 ./gradlew bootRun + curl /long-running 호출 + SIGTERM → 응답 도착 timeout < 25s verify planned
-XX:MaxRAMPercentage=75 가 container memory limit 을 정확히 인식 JDK 10+ UseContainerSupport 기본값이 모든 cgroup 환경에서 정상 동작한다는 직접 보장 부재 (cgroup v2 는 11.0.16+/17.0.4+/21 — RHAT-JCONT-C3) container memory limit 변화 시 Runtime.getRuntime().maxMemory() 가 75% 로 변화 verify; -Xlog:os+container=trace 로 cgroup 인식 확인 needs-confirmation
JVM OOM → exit 137 + /var/tmp/heap heap dump 생성 (앱 구조화 emit 없음 — 설계상 위임) ExitOnOutOfMemoryError 가 핸들러보다 먼저 abort → 앱 emit 불가; 구분은 heap dump + 네이티브 msg -Xmx16m 강제 OOM → exit 137 + heap dump 파일 존재 verify (앱 부팅 필요 — 단독 미검증) planned
컨테이너에 C.UTF-8 locale 존재 + JVM file.encoding=UTF-8 Temurin JRE slim(Debian) 에 C.UTF-8 내장 여부 + en_US.UTF-8 은 locales 패키지 필요 — minimal image 에서 미존재 가능 docker run <img> locale + java -XshowSettings:properties 2>&1 | grep file.encoding verify planned
readiness failure → drain 순서가 SIGTERM 처리 시 자동 보장 preStop sleep 5s + readiness probe cache delay 일치 보장 부재 k8s 환경에서 SIGTERM 시 readiness false 전환 후 drain 시작 트레이스 verify planned
temp file cleanup (3 메커니즘) 이 모두 활성 + 누락 없음 try-with-resources / startup sweeper / shutdown hook 중 하나만 누락되어도 leak (OrphanTempFileSweeper 미구현) 1h-old file 을 /var/tmp/upload/ 에 두고 재시작 → 5분 이내 삭제 verify planned
$HOME(/home/app) write 가 read-only root fs 에서 실패하지 않는다 useradd --no-create-home + read-only fs → java.util.prefs(~/.java/.userPrefs) 등 $HOME write 라이브러리 실패 가능 (2026-06-15 docker 검증서 발견 → Dockerfile ENV HOME=/tmp 로 mitigate) 앱 부팅 후 prefs/SDK 의 $HOME(=/tmp tmpfs) write 성공 + read-only-fs WARN 부재 verify planned
container exit 137 (SIGKILL by kubelet) 와 JVM OOM (137 by ExitOnOutOfMemoryError) 가 구분 가능 두 케이스 모두 exit 137 → heap dump 유무 + JVM 네이티브 OOM msg 로 구별(앱 구조화 로그 아님) cgroup limit 초과(OOMKill, dump 없음) vs JVM heap 한계(dump 생성) 각각 분류 verify needs-confirmation
distroless 채택 시 in-container 진단 도구 부재 영향이 runbook 으로 완화 CDG-C5:debug variant 는 busybox shell 만, jcmd/jstack/heap dump 별도 distroless prod pod 에서 ephemeral container/sidecar 로 heap dump 추출 PoC + runbook planned
Alpine + musl 채택 시 Testcontainers / native lib (snappy, zstd-jni 등) 정상 동작 CAJM-C4 공식 경고 — musl 호환성 risk alpine + Temurin musl 이미지에서 ca-tmpl integration test suite + native lib 호출 verify planned
read-only root fs 강제 시 모든 write-path 가 emptyDir/tmpfs 로 redirect application 코드의 file write 가 누락된 path 에서 발생 가능; Tomcat basedir 미설정 위험 k8s securityContext readOnlyRootFilesystem: true + smoke test 로 startup/runtime write 실패 catch planned
startup probe total budget (150s) 이 migration/validation 시간을 모두 커버 DB migration 사이즈에 따라 150s 초과 가능 (budget owner = sibling) 대용량 migration scenario 에서 startup probe success verify; 초과 시 fail planned

Audit & Findings (ca-tmpl ground-truth 대조 2026-06-14, /branch-spec)

코드/registry/governing doc/sibling 대조에서 발견한 drift. 사용자 작성 결정 영역은 자동 rewrite 하지 않고 정합 권고만(CLAUDE.md §11).

  • SHUTDOWN_BUDGET_DRIFT ( 해소 2026-06-15 — option (a) 채택): 본 노트 app shutdown timeout=20s 이나 registry env-key APP_SERVER_SHUTDOWN_TIMEOUT default=30s (owner raw/branch-notes/feature-env-driven-runtime-configuration D2, 2026-06-05 — 본 노트 2026-05-22 이후 갱신). env default 30s 적용 시 preStop 5s + drain 30s = 35s = terminationGracePeriodSeconds → 본 노트의 10s safety margin 이 0 으로 붕괴. validation rule(spring_duration_shorthand_le_termination_grace)은 만 강제하므로 통과하나 margin 의도 상실. 권고: (a) app budget 을 30s 로 정합하고 grace 를 40s 로 상향, 또는 (b) env default 를 20s 로 낮춤 — 둘 다 사용자/env-config branch 결정. 해소(2026-06-15, /ca-parallel 후속): 권고 (a) 채택 — env-keys.yaml 이 app shutdown (30s)의 SSOT 이고 본 branch 는 관계(grace ≥ timeout+preStop+margin)의 owner 이므로, app drain 30s 를 보존한 채 stop_grace_period35s→40s(30s+preStop 5s+margin 5s) 로 상향. docker-compose.yml/docker-compose.local.yml 의 fallback :-20s:-30s 정합 + sync-table 주석/stop_grace_period 갱신. .env/env-keys 는 불변(30s). 설계상 정합; docker 런타임 스모크는 미검증.
  • OOM_LOG_LOSS ( 해소 — 위임 결정 2026-06-15): 검증 결과 앱 코드에 JVM_OOM emitter 없음(grep 확인 — enum/comment/test 만 참조). -XX:+ExitOnOutOfMemoryError 가 OOM 즉시 abort → 앱 핸들러로 구조화 emit 은 원천적으로 불안정. 결정: 앱은 emit 하지 않음. 런타임 구분 = exit 137 + heap dump(/var/tmp/heap, HeapDumpOnOutOfMemoryError) + JVM 네이티브 OOM stderr. 구조화 error.code=JVM_OOM 로그-기반 alert 는 observability 브랜치(log-pattern)로 위임. enum/registry 는 분류 SSOT(parity test GREEN). §구현 가이드 §5 반영.
  • LOCALE_DRIFT (🟡 Should-fix): 본 노트 결정문 LANG=en_US.UTF-8; governing doc(runtime-container-health-migration §Container) + sibling raw/branch-notes/feature-runtime-health-lifecycle-contract §6 = LANG=C.UTF-8. D4 자동조사(RHAT) 결론: C.UTF-8 이 Debian slim 내장(locales 패키지 불필요) → minimal image 정석. 권고: C.UTF-8 로 정합(§구현 가이드 §2 는 이미 C.UTF-8 + 주석 표기). 결정문은 사용자 영역이라 미수정.
  • HEAP_DUMP_PATH_DRIFT ( Advisory): 본 노트 -XX:HeapDumpPath=/var/tmp/heap/<pod>-<ts>.hprof; runbook internal-error-spike.md L39 = /var/tmp/heap/heapdump-<pid>.hprof. 구현 시 단일 path 규약으로 정합 필요.
  • PROBE_OWNERSHIP_DELEGATION (정합 OK, 위임 명시): health probe endpoint shape/group membership 은 feature-runtime-health-lifecycle-contract §1 owner. 본 branch 는 manifest-side probe timing field 만. §구현 가이드 §6 에 위임 표기 완료(R3).
  • DOCKERFILE_IMPLEMENTED (사실 등급 — 2026-06-15 갱신): ca-tmpl src/Dockerfile 구현 완료 (actually-implemented). multi-stage(JDK builder → JRE slim runtime), non-root app user(uid 1000), JAVA_TOOL_OPTIONS 전체 셋(-XX:MaxRAMPercentage=75/-XX:+UseContainerSupport/-XX:+ExitOnOutOfMemoryError/-XX:+HeapDumpOnOutOfMemoryError/-XX:HeapDumpPath=/var/tmp/heap/-Dserver.tomcat.basedir=/tmp), C.UTF-8 locale(LOCALE_DRIFT 해소), EXPOSE 8080/9001, src/.dockerignore 신규, compose 3개(base/dev/local) 모두 작성. Gradle test 검증 가능한 항목(JVM_OOM enum + parity test) locally-verified. Docker build + 런타임 표면 검증 완료 (2026-06-15, docker 29.5.3, /verify): 이미지 빌드 성공(multi-stage, JRE-only 534MB); --memory 256/512/1024m 에서 heap 185/371/742M 로 cgroup 추적 확인(UseContainerSupport 실동작); non-root uid 1000; C.UTF-8 + file.encoding=UTF-8 + TZ=UTC; read-only root fs + tmpfs(/tmp·/var/tmp/heap writable, /app·/ write 거부) 모두 PASS → locally-verified. $HOME 수정: useradd --no-create-home + read-only fs 에서 $HOME(/home/app) write 실패 발견 → Dockerfile ENV HOME=/tmp 추가(writable tmpfs redirect).
  • CONTRACT_OK: registry JVM_OOM row 가 owner_branch: feature-container-runtime-contract 로 본 branch 를 명시 — 계약 정합 확인. NO_GROUND_TRUTH 아님(ca-tmpl 경로 존재).
  • WEAK_DEFAULT_PASSWORD_FIXED ( 해소 — 2026-06-15 ca-quality-reviewer advisory): docker-compose.local.yml${POSTGRES_PASSWORD:-changeme} 가 weak default password 를 bake-in 함. ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in your local .env — no default provided} (required-variable form) 으로 교체 → .env 미설정 시 compose up 즉시 실패. db service POSTGRES_PASSWORD + app service SPRING_DATASOURCE_PASSWORD 2곳 모두 교체.
  • DOCKERFILE_DEPENDENCY_WARMUP_SWALLOWED_FIXED ( 해소 — 2026-06-15 ca-quality-reviewer advisory): src/DockerfileRUN ./gradlew dependencies --no-daemon --quiet 2>/dev/null || true 가 dependency resolution 실패를 조용히 삼켜 CI 에서 빈 캐시 레이어 + 후속 빌드 실패를 유발할 수 있었음. 2>/dev/null || true 제거 → fail-fast (resolution 실패 시 빌드 즉시 중단 + 정확한 에러 노출). --continue partial-resolution 허용이 불필요한 구조이므로 단순 제거로 충분.

관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)

/coverage 가 채우는 생성물 — 손으로 유지하지 않는다. governing 문서(frontmatter governing_docs: wiki/projects/ca-tmpl/runtime-container-health-migration §Container 슬라이스)가 요구하는 관심사를 이 브랜치가 빠짐없이 덮는지의 결과. 기준: rules/coverage-gate.md. 상태: covered-here(이 브랜치 결정) / delegated(다른 owner 브랜치) / missing(아무도 안 맡음 → Blocking).

관심사 상태 owner 심각도 근거
Container base image + JVM ergonomics covered-here D3, D4
Locale / timezone (UTC, UTF-8) covered-here D4 (§Audit LOCALE_DRIFT)
Writable filesystem 최소화 (read-only root fs) covered-here D2
Graceful shutdown budget (manifest-side) covered-here D5
Graceful shutdown ordering invariant (app-side) delegated raw/branch-notes/feature-runtime-health-lifecycle-contract OK §구현 가이드 §4 위임 링크
Health probe endpoint shape / group membership delegated raw/branch-notes/feature-runtime-health-lifecycle-contract OK §구현 가이드 §6 위임 링크
Migration / startup probe budget delegated raw/branch-notes/feature-migration-startup-contract OK §엣지·실패·의존 의존 링크
OOM 분류 + JVM_OOM error code covered-here §구현 가이드 §5 + registry owner

마주친 문제

  • JVM_OOM 테스트 TDD red: OperationalError.JVM_OOM 미존재 → compileTestJava 컴파일 에러 → 의도한 RED 확인 후 enum 추가 → GREEN. 전형적 TDD red 확인 흐름.
  • Dockerfile 0-byte placeholder: src/Dockerfile 이 0-byte 추적 파일 — Write 도구 첫 시도에서 "File has not been read yet" 에러. Read 먼저 한 뒤 Write 성공.
  • internal_category_codes_are_retryable 루프: JVM_OOM(INTERNAL/retryable=false) 추가 시 기존 루프가 실패함 — 예상된 변경. && e != OperationalError.JVM_OOM 제외 조건 + 별도 focused assertion 추가로 해소.

유지보수 로그

2026-07-05 — builder-stage 모듈 COPY 목록 stale 수정 (develop, k3s 배포 준비)

  • 문제: inbound/outbound 어댑터 재구조화 + 신규 어댑터(outbound objectstorage/fileserver/persistence-mongo, inbound grpc/graphql/websocket) 추가 후, src/Dockerfile builder 스테이지의 하드코딩 per-module COPY <module>/build.gradle + gradle.lockfile 목록이 6개 모듈 누락 상태로 방치됨. verifyDependencyLocks(COPY . . 이전 실행)는 settings.gradle 전체 leaf 모듈을 STRICT resolve하는데, app-bootstrap이 누락 모듈을 project 의존으로 참조 → 릴리스 이미지 빌드가 깨질 상태였음. "레거시"의 실체는 스타일이 아니라 모듈 구조와의 drift.
  • 수정: 28줄 하드코딩 COPY 블록 → COPY --parents settings.gradle build.gradle **/build.gradle **/gradle.lockfile ./ 1줄로 교체. --parents가 디렉토리 구조를 보존하므로 신규 모듈이 자동 포함 → 다시는 settings.gradle과 drift 안 남 (D8 락 캐싱 전략·runtime 스테이지 모두 무변경).
  • labs 프론트엔드 digest 고정: --parents는 labs Dockerfile frontend 필요 → 최상단에 # syntax=docker/dockerfile:1.7-labs@sha256:b99fecfe00268a8b556fad7d9c37ee25d716ae08a5d7320e6d51c4dd83246894 추가. 떠다니는 태그 대신 digest 고정으로 빌드-타임 공급망 표면 최소화 (이 리포는 Cosign/SLSA/Trivy 파이프라인).
  • 검증 (3중, 마지막이 end-to-end 실증):
    1. 호스트 ./gradlew verifyDependencyLocks → BUILD SUCCESSFUL 9s, leaf 19개 모듈 전부 STRICT 락 통과.
    2. 경량 throwaway 이미지(alpine + COPY --parents + find, Gradle 미실행) → glob이 build.gradle 20개(19 모듈 + 루트) + gradle.lockfile 19개를 구조 보존 스테이징 (누락 6개 포함).
    3. 실제 src/Dockerfile 전체 빌드 성공 (exit 0): docker build -f src/Dockerfile src/ --build-arg RELEASE_VERSION=0.0.1 ...#15 COPY --parents DONE 0.2s → #16 verifyDependencyLocks DONE 120.8s(컨테이너 내 STRICT resolve) → #20 :app-bootstrap:bootJar BUILD SUCCESSFUL 25scaskeleton:verify-local 이미지 생성. 이전 노트의 "Docker build unverified (no docker in worktree)" 상태를 여기서 해소.
  • 이 시점 develop 워킹트리 clean, 커밋은 사용자가 직접 수행.

2026-07-05 — sample-portfolio standalone 데모 이미지 추가 (src/Dockerfile.sample)

  • 동기: 프로덕션 bootstrap 이미지(src/DockerfileCaSkeletonApplication)는 기본값 없는 env 72개 + JWT issuer + prod 시크릿/DB 검증으로 "그냥 띄워 테스트"가 어려움. sample-portfolio(SamplePortfolioApplication)는 자체 application.yml이 모든 env에 기본값을 주고 SamplePublicAccessSecurityConfig로 열려 있어 데모/서버 테스트에 적합 → k3s 배포용으로 별도 Dockerfile 신설.
  • 구조 = src/Dockerfile 트윈: builder 스테이지(labs # syntax + COPY --parents glob + STRICT verifyDependencyLocks + COPY . .)와 런타임 하드닝(non-root, read-only-fs 쓰기 마운트, JVM ergonomics, EXPOSE 8080/9001, HEALTHCHECK)을 그대로 상속. 차이는 5가지뿐: (1) ARG 기본값으로 argless 빌드, (2) :sample-portfolio:bootJar 타깃, (3) jar 경로, (4) LABEL caskeleton-sample, (5) 릴리스 메타데이터 hard-fail 가드 제거(데모라 불필요).
  • 런타임 사실: PG 드라이버 org.postgresql:postgresql:42.7.8adapter:outbound:persistence-jpa(runtimeOnly, RDBMS base + PG 벤더 병합 모듈)를 통해 샘플 runtimeClasspath에 존재 → bootJar 실행 가능. 부팅엔 reachable PostgreSQL만 있으면 됨(자체 Flyway db/sample-migration/V2,V6).
  • 함정 (해소): ARG RELEASE_VERSION=0.0.0-sample으로 최초 argless 빌드가 build.gradle SemVer 가드(\d+\.\d+\.\d+, L21)에 걸려 verifyDependencyLocks exit 1로 실패. --quiet가 원인 메시지를 가려 BuildKit 백그라운드 알림이 "exit 0" 오해를 줌(실제 REAL_EXIT=1). → RELEASE_VERSION은 순수 SemVer여야 하고 -sample 마커는 라벨 전용 BUILD_VERSION에만. RELEASE_VERSION=0.0.0으로 교정 후 재빌드 성공.
  • 검증: docker build -f src/Dockerfile.sample src/ -t ca-sample:local(argless) → REAL_EXIT=0, :sample-portfolio:bootJar BUILD SUCCESSFUL 40s, 이미지 ca-sample:local(581MB) 생성. glob 레이어는 src/Dockerfile 빌드와 CACHED 공유. 커밋은 사용자가 직접 수행.

묶음

Sub-branches (세부 작업)

  • (없음)

오류 기록 (본 feature 작업 중 발생)

  • (없음 — 마주친 문제 섹션에서 인라인 처리. 별도 error 노트 분리 불필요)

면접 준비 (이 작업에서 나올 수 있는 면접 질문)

  • JVM -XX:+ExitOnOutOfMemoryError 와 kubelet OOMKill 은 모두 exit 137 — 어떻게 구별하는가?
  • -XX:MaxRAMPercentage=75 가 의미 있으려면 컨테이너 memory limit 이 반드시 설정되어야 하는 이유는?
  • read_only: true 컨테이너에서 Spring Boot Tomcat 이 CrashLoop 하는 원인과 해결책?
  • Graceful shutdown budget: app drain 20s + preStop 5s + safety margin 10s → stop_grace_period=35s. .env의 30s 값과의 drift를 어떻게 처리했나?
  • 왜 final stage에 JDK가 아닌 JRE만 포함하는가?
  • 멀티모듈 Gradle 빌드에서 per-module COPY build.gradle 하드코딩 목록이 왜 stale 취약점인가? BuildKit COPY --parents glob으로 레이어 캐싱을 유지하면서 drift를 없애는 방법은? (일반 COPY **/build.gradle는 왜 안 되는가 — 경로 평탄화/충돌)
  • Dockerfile # syntax frontend를 태그가 아닌 digest로 고정하는 공급망(supply-chain) 근거는? cache mount(--mount=type=cache) 전략이 GitHub Actions type=gha 캐시와 왜 안 맞는가?
  • 하나의 멀티모듈 리포에서 "엄격한 릴리스 이미지(메타데이터 hard-fail·build-arg 필수)"와 "처분형 데모 이미지(argless·가드 없음)"를 별도 Dockerfile로 분리하는 기준은? builder 스테이지를 공유(동일 glob 레이어 → CACHED)하면서 무엇만 갈라내야 하는가?
  • RUN ./gradlew ... --quiet 가 실패했는데 BuildKit 백그라운드 알림은 "exit 0"으로 보였다 — --quiet가 원인 로그를 가리는 함정, 그리고 RELEASE_VERSION=0.0.0-sample 이 SemVer 가드(\d+\.\d+\.\d+)에 걸린 근본 원인을 어떻게 특정했나?

블로그·채용공고 연계 글감

  • "JVM OOM과 컨테이너 OOMKill은 왜 같은 exit 137인가 — 구별법과 error.code 전략"
  • "Spring Boot 컨테이너 graceful shutdown budget 계산 — preStop/drain/grace margin 조합"
  • "docker-compose read_only: true + Spring Boot — Tomcat basedir 를 /tmp 로 redirect 해야 하는 이유"
  • "멀티모듈 Gradle Dockerfile의 per-module COPY 목록이 조용히 stale해지는 문제 — COPY --parents glob 한 줄로 drift 제거 + 레이어 캐싱 유지"

관련 일일 노트

  • [[raw/daily-notes/2026-06-15]]

완료 후 wiki 추출 대상

완료 후 정리

머지/종료 시점에 채움.

  • PR 링크: (pending)
  • 리뷰 메모: (pending)
  • 머지 결과 / 배포 환경: 로컬 worktree (ca-tmpl-container-runtime) — Gradle test locally-verified; Docker build unverified (no docker in worktree)
  • wiki 추출 대상 (verified만, wiki/projects/로만 추출):
    • actually-implemented 항목: OperationalError.JVM_OOM 추가, ContainerRuntimeOomContractTest, OperationalErrorTest JVM_OOM 테스트
    • locally-verified 항목: src/Dockerfile, src/.dockerignore, docker-compose.yml, docker-compose.dev.yml, docker-compose.local.yml — 내용 locally-verified but docker runtime 미실행
    • prod-verified 항목: (없음)
  • 추출하지 않을 항목 (planned / documented-only / abandoned): Docker build 런타임 행동(locale, read-only-fs smoke, OOM exit 137) — docker-only-unverified