Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/deploy-churn.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Deploy (churn)

on:
repository_dispatch:
types:
- deploy-churn

jobs:
deploy:
runs-on: ubuntu-latest

steps:
- name: kubectl μ„€μ •
run: |
mkdir -p $HOME/.kube
echo "${{ secrets.KUBECONFIG_PRODUCTION }}" | base64 -d > $HOME/.kube/config

- name: model-api νŒŒλ“œ μž¬μ‹œμž‘
run: kubectl rollout restart deployment/model-api -n model-api

- name: model-api 배포 확인
run: kubectl rollout status deployment/model-api -n model-api
46 changes: 0 additions & 46 deletions .github/workflows/retrain-churn.yaml

This file was deleted.

35 changes: 28 additions & 7 deletions k8s/alert-bridge/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,33 @@ spec:
ports:
- containerPort: 8080
env:
- name: GITHUB_TOKEN
- name: SMTP_HOST
valueFrom:
secretKeyRef:
name: github-credentials
key: token
- name: GITHUB_REPO_OWNER
value: simGPT
- name: GITHUB_REPO_NAME
value: sw-mlops
name: smtp-credentials
key: host
- name: SMTP_PORT
valueFrom:
secretKeyRef:
name: smtp-credentials
key: port
- name: SMTP_USER
valueFrom:
secretKeyRef:
name: smtp-credentials
key: user
- name: SMTP_PASSWORD
valueFrom:
secretKeyRef:
name: smtp-credentials
key: password
- name: ALERT_MAIL_FROM
valueFrom:
secretKeyRef:
name: smtp-credentials
key: mail-from
- name: ALERT_MAIL_TO
valueFrom:
secretKeyRef:
name: smtp-credentials
key: mail-to
63 changes: 42 additions & 21 deletions services/alert-bridge/main.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,57 @@
import os
import httpx
import smtplib
import ssl
from datetime import datetime, timezone
from email.mime.text import MIMEText
from fastapi import FastAPI, Request

app = FastAPI(title="Alert Bridge")

GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
GITHUB_REPO_OWNER = os.getenv("GITHUB_REPO_OWNER")
GITHUB_REPO_NAME = os.getenv("GITHUB_REPO_NAME")
SMTP_HOST = os.getenv("SMTP_HOST")
SMTP_PORT = int(os.getenv("SMTP_PORT", "465"))
SMTP_USER = os.getenv("SMTP_USER")
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD")
ALERT_MAIL_FROM = os.getenv("ALERT_MAIL_FROM")
ALERT_MAIL_TO = os.getenv("ALERT_MAIL_TO")

# Alertmanagerμ—μ„œ μ•Œλ¦Όμ„ λ°›μ•„μ„œ GitHub Actions둜 μ „λ‹¬ν•˜λŠ” 역할을 ν•˜λŠ” api μ—”λ“œν¬μΈνŠΈ(Alertmanagerκ°€ μš”μ²­μ„ 보냄)
# μ•Œλ¦Ό λ‚΄μš©μ„ μ΄λ©”μΌλ‘œ μž‘μ„±ν•˜λŠ” ν•¨μˆ˜
def build_email_body(alerts: list) -> str:
lines = [f"[sw-mlops μ•Œλ¦Ό] {len(alerts)}개의 이상 감지 - {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}\n"]
for a in alerts:
labels = a.get("labels", {})
annotations = a.get("annotations", {})
lines.append(f"β€’ Alert : {labels.get('alertname', '-')}")
lines.append(f" Severity: {labels.get('severity', '-')}")
lines.append(f" Summary : {annotations.get('summary', '-')}")
lines.append(f" Detail : {annotations.get('description', '-')}\n")
return "\n".join(lines)

# 이메일 μ „μ†‘ν•˜λŠ” ν•¨μˆ˜
def send_email(subject: str, body: str):
msg = MIMEText(body, "plain", "utf-8")
msg["Subject"] = subject
msg["From"] = ALERT_MAIL_FROM
msg["To"] = ALERT_MAIL_TO

with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server:
server.starttls(context=ssl.create_default_context())
server.login(SMTP_USER, SMTP_PASSWORD)
server.sendmail(ALERT_MAIL_FROM, ALERT_MAIL_TO, msg.as_string())


# μ•Œλ¦Ό λ°›μ•„μ„œ 이메일 μ „μ†‘ν•˜λŠ” api
@app.post("/alert")
async def receive_alert(request: Request):
payload = await request.json()

# alertmanagerμ—μ„œ 받은 μ•Œλ¦Ό μ€‘μ—μ„œ firing μƒνƒœμΈ μ•Œλ¦Όλ§Œ 필터링
firing_alerts = [a for a in payload.get("alerts", []) if a["status"] == "firing"]
if not firing_alerts:
return {"message": "no firing alerts"}
return {"message": "λ°œμƒμ€‘μΈ μ•Œλ¦Όμ΄ μ—†μŠ΅λ‹ˆλ‹€."}

alert_names = [a["labels"].get("alertname", "") for a in firing_alerts]
subject = f"[sw-mlops μ•Œλ¦Ό] {', '.join(alert_names)}"
body = build_email_body(firing_alerts) # μ•Œλ¦Ό λ‚΄μš©μ„ 이메일 body둜 μž‘μ„±

send_email(subject, body) # 이메일 전솑

async with httpx.AsyncClient() as client:
response = await client.post(
f"https://api.github.com/repos/{GITHUB_REPO_OWNER}/{GITHUB_REPO_NAME}/dispatches",
headers={
"Authorization": f"Bearer {GITHUB_TOKEN}",
"Accept": "application/vnd.github+json",
},
json={
"event_type": "model-retrain",
"client_payload": {"alerts": alert_names},
},
)

return {"status": response.status_code, "alerts": alert_names}
return {"message": "이메일이 μ „μ†‘λ˜μ—ˆμŠ΅λ‹ˆλ‹€.", "alerts": alert_names}
1 change: 0 additions & 1 deletion services/alert-bridge/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
fastapi
uvicorn
httpx
19 changes: 19 additions & 0 deletions services/model-api/training/churn/train.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import argparse
import os
import sys
import requests

sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))

Expand Down Expand Up @@ -58,6 +59,24 @@ def main(args):
print(f"test_roc_auc: {test_metrics['roc_auc']:.4f}")
print(f'MLflow에 λͺ¨λΈ 등둝 μ™„λ£Œ: churn-{args.version}')

github_token = os.getenv("GITHUB_TOKEN")
github_owner = os.getenv("GITHUB_REPO_OWNER")
github_repo = os.getenv("GITHUB_REPO_NAME")

if github_token and github_owner and github_repo:
response = requests.post(
f"https://api.github.com/repos/{github_owner}/{github_repo}/dispatches",
headers={
"Authorization": f"Bearer {github_token}",
"Accept": "application/vnd.github+json",
},
json={"event_type": "deploy-churn"},
)
if response.status_code == 204:
print("배포 트리거 μ™„λ£Œ")
else:
print(f"배포 트리거 μ‹€νŒ¨: {response.status_code}")


if __name__ == '__main__':
parser = argparse.ArgumentParser()
Expand Down
Loading