51 KiB
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 |
|
|
|
2026-05-22 | in-progress | BR-CA-SKELETON-OPERATIONAL-CONTRACT-030 | project-work-item | ca-skeleton-operational-contract | WI-CA-SKELETON-OPERATIONAL-CONTRACT-030 |
|
1 | 4ea821a22a546fda0f4407358f63672198be276985d938779551dfde818ab5e8 |
branch: feature-container-runtime-contract
Layer:
raw/branch-notes/— JVM application이 container 환경에서 예측 가능하게 동작하기 위한 runtime 기준을 정의합니다.
부모 (필수)
- Parent project (canonical SSOT): raw/project-notes/ca-skeleton-operational-contract
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 기본 정책 정합)
- 검토한 대안:
- 대안 1: Distroless (Google) — raw/official-docs/container-distroless-google-github (보안 surface 축소 vs in-container 디버깅 손실)
- 대안 2: Alpine + musl libc — raw/official-docs/container-alpine-java-musl-tradeoffs (image 크기 작음 vs native lib/DNS resolver 호환성 risk)
- 대안 3: GraalVM Native Image + Spring Boot Native — 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 채택 결론
- 비교 핵심: 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 내장).
- D2 (read-only root fs): K8s 공식 Application Security Checklist + NSA/CISA Hardening Guide 가 read-only root fs + tmpfs/emptyDir writable mount 패턴을 권고. 단 PSS Restricted admission 은
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:testPASS. SHUTDOWN_BUDGET_DRIFT 주의: composestop_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), ②$HOMEread-only fs write 위험 발견 → DockerfileENV 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 || true→2>/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, localeen_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-8 → C.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-tradeoffs는company-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-tmplsrc/Dockerfile은 빈 파일 → 본 § 의 base image/JVM/securityContext detail 은 전부planned.server.shutdown/timeout-per-shutdown-phaseconfig 만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% 안이라는 가정.HeapDumpPathnaming<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: 경로별
emptyDirvstmpfs(medium: Memory)선택은 trade-off(tmpfs=pod 메모리 소비/≈0 latency vs emptyDir=node disk I/O).server.tomcat.basedir=/tmpredirect 는 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.
- UNSUPPORTED_IMPL_DECISION: 35s/20s/5s/10s 조합은 Datadog 사례(
RH-DD-C2needs-confirmation) 기반 ca-tmpl 운영 가정 — 실측 없음.- OUT_OF_BRANCH_SCOPE: shutdown ordering invariant (SIGTERM→readiness DOWN→drain→exit) 는 raw/branch-notes/feature-runtime-health-lifecycle-contract D4(app-side) owner.
APP_SERVER_SHUTDOWN_TIMEOUTenv-key 값/validation 은 raw/branch-notes/feature-env-driven-runtime-configuration owner — 본 branch 는 그 값을 consume.
| 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=75workload-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 owner —docs/registries/error-codes.yamlL213: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:
/tmpmount 누락 시 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/heapemptyDir 이 node ephemeral storage quota 초과 → pod eviction.emptyDir.sizeLimit미설정 risk.
- read-only root fs + unmounted write path:
- 다른 계약 의존:
- raw/branch-notes/feature-env-driven-runtime-configuration
D2—APP_SERVER_SHUTDOWN(default graceful) /APP_SERVER_SHUTDOWN_TIMEOUT(default 30s, validationspring_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.yamlJVM_OOM— 본 branch owner(INTERNAL/500/retryable=false/owner_layer=infrastructure/required_test=contract-verification:container-runtime-oom). - registry
metrics.yamljvm.memory.used/jvm.gc.pause(ownerfeature-metrics-alerting-contract) — OOM/heap alert 연계(heap used/max > 0.85 for 10m).
- raw/branch-notes/feature-env-driven-runtime-configuration
테스트 계약
- inflight request 처리 검사:
server.shutdown=graceful+spring.lifecycle.timeout-per-shutdown-phaseproperty가 명시되어 있어야 함. 측정 방법:./gradlew bootRun후curl 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 viaMultipartFile.transferTocleanup (b) startup sweeper bean (OrphanTempFileSweeper) 등록 —/var/tmp/upload/*1시간 초과 파일 삭제 (c) JVM shutdown hook. 측정 방법: 1h-old file을/var/tmp/upload/에 두고 application 재시작 → 5분 이내 파일 삭제 verify. (주의:OrphanTempFileSweeper는 ca-tmplsrc/에 미존재 →planned) - app shutdown timeout이 manifest termination grace보다 길면 실패.
- OOM 분류 검사: container exit code 137(SIGKILL) →
OOMKilled(container OOM, kubelet); JVMOutOfMemoryError→-XX:+ExitOnOutOfMemoryError로 137 exit +/var/tmp/heapheap dump. 측정 방법:-Xmx16m로 강제 JVM OOM 트리거 → exit code 137 + heap dump 파일 생성 verify (앱은error.code=JVM_OOM을 직접 emit 하지 않음 — 구조화 alert 는 observability log-pattern). required_testcontract-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_TIMEOUTdefault=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_period를 35s→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; runbookinternal-error-spike.mdL39 =/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-rootappuser(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-8locale(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);--memory256/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/heapwritable,/app·/write 거부) 모두 PASS →locally-verified. $HOME 수정: useradd --no-create-home + read-only fs 에서$HOME(/home/app)write 실패 발견 → DockerfileENV HOME=/tmp추가(writable tmpfs redirect). - CONTRACT_OK: registry
JVM_OOMrow 가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 즉시 실패.dbservicePOSTGRES_PASSWORD+appserviceSPRING_DATASOURCE_PASSWORD2곳 모두 교체. - DOCKERFILE_DEPENDENCY_WARMUP_SWALLOWED_FIXED (✅ 해소 — 2026-06-15 ca-quality-reviewer advisory):
src/Dockerfile의RUN ./gradlew dependencies --no-daemon --quiet 2>/dev/null || true가 dependency resolution 실패를 조용히 삼켜 CI 에서 빈 캐시 레이어 + 후속 빌드 실패를 유발할 수 있었음.2>/dev/null || true제거 → fail-fast (resolution 실패 시 빌드 즉시 중단 + 정확한 에러 노출).--continuepartial-resolution 허용이 불필요한 구조이므로 단순 제거로 충분.
관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
/coverage가 채우는 생성물 — 손으로 유지하지 않는다. governing 문서(frontmattergoverning_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, inboundgrpc/graphql/websocket) 추가 후,src/Dockerfilebuilder 스테이지의 하드코딩 per-moduleCOPY <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 실증):
- 호스트
./gradlew verifyDependencyLocks→ BUILD SUCCESSFUL 9s, leaf 19개 모듈 전부 STRICT 락 통과. - 경량 throwaway 이미지(alpine +
COPY --parents+find, Gradle 미실행) → glob이 build.gradle 20개(19 모듈 + 루트) + gradle.lockfile 19개를 구조 보존 스테이징 (누락 6개 포함). - 실제
src/Dockerfile전체 빌드 성공 (exit 0):docker build -f src/Dockerfile src/ --build-arg RELEASE_VERSION=0.0.1 ...—#15 COPY --parentsDONE 0.2s →#16 verifyDependencyLocksDONE 120.8s(컨테이너 내 STRICT resolve) →#20 :app-bootstrap:bootJarBUILD SUCCESSFUL 25s →caskeleton:verify-local이미지 생성. 이전 노트의 "Docker build unverified (no docker in worktree)" 상태를 여기서 해소.
- 호스트
- 이 시점
develop워킹트리 clean, 커밋은 사용자가 직접 수행.
2026-07-05 — sample-portfolio standalone 데모 이미지 추가 (src/Dockerfile.sample)
- 동기: 프로덕션 bootstrap 이미지(
src/Dockerfile→CaSkeletonApplication)는 기본값 없는 env 72개 + JWT issuer + prod 시크릿/DB 검증으로 "그냥 띄워 테스트"가 어려움.sample-portfolio(SamplePortfolioApplication)는 자체application.yml이 모든 env에 기본값을 주고SamplePublicAccessSecurityConfig로 열려 있어 데모/서버 테스트에 적합 → k3s 배포용으로 별도 Dockerfile 신설. - 구조 = src/Dockerfile 트윈: builder 스테이지(labs
# syntax+COPY --parentsglob + STRICTverifyDependencyLocks+COPY . .)와 런타임 하드닝(non-root, read-only-fs 쓰기 마운트, JVM ergonomics, EXPOSE 8080/9001, HEALTHCHECK)을 그대로 상속. 차이는 5가지뿐: (1) ARG 기본값으로 argless 빌드, (2):sample-portfolio:bootJar타깃, (3) jar 경로, (4) LABELcaskeleton-sample, (5) 릴리스 메타데이터 hard-fail 가드 제거(데모라 불필요). - 런타임 사실: PG 드라이버
org.postgresql:postgresql:42.7.8는adapter:outbound:persistence-jpa(runtimeOnly, RDBMS base + PG 벤더 병합 모듈)를 통해 샘플 runtimeClasspath에 존재 → bootJar 실행 가능. 부팅엔 reachable PostgreSQL만 있으면 됨(자체 Flywaydb/sample-migration/V2,V6). - 함정 (해소): ARG
RELEASE_VERSION=0.0.0-sample으로 최초 argless 빌드가 build.gradle SemVer 가드(\d+\.\d+\.\d+, L21)에 걸려verifyDependencyLocksexit 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:bootJarBUILD SUCCESSFUL 40s, 이미지ca-sample:local(581MB) 생성. glob 레이어는src/Dockerfile빌드와 CACHED 공유. 커밋은 사용자가 직접 수행.
묶음
- raw/company-tech-blogs/container-woowahan-spring-native-tradeoffs
- raw/company-tech-blogs/runtime-health-datadog-engineering-graceful-shutdown
- raw/official-docs/container-alpine-java-musl-tradeoffs
- raw/official-docs/container-distroless-google-github
- raw/official-docs/container-graalvm-native-image-spring-boot
- raw/official-docs/k8s-application-security-checklist-readonly-fs
- raw/official-docs/k8s-pod-security-standards-restricted
- raw/official-docs/openjdk-jdk-8196595-container-support
- raw/official-docs/redhat-openjdk-container-awareness-java17
- raw/official-docs/runtime-health-k8s-probes-official
- raw/official-docs/supply-chain-cosign-keyless-sigstore
- raw/official-docs/supply-chain-slsa-provenance-framework
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 취약점인가? BuildKitCOPY --parentsglob으로 레이어 캐싱을 유지하면서 drift를 없애는 방법은? (일반COPY **/build.gradle는 왜 안 되는가 — 경로 평탄화/충돌) - Dockerfile
# syntaxfrontend를 태그가 아닌 digest로 고정하는 공급망(supply-chain) 근거는? cache mount(--mount=type=cache) 전략이 GitHub Actionstype=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 --parentsglob 한 줄로 drift 제거 + 레이어 캐싱 유지"
관련 일일 노트
[[raw/daily-notes/2026-06-15]]
완료 후 wiki 추출 대상
- wiki/projects/ca-tmpl/runtime-container-health-migration 의 container runtime canonical section (§Container).
완료 후 정리
머지/종료 시점에 채움.
- 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