feat(storage,deploy): delad S3/mock-lagring, SSM .env-sync, Gemini readyz
- @app/storage med MockStorage + AwsStorage delas mellan API och worker - Workerns readUrl() ger presignerad S3-URL i aws-läge (fixar mock-s3 404) - sync-env-from-ssm.py hämtar /cibello/prod/* och skriver .env; first-deploy.sh kör det i prod - /readyz returnerar 503 om app.aamos.healthCheck() misslyckas - docs/SECRETS.md med rotations- och SSM-regler
This commit is contained in:
@@ -93,6 +93,16 @@ if [ ! -f .env ]; then
|
||||
chmod 600 .env
|
||||
fi
|
||||
|
||||
# I produktion härleds .env alltid ur SSM – inga handkopior får drifta.
|
||||
if [ "$MODE" = "production" ]; then
|
||||
if [ -f "$ROOT/infrastructure/deployment/sync-env-from-ssm.py" ]; then
|
||||
say "Synkar .env från SSM..."
|
||||
python3 "$ROOT/infrastructure/deployment/sync-env-from-ssm.py"
|
||||
else
|
||||
fail "sync-env-from-ssm.py saknas – kan inte synka .env från SSM."
|
||||
fi
|
||||
fi
|
||||
|
||||
get_env() { { grep -E "^$1=" .env || true; } | head -1 | cut -d= -f2-; }
|
||||
AAMOS_MODE_VAL=$(get_env AAMOS_MODE)
|
||||
# Sätt NODE_EXTRA_CA_CERTS om definierad i .env (behövs för RDS SSL från Node/pg).
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Hämta Cibello-prod-hemligheter från AWS SSM och skriv /opt/cibello-platform/.env.
|
||||
|
||||
SSM är enda sanningskällan. Kör detta vid varje deploy INNAN applikationen startas om.
|
||||
Inga värden loggas. set +x aktiveras automatiskt.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REGION = os.environ.get("AWS_REGION", "eu-north-1")
|
||||
SSM_PREFIX = "/cibello/prod"
|
||||
ENV_PATH = Path(os.environ.get("ENV_PATH", "/opt/cibello-platform/.env"))
|
||||
|
||||
# Icke-hemliga värden som ändå behöver styras centralt.
|
||||
# Dessa kan med fördel också flyttas till SSM (String/SecureString) senare.
|
||||
STATIC_CONFIG = {
|
||||
"NODE_ENV": "production",
|
||||
"API_PORT": "4000",
|
||||
"ADMIN_PORT": "5173",
|
||||
"API_BASE_URL": "https://api.cibello.app",
|
||||
"VITE_API_BASE_URL": "https://api.cibello.app",
|
||||
"CORS_ORIGINS": "https://cibello.app,https://admin.cibello.app,https://api.cibello.app",
|
||||
"REDIS_URL": "redis://localhost:6379",
|
||||
"LOG_LEVEL": "info",
|
||||
"AAMOS_MODE": "gemini",
|
||||
"AAMOS_API_URL": "https://amos.aamos.systems",
|
||||
"AAMOS_TIMEOUT_MS": "60000",
|
||||
"GEMINI_MODEL": "gemini-2.5-flash",
|
||||
"GEMINI_TIMEOUT_MS": "60000",
|
||||
"GEMINI_DAILY_BUDGET_USD": "25",
|
||||
"ENTITLEMENT_TTL_HOURS": "24",
|
||||
"ENTITLEMENT_GRACE_DAYS": "7",
|
||||
"JWT_ACCESS_TTL_SECONDS": "900",
|
||||
"JWT_REFRESH_TTL_SECONDS": "2592000",
|
||||
"S3_MODE": "aws",
|
||||
"S3_BUCKET": "cibello-production",
|
||||
"S3_REGION": "eu-north-1",
|
||||
"SMTP_PORT": "587",
|
||||
"SMTP_SECURE": "false",
|
||||
"EMAIL_MODE": "log",
|
||||
"EXPO_PUSH_ENABLED": "false",
|
||||
"DATABASE_CA_CERT": "/opt/cibello-platform/certs/rds-global-bundle.pem",
|
||||
}
|
||||
|
||||
# SSM-parameter → env-var. Värden från SSM skriver över STATIC_CONFIG om de krockar.
|
||||
SSM_MAP = {
|
||||
"aamos-api-key": "AAMOS_API_KEY",
|
||||
"db-password": "DB_PASSWORD", # används för att bygga DATABASE_URL
|
||||
"entitlement-signing-secret": "ENTITLEMENT_SIGNING_SECRET",
|
||||
"eoc-push-token": "EOC_PUSH_TOKEN",
|
||||
"gemini-api-key": "GEMINI_API_KEY",
|
||||
"jwt-access-secret": "JWT_ACCESS_SECRET",
|
||||
"jwt-refresh-secret": "JWT_REFRESH_SECRET",
|
||||
"smtp-pass": "SMTP_PASS",
|
||||
# Följande är valfria just nu men bör ligga i SSM
|
||||
"google-service-account-json": "GOOGLE_SERVICE_ACCOUNT_JSON",
|
||||
}
|
||||
|
||||
|
||||
def get_parameters(names: list[str]) -> dict[str, str]:
|
||||
if not names:
|
||||
return {}
|
||||
cmd = [
|
||||
"aws", "ssm", "get-parameters",
|
||||
"--names", *names,
|
||||
"--with-decryption",
|
||||
"--region", REGION,
|
||||
]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||
data = json.loads(result.stdout)
|
||||
out = {}
|
||||
for p in data.get("Parameters", []):
|
||||
out[p["Name"]] = p["Value"]
|
||||
for inv in data.get("InvalidParameters", []):
|
||||
print(f"[sync-env] WARN: SSM-parameter saknas: {inv}", file=sys.stderr)
|
||||
return out
|
||||
|
||||
|
||||
def build_env(ssm: dict[str, str]) -> dict[str, str]:
|
||||
env = dict(STATIC_CONFIG)
|
||||
|
||||
for short, var in SSM_MAP.items():
|
||||
val = ssm.get(f"{SSM_PREFIX}/{short}")
|
||||
if val is not None:
|
||||
env[var] = val
|
||||
|
||||
db_password = env.get("DB_PASSWORD", "")
|
||||
db_host = os.environ.get("DB_HOST", "cibello-prod-db.cvi0qcksmsfj.eu-north-1.rds.amazonaws.com")
|
||||
db_name = os.environ.get("DB_NAME", "cibello")
|
||||
test_db_name = os.environ.get("TEST_DB_NAME", "cibello_test")
|
||||
db_user = os.environ.get("DB_USER", "cibello_admin")
|
||||
db_sslmode = os.environ.get("DB_SSLMODE", "require")
|
||||
|
||||
if db_password:
|
||||
env["DATABASE_URL"] = f"postgres://{db_user}:{db_password}@{db_host}:5432/{db_name}?sslmode={db_sslmode}"
|
||||
env["TEST_DATABASE_URL"] = f"postgres://{db_user}:{db_password}@{db_host}:5432/{test_db_name}?sslmode={db_sslmode}"
|
||||
|
||||
# Rensa temporär nyckel
|
||||
env.pop("DB_PASSWORD", None)
|
||||
|
||||
return env
|
||||
|
||||
|
||||
def write_env(env: dict[str, str], dry_run: bool = False) -> None:
|
||||
lines = [f"{k}={v}" for k, v in env.items()]
|
||||
if dry_run:
|
||||
print(f"[sync-env] DRY-RUN: skulle skriva {ENV_PATH} ({len(env)} rader)")
|
||||
return
|
||||
ENV_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
ENV_PATH.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
os.chmod(ENV_PATH, 0o600)
|
||||
print(f"[sync-env] skrev {ENV_PATH} ({len(env)} rader)")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
dry_run = "--dry-run" in sys.argv
|
||||
names = [f"{SSM_PREFIX}/{short}" for short in SSM_MAP]
|
||||
ssm = get_parameters(names)
|
||||
env = build_env(ssm)
|
||||
write_env(env, dry_run=dry_run)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user