Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
de58741
๐Ÿ”€ Merge develop into infra/prometheus-grafana (#12)
simGPT Apr 23, 2026
2c0766e
:bug:Fix: cd ์‹œ์— ์ด๋ฏธ์ง€ ํƒœ๊ทธ ์—…๋ฐ์ดํŠธ ํ•˜๋„๋ก ์ˆ˜์ •(v1 -> ์ปค๋ฐ‹SHA) (#14)
simGPT Apr 23, 2026
663b1b2
โœจ Feat: MLflow ๊ธฐ๋ฐ˜ S3 ๋ชจ๋ธ ์ €์žฅ ๋ฐ ๋กœ๋“œ ํ™˜๊ฒฝ ๊ตฌ์ถ• (#17)
simGPT May 12, 2026
aaa9dce
:bug:Fix: volume mount ์ถ”๊ฐ€
simGPT May 12, 2026
edc1425
:bug:Fix: pvc.yaml(ํด๋Ÿฌ์Šคํ„ฐ์šฉ ์ €์žฅ์†Œ)๊ตฌ์ถ•์„ ์œ„ํ•œ ์„ค์ •ํŒŒ์ผ ์ถ”๊ฐ€
simGPT May 12, 2026
861bf16
๐Ÿ› Fix: mlflow-db ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ๋˜๋Š” ๋ฌธ์ œ (#20)
simGPT May 12, 2026
0568e8b
Merge branch 'develop' of https://github.com/simGPT/sw-mlops into devโ€ฆ
simGPT May 13, 2026
f467d73
Merge branch 'main' into develop
simGPT May 13, 2026
969359b
:twisted_rightwards_arrows:Merge: main<-develop pr ์ถฉ๋Œ ํ•ด๊ฒฐ
simGPT May 13, 2026
7492ca9
๐Ÿ› Fix: ๋ชจ๋ธ ์„œ๋น™ ์‹œ mlflow์˜ 403 ์—๋Ÿฌ ํ•ด๊ฒฐ (#23)
simGPT May 15, 2026
d34fc68
ci: trigger CI
simGPT May 15, 2026
261977c
:wrench:Settings: retrigger CI
simGPT May 15, 2026
ec12462
:wrench:Infra: ci ํŠธ๋ฆฌ๊ฑฐ์— pull_request ์ถ”๊ฐ€
simGPT May 15, 2026
9c5ccc7
:wrench:Settings: ci ํŠธ๋ฆฌ๊ฑฐ main push ์ œ๊ฑฐํ•˜๋„๋ก ์ˆ˜์ •
simGPT May 18, 2026
76ace0a
:wrench:Settings: aws๊ด€๋ จ secrets env๋กœ ์ถ”๊ฐ€
simGPT May 18, 2026
a6eaacd
:card_file_box:Comment: ์ฃผ์„ ์ˆ˜์ •
simGPT May 18, 2026
49224cc
:wrench:Settings: ์ด๋ฏธ์ง€ ํƒœ๊ทธ v1 -> latest๋กœ ์ˆ˜์ •
simGPT May 18, 2026
a6fbfb0
:wrench:Settings: localhost:5000 ํ—ˆ์šฉ ์„ค์ •
simGPT May 18, 2026
63604fd
๐Ÿ”ง Infra: EC2 k3s ์šด์˜ ํ™˜๊ฒฝ ๋ฐฐํฌ ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์ถ• (#26)
simGPT May 20, 2026
4fdf761
:card_file_box:Comment: CI/CD ํŒŒ์ดํ”„๋ผ์ธ ํŠธ๋ฆฌ๊ฑฐ ํ™•์ธ์šฉ ์ฃผ์„ ์ถ”๊ฐ€
simGPT May 20, 2026
962b61c
:twisted_rightwards_arrows:Merge: merge origin main into develop
simGPT May 20, 2026
816bf5d
:bug:Fix: pull_request ์ค‘๋ณต ์‚ญ์ œ
simGPT May 20, 2026
b947c07
:sparkles:Feat: churn modeling test (#29)
simGPT May 27, 2026
94f20c9
โœจ Feat: ๊ณ ๊ฐ ์ดํƒˆ ์˜ˆ์ธก ๋ชจ๋ธ ํ•™์Šต ๋ฐ ์„œ๋น™ (#30)
simGPT May 27, 2026
88b75ab
โœจ Feat: ๋„๋ฉ”์ธ ๊ตฌ๋งค ๋ฐ ์„œ๋น„์Šค ์ธํ”„๋ผ ์—ฐ๊ฒฐ (#32)
simGPT May 30, 2026
ce3cabe
:twisted_rightwards_arrows:Merge: develop -> main ์ถฉ๋Œ ํ•ด๊ฒฐ
simGPT May 30, 2026
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
4 changes: 2 additions & 2 deletions .github/workflows/cd-production.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
run: kubectl apply -k k8s/model-api/overlays/production/

- name: ํŒŒ๋“œ ์žฌ์‹œ์ž‘ # (์ด๋ฏธ์ง€ ํƒœ๊ทธ๊ฐ€ ๊ณ„์† latest์—ฌ๋„ ์ƒˆ ์ด๋ฏธ์ง€๋กœ ๋ฐฐํฌ๋˜๋„๋ก)
run: kubectl rollout restart deployment/model-api
run: kubectl rollout restart deployment/model-api -n model-api # ๋„ค์ž„์ŠคํŽ˜์ด์Šค ์ง€์ •

- name: ๋ฐฐํฌ ํ™•์ธ
run: kubectl rollout status deployment/model-api
run: kubectl rollout status deployment/model-api -n model-api # ๋„ค์ž„์ŠคํŽ˜์ด์Šค ์ง€์ •
14 changes: 14 additions & 0 deletions k8s/cert-manager/cluster-issuer.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
apiVersion: cert-manager.io/v1
kind: ClusterIssuer # ์ธ์ฆ์„œ ๋ฐœ๊ธ‰, ํด๋Ÿฌ์Šคํ„ฐ ์ „์ฒด์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
metadata:
name: swmlops-issuer
spec:
acme: # acme ํ”„๋กœํ† ์ฝœ์ด Letโ€™s Encrypt์™€ ํ†ต์‹ ํ•˜๊ธฐ ์œ„ํ•œ ํ‘œ์ค€ ํ”„๋กœํ† ์ฝœ
server: https://acme-v02.api.letsencrypt.org/directory
email: simgpt02@gmail.com
privateKeySecretRef:
name: swmlops-issuer
solvers:
- http01:
ingress:
ingressClassName: traefik # k3s ์—์„œ ๊ธฐ๋ณธ ์ œ๊ณตํ•ด์ฃผ๋Š” ingress controller
1 change: 1 addition & 0 deletions k8s/mlflow-db/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ apiVersion: apps/v1
kind: Deployment
metadata:
name: mlflow-db
namespace: mlflow
spec:
replicas: 1
selector:
Expand Down
1 change: 1 addition & 0 deletions k8s/mlflow-db/pvc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mlflow-db-pvc
namespace: mlflow
spec:
accessModes:
- ReadWriteOnce
Expand Down
1 change: 1 addition & 0 deletions k8s/mlflow-db/service.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ apiVersion: v1
kind: Service
metadata:
name: mlflow-db
namespace: mlflow
spec:
type: ClusterIP
selector:
Expand Down
4 changes: 3 additions & 1 deletion k8s/mlflow/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ apiVersion: apps/v1
kind: Deployment
metadata:
name: mlflow
namespace: mlflow
spec:
replicas: 1
selector:
Expand All @@ -14,6 +15,7 @@ spec:
spec:
containers:
- name: mlflow
image: burakince/mlflow:3.7.0 # postgreSQL ์—ฐ๊ฒฐ์ด ๋˜๋Š” ๋ฒ„์ „ ์ด๋ฏธ์ง€
image: burakince/mlflow:3.7.0 # postgreSQL ์—ฐ๊ฒฐ์ด ๋˜๋Š” ๋ฒ„์ „ ์ด๋ฏธ์ง€
ports:
- containerPort: 5000
Expand All @@ -39,5 +41,5 @@ spec:
- --port=5000
- --default-artifact-root=s3://kube-ml-bucket/mlflow-artifacts # ๋ชจ๋ธ ์ €์žฅ
- --backend-store-uri=postgresql://mlflow:1234@mlflow-db:5432/mlflow
- --allowed-hosts=mlflow,mlflow:5000,mlflow.default.svc.cluster.local,localhost,localhost:5000 # ์‹ค์ œ ์šด์˜ ํ™˜๊ฒฝ์—์„œ๋Š” ์‹ค์ œ ๋„๋ฉ”์ธ์„ ์ž…๋ ฅํ•˜๋ฉด ๋  ๋“ฏ
- --allowed-hosts=mlflow.mlflow.svc.cluster.local,localhost,localhost:5000,mlflow.swmlops.site # ํ’€ DNS, localhost, ๋„๋ฉ”์ธ ํ—ˆ์šฉ

24 changes: 24 additions & 0 deletions k8s/mlflow/ingress.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mlflow
namespace: mlflow
annotations:
cert-manager.io/cluster-issuer: swmlops-issuer # cert-manager์—์„œ ์ธ์ฆ์„œ๋ฅผ ๋ฐœ๊ธ‰๋ฐ›์„ ๋•Œ ์‚ฌ์šฉํ•  ClusterIssuer ์ง€์ •
spec:
ingressClassName: traefik # k3s ์—์„œ ๊ธฐ๋ณธ ์ œ๊ณตํ•ด์ฃผ๋Š” ingress controller
tls:
- hosts:
- mlflow.swmlops.site # ์ด ๋„๋ฉ”์ธ์œผ๋กœ ์ ‘์†ํ•  ๋•Œ TLS ์ธ์ฆ์„œ ์‚ฌ์šฉ
secretName: mlflow-tls
rules:
- host: mlflow.swmlops.site # ์ด ๋„๋ฉ”์ธ์œผ๋กœ ์ ‘์†ํ•  ๋•Œ ์•„๋ž˜์˜ http ๊ฒฝ๋กœ๋กœ ๋ผ์šฐํŒ…
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: mlflow # mlflow ์„œ๋น„์Šค๋กœ ์ด๋™ + 5000 ํฌํŠธ๋กœ ์ด๋™
port:
number: 5000
4 changes: 2 additions & 2 deletions k8s/mlflow/service.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ apiVersion: v1
kind: Service
metadata:
name: mlflow
namespace: mlflow
spec:
type: NodePort
type: ClusterIP
selector:
app: mlflow
ports:
- port: 5000
targetPort: 5000
nodePort: 30050
5 changes: 4 additions & 1 deletion k8s/model-api/base/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ spec:
spec:
containers:
- name: model-api
image: simgpt/model-api:latest # ์ด๋ ‡๊ฒŒ ๋˜์–ด์žˆ์–ด๋„ ์–ด์ฐจํ”ผ cd์‹œ์— ํƒœ๊ทธ๊ฐ€ ์ปค๋ฐ‹ sha๋กœ ์ •ํ™•ํ•œ ๋ฒ„์ „ ์ถ”์ ํ•˜๋„๋ก ํ•ด๋†“์Œ
image: simgpt/model-api:latest # ์ด๋ ‡๊ฒŒ ๋˜์–ด์žˆ์–ด๋„ ์–ด์ฐจํ”ผ cd์‹œ์— ํƒœ๊ทธ๊ฐ€ ์ปค๋ฐ‹ sha๋กœ ์ •ํ™•ํ•œ ๋ฒ„์ „ ์ถ”์ ํ•˜๋„๋ก ํ•ด๋†“์Œ
ports:
- containerPort: 8000
env:
- name: MLFLOW_TRACKING_URI
value: http://mlflow:5000
value: http://mlflow.mlflow.svc.cluster.local:5000
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
Expand All @@ -32,6 +33,8 @@ spec:
key: secret-access-key
- name: AWS_DEFAULT_REGION
value: ap-northeast-2
- name: CHURN_MODEL_VERSION # ๋ชจ๋ธ ๋ฒ„์ „ ์ถ”๊ฐ€
value: "v5"
resources:
requests:
cpu: "250m"
Expand Down
2 changes: 2 additions & 0 deletions k8s/model-api/overlays/local/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

namespace: model-api

resources:
- ../../base # base ๊ฐ€์ ธ๋‹ค ์“ด๋‹ค
- service.yaml
11 changes: 10 additions & 1 deletion k8s/model-api/overlays/production/ingress.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,18 @@ apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: model-api
namespace: model-api
annotations:
cert-manager.io/cluster-issuer: swmlops-issuer # cert-manager์—์„œ ์ธ์ฆ์„œ๋ฅผ ๋ฐœ๊ธ‰๋ฐ›์„ ๋•Œ ์‚ฌ์šฉํ•  ClusterIssuer ์ง€์ •
spec:
ingressClassName: traefik # k3s ์—์„œ ๊ธฐ๋ณธ ์ œ๊ณตํ•ด์ฃผ๋Š” ingress controller
tls:
- hosts:
- model.swmlops.site # ์ด ๋„๋ฉ”์ธ์œผ๋กœ ์ ‘์†ํ•  ๋•Œ TLS ์ธ์ฆ์„œ ์‚ฌ์šฉ
secretName: model-api-tls
rules:
- http:
- host: model.swmlops.site # ์ด ๋„๋ฉ”์ธ์œผ๋กœ ์ ‘์†ํ•  ๋•Œ ์•„๋ž˜์˜ http ๊ฒฝ๋กœ๋กœ ๋ผ์šฐํŒ…
http:
paths:
- path: /
pathType: Prefix
Expand Down
2 changes: 2 additions & 0 deletions k8s/model-api/overlays/production/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

namespace: model-api # ๋„ค์ž„์ŠคํŽ˜์ด์Šค ์ถ”๊ฐ€(์ผ๋ถ€๋Ÿฌ deployment.yaml์—์„œ ๋„ค์ž„์ŠคํŽ˜์ด์Šค ์•ˆ์“ฐ๊ณ  ์—ฌ๊ธฐ์„œ๋งŒ ์”€)

resources:
- ../../base
- service.yaml
Expand Down
15 changes: 14 additions & 1 deletion k8s/monitoring/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,17 @@ prometheus:
- job_name: model-api
static_configs:
- targets:
- model-api.default.svc.cluster.local:8000
- model-api.model-api.svc.cluster.local:8000 # ๋‹ค๋ฅธ ๋„ค์ž„์ŠคํŽ˜์ด์Šค์— ์žˆ๋Š” model-api์˜ ํด๋Ÿฌ์Šคํ„ฐ ๋‚ด๋ถ€ ์ฃผ์†Œ์™€ ํฌํŠธ

grafana:
ingress:
enabled: true
ingressClassName: traefik
annotations:
cert-manager.io/cluster-issuer: swmlops-issuer
hosts:
- monitoring.swmlops.site
tls:
- secretName: grafana-tls
hosts:
- monitoring.swmlops.site
14 changes: 14 additions & 0 deletions k8s/namespaces.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
apiVersion: v1
kind: Namespace
metadata:
name: mlflow
---
apiVersion: v1
kind: Namespace
metadata:
name: model-api
---
apiVersion: v1
kind: Namespace
metadata:
name: monitoring
30 changes: 27 additions & 3 deletions services/model-api/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ def health():

# MNIST ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ์˜ˆ์ธก API
@app.post("/mnist_predict", response_model=ResponseSchema)
def predict(request: RequestSchema):
def mnist_predict(request: RequestSchema):
try:
service = get_service(request.type) # ๋ชจ๋ธ์— ๋งž๋Š” service ์ง€์ •
output = service.predict(request.data) # ์˜ˆ์ธก ๊ฒฐ๊ณผ
service = get_service(request.type)
output = service.predict(request.data)

return ResponseSchema(
success=True,
Expand All @@ -37,3 +37,27 @@ def predict(request: RequestSchema):
metadata=None,
error=str(e),
)


# ๊ณ ๊ฐ ์ดํƒˆ ์˜ˆ์ธก API
@app.post("/churn_predict", response_model=ResponseSchema)
def churn_predict(request: RequestSchema):
try:
service = get_service("churn")
output = service.predict(request.data)

return ResponseSchema(
success=True,
model="churn",
result=output["result"],
metadata=output["metadata"],
error=None,
)
except Exception as e:
return ResponseSchema(
success=False,
model="churn",
result=None,
metadata=None,
error=str(e),
)
20 changes: 18 additions & 2 deletions services/model-api/app/models/loader.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import os
import mlflow.pytorch
import mlflow.sklearn


_model_cache: dict = {}


# mlflow์—์„œ mnist ๋ชจ๋ธ ๋กœ๋“œํ•˜๋Š” ํ•จ์ˆ˜
def load_model(model_name: str, version: str):
cache_key = f"{model_name}_{version}"
if cache_key in _model_cache:
return _model_cache[cache_key]

mlflow_uri = os.getenv("MLFLOW_TRACKING_URI", "http://mlflow:5000")
mlflow_uri = os.getenv("MLFLOW_TRACKING_URI", "http://mlflow:5000") # ํ™˜๊ฒฝ๋ณ€์ˆ˜์—์„œ mlflow tracking uri ๊ฐ€์ ธ์˜ค๊ธฐ, ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ http://mlflow:5000 ์‚ฌ์šฉ
mlflow.set_tracking_uri(mlflow_uri)

model_uri = f"models:/{model_name}-{version}/latest"
Expand All @@ -19,3 +20,18 @@ def load_model(model_name: str, version: str):

_model_cache[cache_key] = model
return model

# mlflow์—์„œ ๊ณ ๊ฐ ์ดํƒˆ ์˜ˆ์ธก ๋ชจ๋ธ ๋กœ๋“œํ•˜๋Š” ํ•จ์ˆ˜
def load_churn_model(model_name: str, version: str):
cache_key = f"{model_name}_{version}_sklearn"
if cache_key in _model_cache:
return _model_cache[cache_key]

mlflow_uri = os.getenv("MLFLOW_TRACKING_URI", "http://mlflow:5000")
mlflow.set_tracking_uri(mlflow_uri)

model_uri = f"models:/{model_name}-{version}/latest"
model = mlflow.sklearn.load_model(model_uri)

_model_cache[cache_key] = model
return model
65 changes: 65 additions & 0 deletions services/model-api/app/services/churn_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import os
import time
from datetime import datetime, timezone

from app.models.loader import load_churn_model
from prometheus_client import Counter, Histogram

MODEL_NAME = "churn"
MODEL_VERSION = os.getenv("CHURN_MODEL_VERSION", "v5")

FEATURES = [
'account_age_months',
'avg_order_value',
'total_orders',
'days_since_last_purchase',
'discount_usage_rate',
'return_rate',
'browsing_frequency_per_week',
'cart_abandonment_rate',
]

prediction_counter = Counter(
"churn_predictions_total",
"์ดํƒˆ ์˜ˆ์ธก ํšŸ์ˆ˜",
["predicted"],
)
confidence_histogram = Histogram(
"churn_confidence_score",
"์ดํƒˆ ์˜ˆ์ธก ์‹ ๋ขฐ๋„ ๋ถ„ํฌ",
buckets=[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
)


def predict(data: dict) -> dict:
missing = [f for f in FEATURES if f not in data]
if missing:
raise ValueError(f"๋ˆ„๋ฝ๋œ ํ”ผ์ฒ˜: {missing}")

model = load_churn_model(MODEL_NAME, MODEL_VERSION)

x = [[data[f] for f in FEATURES]]

start = time.time()
predicted = int(model.predict(x)[0])
proba = model.predict_proba(x)[0]
confidence = float(proba[predicted])
time_ms = round((time.time() - start) * 1000, 3)

prediction_counter.labels(predicted=str(predicted)).inc()
confidence_histogram.observe(confidence)

return {
"result": {
"predicted_class": predicted,
"churned": bool(predicted),
"confidence": confidence,
"probabilities": {"retained": float(proba[0]), "churned": float(proba[1])},
},
"metadata": {
"model": MODEL_NAME,
"version": MODEL_VERSION,
"inference_time_ms": time_ms,
"timestamp": datetime.now(timezone.utc).isoformat(),
},
}
3 changes: 2 additions & 1 deletion services/model-api/app/services/service_factory.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from app.services import mnist_service
from app.services import mnist_service, churn_service


_services = {
"mnist": mnist_service,
"churn": churn_service,
}

# ๋ชจ๋ธ์˜ ์ข…๋ฅ˜์— ๋งž๊ฒŒ service ํŒŒ์ผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋ผ์šฐํ„ฐ
Expand Down
Loading
Loading