CI/CD עבור תמונות Docker: בנייה, סריקה, חתימה

בנייה ידנית של תמונות Docker ופרסומן מאטים את קצב השחרורים ומשאירות פרצות אבטחה בסביבת הייצור. אנחנו הופכים את מחזור ה-CI/CD כולו לאוטומטי: החל מבנייה רב-שלבית עם caching, ועד לסריקה וחתימה של התמונות. הצוות שלנו מספק את הפרויקט במלואו, עם pipeline אמין ותמיכה שוטפת.

היכולות שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1359
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1330
  • עיצוב לוגו לחברת B2B Advance
    עיצוב לוגו לחברת B2B Advance
    776
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1077
  • פיתוח לוגו חברת AIDER
    פיתוח לוגו חברת AIDER
    1022
  • פיתוח CRM עבור Chasseurs
    פיתוח CRM עבור Chasseurs
    1113

CI/CD לתמונות Docker: בנייה, סריקה, חתימה

סיפור טיפוסי: מפתח דוחף ל-main, GitLab Runner מבצע docker build && docker push, והתמונה עוברת לייצור. ללא שכבת מטמון (layer caching), הבנייה אורכת 8 דקות בכל commit. ללא סריקה, ה-registry מארח תמונות עם פגיעויות CRITICAL. ללא תיוג סמנטי, היסטוריית התמונות היא latest, latest, latest. נתקלנו בזה עשרות פעמים: בפרויקט אחד הבנייה ארכה 12 דקות, ומחצית מהתמונות ב-registry הכילו פגיעויות ברמת HIGH. הגדרת pipeline נכון פותרת את כל שלוש הבעיות בבת אחת והופכת את הכאוס לתהליך צפוי.

מה Pipeline CI/CD מלא ל-Docker צריך לעשות

Build → Scan → Sign → Publish → Tag. זה לא docker build -t myapp:latest && docker push אחד, אלא חמישה שלבים נפרדים עם כלים ונקודות כשל שונות. כל שלב הוא שלב CI נפרד, כך שאם מתרחשת שגיאה, התהליך כולו לא מופעל מחדש.

ה-pipeline צריך להיכשל אם הסורק מוצא פגיעות CRITICAL — ולעולם לא לדחוף תמונה לא בטוחה ל-registry. זה מונע מהצוות אפילו לדעת שהם מפרסים תמונות עם חורים. מניסיוננו, סריקה אוטומטית מפחיתה אירועי אבטחה ב-70% (ראה אבטחת שרשרת אספקת תוכנה).

Dockerfile רב-שלבי עם BuildKit

הבסיס ל-pipeline תקין הוא Dockerfile טוב. בנייה רב-שלבית מקטינה את התמונה הסופית פי 5–10 ומשפרת את המטמון. השווה: Dockerfile חד-שלבי עם SDK מלא שוקל 1.2 GB, בעוד שרב-שלבי שוקל 150 MB. ההבדל במהירות הפריסה ובעלויות האחסון ברור.

# syntax=docker/dockerfile:1.4
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production

FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN --mount=type=cache,target=/root/.npm \
    npm run build

FROM node:20-alpine AS runner
RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001
WORKDIR /app
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=deps /app/node_modules ./node_modules
USER nextjs
EXPOSE 3000
CMD ["node", "dist/server.js"]

התמונה לייצור אינה מכילה devDependencies, קוד מקור או מטמון. היא רצה תחת משתמש לא-מורשה. BuildKit cache mounts מאיצים את # syntax=docker/dockerfile:1.4 FROM node:20-alpine AS deps WORKDIR /app COPY package*.json ./ RUN --mount=type=cache,target=/root/.npm \ npm ci --only=production FROM node:20-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN --mount=type=cache,target=/root/.npm \ npm run build FROM node:20-alpine AS runner RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001 WORKDIR /app COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist COPY --from=deps /app/node_modules ./node_modules USER nextjs EXPOSE 3000 CMD ["node", "dist/server.js"] — התקנה מחדש אורכת שניות, לא דקות.

GitLab CI: Pipeline מלא

---
# .gitlab-ci.yml
variables:
  DOCKER_BUILDKIT: "1"
  IMAGE_NAME: ${CI_REGISTRY_IMAGE}
  IMAGE_TAG: ${CI_COMMIT_SHA}

stages:
  - build
  - scan
  - sign
  - push

build:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  before_script:
    - docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY}
  script:
    - docker buildx create --use
    - docker buildx build --cache-from type=registry,ref=${IMAGE_NAME}:cache --cache-to type=registry,ref=${IMAGE_NAME}:cache,mode=max --tag ${IMAGE_NAME}:${IMAGE_TAG} --output type=image,push=false --load .
    - docker save ${IMAGE_NAME}:${IMAGE_TAG} > image.tar
  artifacts:
    paths:
      - image.tar
    expire_in: 1 hour

scan:
  stage: scan
  image: aquasec/trivy:latest
  before_script:
    - docker load < image.tar
  script:
    - trivy image --severity CRITICAL,HIGH --ignore-unfixed --exit-code 1 --format table ${IMAGE_NAME}:${IMAGE_TAG}
  dependencies:
    - build

push:
  stage: push
  image: docker:24
  services:
    - docker:24-dind
  before_script:
    - docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY}
    - docker load < image.tar
  script:
    - docker push ${IMAGE_NAME}:${IMAGE_TAG}
    - |
      if [ "${CI_COMMIT_BRANCH}" = "main" ]; then
        docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:latest
        docker push ${IMAGE_NAME}:latest
      fi
    - |
      if [ -n "${CI_COMMIT_TAG}" ]; then
        docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:${CI_COMMIT_TAG}
        docker push ${IMAGE_NAME}:${CI_COMMIT_TAG}
      fi
  dependencies:
    - build
  only:
    - main
    - tags

העברת התמונה בין שלבים דרך npm install מונעת בנייה חוזרת. אנו סורקים בדיוק את התמונה שתיפרס, לא תמונה שנבנתה מחדש. זה חוסך עד 40% מזמן ה-pipeline של CI.

חתימת תמונות עם Cosign

---
sign:
  stage: sign
  image: cgr.dev/chainguard/cosign:latest
  script:
    - docker load < image.tar
    - cosign sign --yes ${IMAGE_NAME}:${IMAGE_TAG}@$(docker inspect --format='{{index .RepoDigests 0}}' ${IMAGE_NAME}:${IMAGE_TAG})
  variables:
    COSIGN_EXPERIMENTAL: "1"
  dependencies:
    - build
  only:
    - main
    - tags
---

Cosign עם חתימה ללא מפתח (keyless signing) משתמש ב-GitLab OIDC token כזהות — אין צורך לאחסן מפתח פרטי. זה מבטיח עמידה ברמת SLSA Level 2 באמצעות attestations חתומים, לפי תקן sigstore לתשתית בלתי-ניתנת לשינוי. החתימה מחוברת לתמונה ב-registry לפי digest. במהלך הפריסה ניתן לאמת: # .gitlab-ci.yml variables: DOCKER_BUILDKIT: "1" IMAGE_NAME: ${CI_REGISTRY_IMAGE} IMAGE_TAG: ${CI_COMMIT_SHA} stages: - build - scan - sign - push build: stage: build image: docker:24 services: - docker:24-dind before_script: - docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY} script: - docker buildx create --use - docker buildx build --cache-from type=registry,ref=${IMAGE_NAME}:cache --cache-to type=registry,ref=${IMAGE_NAME}:cache,mode=max --tag ${IMAGE_NAME}:${IMAGE_TAG} --output type=image,push=false --load . - docker save ${IMAGE_NAME}:${IMAGE_TAG} > image.tar artifacts: paths: - image.tar expire_in: 1 hour scan: stage: scan image: aquasec/trivy:latest before_script: - docker load < image.tar script: - trivy image --severity CRITICAL,HIGH --ignore-unfixed --exit-code 1 --format table ${IMAGE_NAME}:${IMAGE_TAG} dependencies: - build push: stage: push image: docker:24 services: - docker:24-dind before_script: - docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY} - docker load < image.tar script: - docker push ${IMAGE_NAME}:${IMAGE_TAG} - | if [ "${CI_COMMIT_BRANCH}" = "main" ]; then docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:latest docker push ${IMAGE_NAME}:latest fi - | if [ -n "${CI_COMMIT_TAG}" ]; then docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:${CI_COMMIT_TAG} docker push ${IMAGE_NAME}:${CI_COMMIT_TAG} fi dependencies: - build only: - main - tags . זהו שלב חובה לעמידה ברמת SLSA Level 2.

אסטרטגיות מטמון

image.tar עבור cache-from/cache-to — המטמון מאוחסן ישירות ב-container registry. עובד על כל GitLab Runner, ללא צורך ב-shared volume. sign: stage: sign image: cgr.dev/chainguard/cosign:latest script: - docker load < image.tar - cosign sign --yes ${IMAGE_NAME}:${IMAGE_TAG}@$(docker inspect --format='{{index .RepoDigests 0}}' ${IMAGE_NAME}:${IMAGE_TAG}) variables: COSIGN_EXPERIMENTAL: "1" dependencies: - build only: - main - tags מאחסן את כל השכבות הביניים, לא רק את הסופית. מטמון registry מהיר פי 5-7 ממטמון דיסק מקומי לבנייה מבוזרת, כפי שהוכח במדדים של צוות Docker BuildKit. בהשוואה למטמון דיסק מקומי, מטמון registry נותן האצה של 30–40% בבנייה מקבילה. התיעוד הרשמי של BuildKit ממליץ להשתמש במטמון registry לבנייה מבוזרת.

חלופה ל-GitHub Actions היא cosign verify ${IMAGE_NAME}:${CI_COMMIT_SHA} עם actions/cache. הוא מאוחסן ב-GitHub Artifacts, מקסימום 10GB, ופג אוטומטית. אבל מטמון registry אמין יותר — הוא אינו תלוי במגבלות artifacts.

אופטימיזציה נוספת למטמון כדי להאיץ בניות, השתמש ב-`--cache-to` עם `mode=max` ו-`compression=zstd` להקטנת גודל המטמון. בבנייה מקבילה, זה מוסיף עד 20% מהירות נוספת.

תיוג סמנטי

אירוע תגיות
Commit ל-main type=registry, mode=max
Git tag cache-from: type=gha sha-abc1234, main-latest, v1.2.3, v1.2.3, 1.2.3
PR/MR 1.2, ללא latest
Cron לילי 1

לתיוג סמנטי אוטומטי ב-GitHub Actions — latest. ב-GitLab — סקריפט המבוסס על pr-123-sha ו-nightly-YYYYMMDD. תיוג נכון מאפשר חזרה קלה לכל גרסה וביקורת על שינויים.

למה שכבת מטמון חשובה?

ללא מטמון, כל commit בונה מחדש את כל השכבות: התקנת תלויות, בנייה, העתקה. אם התלויות לא השתנו, הן צריכות להגיע מהמטמון. BuildKit עם docker/metadata-action נותן האצה של פי 5–7 בפרויקט טיפוסי. בפרויקט אחד, קיצרנו את זמן הבנייה מ-9 דקות ל-1.5 דקות — מפתחים הפסיקו לחכות והאיצו את מחזור השחרור. זה מקצץ בעלויות CI runner בכ-$200 לחודש עבור צוות טיפוסי של 10 מפתחים.

איך להגדיר שכבת מטמון ב-GitLab CI?

הוראות שלב-אחר-שלב להוספת שכבת מטמון:

  1. הפעל Docker BuildKit: CI_COMMIT_TAG במשתנים.
  2. הגדר מטמון registry: CI_COMMIT_SHA ו---cache-from registry.
  3. ודא ש-GitLab Runner משתמש ב-Docker-in-Docker (dind) עם מספיק שטח דיסק.
  4. בדוק invalidation של מטמון: אם DOCKER_BUILDKIT: "1" משתנה, BuildKit בונה מחדש רק את שלב התלויות, לא את כל ה-pipeline.
  5. נטר את גודל המטמון: נקה מעת לעת רשומות ישנות עם --cache-from type=registry,ref=${IMAGE_NAME}:cache או הגדר מדיניות מחיקה אוטומטית.

איך להימנע מבנייה חוזרת של תמונות?

השתמש ב-artifacts כדי להעביר את התמונה הבנויה בין שלבים. ב-GitLab זה --cache-to type=registry,ref=${IMAGE_NAME}:cache,mode=max; ב-GitHub Actions, package.json + upload-artifact. סריקה וחתימה עובדות עם אותו artifact, לא עם תמונה שנבנתה מחדש. זה מבטל פערים בין מה שנסרק למה שנפרס.

מה כלול בעבודה

  • ביקורת על ה-Dockerfile הנוכחי ותצורת CI.
  • שכתוב ה-Dockerfile לרב-שלבי עם BuildKit cache mounts.
  • הגדרת pipeline עם שלבי build/scan/push.
  • הגדרת מטמון registry (cache-from/cache-to).
  • הוספת חתימת Cosign עם keyless signing.
  • הגדרת תיוג סמנטי המבוסס על git tags.
  • תיעוד והדרכת צוות (1–2 שעות).
  • תמיכה לשבוע אחד לאחר היישום.

המתודולוגיה המוכחת שלנו מבטיחה פריסות אמינות, והאינטגרציה המוסמכת שלנו מבטיחה אבטחת שרשרת אספקה. לצוות שלנו ניסיון של 5+ שנים ב-Docker CI/CD והוא ביצע 30+ יישומי pipeline דומים לארגונים שונים, מה שמבטיח פתרונות שנבדקו בתעשייה.

היקף לוח זמנים
Pipeline בסיסי build/scan/push יום אחד
+ Cosign + תיוג semver 2–3 ימים
בנייה מרובת-ארכיטקטורות + מספר registries 3–4 ימים

העלות מחושבת באופן אישי לאחר ביקורת על תצורת ה-CI הנוכחית ותשתית ה-registry. צור קשר — נעריך את הפרויקט שלך תוך יום עסקים אחד. הזמן יישום pipeline מלא: מביקורת ועד תיעוד. קבל ייעוץ לשיפור ה-pipeline — כתוב לנו.