Files
service-catalog/.claude/skills/self-build-image/SKILL.md
T
wbsong111 63d25221df MEMORY.md 를 상태 파일로 되돌린다 (264줄 → 64줄)
MEMORY.md 는 3번째 줄에서 "지금 시점의 상태와 다음에 할 일만 담는다" 고 스스로 선언하는데
실제로는 완료 기록이 쌓여 264줄이 됐고, 이슈 참조가 한 건도 없었다. 그 결과:

- SBOM_PIPELINE_IMAGE 재빌드가 파일 안에서 3번 반복된다 (215·234·262줄)
- 게이트 강제력 전환이 2번 반복되고, 그 내용은 이미 이슈 #7 체크리스트와 #19 에 있다
- argo-cd 절 55줄은 커밋 33e91c0 메시지의 요약본이다
- 베이스 OS 정책·"벤더 릴리스 노트 믿지 말 것" 은 본문이 스스로 "이미 image-authoring.md /
  skill 에 있다" 고 밝히면서 중복 서술한다

같은 사실이 여러 곳에 있으면 나중에 어느 쪽이 맞는지가 새 문제가 된다.

성격별로 목적지를 정하고 내보냈다
---------------------------------
  추적이 필요한 미결   →  GitHub 이슈 (#29~#33 발행, MEMORY.md 엔 한 줄 링크)
  재발 방지 교훈       →  .claude/ 문서·skill
  단순 완료 기록       →  삭제 (커밋·PR·images/<image>/README.md 가 권위 있는 기록)

교훈 이관 — 아직 어디에도 없던 것만 옮겼다:

- image-authoring.md: 베이스 OS 를 CVE 수치만으로 고르면 배포에서 죽는다.
  cp --update=none(coreutils 9.3+) 실측과, 차트가 실행하는 명령을 verify.sh 에서
  재현하라는 규칙. 원칙 2 의 "BCI 버전은 실측해서 정한다" 바로 뒤에 붙였다.
- self-build-image skill: 같은 날 재빌드하면 태그가 겹쳐 노드 캐시가 반영을 막는다.
  근본 해결은 #31, 여기엔 우회법만.
- cve-remediation skill: 레버 표를 보기 전에 "이 컴포넌트가 필요하긴 한가" 를 먼저 묻는다.
  dex 가 한 줄로 차단 57건(58%)이었다. 가장 값싼 레버인데 표에 없어서 새 단계로 넣었다.

유지 규칙을 명시했다
--------------------
트리거는 시간이 아니라 상태다 — "1달 지났으니 정리" 는 아무도 시작하지 않고, 날짜로 자르면
오래됐지만 유효한 미결까지 잘린다. 항목이 "다음에 할 일" 이 아니게 되는 순간 내보낸다.
다만 놓친 것을 걷어내기 위해 월 1회 점검하고 파일 상단의 점검 날짜를 갱신한다.

규칙은 MEMORY.md 자체와 CLAUDE.md("작업 기록을 어디에 남기는가") 양쪽에 뒀다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 16:56:05 +09:00

55 lines
3.6 KiB
Markdown

---
name: self-build-image
description: 자체 빌드 하드닝 이미지를 추가하거나 기존 빌드 정의를 변경할 때 사용한다. "이미지 자체 빌드해줘", "하드닝 이미지 추가", "차단 CVE를 자체 빌드로 해소", "build-hardened-image.sh 실행", "images/ 아래 새 이미지" 같은 요청이 해당한다. 현재 adc, apisix, apisix-ingress-controller, cloudnative-pg, cnpg-postgresql, etcd, keycloak 7종이 있다(정확한 목록은 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`
추가하면 워크플로 수정 없이 태울 수 있다.
## 실측된 함정
- **`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`).
- **단, 같은 날 다시 빌드하면 그 태그가 겹친다.** 노드가 캐시한 옛 digest 가 그대로 쓰여
(`imagePullPolicy: IfNotPresent`) 고친 것이 반영되지 않은 채 "안 고쳐졌다" 로 보인다.
배포 검증 중이라면 `imagePullPolicy: Always` 로 우회하고, digest 로 확인한다 —
`kubectl get pod -o jsonpath='{..imageID}'`. 프레임워크 차원의 미결 사항이다.
## 마무리
카탈로그 values(`custom-values.yaml`/`dip-values.yaml`) 태그 갱신은
`scripts/build/patch-catalog-tag.py`가 한다. 조사·결정 근거는 PR 설명과
[MEMORY.md](../../../MEMORY.md)에 남긴다. `images/**`+`manifests/helm/**` 변경은 PR로만 반영한다.