CI/CD 스터디 06: Container Image Build와 보안 스캔

Dockerfile, BuildKit/buildx, image tag와 digest, container scan, SBOM 관점에서 CI/CD 이미지 빌드 보안 게이트를 정리합니다.

컨테이너 기반 CI/CD에서는 Dockerfile이 운영 이미지의 출발점입니다. Dockerfile 품질이 낮으면 파이프라인이 아무리 좋아도 배포 결과가 불안정합니다.

컨테이너 빌드 컴포넌트

  • Dockerfile: runtime filesystem과 entrypoint를 정의합니다.
  • builder: BuildKit, buildx, kaniko, buildah처럼 image layer를 생성합니다.
  • base image: 보안 패치와 패키지 기준이 되는 부모 이미지입니다.
  • registry: 빌드 결과를 저장하고 배포 시스템이 pull하는 장소입니다.
  • scanner: 취약점, secret-like file, misconfiguration, license risk를 검사합니다.

좋은 Dockerfile의 기본

운영 이미지는 작고, 재현 가능하며, 권한이 낮아야 합니다. build dependency와 runtime dependency를 분리하기 위해 multi-stage build를 사용합니다.

FROM node:22-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:22-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=build /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]

빌드 명령

IMAGE=registry.example.com/team/app
VERSION=v1.4.0

docker buildx create --use --name app-builder || docker buildx use app-builder
docker buildx build --platform linux/amd64 --tag ${IMAGE}:${VERSION} --tag ${IMAGE}:sha-$(git rev-parse --short=12 HEAD) --push .

스캔 게이트 배치

보안 스캔은 너무 늦게 돌리면 배포 직전에 막히고, 너무 빡세게 걸면 개발 속도가 멈춥니다. 기본은 PR 단계에서 Dockerfile/static scan, main 단계에서 image scan, release 단계에서 digest 기반 evidence 저장입니다.

# 예시 도구. 실제 정책은 조직 기준에 맞게 severity와 ignore 만료일을 정한다.
trivy fs --exit-code 1 --severity HIGH,CRITICAL .
hadolint Dockerfile
trivy image --exit-code 1 --severity HIGH,CRITICAL registry.example.com/team/app:v1.4.0

GitHub Actions 예시

jobs:
  image:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - name: Build image
        run: |
          docker buildx build --load -t local/app:${{ github.sha }} .
      - name: Scan image
        run: |
          trivy image --exit-code 1 --severity HIGH,CRITICAL local/app:${{ github.sha }}

위험 신호

  • Dockerfile에서 root 사용자로 runtime을 실행합니다.
  • build context 전체를 무분별하게 COPY합니다.
  • base image tag가 latest입니다.
  • 패키지 설치 후 cache와 index 파일을 그대로 남깁니다.
  • scanner 실패를 무조건 allow_failure로 처리합니다.
  • image tag만 기록하고 digest를 남기지 않습니다.

실무 체크리스트

  1. .dockerignore를 먼저 정리합니다.
  2. base image는 digest 또는 명확한 version tag로 고정합니다.
  3. runtime stage에는 빌드 도구와 test dependency를 남기지 않습니다.
  4. 스캔 예외는 파일로 관리하고 만료일과 근거를 둡니다.
  5. 운영 배포는 image digest를 release record에 저장합니다.

참고 문서

BGM EVER