DevOps Academy

CI/CD — GitHub Actions

אוטומציה מלאה של build, test ו-deploy — הנושא הנפוץ ביותר בשוק העבודה הישראלי

תיאוריה

⏱ ~60 דקות

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 מתפשט לכולם.

פער production

זהו הנושא שקורסים מדלגים עליו ויוצר 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.log
chmod +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.

מחבר ל:
Docker

שאלת חיבור 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` ישיר.

מחבר ל:
Git Advanced

מוכן לבחינה?

בצע הערכה תיאורטית, תרגול CLI ושאלות חיבור כדי לסיים את הנושא.