์ด ๊ธ์์ ๋ค๋ฃจ๋ ๊ฒ
Feature Store-lite ์์ Feast๋ฅผ ์น์ด, S3 Offline + Redis Online + Feature Server ๊ตฌ์ฑ์ผ๋ก “์ ์ฅํ๋ ํ์ดํ๋ผ์ธ"์ “์กฐํ ๊ฐ๋ฅํ ํผ์ฒ ํ๋ซํผ"์ผ๋ก ํ์ฅํ๋ ๊ณผ์
์ ์์ง์
์ด ๋จ๊ณ์์ ํด๊ฒฐํ๋ ค๋ ๋ฌธ์
์ด์ ๊ธ(Feature Store-lite)์์ ๊ณ์ฝ(์คํค๋ง/๋ฉํ) + ๋ฒ์ ํ ์ ์ฅ + ์ฌํ์ฑ๊น์ง ๊ณ ์ ํ์ต๋๋ค.
ํ์ง๋ง ์ค๋ฌด์์๋ “์ ์ฅ"์์ ๋๋์ง ์์ต๋๋ค. ๊ฒฐ๊ตญ ์ค์ํ ๊ฑด ์กฐํ ๊ฐ๋ฅ(Serving-ready) ์ํ์ ๋๋ค.
์ด๋ฒ ๊ธ์ Feature Store-lite ์์ Feast๋ฅผ ์น์ด ์๋๋ฅผ ์์ฑํฉ๋๋ค.
- Offline Source: S3์
latest/features.parquet(Feast๊ฐ ์ฝ๋ ๊ณ ์ ํฌ์ธํฐ) - Registry: S3์
registry.pb์ ์ฅ(ํ๊ฒฝ๋ณ ๋ถ๋ฆฌ) - Online Store: Redis ์ ์ฌ(materialize)๋ก ์จ๋ผ์ธ ์กฐํ ๊ฐ๋ฅ
- Feature Server: ์์ ์๋น์ค + startup ์
feast apply
์ฆ, “์ ์ฅํ๋ ํ์ดํ๋ผ์ธ” -> “์กฐํ ๊ฐ๋ฅํ ํผ์ฒ ํ๋ซํผ"์ผ๋ก ํ์ฅํ๋ ๋จ๊ณ์ ๋๋ค.
๐ฏ ์๋ฃ ๊ธฐ์ค
- GitOps๋ก Feast ๊ณ์ฝ(repo.py/feature_store.yaml) ๋ฐฐํฌ
- Airflow๋ก S3์ version ์ ์ง + latest overwrite ์ ์ฅ
- Feast materialize ์คํ -> Redis online ์ ์ฌ
- Feast SDK๋ก online ์กฐํ ๊ฒฐ๊ณผ๊ฐ None์ด ์๋ ์ค์ ๊ฐ์ผ๋ก ํ์ธ
1๏ธโฃ ์ ์ฒด ๊ตฌ์กฐ

2๏ธโฃ ์ฝ๋/๋ฆฌ์์ค ํธ๋ฆฌ
(A) GitOps (mlops-infra-gitops)
charts/
feast/
templates/
feast-repo-configmap.yaml
feast-server-deployment.yaml
feast-server-service.yaml
redis-deployment.yaml
redis-service.yaml
values/
base.yaml
dev.yaml
prod.yaml
apps/
feast-dev.yaml
feast-prod.yaml
# ... (sealed-secrets ์๋ต)
(B) Airflow (airflow-dags-dev)
dags/
dag_data_pipeline_daily_dev_v5.py
mlops_lib/
dp/
build.py
store.py
.airflowignore
3๏ธโฃ ์ค๊ณ ํฌ์ธํธ (์ด์ ๊ด์ )
(1) Feast repo ์์ฒด๋ฅผ “๊ณ์ฝ(Contract)“์ผ๋ก ์ทจ๊ธ
Feast์์ ์ค์ง์ ์ธ ๊ณ์ฝ์ repo.py + feature_store.yaml์
๋๋ค.
์ด๊ฑธ ConfigMap์ผ๋ก GitOps ๋ฐฐํฌํ๋ฉด:
- dev/prod์์ ๋์ผ ๊ณ์ฝ ์ ์ง
- ์ฝ๋ ์์ ์์ด ๋ฐฐํฌ/๋กค๋ฐฑ ๊ฐ๋ฅ
- “์ด์ ๋ฆฌ์์ค"๋ก ์ถ์ ๊ฐ๋ฅ
(2) S3 ์ ์ฅ ์ ์ฑ ์ “๋ฒ์ + latest ํฌ์ธํฐ"๋ก ๋๋ธ๋ค
.../<feature_set>/<version>/: ์ฌํ์ฑ.../<feature_set>/latest/: ์ด์ ํธ์
Feast Offline์ latest๋ง ์ฝ์ผ๋ฉด ๋๊ธฐ ๋๋ฌธ์, ์ฌ์ฉ์๋ ๋ฒ์ ์ ๋ชฐ๋ผ๋ ๋ฉ๋๋ค. ๋ฒ์ ์ “์ฌํ"์ด ํ์ํ ๋๋ง ๊บผ๋ด ์ฐ๋ ๊ตฌ์กฐ๊ฐ ๊ฐ์ฅ ๋จ๋จํฉ๋๋ค.
(3) Airflow๋ ์์ฑ/์ ์ฅ๊น์ง๋ง, Online ์ ์ฌ๋ Feast๊ฐ ๋ด๋น
Airflow ์ด๋ฏธ์ง์ feast/pyarrow/s3fs๊น์ง ์น๊ธฐ ์์ํ๋ฉด ์ด์ ๋์ด๋๊ฐ ๊ธ์์นํฉ๋๋ค. ๊ทธ๋์ ์ญํ ์ ๋๋ด์ต๋๋ค.
- Airflow: feature ์์ฑ + S3 ์ ์ฅ(๋ฒ์ /latest)
- Feast: materialize + online(redis) ์ ์ฌ + ์กฐํ
4๏ธโฃ GitOps: Feast ๋ฐฐํฌ (Redis + Feature Server + Repo ConfigMap)
4-1) values (base/dev/prod)
# charts/feast/values/base.yaml
aws:
region: ap-northeast-2
credentialsSecretName: aws-credentials
s3:
bucket: datapipeline-raw-data-keonho
featurePrefix: feature-store/user_features
registryPrefix: feast
feastServer:
image: hoizz/feast-server:0.40.1-s3fs
port: 6566
redis:
image: redis:7.2-alpine
persistence: true
4-2) Feast repo ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: feast-repo
namespace: {{ .Release.Namespace }}
data:
feature_store.yaml: |
project: feature_store_{{ .Values.env }}
registry:
path: s3://{{ .Values.s3.bucket }}/{{ .Values.s3.registryPrefix }}/{{ .Values.env }}/registry.pb
provider: local
offline_store:
type: file
online_store:
type: redis
connection_string: redis.{{ .Release.Namespace }}.svc.cluster.local:6379
entity_key_serialization_version: 2
repo.py: |
from datetime import timedelta
from feast import Entity, FeatureView, Field
from feast.infra.offline_stores.file_source import FileSource
from feast.types import Int64, Float64
user_features_source = FileSource(
path="s3://{{ .Values.s3.bucket }}/{{ .Values.s3.featurePrefix }}/latest/features.parquet",
timestamp_field="event_timestamp",
)
# ... (Entity, FeatureView ์ ์ ์๋ต)
4-3) Feast ์๋ฒ Deployment (subPath + sh ํธํ + startup apply)
apiVersion: apps/v1
kind: Deployment
metadata:
name: feast-server
namespace: {{ .Release.Namespace }}
spec:
replicas: 1
selector:
matchLabels:
app: feast-server
template:
spec:
containers:
- name: feast-server
image: {{ .Values.feastServer.image }}
volumeMounts:
- name: feast-repo
mountPath: /feast-repo/feature_store.yaml
subPath: feature_store.yaml
readOnly: true
- name: feast-repo
mountPath: /feast-repo/repo.py
subPath: repo.py
readOnly: true
# ... (aws-credentials mount ์๋ต)
command: ["/bin/sh","-c"]
args:
- |
set -eux
cd /feast-repo
feast apply
feast serve --host 0.0.0.0 --port 6566
# ... (volumes ์๋ต)
5๏ธโฃ s3fs ํฌํจ Feast ์ด๋ฏธ์ง ์ค๋น (ํ์)
Feast materialize๊ฐ S3 parquet๋ฅผ ์ฝ์ผ๋ ค๋ฉด s3fs๊ฐ ํ์ํฉ๋๋ค.
FROM feastdev/feature-server:0.40.1
RUN pip install --no-cache-dir s3fs fsspec
docker build -t hoizz/feast-server:0.40.1-s3fs .
docker push hoizz/feast-server:0.40.1-s3fs
6๏ธโฃ Airflow: “๋ฒ์ ํ ์ ์ฅ + latest overwrite” (parquet ๊ธฐ์ค)
Feast๋ Offline์ latest/features.parquet๋ก ๊ณ ์ ํด ์ฝ๊ธฐ ๋๋ฌธ์, Airflow๊ฐ latest๋ฅผ ๋งค๋ฒ ๊ฐฑ์ ํด์ค์ผ ํฉ๋๋ค.
(1) DAG: ํ๋ฆ๋ง ๋ด๋น
# dags/dag_data_pipeline_daily_dev_v5.py (์์ฝ)
with DAG(
dag_id="data_pipeline_daily_dev_v5",
default_args=DEFAULT_ARGS,
start_date=datetime(2026, 1, 1),
schedule="0 0 * * *",
catchup=False,
tags=["dp", "feature-store"],
) as dag:
t_build = PythonOperator(task_id="build_features", python_callable=build_features)
t_store = PythonOperator(
task_id="store_features",
python_callable=store_features,
# ... (op_kwargs ์๋ต)
)
t_build >> t_store
(2) build: parquet/csv + event_timestamp ๋ณด์ฅ
- Feast ๊ธฐ์ค: event_timestamp ํ์
- build ๋จ๊ณ์์ Feast ์๊ตฌ์ฌํญ์ ์ถฉ์กฑ์ํค๋ฉด ํ์ดํ๋ผ์ธ์ด ์์ ํด์ง๋๋ค.
(3) store: version + latest ๋์ ์ ์ฅ(4ํ์ผ ์ธํธ)
.../<feature_set>/<version>/์ ์ฅ (์ฌํ์ฑ).../<feature_set>/latest/overwrite (์ด์ ํธ์)- ๋์ผ prefix์ parquet/csv/schema/metadata๋ฅผ ๋ฌถ์ด์ ์ ์ฅ
7๏ธโฃ Feast ์ด์/๊ฒ์ฆ ๋ฃจํด (์ฑ๊ณต ์ฒดํฌ)
7-1) s3fs ํฌํจ ์ฌ๋ถ ํ์ธ
POD=$(kubectl -n feature-store-dev get pod -l app=feast-server \
-o jsonpath='{.items[0].metadata.name}')
kubectl -n feature-store-dev exec -it "$POD" -- sh -lc '
python - << "PY"
import s3fs, fsspec
print("s3fs OK:", s3fs.__version__)
PY
'
7-2) materialize ์คํ (์: ์ต๊ทผ 2์ผ)
kubectl -n feature-store-dev exec -it "$POD" -- sh -lc '
cd /feast-repo
feast materialize \
"$(date -u -d "2 days ago" +%Y-%m-%dT%H:%M:%S)" \
"$(date -u +%Y-%m-%dT%H:%M:%S)"
'
7-3) online ์กฐํ ํ์ธ
kubectl -n feature-store-dev exec -it "$POD" -- sh -lc '
python - << "PY"
from feast import FeatureStore
store = FeatureStore("/feast-repo")
resp = store.get_online_features(
features=["user_features:f_total_events_7d"],
entity_rows=[{"user_id": 1001}],
)
print(resp.to_df())
PY
'
8๏ธโฃ ํธ๋ฌ๋ธ์ํ
- ConfigMap ์ ์ฒด mount๋ก repo๊ฐ ๊ผฌ์ ->
subPath๋ก ํ์ผ๋ง ๋ง์ดํธ - /bin/sh์์ pipefail ๊นจ์ง ->
set -eux๋ก ๊ณ ์ - materialize ์ “Install s3fs” ์ค๋ฅ -> s3fs ํฌํจ ์ปค์คํ ์ด๋ฏธ์ง๋ก ๊ต์ฒด
- image repo ์ด๋ฆ ์คํ๋ก ErrImagePull -> values/base.yaml์ image repo ์ ํํ ๊ต์
์ค๊ณ ํ๋จ (Why This Way?)
์คํค๋งยท๋ฒ์ ํ๊ฐ ์ ํ๋ ์ํ์์ Feast๋ฅผ ์กฐํ ๊ณ์ธต์ผ๋ก๋ง ์ถ๊ฐํ์ฌ ๋์ ๋ฒ์๋ฅผ ์ต์ํํ๊ณ , latest ๊ณ ์ ๊ฒฝ๋ก + version ๋๋ ํ ๋ฆฌ ์ด์ค ๊ตฌ์กฐ๋ก Feast ์ค์ ๋ณ๊ฒฝ ์์ด ์ต์ ํผ์ฒ ์ ๊ทผ๊ณผ ์ฌํ์ฑ์ ๋์์ ํ๋ณดํ์ต๋๋ค. Airflow์ Feast์ ์ญํ ์ ๋ถ๋ฆฌํ์ฌ ์ด๋ฏธ์ง ์์กด์ฑ ์ถฉ๋๊ณผ ์ฅ์ ๋ฒ์๋ฅผ ๊ฒฉ๋ฆฌํ์ต๋๋ค.
๋ค์์ ์ฝ์ ๊ธ
โ MLflow Model Registry: alias ๊ธฐ๋ฐ ๋ชจ๋ธ ๋ฒ์ ๊ด๋ฆฌ โ promotion/shadow alias ์ ๋ต