DevOps Academy

AWS (reinforce)

תשתית ענן בשימוש אמיתי: Bastion, Secrets, PostgreSQL, FinOps ו-IAM

תיאוריה

⏱ ~60 דקות

למה AWS Reinforce עכשיו? — שוק ה-DevOps הישראלי

תדירות AWS במשרות DevOps ישראליות עומדת על 9.4 מתוך 10 — כמעט כל משרה בינונית ומעלה מצריכה ידע AWS מעשי. אבל הקורסים הסטנדרטיים מלמדים 'צור EC2, צור S3'. הפער האמיתי הוא בתשתית שמסביב:

- **SSH דרך bastion** — אף שרת production לא נגיש ישירות מהאינטרנט
- **ניהול סודות** — פרטי חיבור ב-AWS Secrets Manager ו-HashiCorp Vault, לא בקובץ .env
- **גיבוי PostgreSQL** — pg_dump + העברה ל-S3, כי managed RDS יקר לכולם
- **FinOps** — Cost Explorer + Reserved vs Spot, כי כסף ענן בוצא
- **IAM least-privilege** — כי כל breach גדול מתחיל ב-AdministratorAccess שמישהו שכח להסיר

זהו הנושא עם ה-market_frequency הגבוה ביותר בסמסטר הזה. כל הפערים בסעיף זה מגיעים מניתוח משרות ריאליות ממאגר job_report.

SSH Jump Hosts ו-ProxyJump — גישה לשרתים בסאבנט פרטי

פער production

כל ארכיטקטורת production מכניסה שרתי application לסאבנט פרטי ללא IP ציבורי. הדרך הנכונה להגיע אליהם היא דרך **bastion host** — שרת ייעודי עם IP ציבורי שמשמש כ-jump point.

**שיטה 1 — ProxyJump בשורת הפקודה:**

ssh -J ubuntu@bastion.example.com ec2-user@10.0.1.50

**שיטה 2 — הגדרת ~/.ssh/config (הדרך הנכונה לproduction):**

Host bastion
  HostName 54.200.1.100
  User ubuntu
  IdentityFile ~/.ssh/bastion_key.pem
  ServerAliveInterval 60

Host app-server
  HostName 10.0.1.50
  User ec2-user
  IdentityFile ~/.ssh/app_key.pem
  ProxyJump bastion

אחרי ההגדרה:

ssh app-server   # עובר אוטומטית דרך bastion
scp file.tar.gz app-server:/tmp/   # גם scp עובר דרך ProxyJump

**ProxyCommand — שיטה עבור AWS SSM (בלי SSH פתוח לאינטרנט):**

Host i-*
  ProxyCommand aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'
  User ec2-user
  IdentityFile ~/.ssh/app_key.pem

**סיכוני agent forwarding** — לעולם אל תשתמש ב-ssh -A אל bastion בלי הבנה מלאה. אם ה-bastion נפרץ, כל ה-agent keys נחשפות. העדף ProxyJump שלא מוסר credentials ל-bastion.

AWS Secrets Manager — ניהול סודות בענן

פער production

הטעות הנפוצה ביותר היא אחסון passwords ב-.env files ב-git. AWS Secrets Manager פותר זאת: הצפנת KMS, rotation אוטומטי, ו-audit trail ב-CloudTrail.

**יצירת secret:**

aws secretsmanager create-secret \
  --name prod/myapp/db-password \
  --description "PostgreSQL production password" \
  --secret-string '{"username":"dbadmin","password":"s3cr3t!"}'

**קריאת secret בסקריפט:**

SECRET=$(aws secretsmanager get-secret-value \
  --secret-id prod/myapp/db-password \
  --query SecretString \
  --output text)

DB_PASS=$(echo "$SECRET" | python3 -c "import sys,json; print(json.load(sys.stdin)['password'])")
export PGPASSWORD="$DB_PASS"

**Rotation אוטומטי — 30 יום:**

aws secretsmanager rotate-secret \
  --secret-id prod/myapp/db-password \
  --rotation-lambda-arn arn:aws:lambda:us-east-1:123456789:function:SecretsRotation \
  --rotation-rules AutomaticallyAfterDays=30

**IAM Policy מינימלית לאפליקציה:**

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": ["secretsmanager:GetSecretValue"],
    "Resource": "arn:aws:secretsmanager:us-east-1:123456789:secret:prod/myapp/*"
  }]
}

חשוב: הגבל גישה ל-secret ספציפי (prod/myapp/*) ולא ל-* כללי.

HashiCorp Vault — ניהול סודות דינמי

פער production

Vault הוא הפתרון הפופולרי ב-on-premise ובמצבים שבהם AWS Secrets Manager אינו מספיק — בעיקר כשצריך **dynamic secrets** (credentials שנוצרים ונמחקים per-request).

**הפקודות הבסיסיות:**

# אתחול (פעם אחת)
vault operator init
vault operator unseal <key1>
vault operator unseal <key2>

# כתיבת secret
vault kv put secret/myapp/db \
  username=dbadmin \
  password=s3cr3t!

# קריאת secret
vault kv get secret/myapp/db
vault kv get -field=password secret/myapp/db

**AppRole Authentication — לאפליקציות production:**

# Enable AppRole
vault auth enable approle

# יצירת role לאפליקציה
vault write auth/approle/role/myapp-role \
  token_ttl=1h \
  token_max_ttl=4h \
  secret_id_ttl=24h

# הוצאת credentials לאפליקציה
ROLE_ID=$(vault read -field=role_id auth/approle/role/myapp-role/role-id)
SECRET_ID=$(vault write -f -field=secret_id auth/approle/role/myapp-role/secret-id)

# Login
vault write auth/approle/login role_id=$ROLE_ID secret_id=$SECRET_ID

**Dynamic Secrets לPostgreSQL — הנשק הסודי:**

vault secrets enable database

vault write database/config/myapp-db \
  plugin_name=postgresql-database-plugin \
  allowed_roles="myapp-role" \
  connection_url="postgresql://{{username}}:{{password}}@localhost:5432/mydb"

vault write database/roles/myapp-role \
  db_name=myapp-db \
  creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'" \
  default_ttl=1h max_ttl=24h

# כל קריאה מחזירה credentials חדשים שתקפים שעה
vault read database/creds/myapp-role

במצב זה, לאפליקציה אין סיסמה קבועה — כל instance מקבל credentials ייחודיים עם TTL מוגדר.

pg_dump — גיבוי PostgreSQL ו-העברה ל-S3

פער production

Managed RDS עולה כסף. אפליקציות רבות רצות על PostgreSQL עצמאי בEC2 או VPS, וגיבוי הוא אחריות המהנדס.

**פקודת pg_dump עם כל הפלאגים הנכונים:**

pg_dump \
  --host=localhost \
  --port=5432 \
  --username=backup_user \
  --format=custom \
  --compress=9 \
  --no-owner \
  --no-acl \
  --verbose \
  myapp_db > /backups/myapp_$(date +%Y%m%d_%H%M%S).dump

**הסבר הפלאגים:**
- --format=custom — פורמט בינארי עם דחיסה מובנית, תומך ב-parallel restore
- --compress=9 — דחיסה מקסימלית (פחות קריטי עם --format=custom)
- --no-owner — לא שומר ownership (נכון לrestore בסביבה אחרת)
- --no-acl — לא שומר GRANT/REVOKE permissions

**סקריפט גיבוי מלא עם העברה ל-S3:**

#!/usr/bin/env bash
set -euo pipefail

DB_NAME="myapp_db"
BACKUP_DIR="/backups"
S3_BUCKET="s3://my-company-backups/postgres"
DATE=$(date +%Y%m%d_%H%M%S)
FILE="${BACKUP_DIR}/${DB_NAME}_${DATE}.dump"
RETENTION_DAYS=7

pg_dump \
  --host=localhost \
  --username=backup_user \
  --format=custom \
  --no-owner \
  --no-acl \
  "${DB_NAME}" > "${FILE}"

gzip "${FILE}"

aws s3 cp "${FILE}.gz" "${S3_BUCKET}/" \
  --sse aws:kms \
  --storage-class STANDARD_IA

find "${BACKUP_DIR}" -name "*.gz" -mtime +${RETENTION_DAYS} -delete

echo "Backup complete: ${FILE}.gz uploaded to ${S3_BUCKET}"

**הוספה ל-cron (גיבוי כל לילה בשעה 2:00):**

0 2 * * * /opt/scripts/backup_postgres.sh >> /var/log/postgres_backup.log 2>&1

**שחזור מgzip dump:**

gunzip -c myapp_20240115_020000.dump.gz | \
  pg_restore --host=localhost --username=dbadmin --dbname=myapp_db --clean

PostgreSQL Point-in-Time Recovery ו-WAL Archiving

פער production

pg_dump מגבה רק snapshot. אם יש 100,000 transactions ביום ומישהו מוחק טבלה בשעה 14:35 — הגיבוי מהשעה 02:00 לא מספיק. **WAL archiving** מאפשר שחזור לכל שנייה.

**מה זה WAL?**
Write-Ahead Log — PostgreSQL כותב כל שינוי ל-WAL לפני שהוא מגיע לדיסק. WAL archiving שומר את הקבצים האלה (בד"כ 16MB כל אחד) ב-S3 או NFS.

**הגדרת WAL archiving ב-postgresql.conf:**

wal_level = replica
archive_mode = on
archive_command = 'aws s3 cp %p s3://my-backups/wal/%f'
archive_timeout = 300
max_wal_senders = 3

**שיחזור Point-in-Time (recovery.conf / postgresql.conf בגרסאות חדשות):**

restore_command = 'aws s3 cp s3://my-backups/wal/%f %p'
recovery_target_time = '2024-01-15 14:34:59'
recovery_target_action = promote

**תהליך שחזור מלא:**

# 1. עצור PostgreSQL
systemctl stop postgresql

# 2. נקה data directory
rm -rf /var/lib/postgresql/14/main/*

# 3. שחזר את ה-base backup
pg_basebackup -h s3-source -D /var/lib/postgresql/14/main

# 4. הוסף קובץ recovery.conf / postgresql.conf עם recovery_target_time
cat > /var/lib/postgresql/14/main/recovery.signal << 'EOF'
EOF

# 5. הפעל PostgreSQL — יתחיל לhאחזר WAL עד לזמן שנקבע
systemctl start postgresql

**כלי מקצועי — pgBackRest:**

pgbackrest --stanza=myapp backup --type=full
pgbackrest --stanza=myapp restore --target='2024-01-15 14:34:59' --target-action=promote

בפרקטיקה: WAL archiving חיוני לכל database עם SLA של RPO < 1 שעה.

PgBouncer — Connection Pooling לPostgreSQL

פער production

PostgreSQL יוצר process נפרד לכל connection. בעומס של 500 connections בו-זמנית, הserver שורף CPU ו-RAM רק על ניהול connections — לפני שמעבד שאילתה אחת.

**PgBouncer** יושב בין האפליקציה לבסיס הנתונים ומשתף connections:

App (5000 connections) → PgBouncer → PostgreSQL (20 connections)

**שלושת מצבי ה-pooling:**

| מצב | מתי | עלות |
|-----|-----|------|
| session | connection של PgBouncer מוחזק כל session | נמוכה, ללא side effects |
| transaction | connection מוחזר לpool אחרי כל transaction | ביצועים גבוהים, SET LOCAL לא נשמר |
| statement | connection מוחזר אחרי כל statement | לא מומלץ, שובר transactions |

**/etc/pgbouncer/pgbouncer.ini:**

[databases]
myapp_db = host=127.0.0.1 port=5432 dbname=myapp_db

[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 10000
default_pool_size = 20
min_pool_size = 5
reserve_pool_size = 5
reserve_pool_timeout = 3
server_idle_timeout = 600
log_connections = 0
log_disconnections = 0

**/etc/pgbouncer/userlist.txt:**

"myapp_user" "md5_hashed_password_here"

**האפליקציה מתחברת לport 6432 (PgBouncer) במקום 5432:**

DATABASE_URL=postgresql://myapp_user:password@127.0.0.1:6432/myapp_db

**ניטור PgBouncer:**

# התחבר ל-pgbouncer console
psql -h 127.0.0.1 -p 6432 -U pgbouncer pgbouncer

# סטטיסטיקות pool
SHOW POOLS;
SHOW CLIENTS;
SHOW SERVERS;
SHOW STATS;

pg_stat_statements ו-EXPLAIN ANALYZE — זיהוי שאילתות איטיות

פער production

90% מבעיות הביצועים של אפליקציות מקורן בשאילתות בסיס נתונים איטיות. שני הכלים הקריטיים:

**הפעלת pg_stat_statements:**

-- postgresql.conf
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.track = all

-- אחרי restart
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

**שאילתות שזהות את ה-top offenders:**

-- TOP 10 שאילתות איטיות ביותר לפי זמן ממוצע
SELECT
  query,
  calls,
  round(total_exec_time::numeric, 2) AS total_ms,
  round(mean_exec_time::numeric, 2) AS avg_ms,
  round(stddev_exec_time::numeric, 2) AS stddev_ms,
  rows
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;

-- שאילתות עם הכי הרבה קריאות לדיסק
SELECT query, shared_blks_read
FROM pg_stat_statements
ORDER BY shared_blks_read DESC
LIMIT 5;

**EXPLAIN ANALYZE — ניתוח תוכנית ביצוע:**

EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT u.name, COUNT(o.id) AS order_count
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.created_at > '2024-01-01'
GROUP BY u.name
ORDER BY order_count DESC
LIMIT 10;

**קריאת פלט EXPLAIN — הנקודות הקריטיות:**

Sort  (cost=1234.56..1235.06 rows=200 width=40) (actual time=89.123..89.234 rows=10 loops=1)
  ->  HashAggregate  (cost=1224.56..1226.56 rows=200 width=40) (actual time=88.900..89.050 rows=200 loops=1)
        ->  Hash Join  (cost=500.00..1100.00 rows=4990 width=32) (actual time=10.500..75.000 rows=4990 loops=1)
              ->  Seq Scan on orders  (cost=0.00..450.00 rows=10000 width=16)  # <-- בעיה!
              ->  Hash  (cost=300.00..300.00 rows=5000 width=20) (actual time=8.000..8.000 rows=5000 loops=1)
                    ->  Index Scan using idx_users_created_at on users  (cost=0.00..300.00 rows=5000)
Execution Time: 89.456 ms

**Seq Scan הוא הסימן לאינדקס חסר:**

-- יצירת אינדקס שיפתור את ה-Seq Scan על orders
CREATE INDEX CONCURRENTLY idx_orders_user_id ON orders(user_id);

-- CONCURRENTLY — לא נועל את הטבלה בproduction

FinOps — AWS Cost Explorer, Reserved Instances ו-Spot

AWS חשבונות גדלים בשקט. חברות מגלות עלויות מפתיעות רק כשמגיע החשבון החודשי. FinOps הוא מקצוע שלם — הנה הבסיס שכל DevOps Engineer חייב לדעת.

**AWS Cost Explorer — CLI:**

# עלות חודש שעבר לפי service
aws ce get-cost-and-usage \
  --time-period Start=2024-01-01,End=2024-02-01 \
  --granularity MONTHLY \
  --metrics "BlendedCost" \
  --group-by Type=DIMENSION,Key=SERVICE \
  --query 'ResultsByTime[0].Groups[*].[Keys[0],Metrics.BlendedCost.Amount]' \
  --output table

# Top 5 resources היקרים ביותר
aws ce get-cost-and-usage \
  --time-period Start=2024-01-01,End=2024-02-01 \
  --granularity MONTHLY \
  --metrics "UnblendedCost" \
  --group-by Type=DIMENSION,Key=RESOURCE_ID \
  --filter file://cost_filter.json

**Reserved Instances vs Spot vs On-Demand:**

| סוג | חיסכון | מתאים ל |
|-----|--------|----------|
| On-Demand | 0% (baseline) | dev, unpredictable |
| Reserved (1yr, no upfront) | ~30% | production baseline load |
| Reserved (3yr, all upfront) | ~60% | stable, long-running workloads |
| Spot Instances | ~70-90% | batch jobs, CI runners, stateless workers |
| Savings Plans | ~20-66% | flexible compute commitment |

**Spot Instances בGitHub Actions CI:**

# Spot-based CI runner — 80% חיסכון
- name: Launch Spot Runner
  run: |
    aws ec2 request-spot-instances \
      --spot-price "0.05" \
      --instance-count 1 \
      --type "one-time" \
      --launch-specification file://spot-spec.json

**AWS Trusted Advisor — alerts על בזבוז:**

awsaws support describe-trusted-advisor-checks --language en \
  --query 'checks[?category==`cost_optimizing`].[name,id]' \
  --output table

**Tags חובה לכל resource:**

aws ec2 create-tags \
  --resources i-1234567890abcdef0 \
  --tags Key=Environment,Value=production Key=Team,Value=backend Key=CostCenter,Value=engineering

IAM Least-Privilege — Policy Simulation, Conditions ו-SCPs

פער production

IAM הוא שכבת ההגנה הראשונה והאחרונה ב-AWS. הרוב מתחילים עם AdministratorAccess ו'ינקו אחר כך' — אחר כך לעולם לא מגיע.

**עיקרון Least-Privilege בפרקטיקה:**

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowS3BackupBucketOnly",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::my-company-backups",
        "arn:aws:s3:::my-company-backups/*"
      ]
    },
    {
      "Sid": "DenyDeleteUnlessTagged",
      "Effect": "Deny",
      "Action": "s3:DeleteObject",
      "Resource": "arn:aws:s3:::my-company-backups/*",
      "Condition": {
        "StringNotEquals": {
          "s3:ExistingObjectTag/deletable": "true"
        }
      }
    }
  ]
}

**Policy Simulator — בדיקת policy לפני deploy:**

aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123456789:user/backup-script \
  --action-names s3:PutObject s3:DeleteObject \
  --resource-arns arn:aws:s3:::my-company-backups/test.dump \
  --query 'EvaluationResults[*].[EvalActionName,EvalDecision]' \
  --output table

**SCPs — Service Control Policies (ברמת Organization):**

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyRegionsOutsideIL",
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:RequestedRegion": ["eu-west-1", "il-central-1"]
        }
      }
    },
    {
      "Sid": "DenyRootAccountUsage",
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "aws:PrincipalArn": "arn:aws:iam::*:root"
        }
      }
    }
  ]
}

**Conditions שחייבים להיות בכל production policy:**
- aws:RequestedRegion — מונע יצירת resources באזורים לא מורשים
- aws:MultiFactorAuthPresent — חובת MFA לפעולות הרסניות
- ec2:ResourceTag/Environment — מגביל פעולות לresources עם tag מסוים

**בדיקת IAM מהיר — מי יש לו AdministratorAccess:**

aws iam list-entities-for-policy \
  --policy-arn arn:aws:iam::aws:policy/AdministratorAccess \
  --query '[PolicyUsers[*].UserName, PolicyGroups[*].GroupName, PolicyRoles[*].RoleName]'

תרגול מעשי

1.

2.

3.


שאלות חיבור

חבר את מה שלמדת בנושא זה לנושאים קודמים. אין תשובה אחת נכונה — חשיבה ביקורתית היא המטרה.

שאלת חיבור 1

הגדרת ProxyJump ב-~/.ssh/config (מ-Linux Advanced) נחוצה כדי להגיע לשרתי application בסאבנט פרטי ב-AWS. הסבר: (1) מה ה-connection flow המלא מהמחשב שלך לשרת app ב-private subnet דרך bastion, (2) למה AWS SSM Session Manager מהווה חלופה ל-SSH פתוח לאינטרנט, ו-(3) מה הסיכון ב-ssh -A (agent forwarding) שלמדנו להימנע ממנו.

מחבר ל:
Linux Advanced

שאלת חיבור 2

בPython for DevOps לימדנו שימוש ב-boto3 ו-os.environ. כיצד משלבים את שניהם עם AWS Secrets Manager בסביבת production? הסבר: (1) כיצד boto3 קורא secret מ-Secrets Manager ב-Python תוך שימוש נכון בos.environ לאזור ה-AWS, (2) מדוע os.environ עדיף על hardcoded region string, ו-(3) מה הpath הנכון ל-secret name (naming convention) שמקשה על שגיאות ב-production.

מחבר ל:
Python for DevOps

מוכן לבחינה?

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