c3283bf7a9
#31 코멘트에 "자체 빌드 이미지를 쓰는 차트 7곳을 Always 로 바꿔야 한다" 고 적었는데 잘못됐다. Always 를 카탈로그 기본값으로 넣으면 파드 시작마다 kubelet 이 레지스트리에서 digest 를 해석하고, 레지스트리 장애·Docker Hub rate limit 이 파드 기동을 막는 경로가 생긴다. 태그 겹침은 같은 날 재빌드한 검증 시점에만 생기는 문제인데 그 대가를 운영 내내 지불하는 셈이다. 절차는 deploy-test-procedure.md 로 옮긴다 — 검증 대상 워크로드에만 patch 로 걸고 digest(imageID)로 확인한다. 태그가 같으므로 태그를 보는 것은 아무것도 증명하지 않는다. self-build-image SKILL 의 함정 항목은 이 문서를 가리키게 하고 "프레임워크 차원의 미결" 을 지운다 — 미결이 아니라 결정됐다. MEMORY.md 의 "values 반영 안 됨" 항목은 반영할 것이 없으므로 지운다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
109 lines
6.9 KiB
Markdown
109 lines
6.9 KiB
Markdown
---
|
|
name: self-build-image
|
|
description: 자체 빌드 하드닝 이미지를 추가하거나 기존 빌드 정의를 변경할 때 사용한다. "이미지 자체 빌드해줘", "하드닝 이미지 추가", "차단 CVE를 자체 빌드로 해소", "build-hardened-image.sh 실행", "images/ 아래 새 이미지" 같은 요청이 해당한다. 현재 adc, apisix, apisix-ingress-controller, argocd, cloudnative-pg, cnpg-postgresql, etcd, keycloak 8종이 있다(정확한 목록은 images/ 디렉토리가 단일 출처).
|
|
---
|
|
|
|
# 자체 빌드 하드닝 이미지
|
|
|
|
CVE 게이트 대응 우선순위(상위 태그 교체 → 베이스 OS 교체 → **자체 빌드** → 예외 승인)에서
|
|
앞의 두 단계로 해소가 안 될 때만 온다.
|
|
|
|
## 원칙 — 오케스트레이션은 항상 하나다
|
|
|
|
**`scripts/build/build-hardened-image.sh` 하나가 모든 자체 빌드 이미지를 빌드한다.**
|
|
OS 패키지 재설치든 소스 컴파일이든 스크립트는 같고, 차이는 전부 `images/<image>/` 안에 있다.
|
|
|
|
**"이 이미지는 성격이 다르다"는 이유로 새 오케스트레이션 스크립트를 만들지 않는다** —
|
|
절차(빌드 → 기능검증 → SBOM → 스캔 → 게이트 → push)는 이미지 종류와 무관하게 동일하다.
|
|
SBOM·스캔·게이트도 다시 만들지 않는다 — `build-hardened-image.sh`가 이미
|
|
`scan-sbom.sh`/`cve-gate.py`를 호출한다.
|
|
|
|
## 실행
|
|
|
|
```sh
|
|
IMAGE=<image> BASE_OS=<variant> bash scripts/build/build-hardened-image.sh /tmp/out
|
|
```
|
|
|
|
`images/<image>/<variant>.build.env`가 계약(`DOCKERFILE`, `TARGET`, `BUILD_ARGS` 등)을
|
|
선언하면 스크립트는 이미지 종류를 몰라도 된다. 전체 계약표와 신규 이미지 추가 7단계 절차는
|
|
[.claude/image-authoring.md](../../image-authoring.md)에 있다 — 여기 복제하지 않는다.
|
|
|
|
CI는 `build-image.yml`이 `image` 입력으로 이미 파라미터화돼 있다. `images/<image>/catalog.env`만
|
|
추가하면 워크플로 수정 없이 태울 수 있다.
|
|
|
|
## 새 Dockerfile 은 두 축을 먼저 고른다
|
|
|
|
**작성 규칙 본문은 [.claude/image-authoring.md](../../image-authoring.md)가 단일 출처다** —
|
|
여기 복제하지 않는다. 이 표는 "어느 절을 읽어야 하는가" 만 정한다.
|
|
|
|
두 축은 **독립**이다. 같은 `bci-micro` 최종 위에 Go 빌더가 오기도 하고 Node 빌더가 오기도 한다.
|
|
|
|
**축 1 — 최종 런타임 베이스** (스캔·배포 대상. 원칙 2)
|
|
|
|
| 런타임이 필요로 하는 것 | 고른다 | 선례 |
|
|
|---|---|---|
|
|
| OS 패키지·셸 (zypper 설치, 셸 entrypoint) | `bci-base` | `adc` `apisix` `argocd` `cnpg-postgresql` |
|
|
| 정적 링크 바이너리 하나뿐 | `bci-micro` | `apisix-ingress-controller` `cloudnative-pg` `etcd` |
|
|
| 런타임 트리를 builder 에서 조립 (JVM 등) | `scratch` + micro rootfs 씨앗 | `keycloak` |
|
|
|
|
→ 세 조합 밖으로 나가려면 **왜 셋으로 안 되는지 먼저 적는다.** `micro`/`scratch` 는
|
|
nonroot 계정·`sed`/`grep` 부재를 직접 감당해야 한다(image-authoring.md "어느 BCI 변종").
|
|
|
|
**축 2 — 빌더 스테이지** (최종에 남지 않음. 원칙 2 대상 아님 → 공식 언어 이미지 그대로)
|
|
|
|
| 언어 | 빌더 | 핀 키 |
|
|
|---|---|---|
|
|
| Go | `golang:${GO_BUILDER_TAG}` + `$BUILDPLATFORM`/`TARGETARCH` | `GO_BUILDER_TAG` · `GO_MODULE_UPGRADES` |
|
|
| Node | `node:*` (런타임은 **OS 패키지** `nodejs24`) | `NODE_BUILDER_TAG` · `NODE_PKG` |
|
|
| JVM | BCI base — 재컴파일 없이 **취약 jar 만 교체** | `<LIB>_OLD` / `<LIB>_VERSION` 쌍 |
|
|
| C · Lua | BCI base — **정적 링크 금지**(static glibc 없음) | 컴포넌트별 버전 ARG |
|
|
|
|
→ 상세는 image-authoring.md "언어별 빌더 규칙". 세 가지가 언어와 무관하게 공통이다:
|
|
**버전은 Dockerfile 에 박지 말고 `build.env` 값으로**, `BUILD_ARGS` 에 **반드시 등록**(빠뜨리면
|
|
조용히 기본값으로 빌드된다), 그리고 **업스트림 런타임 계약(`USER`·`ENTRYPOINT`·파일 레이아웃)은
|
|
보존**한다.
|
|
|
|
## 자동 재빌드 — 실행 계약만
|
|
|
|
`build-image.yml` 이 주간(월요일 02:00 UTC)으로 배포 중인 이미지를 스캔해 재빌드가 필요한
|
|
것을 찾아 빌드·검증·게이트까지 돈다. **트리거 조건과 왜 그 조건인지는
|
|
[.claude/image-authoring.md](../../image-authoring.md) "핀은 가만히 있어도 뒤처진다" 가
|
|
단일 출처다** — 여기 복제하지 않는다.
|
|
|
|
```sh
|
|
# 수동으로 같은 것을 돌린다
|
|
gh workflow run self-build-image --repo <org>/dip-catalog -f mode=drift
|
|
# 판정만 로컬에서 본다
|
|
python3 scripts/build/check-rebuild-needed.py --list-refs
|
|
python3 scripts/build/check-rebuild-needed.py --reports <trivy-reports> [--apply]
|
|
```
|
|
|
|
이 경로는 **레지스트리 push 와 카탈로그 반영을 하지 않는다** — push 는
|
|
`workflow_dispatch` + `mode=image` 에서만 켜진다.
|
|
|
|
## 실측된 함정
|
|
|
|
- **`FROM`에 쓰는 `ARG`는 첫 `FROM` 이전(전역 스코프)에 선언한다.** 스테이지 내부에 선언하면
|
|
그 스테이지 지역 변수가 되어 이후 `FROM`의 이미지명 해석에 쓰이지 않고 빈 이미지명 에러가 난다.
|
|
- **게이트 PASS는 "동작한다"를 증명하지 않는다.** CVE 스캐너는 런타임 요구사항(오퍼레이터가
|
|
자신의 파일 레이아웃에 의존하는 것 등)을 전혀 보지 못한다. 배포 검증을 반드시 한다 —
|
|
절차는 [.claude/deploy-test-procedure.md](../../deploy-test-procedure.md).
|
|
- **`CoverageProbe`가 `ok`인지 확인한다.** `none`이면 findings 0건이 진짜 0건이 아니라
|
|
스캐너에 그 배포판 데이터가 없다는 뜻이다(`sbom-cve-gate` skill 참고).
|
|
- **롤링 태그를 쓰지 않는다.** 같은 앱 버전이라도 베이스 업데이트 결과가 시점마다 달라
|
|
태그에 빌드일을 포함한다(예: `1.30.0-security-hardened-20260804`).
|
|
- **핀을 고정한 대가로 핀이 뒤처진다.** 소스를 안 바꿔도 새 CVE 가 공개되면 어제 PASS 였던
|
|
핀이 오늘 FAIL 이 된다. 위 "자동 재빌드" 가 이것을 잡는다. 조치 전에 그 결과를 먼저 본다:
|
|
`python3 scripts/build/check-rebuild-needed.py --reports <trivy-reports> [--image <name>] [--apply]`
|
|
- **단, 같은 날 다시 빌드하면 그 태그가 겹친다.** 노드가 캐시한 옛 digest 가 그대로 쓰여
|
|
(`imagePullPolicy: IfNotPresent`) 고친 것이 반영되지 않은 채 "안 고쳐졌다" 로 보인다.
|
|
**검증 대상 워크로드에 `imagePullPolicy: Always` 를 수동으로 걸고 digest 로 확인한다** —
|
|
절차는 [deploy-test-procedure.md](../../deploy-test-procedure.md) "같은 날 재빌드했다면"
|
|
이 단일 출처다. **카탈로그 values 에는 넣지 않는다**(같은 문서에 이유).
|
|
|
|
## 마무리
|
|
|
|
카탈로그 values(`custom-values.yaml`/`dip-values.yaml`) 태그 갱신은
|
|
`scripts/build/patch-catalog-tag.py`가 한다. 조사·결정 근거는 PR 설명과
|
|
[MEMORY.md](../../../MEMORY.md)에 남긴다. `images/**`+`manifests/helm/**` 변경은 PR로만 반영한다.
|