CI/CD — GitHub Actions
אוטומציה מלאה של build, test ו-deploy — הנושא הנפוץ ביותר בשוק העבודה הישראלי
תיאוריה
CI/CD מופיע ב-17.6% ממשרות ה-DevOps בישראל — המספר הגבוה ביותר מבין כל הכלים הנסרקים. זה אומר שמעסיק שמראיין אותך על DevOps ייצפה שאתה יכול לכתוב pipeline מאפס ולנפות באגים ב-workflow קיים.
השוק מגוון: Jenkins עדיין רלוונטי (4.7%), Azure DevOps נפוץ בחברות enterprise (2.4%), ו-GitHub Actions עולה בהתמדה. עם זאת, GitHub Actions הפך לברירת המחדל ל-startups ו-scale-ups ישראליות מסיבות פשוטות:
- אין צורך לתחזק Jenkins server
- YAML workflow קיים באותו repo כמו הקוד
- Marketplace עם אלפי actions מוכנות
- OIDC native integration עם AWS, GCP, Azure — אין צורך ב-long-lived credentials
- GitHub-hosted runners בחינם עד 2,000 דקות בחודש
הציפייה המעשית בראיון: "כתוב workflow שמריץ tests, בונה Docker image, ומדפלוי ל-VPS". זה בדיוק מה שנבנה בפרק הזה.
כל GitHub Actions workflow הוא קובץ YAML תחת .github/workflows/. המבנה הבסיסי:
name: CI Pipeline
on:
push:
branches: [main, master]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Run tests
run: npm test**on — triggers:**
- push / pull_request: אירועי git. שים לב ל-branches filter — בלעדיו כל branch יפעיל את ה-workflow.
- workflow_dispatch: הפעלה ידנית מה-UI — חיוני ל-deploy workflows.
- schedule: cron expression, למשל '0 2 * * *' (כל יום ב-02:00 UTC).
- workflow_call: הופך את ה-workflow ל-reusable (ראה סעיף 8).
**jobs:**
- כל job רץ על runner נפרד ומתחיל מ-fresh environment.
- jobs רצים במקביל כברירת מחדל.
- needs: [other-job] יוצר תלות — הסדר הוא DAG, לא רשימה רציפה.
**steps:**
- uses: actions/checkout@v4 — action מוכנה ממרקטפלייס. **תמיד pin ל-version tag, לא @main.**
- run: echo hello — פקודת shell רגילה (bash כברירת מחדל).
- name: — שם שמוצג ב-UI, לא חובה אבל קריטי לקריאות.
- env: ו-with: — מעבירים פרמטרים ל-action או env vars לסקריפט.
**context expressions:**${{ github.sha }}, ${{ github.ref_name }}, ${{ secrets.MY_SECRET }}, ${{ env.MY_VAR }}
ה-build/test job הוא הליבה של כל CI pipeline. שלושה עקרונות חשובים:
**1. Dependency caching — חוסך 40-60 שניות לכל run:**actions/setup-node@v4 כולל caching מובנה דרך cache: 'npm'. ל-Python משתמשים ב-actions/setup-python@v5 עם cache: 'pip'.
**2. npm ci ולא npm install:**npm ci קורא מ-package-lock.json ישירות, לא מ-package.json. הוא מוחק node_modules לפני ומבטיח build דטרמיניסטי. npm install עלול לשנות את ה-lockfile.
**3. Test artifacts on failure:**
כאשר tests נכשלים, תמיד העלה את קובצי ה-report. ה-if: failure() מבטיח שה-step רץ גם כשה-job נכשל.
ה-Docker build job הוא הגשר בין קוד ל-deploy. שתי בעיות נפוצות שמאטות pipelines:
**בעיה 1: אין layer caching — כל build מחדש.**
הפתרון: cache-from: type=gha ו-cache-to: type=gha,mode=max. GitHub Actions כולל cache storage מובנה שמשמש כ-BuildKit cache backend.
**בעיה 2: הרשאות push לרגיסטרי.**
לא מחסנים credentials בקוד. משתמשים ב-secrets.GITHUB_TOKEN ל-GitHub Container Registry (ghcr.io) — אין צורך להגדיר כלום, GitHub מספק אותו אוטומטית לכל workflow.
**Image tagging strategy:**
- latest — רק ל-main/master branch
- sha-${GITHUB_SHA::7} — כל commit, לאפשר rollback
- semantic version tag — בעת release
docker/metadata-action@v5 מייצר את כל ה-tags אוטומטית לפי הגדרות.
ה-deploy job הוא המקום שרוב אנשים נתקעים בייצור. הבעיות הנפוצות:
**בעיה 1: deploy מריץ על production ב-push לכל branch.**
פתרון: if: github.ref == 'refs/heads/main' על ה-job, ו-branch protection שמחייב PR.
**בעיה 2: SSH key בקוד.**
פתרון: secrets.SSH_PRIVATE_KEY — מאוחסן ב-GitHub Secrets, לא בקוד.
**בעיה 3: אין validation שה-deploy עבד.**
פתרון: smoke test אחרי deploy — curl -f ל-/health endpoint. -f גורם ל-curl להחזיר exit code != 0 על HTTP error.
**appleboy/ssh-action@v1** מריץ פקודות shell מרחוק דרך SSH. הוא תומך ב-script_stop: true — אם פקודה אחת נכשלת, כל הסקריפט נעצר.
**Rolling restart pattern:**
docker pull ghcr.io/org/app:latest
docker stop app || true
docker rm app || true
docker run -d --name app --restart=unless-stopped \
-p 3000:3000 \
ghcr.io/org/app:latestה-|| true מונע כשלון כשה-container לא קיים בפעם הראשונה.
ניהול secrets לא נכון הוא הסיבה מספר אחת ל-credential leaks ב-CI/CD. שלושה רמות:
**רמה 1: secrets.GITHUB_TOKEN**
נוצר אוטומטית לכל workflow run. הרשאותיו מוגדרות ב-permissions: block. כברירת מחדל (בהגדרות repo) הוא read-all — **תמיד הגדר least-privilege בכל workflow:**
permissions:
contents: read
packages: write**רמה 2: Repository/Environment Secrets**
סיסמאות, SSH keys, API tokens — מוגדרים ב-Settings > Secrets. ה-secret values לא נחשפים בלוגים (מוסתרים אוטומטית). שים לב: environment secrets זמינים רק כשה-job משתמש ב-environment: production — מוסיף approval gate option.
**רמה 3: OIDC ל-AWS — ללא long-lived credentials (הסטנדרט בשוק)**
הגישה הנכונה ב-2025+: GitHub Actions ו-AWS יוצרים trust federation דרך OIDC. ה-workflow מבקש temporary credentials שתקפות לדקות ספורות — אין AWS_ACCESS_KEY_ID קבוע.
סדר ההגדרה:
1. צור OIDC Identity Provider ב-AWS IAM: token.actions.githubusercontent.com
2. צור IAM Role עם Trust Policy שמגדיר Condition על sub (שם הrepo + branch)
3. ב-workflow: uses: aws-actions/configure-aws-credentials@v4 עם role-to-assume
זה אומר שגם אם ה-workflow file נחשף, אין credentials לגנוב.
Matrix strategy מריצה את אותו job על מספר קונפיגורציות במקביל — ללא שכפול קוד.
**שימושים נפוצים בשוק:**
- Node.js versions: [18, 20, 22]
- OS combinations: [ubuntu-24.04, windows-latest]
- Python versions: [3.11, 3.12]
- ארכיטקטורות: [linux/amd64, linux/arm64] (לבניית Docker images multi-arch)
**fail-fast: false:**
כברירת מחדל, אם matrix job אחד נכשל, GitHub Actions מבטל את כולם. ב-integration tests, לרוב רוצים לראות את כל הכשלונות — fail-fast: false.
**include ו-exclude:**
- exclude מסיר קומבינציות ספציפיות
- include מוסיף קומבינציות שאינן בmatrix, או מוסיף variables ל-combinations קיימות
**context:** ${{ matrix.node-version }}, ${{ matrix.os }}
כאשר יש לך 5 repos שכולם עושים npm test + docker build + deploy, כפילות ה-YAML הופכת לבעיה. Reusable workflows פותרים זאת — קובץ workflow אחד שאחרים קוראים לו.
**מבנה:**
- ה-reusable workflow מגדיר on: workflow_call: עם inputs: ו-secrets:
- ה-caller workflow מגדיר uses: org/repo/.github/workflows/deploy.yml@main
**secrets: inherit:**
מעביר את כל ה-secrets של ה-caller ל-reusable workflow. נוח אבל פחות explicit. לשליטה מדויקת, הגדר כל secret בנפרד.
**מגבלות חשובות:**
- reusable workflow לא יכול לקרוא ל-reusable workflow נוסף יותר מ-3 רמות עומק
- env: context של ה-caller לא עובר אוטומטית — רק דרך inputs:
- Actions artifacts מה-reusable workflow זמינים ל-caller
**שימוש מעשי בשוק:** חברות עם 10+ repos DevOps שומרות .github/workflows/ shared ב-repo מרכזי. כל שינוי ב-pipeline מתפשט לכולם.
זהו הנושא שקורסים מדלגים עליו ויוצר outages ב-3 לפנות בוקר. Let's Encrypt מנפיק certificates לתקופה של 90 יום בלבד. כשה-certificate פג תוקפו, המשתמשים רואים שגיאת SSL והאתר נתפס כ'לא בטוח'.
**שלב 1 — הנפקת certificate ראשונית:**
sudo certbot --nginx -d devops.backbok.com --non-interactive --agree-tos -m admin@backbok.comהפקודה עורכת את /etc/nginx/sites-enabled/devops.backbok.com.conf אוטומטית — מוסיפה ssl_certificate, ssl_certificate_key, ומוסיפה redirect מ-80 ל-443.
**שלב 2 — אוטומציה של renewal:**
certbot מתקין systemd timer אוטומטית ב-Ubuntu כשמתקינים אותו:
systemctl list-timers | grep certbot
# certbot.timer active Fri 2026-04-17 12:00:00 UTC 11h leftה-timer מריץ certbot renew פעמיים ביום (לא renews אלא רק בודק — certbot מחדש רק כשנשארו פחות מ-30 יום).
**שלב 3 — Deploy Hook לטעינה מחדש של nginx:**
זו הנקודה הקריטית שקורסים מדלגים עליה. certbot יחדש את ה-certificate, אבל nginx ממשיך להשתמש בתעודה הישנה שטעון לזיכרון עד שמריצים nginx -s reload. בלי deploy hook, ה-renewal הוא חסר תועלת.
# /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
#!/bin/bash
set -euo pipefail
systemctl reload nginx
echo "$(date): nginx reloaded after cert renewal" >> /var/log/certbot-deploy.logchmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh**שלב 4 — Dry run לאימות:**
sudo certbot renew --dry-run
# Cert not yet due for renewal
# No renewals were attempted.
# Congratulations, all simulated renewals succeeded.**שלב 5 — ניטור פקיעת תוקף (monitoring expiry):**
ה-renewal האוטומטי יכול להיכשל בגלל DNS misconfiguration, port 80 blocked, או certbot bug. **תמיד** הגדר alerting חיצוני:
# בדיקת תוקף certificate דרך CLI:
openssl s_client -connect devops.backbok.com:443 -servername devops.backbok.com < /dev/null 2>/dev/null | \
openssl x509 -noout -dates
# notAfter=Jun 15 12:00:00 2026 GMT
# חישוב ימים עד פקיעה:
EXPIRY=$(openssl s_client -connect devops.backbok.com:443 -servername devops.backbok.com < /dev/null 2>/dev/null | \
openssl x509 -noout -enddate | cut -d= -f2)
DAYS=$(( ( $(date -d "$EXPIRY" +%s) - $(date +%s) ) / 86400 ))
echo "Certificate expires in $DAYS days"
[ $DAYS -lt 14 ] && echo "WARNING: cert expires soon!"ב-production: הגדר Prometheus ssl_expiry_days metric ו-alert rule, או השתמש ב-UptimeRobot/Checkly SSL monitoring.
**מבנה תיקיות certbot:**
/etc/letsencrypt/
live/devops.backbok.com/ # symlinks לתעודות הנוכחיות
cert.pem
chain.pem
fullchain.pem # זה מה-nginx צריך
privkey.pem
renewal/
devops.backbok.com.conf # קונפיגורציית renewal
renewal-hooks/
deploy/ # רצים אחרי renewal מוצלח
post/ # רצים אחרי כל ניסיון renewal
pre/ # רצים לפני ניסיון renewal
archive/ # תעודות היסטוריות (לא למחוק!)**שילוב עם GitHub Actions:**
ניתן לטמן בדיקת תוקף certificate כ-step ב-deploy workflow:
- name: Verify TLS certificate not expiring soon
run: |
DAYS=$(openssl s_client -connect ${{ secrets.VPS_HOST }}:443 \
-servername ${{ secrets.VPS_HOST }} < /dev/null 2>/dev/null | \
openssl x509 -noout -enddate | \
awk -F= '{print $2}' | \
xargs -I{} date -d {} +%s | \
xargs -I{} bash -c 'echo $(( ({} - $(date +%s)) / 86400 ))')
echo "TLS cert expires in $DAYS days"
[ $DAYS -lt 14 ] && echo "::warning::TLS certificate expires in $DAYS days"ארבע best practices שמבדילות junior מ-senior DevOps engineer בישראל:
**1. Pinned action versions (Security Supply Chain)**
השימוש ב-@main או @latest ב-action הוא PoisionPill לאבטחה — תוקף שמשתלט על הrepo של ה-action יכול להריץ קוד על ה-runner שלך.
# ❌ מסוכן
uses: actions/checkout@main
# ✅ טוב — tag נקי, ניתן לעדכון
uses: actions/checkout@v4
# ✅ הכי בטוח — commit SHA מלא (immutable)
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2כלי Dependabot יכול לעדכן versions אוטומטית — הגדר ב-.github/dependabot.yml:
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly**2. Least-Privilege GITHUB_TOKEN**
הגדר permissions: בכל workflow או job. ב-org settings ניתן להגדיר default ל-read-all, אבל בdeploy workflows צריך write ל-packages.
permissions:
contents: read # מינימום לrequirements רגיל
packages: write # רק כשדוחפים ל-ghcr.io
id-token: write # רק כשמשתמשים ב-OIDC**3. Branch Protection + Required Status Checks**
ב-Settings > Branches > Branch protection rules:
- Require a pull request before merging
- Require status checks to pass before merging — בחר את ה-jobs: test, lint
- Require branches to be up to date before merging
- Restrict pushes that create matching branches
בלי זה, developer יכול לעקוף את כל ה-CI בpush ישיר ל-main.
**4. Concurrency Control — אל תריץ שני deploys במקביל**
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: true # בטל deploy ישן כשחדש מגיעתרגול מעשי
1.
2.
3.
שאלות חיבור
חבר את מה שלמדת בנושא זה לנושאים קודמים. אין תשובה אחת נכונה — חשיבה ביקורתית היא המטרה.
שאלת חיבור 1
GitHub Actions CI/CD pipeline בונה Docker image ב-docker/build-push-action@v5. Docker (topic #4) לימד אותנו multi-stage builds, layer caching ו-healthcheck. הסבר: (1) כיצד multi-stage build משתלב עם GitHub Actions לצמצום גודל ה-image שנדפס ל-ghcr.io, (2) כיצד cache-from: type=gha פועל ביחס ל-Docker layer cache שלמדנו, ו-(3) מה ה-HEALTHCHECK שמוסיפים ל-Dockerfile ואיך ה-smoke test בCI/CD בודק את אותו endpoint.
שאלת חיבור 2
ה-CI/CD pipeline כולל step שמריץ `terraform plan` ו-`terraform apply` — זה מחבר Git Advanced (topic #2) לTerraform (topic #7) לCI/CD. הסבר: (1) כיצד Git pre-commit hooks (Git Advanced) משלימים את `terraform fmt -check` ב-pipeline, (2) מה הפרקטיקה הנכונה לניהול ה-tfplan binary artifact בין plan job לapply job בpipeline, ו-(3) מדוע `terraform apply` ב-CI/CD חייב להריץ מה-saved plan ולא `terraform apply -auto-approve` ישיר.
מוכן לבחינה?
בצע הערכה תיאורטית, תרגול CLI ושאלות חיבור כדי לסיים את הנושא.