Mục tiêu của TODO3 là chuyển tầng thực thi compute của AIS sang Kubernetes, trong khi tầng lưu trữ và dữ liệu trạng thái vẫn nằm bên ngoài Kubernetes trong pha này.
Kiến trúc đích của TODO3:
- Tầng dưới chỉ giữ vai trò storage/data infrastructure: Kafka/Zookeeper, HDFS hoặc object storage nếu đã cấu hình, Iceberg warehouse/catalog, Cassandra nếu còn dùng làm serving/storage backend, Airflow metadata database, model artifact storage, checkpoint/warehouse volumes.
- Kubernetes là runtime đích cho compute: Spark driver/executor pods, Spark batch jobs, HYSPLIT/trajectory jobs nếu container hóa được, feature engineering, ML training, ML inference Job/CronJob, PM2.5 API, data quality/check jobs, và các workload monitoring/check phù hợp.
- Airflow vẫn là control plane: định nghĩa dependency, rerun, backfill, schedule policy; Kubernetes chỉ thực thi workload.
- Docker Compose Spark master/worker chỉ còn là fallback local/dev trong quá trình migration, không còn là runtime đích.
Quy tắc request-time bắt buộc:
API request -> đọc prediction mới nhất đã materialize -> JSON response
API không được chạy Spark, HYSPLIT, feature engineering, training, hoặc model inference trong request handling.
TODO3 được đổi từ phạm vi cũ “serving/inference trên K8s” sang “compute workloads trên K8s” vì phạm vi cũ vẫn để Spark batch chạy trên Docker Compose và chưa đặt Spark-on-K8s làm target bắt buộc. Cách đó làm kiến trúc bị lệch tầng: API/inference nằm trên Kubernetes, nhưng feature engineering, training và HYSPLIT vẫn phụ thuộc Spark standalone trong Compose. Kết quả là Airflow, Spark, ML và API có nhiều runtime khác nhau, khó backfill, khó kiểm soát tài nguyên và khó triển khai nhất quán.
Quyết định mới:
- Docker Compose được phép tiếp tục chạy storage/data dependencies trong pha này: Kafka, Zookeeper, Namenode, Datanode, Cassandra, Airflow metadata DB, và các volume warehouse/checkpoint.
- Docker Compose Spark master/worker không còn là runtime đích. Chúng chỉ được giữ làm fallback local/dev để không phá TODO1/TODO2 trong lúc migration.
- Spark-on-Kubernetes là yêu cầu bắt buộc của TODO3, không còn là optional/future item.
- Spark driver và Spark executor phải chạy dưới dạng Kubernetes pods.
- Không migrate Kafka/HDFS/Iceberg/Cassandra/Airflow metadata DB vào Kubernetes trong TODO3.
- Không đổi business logic TODO1/TODO2 ngoài các thay đổi cần thiết để submit workload compute lên Kubernetes.
Tiêu chí chấp nhận:
- TODO3 nhất quán rằng Spark-on-K8s là target runtime bắt buộc.
- Mọi workload compute mới hoặc được migrate đều có đường chạy Kubernetes.
- Tài liệu rollback/dev fallback nêu rõ Compose Spark chỉ là fallback, không phải target runtime.
Inventory sau khi scan repo hiện tại:
File hiện có: docker-compose.yml - modify nếu cần thêm fallback/dev config hoặc tách profile, nhưng không migrate storage vào K8s trong TODO3.
Services hiện có:
zookeeperkafkanamenodedatanodespark-masterspark-workeringestopenaq-ingestmaiac-ingestsentinel5p-ingestmonitoring-uicassandraairflow-postgresairflow-initairflow-webserverairflow-schedulerairflow-triggerer
Volumes hiện có:
namenode_datadatanode_datacassandra_dataairflow_postgres_dataspark_ivy_cache
spark/Dockerfile- modify hoặc dùng làm base choais-spark-runtime.- Docker Compose đang build image
ais-spark:3.5.3. docker-compose.ymlchạyspark-mastervàspark-worker.scripts/submit_spark.sh- modify hoặc tách thêm script/template K8s, vì hiện submit vàospark://spark-master:7077.airflow/dags/ais_dag_utils.py- modify, vì hiện tạo commanddocker exec spark-master /opt/spark/bin/spark-submit --master spark://spark-master:7077.
Thư mục hiện có: spark_jobs/
Các job đã có:
spark_jobs/ensure_iceberg_tables.pyspark_jobs/weather_streaming.pyspark_jobs/openaq_hourly_streaming.pyspark_jobs/sentinel5p_summary_streaming.pyspark_jobs/maiac_summary_streaming.pyspark_jobs/era5_files_streaming.pyspark_jobs/hanoi_openaq_silver.pyspark_jobs/hanoi_weather_surface_proxy_silver.pyspark_jobs/era5_surface_hanoi_silver.pyspark_jobs/sentinel5p_hanoi_silver.pyspark_jobs/maiac_hanoi_silver.pyspark_jobs/era5_pressure_levels_to_arl.pyspark_jobs/hysplit_trajectory_run.pyspark_jobs/hysplit_trajectory_parse_silver.pyspark_jobs/hysplit_trajectory_cluster_silver.pyspark_jobs/openaq_spatial_gradient_silver.pyspark_jobs/sentinel5p_grid_silver.pyspark_jobs/trajectory_path_sampling_silver.pyspark_jobs/trajectory_hourly_features_silver.pyspark_jobs/hanoi_pm25_master_features_gold.pyspark_jobs/hanoi_pm25_training_dataset_gold.pyspark_jobs/iceberg_to_cassandra.pyspark_jobs/reconcile_iceberg_cassandra.pyspark_jobs/iceberg_maintenance.pyspark_jobs/runtime_utils.py
Job cần tạo trong TODO3:
spark_jobs/hanoi_pm25_serving_features_gold.py- createspark_jobs/hanoi_pm25_serving_quality_checks.py- create optional nếu check cần chạy bằng Spark.
ml/train_hanoi_pm25.py- modify để chạy tốt trong Kubernetes Job và xuất artifact URI/registry metadata rõ hơn.- Script hiện có
FEATURE_COLUMNS,TARGETS6/12/24, đọcais.features.hanoi_pm25_training_dataset_gold, ghiais.models.hanoi_pm25_model_runs_gold. - Script hiện ghi model artifact mặc định vào
models/hanoi_pm25hoặc/opt/models/hanoi_pm25.
Files cần tạo:
ml/Dockerfile- create nếu chưa có.ml/predict_hanoi_pm25.py- create vì hiện chưa có inference script.ml/promote_hanoi_pm25_model.py- create optional để promote model run sang production registry.
ml/predict_hanoi_pm25.py- create, hiện chưa có.serving/pm25_api/- create, hiện chưa thấy thư mụcserving/.deploy/k8s/api/- create, vìdeploy/hiện chưa tồn tại.
Thư mục hiện có: airflow/dags/
Files hiện có:
airflow/dags/ais_dag_utils.py- modify để thêm submit K8s/Spark-on-K8s.airflow/dags/ais_pipeline_dag.pyairflow/dags/ais_streaming_supervision_dag.pyairflow/dags/ais_maintenance_dag.pyairflow/dags/ais_era5_ingestion_dag.pyairflow/dags/ais_hanoi_silver_gold_dag.py- modify hoặc giữ làm dev fallback, vì hiện gọiscripts/submit_spark.sh.airflow/dags/ais_trajectory_tier2_dag.py- modify hoặc giữ làm dev fallback, vì hiện gọiscripts/submit_spark.sh.airflow/dags/ais_maiac_backfill_dag.py
File cần tạo cho TODO3:
airflow/dags/ais_pm25_k8s_compute_dag.py- create
config/hanoi_pipeline.yaml- modify để thêm serving, registry, inference, K8s runtime config nếu cần.spark_jobs/hanoi_config.py- modify để thêm table names/config keys cho serving features, prediction, model registry..env.example- modify optional để ghi biến môi trường K8s/storage endpoint mẫu, không chứa secret thật.
deploy/- create, hiện chưa tồn tại.deploy/k8s/- create.infra/- hiện tồn tại nhưng chưa có file runtime rõ ràng; verify existing file/path trước khi dùng làm nơi đặt manifest.monitoring/app.py- modify optional để thêm trạng thái serving feature/prediction/API/K8s job.monitoring/Dockerfile,monitoring/requirements.txt- hiện có.
Hiện Cassandra được dùng cho serving weather/openaq:
spark_jobs/iceberg_to_cassandra.py- hardcodeCASSANDRA_HOST = "cassandra"; modify later nếu đưa job này lên K8s.spark_jobs/reconcile_iceberg_cassandra.py- hardcodeCASSANDRA_HOST = "cassandra"; modify later nếu đưa check này lên K8s.airflow/dags/ais_dag_utils.pytạo Cassandra schema choais_serving.weather_hourly_by_province_dayvàais_serving.openaq_hourly_by_city_parameter_day.
Chưa thấy Cassandra forecast serving table cho PM2.5. TODO3 mặc định dùng Iceberg prediction table làm PM2.5 serving contract; Cassandra chỉ dùng nếu có quyết định riêng.
Đã có:
ais.features.hanoi_pm25_master_hourly_goldais.features.hanoi_pm25_training_dataset_goldais.models.hanoi_pm25_model_runs_gold
Cần tạo:
ais.features.hanoi_pm25_serving_features_goldais.predictions.hanoi_pm25_forecast_goldais.models.hanoi_pm25_model_registry_gold- Namespace
ais.predictions
| Component | Layer | Runtime sau TODO3 | Notes |
|---|---|---|---|
| Kafka | Storage/data infrastructure | Docker Compose hoặc external endpoint | Không migrate vào K8s trong TODO3. Endpoint qua KAFKA_BOOTSTRAP_SERVERS, không hardcode kafka:9092 trong source. |
| Zookeeper | Storage/data infrastructure | Docker Compose hoặc external endpoint | Chỉ phục vụ Kafka hiện tại. Không migrate. |
| HDFS Namenode | Storage/data infrastructure | Docker Compose hoặc external endpoint | Không migrate. K8s pod kết nối qua HDFS_NAMENODE/HDFS_WEBHDFS_BASE. |
| HDFS Datanode | Storage/data infrastructure | Docker Compose hoặc external endpoint | Không migrate. Phải verify pod có thể đọc/ghi đường dẫn warehouse/checkpoint. |
| Iceberg warehouse/catalog | Storage/data infrastructure | External to K8s | Warehouse hiện là Hadoop catalog trên HDFS theo ICEBERG_WAREHOUSE. Catalog config qua env. |
| Cassandra | Storage/serving backend | Docker Compose hoặc external endpoint | Chỉ dùng nếu job cần weather/openaq serving hoặc nếu sau này chọn Cassandra cho PM2.5. Không migrate trong TODO3. |
| Airflow metadata DB | Storage/control-plane state | Docker Compose Postgres | Không migrate. |
| Airflow scheduler/webserver/triggerer | Control plane | Docker Compose trong pha này | Airflow orchestrates, Kubernetes executes compute. Có thể submit K8s workload từ Airflow. |
| Spark driver/executors | Compute | Kubernetes pods | Bắt buộc. Không dùng spark-master/spark-worker làm target runtime. |
| Spark batch jobs | Compute | Spark-on-K8s | Gồm ensure table, silver/gold, trajectory, serving features, checks. |
| HYSPLIT jobs | Compute | Kubernetes pods qua Spark-on-K8s hoặc Kubernetes Job | Nếu chạy trong Spark job thì driver/executor pod phải có binary/dependency. |
| ML training | Compute | Kubernetes Job | ml/train_hanoi_pm25.py chạy trong ais-ml-runtime, artifact lưu ngoài K8s. |
| ML inference | Compute | Kubernetes Job/CronJob | ml/predict_hanoi_pm25.py đọc serving features + production registry, ghi prediction table. |
| PM2.5 API | Compute/serving | Kubernetes Deployment + Service | Chỉ đọc prediction table. Không chạy compute nặng trong request. |
| monitoring/check jobs | Compute/check | Kubernetes Job/CronJob khi phù hợp | Có thể giữ monitoring UI hiện tại; check jobs mới nên có K8s path. |
| model artifact storage | Storage/data infrastructure | External volume/HDFS/object storage | Không bake model vào image; không lưu artifact chỉ trong ephemeral pod. |
Tiêu chí chấp nhận:
- Spark driver/executors hiện diện trong
kubectl get pods. - Không có checkpoint hoàn thành nào yêu cầu Spark standalone Compose làm runtime đích.
- Tài liệu nêu rõ thành phần nào nằm ngoài K8s và vì sao.
Luồng end-to-end sau TODO3:
External sources
-> ingest producers
-> Kafka/HDFS/Iceberg storage layer
-> Spark jobs on Kubernetes
-> Iceberg Bronze/Silver/Gold
-> Hanoi PM2.5 master hourly gold
-> PM2.5 serving features gold
-> ML inference Job/CronJob on Kubernetes
-> PM2.5 prediction table
-> PM2.5 API Deployment on Kubernetes
-> dashboard/user
Chi tiết:
- Ingest producers hiện có có thể tiếp tục chạy bằng Compose trong pha này.
- Kafka/HDFS/Iceberg là tầng dữ liệu bên dưới, không phải workload compute của TODO3.
- Spark jobs chạy trên Kubernetes tạo/refresh Bronze/Silver/Gold và serving feature table.
ais.features.hanoi_pm25_master_hourly_goldlà source chính cho serving features.ais.features.hanoi_pm25_serving_features_goldchỉ chứa feature biết tạibase_hour, không chứa target tương lai.ml/predict_hanoi_pm25.pychạy bằng Kubernetes Job/CronJob, dùng production model registry, ghiais.predictions.hanoi_pm25_forecast_gold.serving/pm25_api/chạy bằng Kubernetes Deployment, đọcais.predictions.hanoi_pm25_forecast_gold.
Luồng request-time:
API request
-> prediction table lookup
-> JSON response
Cấm trong request path:
- Không submit Spark.
- Không chạy HYSPLIT.
- Không build serving features.
- Không train model.
- Không load raw Kafka/HDFS để tự tính feature.
- Không chạy model inference trong handler.
Tiêu chí chấp nhận:
- API có thể trả forecast khi prediction table đã có row production.
- API trả lỗi 404 rõ ràng khi chưa có prediction.
- Không có code path API gọi Spark, HYSPLIT, training hoặc inference runtime.
Manifest layout đích:
deploy/k8s/
namespace.yaml
configmap.yaml
secret.example.yaml
serviceaccount.yaml
rbac.yaml
kustomization.yaml
deploy/k8s/spark/
spark-serviceaccount.yaml
spark-rbac.yaml
spark-submit-template.yaml
spark-application-template.yaml # optional nếu chọn Spark Operator sau này
README.md
deploy/k8s/ml/
pm25-train-job.yaml
pm25-predict-cronjob.yaml
pm25-predict-job.yaml # optional/manual run
README.md
deploy/k8s/api/
pm25-api-deployment.yaml
pm25-api-service.yaml
README.md
deploy/k8s/checks/
pm25-serving-check-job.yaml
README.md
Files:
deploy/k8s/namespace.yaml- createdeploy/k8s/configmap.yaml- createdeploy/k8s/secret.example.yaml- createdeploy/k8s/serviceaccount.yaml- createdeploy/k8s/rbac.yaml- createdeploy/k8s/kustomization.yaml- create
Mục đích:
- Tạo namespace
ais. - Tạo ConfigMap cho endpoint storage, table names, runtime defaults.
- Tạo Secret mẫu không chứa secret thật.
- Tạo ServiceAccount/RBAC tối thiểu cho Job/API đọc ConfigMap/Secret và ghi log.
Env vars bắt buộc:
KAFKA_BOOTSTRAP_SERVERS
HDFS_NAMENODE
HDFS_WEBHDFS_BASE
ICEBERG_CATALOG
ICEBERG_CATALOG_URI
ICEBERG_WAREHOUSE
CASSANDRA_HOST
MODEL_ARTIFACT_BASE_URI
HANOI_PIPELINE_CONFIG
FEATURE_VERSION
FEATURE_SET_NAME
LOCATION_ID
LOCATION_NAME
Secrets:
CDS_KEYnếu K8s chạy ERA5 ingest sau này.OPENAQ_API_KEYnếu K8s chạy ingest sau này.CDSE_USERNAME,CDSE_PASSWORDnếu K8s chạy Sentinel ingest sau này.- Object storage credentials nếu
MODEL_ARTIFACT_BASE_URIlà S3/GCS/Azure. - Không đưa secret thật vào
secret.example.yaml.
Resource defaults:
- CPU/memory requests nhỏ cho API/check pod.
- CPU/memory requests lớn hơn cho Spark driver/executor và ML job.
- Mọi manifest phải có
resources.requestsvàresources.limits, kể cả giá trị dev ban đầu.
Smoke test:
kubectl apply -k deploy/k8s
kubectl get ns ais
kubectl -n ais get configmap,secret,serviceaccountAcceptance criteria:
kubectl apply -k deploy/k8schạy được sau khi file được tạo.- ConfigMap chứa endpoint không hardcode trong source.
- Secret mẫu không chứa giá trị thật.
Files:
deploy/k8s/spark/spark-serviceaccount.yaml- createdeploy/k8s/spark/spark-rbac.yaml- createdeploy/k8s/spark/spark-submit-template.yaml- createdeploy/k8s/spark/spark-application-template.yaml- create optionaldeploy/k8s/spark/README.md- create
Mục đích:
- Cấp quyền cho Spark driver tạo executor pods.
- Chuẩn hóa command
spark-submit --master k8s://.... - Ghi rõ cách truyền Iceberg/HDFS/Kafka config vào driver/executor.
Env vars:
SPARK_K8S_MASTER
SPARK_K8S_NAMESPACE
SPARK_DRIVER_SERVICE_ACCOUNT
SPARK_IMAGE
SPARK_EXECUTOR_INSTANCES
SPARK_EXECUTOR_MEMORY
SPARK_EXECUTOR_CORES
SPARK_DRIVER_MEMORY
ICEBERG_CATALOG
ICEBERG_WAREHOUSE
HDFS_NAMENODE
KAFKA_BOOTSTRAP_SERVERS
Secrets:
- Registry pull secret nếu image không public.
- Artifact/object storage secret nếu Spark job cần đọc/ghi model hoặc data.
Resources:
- Driver dev default: request
500mCPU,1Gimemory; limit1CPU,2Gi. - Executor dev default: request
500mCPU,1Gimemory; limit1CPU,2Gi. - TODO3 phải yêu cầu tune theo job size, không hardcode mãi giá trị dev.
Smoke test:
kubectl -n ais get sa spark
kubectl -n ais auth can-i create pods --as=system:serviceaccount:ais:sparkAcceptance criteria:
- Spark driver pod có quyền tạo executor pods.
- Spark README có lệnh submit tối thiểu.
- Không phụ thuộc
spark-masterCompose.
Files:
deploy/k8s/ml/pm25-train-job.yaml- createdeploy/k8s/ml/pm25-predict-cronjob.yaml- createdeploy/k8s/ml/pm25-predict-job.yaml- create optionaldeploy/k8s/ml/README.md- create
Mục đích:
- Chạy training one-off bằng Kubernetes Job.
- Chạy inference định kỳ bằng Kubernetes CronJob.
- Cho phép manual run với
kubectl create job --from=cronjob/....
Env vars:
DATASET_VERSION
FEATURE_SET_NAME
FEATURE_VERSION
MODEL_TYPE
MODEL_ARTIFACT_BASE_URI
MODEL_RUNS_TABLE
MODEL_REGISTRY_TABLE
SERVING_FEATURE_TABLE
PREDICTION_TABLE
ICEBERG_CATALOG
ICEBERG_WAREHOUSE
HDFS_NAMENODE
LOCATION_ID
Secrets:
- Artifact storage credentials.
- Catalog credentials nếu dùng REST catalog trong tương lai.
Resources:
- Training job cần request/limit cao hơn inference.
- Inference job có
concurrencyPolicy: Forbid,backoffLimitnhỏ, history limits rõ.
Smoke test:
kubectl -n ais apply -f deploy/k8s/ml/pm25-predict-job.yaml
kubectl -n ais logs job/pm25-predictAcceptance criteria:
- Job đọc ConfigMap/Secret.
- Inference dry-run có log đầy đủ.
- CronJob không chạy song song nếu lần trước chưa xong.
Files:
deploy/k8s/api/pm25-api-deployment.yaml- createdeploy/k8s/api/pm25-api-service.yaml- createdeploy/k8s/api/README.md- create
Mục đích:
- Chạy PM2.5 API long-running.
- Expose nội bộ cluster bằng Service.
- Probe health/readiness.
Env vars:
API_PORT
PREDICTION_TABLE
ICEBERG_CATALOG
ICEBERG_WAREHOUSE
LOCATION_ID
READINESS_TIMEOUT_SECONDS
Secrets:
- Catalog/object storage credentials nếu API query qua REST/object store.
Resources:
- Dev default: request
100mCPU,256Mi; limit500mCPU,512Mi. - Readiness/liveness probes không được quá đắt.
Smoke test:
kubectl -n ais port-forward svc/pm25-api 8081:80
curl -i http://localhost:8081/healthz
curl -i http://localhost:8081/readyzAcceptance criteria:
/healthztrả 200 khi process alive./readyztrả 200 khi config/catalog/table sẵn sàng, 503 khi dependency lỗi.- Forecast endpoint chỉ đọc prediction table.
Files:
deploy/k8s/checks/pm25-serving-check-job.yaml- createdeploy/k8s/checks/README.md- create
Mục đích:
- Chạy check freshness/schema/model registry/API readiness bằng Kubernetes Job.
Env vars:
SERVING_FEATURE_TABLE
PREDICTION_TABLE
MODEL_REGISTRY_TABLE
FEATURE_FRESHNESS_MAX_MINUTES
PREDICTION_FRESHNESS_MAX_MINUTES
PM25_API_BASE_URL
Smoke test:
kubectl -n ais apply -f deploy/k8s/checks/pm25-serving-check-job.yaml
kubectl -n ais logs job/pm25-serving-checkAcceptance criteria:
- Job exit code 0 khi mọi check pass.
- Job exit code khác 0 khi missing production model, stale prediction, schema mismatch, hoặc API readiness fail.
Checkpoint này đảm bảo pod Kubernetes kết nối được tới tầng storage/data bên ngoài K8s.
Files cần tạo/sửa:
deploy/k8s/configmap.yaml- createdeploy/k8s/secret.example.yaml- createdeploy/k8s/spark/README.md- createdeploy/k8s/ml/README.md- createdeploy/k8s/api/README.md- createspark_jobs/hanoi_config.py- modify để không phụ thuộc hostname Compose mặc định nếu env đã cung cấp.spark_jobs/iceberg_to_cassandra.py- modify later nếu migrate job này, vì hiện hardcodeCASSANDRA_HOST = "cassandra".spark_jobs/reconcile_iceberg_cassandra.py- modify later nếu chạy trên K8s, vì hiện hardcodeCASSANDRA_HOST = "cassandra".
Config bắt buộc:
KAFKA_BOOTSTRAP_SERVERS
HDFS_NAMENODE
HDFS_WEBHDFS_BASE
ICEBERG_CATALOG
ICEBERG_CATALOG_URI
ICEBERG_WAREHOUSE
CASSANDRA_HOST
MODEL_ARTIFACT_BASE_URI
AIRFLOW_K8S_NAMESPACE
AIRFLOW_K8S_SERVICE_ACCOUNT
Quy tắc:
- Không hardcode
kafka,namenode,spark-master,cassandratrực tiếp trong source code mới. - Các giá trị default local có thể còn trong fallback, nhưng production/K8s path phải override qua env/ConfigMap.
- Nếu dùng Docker Desktop/local K8s, phải document cách pod tới Compose services:
host.docker.internalnếu Docker Desktop hỗ trợ.- Host IP cụ thể nếu dùng Linux/minikube/kind.
- NodePort hoặc port publish từ Compose.
- Shared Docker network nếu cluster local cho phép.
- Không ghi endpoint environment-specific vào image.
Debug pod checklist:
kubectl -n ais run debug-net --rm -it --image=busybox:1.36 -- shTrong debug pod, verify:
- DNS/connectivity Kafka:
nc -vz "$KAFKA_HOST" "$KAFKA_PORT"- WebHDFS:
wget -qO- "$HDFS_WEBHDFS_BASE/?op=LISTSTATUS"- Iceberg catalog/warehouse:
# Nếu dùng Hadoop catalog: chạy bằng Spark smoke job để verify warehouse.
# Nếu dùng REST catalog: curl ICEBERG_CATALOG_URI.- Read/write test table nếu có thể:
kubectl -n ais logs <spark-driver-pod>Acceptance criteria:
- Một Kubernetes pod đọc được config từ ConfigMap/Secret.
- Một Kubernetes pod kết nối được Kafka endpoint cần dùng.
- Một Kubernetes pod truy cập được HDFS/WebHDFS hoặc warehouse endpoint.
- Spark-on-K8s job đọc/ghi được một Iceberg table nhỏ hoặc chạy read check.
- Không có source code mới hardcode hostname theo Docker Compose.
Images bắt buộc:
ais-spark-runtimeais-ml-runtimeais-pm25-apiais-checks-runtimeoptional nếu checks tách riêng khỏi ML/API image.
Quy tắc chung:
- Không bake secrets vào image.
- Image tag có convention:
ais-spark-runtime:<git-sha|date|semver>,ais-ml-runtime:<git-sha|date|semver>,ais-pm25-api:<git-sha|date|semver>. - Config mount/read qua env hoặc ConfigMap.
- Image local dev và image K8s dùng cùng command contract.
Dockerfile path:
spark/Dockerfile- modify hoặc giữ làm base.
Build context:
.hoặc./sparktùy quyết định implementation.- Nếu cần copy
spark_jobs/,config/,ml/vào image cho K8s, document rõ thay đổi so với Compose volume mount hiện tại.
Entrypoint/command:
- Spark image phải dùng được cho driver và executor pods.
- Job file được submit qua
local:///opt/spark-jobs/<job>.pyhoặc image path tương đương.
Dependencies cần có:
- Spark runtime.
- Iceberg dependencies.
- Python dependencies trong
spark/Dockerfilehiện có: PyYAML, xarray, netCDF4, h5netcdf, h5py, pyhdf, rasterio, GDAL, pyproj, shapely, numpy, pandas, pyarrow, scikit-learn, lightgbm, optional xgboost. - HYSPLIT binary nếu trajectory jobs cần chạy trong Spark pod.
spark_jobs/vàconfig/phải truy cập được trong driver/executor.- ML dependencies chỉ cần trong Spark image nếu Spark jobs thật sự import/dùng chúng.
Mounted config:
config/hanoi_pipeline.yamlqua ConfigMap hoặc copy versioned vào image và override bằng env.
Local smoke test:
docker build -t ais-spark-runtime:local -f spark/Dockerfile .
docker run --rm ais-spark-runtime:local /opt/spark/bin/spark-submit --versionK8s smoke test:
kubectl -n ais run spark-image-smoke --rm -it --image=ais-spark-runtime:local -- /opt/spark/bin/spark-submit --versionAcceptance criteria:
- Image chạy được driver/executor pods.
- HYSPLIT jobs không fail vì thiếu binary nếu chúng nằm trong phase migration.
- Image không phụ thuộc bind mount Compose.
Dockerfile path:
ml/Dockerfile- create
Build context:
- Repo root để image truy cập
ml/,spark_jobs/hanoi_config.py,config/, và schema shared nếu cần.
Entrypoint/command:
python ml/train_hanoi_pm25.py
python ml/predict_hanoi_pm25.py
python ml/promote_hanoi_pm25_model.py
Dependencies:
- Python.
- pandas, numpy, pyarrow.
- lightgbm/xgboost đúng với
ml/train_hanoi_pm25.py. - Iceberg/table client hoặc PySpark nếu inference/training cần Spark để đọc/ghi Iceberg.
- Shared config loader.
Mounted config:
config/hanoi_pipeline.yaml.- ConfigMap/Secret cho endpoints/artifact storage.
Local smoke test:
docker build -t ais-ml-runtime:local -f ml/Dockerfile .
docker run --rm --env-file .env ais-ml-runtime:local python ml/train_hanoi_pm25.py --helpK8s smoke test:
kubectl -n ais run ml-help --rm -it --image=ais-ml-runtime:local -- python ml/predict_hanoi_pm25.py --helpAcceptance criteria:
- Training và inference command tồn tại.
- Image đọc được config.
- Artifact path không nằm trong ephemeral-only path nếu dùng production.
Dockerfile path:
serving/pm25_api/Dockerfile- create
Build context:
- Repo root hoặc
serving/pm25_apinếu package shared được copy đúng.
Entrypoint/command:
uvicorn main:app --host 0.0.0.0 --port ${API_PORT:-8080}
Dependencies:
- FastAPI/uvicorn hoặc framework được chọn.
- Iceberg/table query dependency.
- Config loader.
- Health/readiness support.
Mounted config:
- Table name, catalog/warehouse, location ID, readiness timeout.
Local smoke test:
docker build -t ais-pm25-api:local -f serving/pm25_api/Dockerfile .
docker run --rm -p 8081:8080 --env-file .env ais-pm25-api:local
curl -i http://localhost:8081/healthzK8s smoke test:
kubectl -n ais port-forward svc/pm25-api 8081:80
curl -i http://localhost:8081/healthz
curl -i http://localhost:8081/readyzAcceptance criteria:
- API image không chứa model artifact hoặc secret.
- API chỉ đọc prediction table.
Dockerfile path:
checks/Dockerfile- create optional, hoặc dùngais-ml-runtime.
Mục đích:
- Chạy
scripts/check_pm25_serving.pyvà các checks phụ thuộc Iceberg/API.
Acceptance criteria:
- Check image có exit code rõ.
- Có thể chạy trong K8s Job.
Mode chính của TODO3: Option A - spark-submit --master k8s://....
Lý do: repo hiện chưa có Spark Operator manifests/CRD. Chọn spark-submit --master k8s://... giúp migration ban đầu ít phụ thuộc hơn. Spark Operator có thể là enhancement sau, nhưng runtime Spark trên K8s vẫn bắt buộc.
Files cần tạo/sửa:
deploy/k8s/spark/spark-serviceaccount.yaml- createdeploy/k8s/spark/spark-rbac.yaml- createdeploy/k8s/spark/spark-submit-template.yaml- createdeploy/k8s/spark/README.md- createscripts/submit_spark.sh- modify để có mode K8s hoặc giữ Compose fallback và trỏ sang script mới.scripts/submit_spark_k8s.sh- create optional nếu tách khỏi script hiện tại sạch hơn.airflow/dags/ais_dag_utils.py- modify để Airflow submit Spark-on-K8s.
Spark configs bắt buộc:
spark.master=k8s://...
spark.kubernetes.container.image=ais-spark-runtime:<tag>
spark.kubernetes.namespace=ais
spark.kubernetes.authenticate.driver.serviceAccountName=spark
spark.executor.instances=<env>
spark.executor.memory=<env>
spark.executor.cores=<env>
spark.driver.memory=<env>
spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions
spark.sql.catalog.<catalog>=org.apache.iceberg.spark.SparkCatalog
spark.sql.catalog.<catalog>.type=hadoop
spark.sql.catalog.<catalog>.warehouse=<ICEBERG_WAREHOUSE>
spark.hadoop.fs.defaultFS=<HDFS_NAMENODE>
Generic submit template:
/opt/spark/bin/spark-submit \
--master "${SPARK_K8S_MASTER}" \
--deploy-mode cluster \
--name "${APP_NAME}" \
--conf "spark.kubernetes.namespace=${SPARK_K8S_NAMESPACE:-ais}" \
--conf "spark.kubernetes.container.image=${SPARK_IMAGE}" \
--conf "spark.kubernetes.authenticate.driver.serviceAccountName=${SPARK_DRIVER_SERVICE_ACCOUNT:-spark}" \
--conf "spark.executor.instances=${SPARK_EXECUTOR_INSTANCES:-2}" \
--conf "spark.executor.memory=${SPARK_EXECUTOR_MEMORY:-2g}" \
--conf "spark.executor.cores=${SPARK_EXECUTOR_CORES:-1}" \
--conf "spark.driver.memory=${SPARK_DRIVER_MEMORY:-1g}" \
--conf "spark.hadoop.fs.defaultFS=${HDFS_NAMENODE}" \
--conf "spark.sql.catalog.${ICEBERG_CATALOG}=org.apache.iceberg.spark.SparkCatalog" \
--conf "spark.sql.catalog.${ICEBERG_CATALOG}.type=hadoop" \
--conf "spark.sql.catalog.${ICEBERG_CATALOG}.warehouse=${ICEBERG_WAREHOUSE}" \
local:///opt/spark-jobs/<job>.py <args>Minimal Spark smoke job:
- Có thể dùng
spark_jobs/ensure_iceberg_tables.pyở dry/minimal mode nếu thêm được, hoặc tạospark_jobs/spark_k8s_smoke.py- create optional. - Job phải:
- Start driver pod.
- Start executor pod.
- Đọc config từ env/ConfigMap.
- Chạy simple Spark action.
- Đọc hoặc ghi một Iceberg table test nhỏ nếu storage sẵn sàng.
Validation commands:
kubectl -n ais get pods
kubectl -n ais logs <spark-driver-pod>
kubectl -n ais describe pod <spark-driver-pod>
kubectl -n ais get events --sort-by=.lastTimestampTable validation:
kubectl -n ais logs <spark-driver-pod> | grep -E "Ensured|input_count|output_count|status=success"Acceptance criteria:
- Spark job chạy trên K8s không cần
spark-master/spark-workerCompose. - Driver/executor pods thấy được bằng
kubectl get pods. - Job truy cập được external storage layer.
- Job logs truy cập được bằng
kubectl logs. - Job exit cleanly.
- Nếu Spark Operator chưa dùng, TODO3 document rõ Operator là optional future enhancement.
Tất cả job dưới đây phải có đường chạy K8s. Migration có thể theo từng đợt, nhưng không checkpoint nào được coi là complete nếu chỉ có Compose Spark path.
Template chung:
current: bash scripts/submit_spark.sh <job-type>
target: scripts/submit_spark_k8s.sh <job-type>
runtime: Spark driver/executor pods trong namespace ais
| Job category | Existing file(s) | Current command/path | Target K8s submit | Input | Output | Idempotency rule | Phase |
|---|---|---|---|---|---|---|---|
| Ensure Iceberg | spark_jobs/ensure_iceberg_tables.py |
bash scripts/submit_spark.sh ensure-iceberg |
scripts/submit_spark_k8s.sh ensure-iceberg |
Catalog/warehouse config | Namespaces/tables | CREATE IF NOT EXISTS, ALTER ADD COLUMN safe |
migrate first |
| Bronze streaming/batch metadata | weather_streaming.py, openaq_hourly_streaming.py, sentinel5p_summary_streaming.py, maiac_summary_streaming.py, era5_files_streaming.py |
`scripts/submit_spark.sh weather | openaq | sentinel5p | maiac | era5-files` | K8s Spark submit with checkpoint config |
| Tier 1 silver | hanoi_openaq_silver.py, hanoi_weather_surface_proxy_silver.py, era5_surface_hanoi_silver.py, sentinel5p_hanoi_silver.py, maiac_hanoi_silver.py |
scripts/submit_spark.sh hanoi-openaq-silver, etc. |
K8s Spark submit | Bronze/raw | Silver tables | MERGE/overwrite partitions as existing | migrate in TODO3 |
| Tier 2 trajectory/HYSPLIT | era5_pressure_levels_to_arl.py, hysplit_trajectory_run.py, hysplit_trajectory_parse_silver.py, hysplit_trajectory_cluster_silver.py, openaq_spatial_gradient_silver.py, sentinel5p_grid_silver.py, trajectory_path_sampling_silver.py, trajectory_hourly_features_silver.py |
`scripts/submit_spark.sh era5-pressure-arl | hysplit-run | ...` | K8s Spark submit or Kubernetes Job for non-Spark binary step if needed | ERA5/ARL/OpenAQ/S5P | Trajectory/feature silver |
| Hanoi PM2.5 master gold | hanoi_pm25_master_features_gold.py |
scripts/submit_spark.sh hanoi-master-features-gold |
K8s Spark submit | Silver + trajectory features | ais.features.hanoi_pm25_master_hourly_gold |
MERGE by hour/partition |
migrate in TODO3 |
| Training dataset gold | hanoi_pm25_training_dataset_gold.py |
scripts/submit_spark.sh hanoi-training-dataset-gold |
K8s Spark submit | Master gold | ais.features.hanoi_pm25_training_dataset_gold |
MERGE by hour + dataset_version + feature_set_name |
migrate in TODO3 |
| PM2.5 serving features | spark_jobs/hanoi_pm25_serving_features_gold.py |
create | K8s Spark submit | Master gold | ais.features.hanoi_pm25_serving_features_gold |
MERGE by base_hour + location_id + feature_version |
create in TODO3 |
| Iceberg maintenance | spark_jobs/iceberg_maintenance.py |
scripts/submit_spark.sh maintenance-iceberg |
K8s Spark submit | Iceberg tables | optimized tables | Procedure idempotent | migrate or document gap |
| Cassandra serving/reconcile | iceberg_to_cassandra.py, reconcile_iceberg_cassandra.py |
`scripts/submit_spark.sh cassandra-weather | cassandra-openaq | reconcile-serving` | K8s Spark submit after removing hardcoded cassandra |
Iceberg/Cassandra | Cassandra/check logs |
Per-job migration checklist:
- Current command/path documented.
- K8s submit command documented.
- Input table/path documented.
- Output table/path documented.
- Required config/env listed.
- Expected logs listed.
- Smoke test listed.
- Idempotency key or rerun behavior documented.
Acceptance criteria:
- Existing Spark jobs have K8s execution plan.
- TODO3 identifies migrated vs deferred jobs.
- No TODO3 checkpoint depends on Spark standalone Compose as target runtime.
- Deferred job must have explicit reason, such as missing HYSPLIT binary in image or missing storage connectivity; không được defer chỉ vì muốn giữ Spark standalone làm target.
Files cần sửa:
spark_jobs/hanoi_config.py- modifyspark_jobs/ensure_iceberg_tables.py- modifyconfig/hanoi_pipeline.yaml- modify optional để thêmserving,prediction,model_registryconfig.
Table names cần thêm vào spark_jobs/hanoi_config.py:
TABLES.update({
"serving_features_gold": f"{ICEBERG_CATALOG}.features.hanoi_pm25_serving_features_gold",
"prediction_gold": f"{ICEBERG_CATALOG}.predictions.hanoi_pm25_forecast_gold",
"model_registry_gold": f"{ICEBERG_CATALOG}.models.hanoi_pm25_model_registry_gold",
})Namespace cần thêm:
ais.predictionsais.modelsđã có nhưng phải verify registry table.
Giữ lại:
ais.models.hanoi_pm25_model_runs_goldlà run history.
Thêm:
ais.models.hanoi_pm25_model_registry_goldlà production pointer, không thay thế run history.
Purpose:
- Materialized serving features cho inference, derived từ master gold.
Source table:
ais.features.hanoi_pm25_master_hourly_gold
Target table:
ais.features.hanoi_pm25_serving_features_gold
Owner files:
spark_jobs/hanoi_config.py- table name.spark_jobs/ensure_iceberg_tables.py- schema.spark_jobs/hanoi_pm25_serving_features_gold.py- writer job.
Schema yêu cầu:
base_hour TIMESTAMP,
location_id STRING,
location_name STRING,
feature_version STRING,
feature_set_name STRING,
dataset_version STRING,
schema_hash STRING,
pm25_median DOUBLE,
pm25_mean DOUBLE,
station_count INT,
coverage_avg DOUBLE,
vis_km DOUBLE,
uv DOUBLE,
condition_code INT,
is_day INT,
will_it_rain INT,
chance_of_rain INT,
wind_u10 DOUBLE,
wind_v10 DOUBLE,
wind_speed DOUBLE,
wind_dir DOUBLE,
pbl_height_m DOUBLE,
low_pbl BOOLEAN,
surface_pressure DOUBLE,
temperature_2m_c DOUBLE,
dewpoint_2m_c DOUBLE,
total_precipitation_mm DOUBLE,
s5p_no2_mean DOUBLE,
s5p_co_mean DOUBLE,
s5p_so2_mean DOUBLE,
s5p_o3_mean DOUBLE,
s5p_aer_ai_mean DOUBLE,
s5p_no2_valid_pct DOUBLE,
s5p_aer_ai_valid_pct DOUBLE,
aod_047_mean DOUBLE,
aod_055_mean DOUBLE,
aod_mean DOUBLE,
aod_max DOUBLE,
aod_valid_pct DOUBLE,
pm25_grad_n DOUBLE,
pm25_grad_s DOUBLE,
pm25_grad_e DOUBLE,
pm25_grad_w DOUBLE,
pm25_spatial_std DOUBLE,
pm25_grad_mag DOUBLE,
dominant_cluster INT,
n_traj INT,
traj_source_lat DOUBLE,
traj_source_lon DOUBLE,
traj_path_no2_mean DOUBLE,
traj_path_aer_mean DOUBLE,
traj_path_no2_aer_ratio DOUBLE,
hour_of_day INT,
day_of_week INT,
month INT,
season STRING,
is_weekend BOOLEAN,
hour_sin DOUBLE,
hour_cos DOUBLE,
dow_sin DOUBLE,
dow_cos DOUBLE,
month_sin DOUBLE,
month_cos DOUBLE,
is_rush_hour BOOLEAN,
pm25_lag_1h DOUBLE,
pm25_lag_3h DOUBLE,
pm25_lag_6h DOUBLE,
pm25_lag_12h DOUBLE,
pm25_lag_24h DOUBLE,
pm25_roll_mean_3h DOUBLE,
pm25_roll_mean_6h DOUBLE,
pm25_roll_mean_24h DOUBLE,
pm25_roll_max_24h DOUBLE,
pm25_roll_std_24h DOUBLE,
year INT,
month_partition INT,
created_at TIMESTAMP,
spark_processed_at TIMESTAMPPartitioning:
PARTITIONED BY (year, month_partition)Merge/idempotency key:
base_hour + location_id + feature_version
Validation rules:
- Drop target/leakage columns:
pm25_next_6hpm25_next_12hpm25_next_24h
- Serving feature schema phải match
ml/train_hanoi_pm25.py::FEATURE_COLUMNSsau khi tính preprocessing rules, ví dụseasoncó thể được one-hot khi train. base_hourphải map từmaster_gold.hour.location_iddefaulthanoi.schema_hashphải stable theo ordered feature list và preprocessing metadata.
Expected logs:
input_count
output_count
min_base_hour
max_base_hour
feature_version
feature_set_name
schema_hash
null_ratio_by_feature
status
Acceptance criteria:
- Table được tạo bằng
ensure_iceberg_tables.pychạy trên K8s. - Job ghi được data bằng Spark-on-K8s.
- Không có target columns trong output.
- Rerun cùng range không duplicate.
Purpose:
- Canonical PM2.5 forecast output cho API.
Source table:
ais.features.hanoi_pm25_serving_features_goldais.models.hanoi_pm25_model_registry_gold
Target table:
ais.predictions.hanoi_pm25_forecast_gold
Owner files:
spark_jobs/hanoi_config.pyspark_jobs/ensure_iceberg_tables.pyml/predict_hanoi_pm25.pyserving/pm25_api/
Schema yêu cầu:
prediction_id STRING,
base_hour TIMESTAMP,
location_id STRING,
location_name STRING,
pm25_now DOUBLE,
pm25_6h DOUBLE,
risk_6h STRING,
pm25_12h DOUBLE,
risk_12h STRING,
pm25_24h DOUBLE,
risk_24h STRING,
dominant_cluster INT,
source_lat DOUBLE,
source_lon DOUBLE,
path_no2_mean DOUBLE,
path_aer_mean DOUBLE,
pm25_grad_mag DOUBLE,
model_version STRING,
model_version_6h STRING,
model_version_12h STRING,
model_version_24h STRING,
model_status STRING,
feature_version STRING,
feature_schema_hash STRING,
inference_run_id STRING,
created_at TIMESTAMP,
year INT,
month_partition INTPartitioning:
PARTITIONED BY (year, month_partition)Merge/idempotency key:
base_hour + location_id + model_version + feature_version
prediction_id:
sha256(base_hour, location_id, model_version, feature_version)
Validation rules:
pm25_6h,pm25_12h,pm25_24hkhông null khi status success.risk_*thuộclow|medium|high|very_high.model_status = 'production'cho rows API đọc.- API chỉ đọc table này cho forecast response.
Expected logs:
input_count
output_count
base_hour
location_id
model_version_6h
model_version_12h
model_version_24h
feature_version
feature_schema_hash
dry_run
status
Acceptance criteria:
- Inference dry-run không ghi table.
- Inference
--dry-run 0ghi hoặc merge đúng row. - Rerun cùng key không tạo duplicate.
- API forecast endpoint không query source/raw/master/serving feature tables.
Purpose:
- Production model pointer theo
location_id,horizon_hour,feature_version,model_version.
Source table:
ais.models.hanoi_pm25_model_runs_gold
Target table:
ais.models.hanoi_pm25_model_registry_gold
Owner files:
spark_jobs/hanoi_config.pyspark_jobs/ensure_iceberg_tables.pyml/promote_hanoi_pm25_model.pyoptionalml/predict_hanoi_pm25.py
Schema yêu cầu:
model_version STRING,
model_run_id STRING,
horizon_hour INT,
location_id STRING,
model_type STRING,
model_path STRING,
artifact_uri STRING,
feature_set_name STRING,
feature_version STRING,
training_dataset_version STRING,
feature_schema_hash STRING,
status STRING,
mae DOUBLE,
rmse DOUBLE,
mape DOUBLE,
promoted_at TIMESTAMP,
promoted_by STRING,
created_at TIMESTAMP,
effective_from TIMESTAMP,
effective_to TIMESTAMPPartitioning:
PARTITIONED BY (status, horizon_hour)Merge/idempotency key:
model_version + horizon_hour + location_id
Validation rules:
- Có đúng một production model cho mỗi
location_id + horizon_hour, trừ khi dùng effective-time versioning rõ ràng. - Inference fail fast nếu thiếu production model cho 6h/12h/24h.
- Inference không được tự lấy latest model run nếu chưa được promote.
feature_schema_hashcủa registry phải match serving feature row.
Expected logs:
promotion_action
model_run_id
model_version
horizon_hour
location_id
old_status
new_status
promoted_by
status
Acceptance criteria:
- Query registry trả đủ 3 production models cho horizon 6/12/24.
- Rollback/demotion có command rõ.
- Registry không xóa run history.
File cần tạo:
spark_jobs/hanoi_pm25_serving_features_gold.py- create
Mục đích:
- Đọc
ais.features.hanoi_pm25_master_hourly_gold. - Chọn feature biết tại
base_hour. - Bỏ target/leakage columns.
- Ghi
ais.features.hanoi_pm25_serving_features_gold. - Chạy bằng Spark-on-K8s, không dùng Spark Compose làm target.
CLI args:
--start-date YYYY-MM-DD
--end-date YYYY-MM-DD
--full-refresh 0|1
--feature-version hanoi_pm25_core_v1
--feature-set-name hanoi_pm25_core_v1
--dataset-version <optional>
--location-id hanoi
--location-name Hanoi
--dry-run 0|1
Env vars:
ICEBERG_CATALOG
ICEBERG_WAREHOUSE
HDFS_NAMENODE
SOURCE_TABLE
TARGET_TABLE
FEATURE_VERSION
FEATURE_SET_NAME
DATASET_VERSION
LOCATION_ID
LOCATION_NAME
Input/output:
- Input:
ais.features.hanoi_pm25_master_hourly_gold - Output:
ais.features.hanoi_pm25_serving_features_gold
Logic bắt buộc:
base_hour = hour.- Select only
ml/train_hanoi_pm25.py::FEATURE_COLUMNScộng metadata serving. - Drop:
pm25_next_6hpm25_next_12hpm25_next_24h
- Add:
feature_versionfeature_set_nameschema_hashlocation_idlocation_namecreated_at
- Validate schema against
FEATURE_COLUMNS. - Idempotent by
base_hour + location_id + feature_version.
Dry-run/check mode:
--dry-run 1chỉ validate schema/count/null ratio, không ghi table.
K8s submit command:
scripts/submit_spark_k8s.sh hanoi-serving-features-gold \
--start-date 2026-05-01 \
--end-date 2026-05-02 \
--feature-version hanoi_pm25_core_v1 \
--dry-run 1Airflow integration point:
- Task trong
airflow/dags/ais_pm25_k8s_compute_dag.pysau upstream Tier 2/master gold và trước inference.
Expected logs:
job=hanoi_pm25_serving_features_gold
input_count
output_count
min_base_hour
max_base_hour
feature_version
schema_hash
dry_run
status
Acceptance criteria:
- Job chạy Spark-on-K8s và tạo driver/executor pods.
- Output không có target leakage.
- Schema hash stable và được dùng bởi inference.
- Rerun không duplicate.
Files cần tạo/sửa:
spark_jobs/ensure_iceberg_tables.py- modifyspark_jobs/hanoi_config.py- modifyml/promote_hanoi_pm25_model.py- create optional nhưng khuyến nghịml/train_hanoi_pm25.py- modify để artifact URI/run metadata đủ cho promotion nếu thiếu.
Registry:
- Table:
ais.models.hanoi_pm25_model_registry_gold - Source run history:
ais.models.hanoi_pm25_model_runs_gold
Production pointer dimensions:
location_id
horizon_hour
feature_version
model_version
Rules:
- Có đúng một production model cho mỗi
location_id + horizon_hour, trừ khi cóeffective_from/effective_to. - Có production model cho cả 6h, 12h, 24h trước khi inference production chạy.
- Inference fail nếu thiếu production model cho bất kỳ horizon nào.
- Promotion là explicit action, không lấy latest run tự động.
Artifact URI convention:
MODEL_ARTIFACT_BASE_URI/hanoi_pm25/{feature_version}/{model_version}/horizon={horizon_hour}/model.<ext>
MODEL_ARTIFACT_BASE_URI/hanoi_pm25/{feature_version}/{model_version}/horizon={horizon_hour}/feature_importance.csv
Promotion command:
python ml/promote_hanoi_pm25_model.py \
--model-run-id <run_id> \
--location-id hanoi \
--horizon-hour 6 \
--feature-version hanoi_pm25_core_v1 \
--status production \
--promoted-by <user>Rollback/demotion behavior:
- Demote current production to
archivedhoặc seteffective_to. - Promote previous model version explicitly.
- Log old/new production pointer.
Acceptance criteria:
- Registry table tồn tại.
- Promotion idempotent.
- Query registry trả đúng production model cho 6/12/24.
- Inference không chạy thành công nếu registry thiếu horizon.
Files cần tạo/sửa:
ml/train_hanoi_pm25.py- modifyml/Dockerfile- createdeploy/k8s/ml/pm25-train-job.yaml- createdeploy/k8s/ml/README.md- create
Runtime:
- Kubernetes Job dùng image
ais-ml-runtime. - Có thể chạy thủ công hoặc Airflow-triggered.
- Không ghi production registry trực tiếp, trừ khi có flag promote rõ và được document. Default chỉ ghi run metadata.
Command:
python ml/train_hanoi_pm25.py \
--dataset-version hanoi_pm25_v1 \
--feature-set-name hanoi_pm25_core_v1 \
--model-type lightgbm \
--output-dir "${MODEL_ARTIFACT_BASE_URI}/hanoi_pm25"Args/env:
DATASET_VERSION
FEATURE_SET_NAME
MODEL_TYPE
MODEL_OUTPUT_DIR
MODEL_ARTIFACT_BASE_URI
ICEBERG_CATALOG
ICEBERG_WAREHOUSE
HDFS_NAMENODE
TRAINING_TABLE
MODEL_RUNS_TABLE
Required resources:
- CPU/memory lớn hơn inference; giá trị dev có thể bắt đầu
1 CPU / 2Gi. - Nếu dùng XGBoost/LightGBM GPU sau này phải có node selector/toleration riêng; không yêu cầu trong TODO3.
Output artifacts:
- Model file cho horizon 6/12/24.
- Feature importance.
- Row metadata trong
ais.models.hanoi_pm25_model_runs_gold.
Logs:
train_metrics
validation_metrics
test_metrics
horizon
mae
rmse
mape
model_path
model_run_id
status
Manual K8s run:
kubectl -n ais apply -f deploy/k8s/ml/pm25-train-job.yaml
kubectl -n ais logs job/pm25-trainAirflow-triggered:
airflow/dags/ais_pm25_k8s_compute_dag.pycó task optionaltrain_hanoi_pm25.
Acceptance criteria:
- Training Job chạy trên K8s.
- Đọc training dataset table.
- Ghi artifact vào external artifact storage.
- Ghi run metadata vào model runs table.
- Không tự promote production trừ khi task promotion explicit.
Files cần tạo:
ml/predict_hanoi_pm25.py- createdeploy/k8s/ml/pm25-predict-cronjob.yaml- createdeploy/k8s/ml/pm25-predict-job.yaml- create optional
Mục đích:
- Đọc latest hoặc specified
base_hourtừ serving features. - Đọc production models từ registry.
- Validate schema hash.
- Predict 6h/12h/24h.
- Tính risk levels.
- Ghi prediction table.
CLI args:
--base-hour <ISO8601 optional>
--location hanoi
--model-status production
--feature-version hanoi_pm25_core_v1
--dry-run 0|1
--max-feature-age-minutes 180
Env vars:
ICEBERG_CATALOG
ICEBERG_WAREHOUSE
HDFS_NAMENODE
SERVING_FEATURE_TABLE
PREDICTION_TABLE
MODEL_REGISTRY_TABLE
MODEL_ARTIFACT_BASE_URI
LOCATION_ID
FEATURE_VERSION
Rules:
- Nếu
--base-hourrỗng, dùng latestbase_hourtrong serving features. - Load production model từ
ais.models.hanoi_pm25_model_registry_gold, không từ latest run. - Fail fast nếu thiếu production model cho 6/12/24.
- Fail fast nếu
feature_schema_hashmismatch. - Idempotent by
base_hour + location_id + model_version + feature_version. --dry-run 1không ghi table.
Risk levels:
low: PM2.5 < 35
medium: 35 <= PM2.5 < 75
high: 75 <= PM2.5 < 150
very_high: PM2.5 >= 150
Logs bắt buộc:
input_count
output_count
model_version_6h
model_version_12h
model_version_24h
feature_version
base_hour
location_id
dry_run
status
CronJob config:
- Schedule dev default: hourly, ví dụ
15 * * * *, nhưng Airflow vẫn là dependency/backfill control plane. concurrencyPolicy: ForbidbackoffLimit: 1hoặc2successfulJobsHistoryLimit: 3failedJobsHistoryLimit: 5restartPolicy: Never
Manual run command:
kubectl -n ais create job pm25-predict-manual-$(date +%Y%m%d%H%M%S) --from=cronjob/pm25-predict
kubectl -n ais logs job/<job-name>Airflow integration:
- Airflow có thể trigger Kubernetes Job từ CronJob hoặc chạy KubernetesPodOperator.
- CronJob không thay thế Airflow dependency/rerun/backfill.
Acceptance criteria:
- Inference dry-run succeed khi registry + features hợp lệ.
--dry-run 0ghi prediction.- Rerun không duplicate.
- Missing model/schema mismatch làm job fail rõ.
- Logs đủ field bắt buộc.
Files cần tạo:
serving/pm25_api/- createserving/pm25_api/main.py- createserving/pm25_api/requirements.txt- createserving/pm25_api/Dockerfile- createserving/pm25_api/README.md- createdeploy/k8s/api/pm25-api-deployment.yaml- createdeploy/k8s/api/pm25-api-service.yaml- create
Required endpoints:
GET /healthzGET /readyzGET /api/v1/hanoi/pm25/forecast/latest
API rules:
- API reads only
ais.predictions.hanoi_pm25_forecast_gold. - API returns latest row for
location_id='hanoi'andmodel_status='production'. - API must not run Spark/model prediction in request.
/healthzonly checks process alive./readyzchecks required config and prediction table/catalog connectivity.- Logs request path, status_code, latency_ms, error_code.
404 response when no prediction:
{
"error": "prediction_not_found",
"location": "hanoi"
}Forecast response contract:
{
"base_hour": "2026-05-19T09:00:00Z",
"location": "hanoi",
"pm25_now": 0.0,
"forecast": {
"6h": {"pm25": 0.0, "risk": "low"},
"12h": {"pm25": 0.0, "risk": "medium"},
"24h": {"pm25": 0.0, "risk": "high"}
},
"source_attribution": {
"dominant_cluster": 0,
"source_lat": 0.0,
"source_lon": 0.0,
"path_no2_mean": 0.0,
"path_aer_mean": 0.0,
"pm25_grad_mag": 0.0
},
"model": {
"model_version_6h": "example",
"model_version_12h": "example",
"model_version_24h": "example",
"feature_version": "hanoi_pm25_core_v1"
},
"created_at": "2026-05-19T09:05:00Z"
}Config:
API_PORT
PREDICTION_TABLE
ICEBERG_CATALOG
ICEBERG_WAREHOUSE
LOCATION_ID
READY_TIMEOUT_SECONDS
Docker build/run:
docker build -t ais-pm25-api:local -f serving/pm25_api/Dockerfile .
docker run --rm -p 8081:8080 --env-file .env ais-pm25-api:localK8s deployment/service:
kubectl -n ais apply -f deploy/k8s/api/pm25-api-deployment.yaml
kubectl -n ais apply -f deploy/k8s/api/pm25-api-service.yamlProbes:
- Liveness:
GET /healthz - Readiness:
GET /readyz
Resource requests/limits:
- Requests:
100mCPU,256Mimemory. - Limits:
500mCPU,512Mimemory. - Tune after load test.
Local smoke test:
curl -i http://localhost:8081/healthz
curl -i http://localhost:8081/readyz
curl -i http://localhost:8081/api/v1/hanoi/pm25/forecast/latestK8s smoke test:
kubectl -n ais port-forward svc/pm25-api 8081:80
curl -i http://localhost:8081/healthz
curl -i http://localhost:8081/readyz
curl -i http://localhost:8081/api/v1/hanoi/pm25/forecast/latestAcceptance criteria:
- API chạy trên K8s Deployment.
- Service route được.
- Health/readiness đúng semantics.
- Forecast endpoint trả JSON khi prediction tồn tại.
- Forecast endpoint trả 404 JSON rõ khi missing prediction.
- Không có request handler chạy compute nặng.
Airflow vẫn là orchestration/control plane. Kubernetes chỉ là runtime thực thi.
Files cần tạo/sửa:
airflow/dags/ais_pm25_k8s_compute_dag.py- createairflow/dags/ais_dag_utils.py- modifyairflow/dags/ais_hanoi_silver_gold_dag.py- modify optional để chuyển path K8s hoặc đánh dấu fallback.airflow/dags/ais_trajectory_tier2_dag.py- modify optional để chuyển path K8s hoặc đánh dấu fallback.
DAG nên orchestrate:
ensure_iceberg_tablestrên Spark-on-K8s.- Upstream Tier 2 dependency.
- Build serving features trên Spark-on-K8s.
- Optional training Job trên K8s.
- Promote model bằng task explicit/manual gate.
- Inference Job trên K8s.
- Prediction freshness check.
- API smoke/readiness check nếu phù hợp.
Integration options:
KubernetesPodOperatornếu Airflow image có providerapache-airflow-providers-cncf-kubernetes.- BashOperator gọi
kubectl apply/create jobnếu Airflow container có kubeconfig và kubectl. - BashOperator chạy
scripts/submit_spark_k8s.shcho Spark jobs. - SparkApplication nếu sau này chọn Spark Operator.
Required documentation:
- Airflow service account/kubeconfig path.
- Namespace
ais. - RBAC cần để create/watch pods/jobs.
- Cách truyền DAG conf:
start_date,end_date,base_hour,feature_version,dry_run.
Example DAG flow:
ensure_iceberg_tables_k8s
-> wait_for_upstream_tier2_or_run_tier2_k8s
-> build_pm25_serving_features_k8s
-> check_serving_features
-> run_pm25_inference_k8s
-> check_prediction_freshness
-> check_api_ready
Acceptance criteria:
- Airflow trigger được K8s compute workloads, hoặc TODO3 ghi chính xác provider/config còn thiếu.
- Airflow giữ vai trò dependency/rerun/backfill.
- CronJob không thay thế Airflow dependency management.
- DAG có manual backfill theo date range/base_hour.
Files cần tạo/sửa:
scripts/check_pm25_serving.py- createspark_jobs/hanoi_pm25_serving_quality_checks.py- create optionaldeploy/k8s/checks/pm25-serving-check-job.yaml- createmonitoring/app.py- modify optional
Checks bắt buộc:
| Check | Command | Expected output | Failure condition | Log fields | Acceptance criteria |
|---|---|---|---|---|---|
| Serving feature freshness | python scripts/check_pm25_serving.py --check serving-freshness |
latest base hour + age | age vượt threshold | latest_base_hour, age_minutes, threshold_minutes, status |
Fail rõ khi feature stale |
| Prediction freshness | python scripts/check_pm25_serving.py --check prediction-freshness |
latest prediction + age | prediction stale/missing | latest_base_hour, created_at, age_minutes, status |
Fail rõ khi stale |
| Critical feature null ratio | python scripts/check_pm25_serving.py --check feature-null-ratio |
null ratio list | critical feature null vượt threshold | feature, null_ratio, threshold, status |
Có threshold config |
| Missing production model | python scripts/check_pm25_serving.py --check model-registry |
6/12/24 model list | missing horizon | horizon_hour, location_id, feature_version, status |
Fail nếu thiếu 1 horizon |
| Schema hash mismatch | inference hoặc check script | expected/current hash | mismatch | expected_hash, actual_hash, feature_version, status |
Fail fast |
| API readiness | curl /readyz hoặc check script |
200 | non-200 | url, status_code, latency_ms, error_code |
Readiness phân biệt dependency |
| Failed inference CronJob | kubectl -n ais get jobs hoặc API K8s client |
last job status | failed job hoặc missing schedule | job_name, schedule_time, status |
Check chạy được bằng K8s Job/Airflow |
| Failed Spark job | kubectl logs/describe hoặc Airflow task status |
driver exit status | non-zero/failed pod | app_name, driver_pod, exit_code, status |
Failure visible |
| Duplicate prediction rows | table query | duplicate count 0 | duplicate key count > 0 | duplicate_count, key, status |
Fail nếu duplicate |
Monitoring app optional updates:
monitoring/app.pycó thể thêm cards cho:- latest serving feature base_hour.
- latest prediction base_hour.
- production model status 6/12/24.
- API readiness.
- K8s CronJob last success.
Acceptance criteria:
- Có command/manual check cho feature freshness và prediction freshness.
- Inference fail fast khi thiếu model hoặc schema mismatch.
- API readiness phân biệt process alive và dependency ready.
- Check job có exit code dùng được trong Airflow/K8s.
Các smoke test bắt buộc:
-
kubectl apply -k deploy/k8s- Pass khi namespace/config/RBAC base apply được.
-
Spark smoke job starts driver/executor pods.
- Command:
scripts/submit_spark_k8s.sh spark-smoke - Verify:
kubectl -n ais get pods.
- Command:
-
Spark smoke job can access Iceberg/HDFS.
- Verify logs có read/write hoặc table list success.
-
ensure_iceberg_tablesruns on K8s.- Command:
scripts/submit_spark_k8s.sh ensure-iceberg - Verify namespace/table tạo được.
- Command:
-
Serving feature job runs on K8s.
- Command:
scripts/submit_spark_k8s.sh hanoi-serving-features-gold --dry-run 1, rồi--dry-run 0. - Verify output table có rows.
- Command:
-
Model registry has production models for 6/12/24.
- Verify query registry theo
location_id=hanoi.
- Verify query registry theo
-
Inference dry-run succeeds.
- Command:
kubectl -n ais create job ... --dry-run=1hoặc manifest manual job.
- Command:
-
Inference
dry-run=0writes prediction.- Verify prediction table có row.
-
Rerun inference does not duplicate rows.
- Verify duplicate count by idempotency key = 0.
-
API
/healthzreturns 200.- Command:
curl -i http://localhost:8081/healthz.
- Command:
-
API
/readyzreturns 200 when dependency is ready.- Command:
curl -i http://localhost:8081/readyz.
- Command:
-
API forecast endpoint returns JSON when prediction exists.
- Command:
curl -i http://localhost:8081/api/v1/hanoi/pm25/forecast/latest.
- Command:
-
API forecast endpoint returns clear 404 when prediction is missing.
- Verify body:
{"error":"prediction_not_found","location":"hanoi"}-
Airflow can trigger K8s workloads or document exact gap.
- Verify DAG/task success or list missing provider/kubeconfig/RBAC.
-
Existing TODO1/TODO2 pipeline behavior is not broken.
- Dev fallback command vẫn chạy nếu cần.
- K8s path không đổi table contract cũ ngoài additions.
Acceptance gates:
- Không đánh dấu TODO3 done nếu Spark vẫn chỉ chạy Compose.
- Không đánh dấu API done nếu inference/prediction table chưa có smoke.
- Không đánh dấu inference done nếu registry production model chưa có 6/12/24.
- Không đánh dấu Airflow done nếu chỉ có CronJob mà không có dependency/backfill story.
- Storage layer vẫn là storage-only trong pha này.
- Spark jobs chạy trên Kubernetes làm target runtime.
- Spark driver/executor pods hiện diện và log truy cập được.
- K8s pods kết nối được external Kafka/HDFS/Iceberg endpoints.
- Không có source code mới hardcode Docker Compose hostnames cho K8s path.
-
ais.features.hanoi_pm25_serving_features_goldtồn tại và không có target leakage. - Serving feature schema khớp
ml/train_hanoi_pm25.py::FEATURE_COLUMNSsau preprocessing. -
ais.models.hanoi_pm25_model_registry_goldcó production pointer cho horizon 6/12/24. - Inference Job/CronJob chạy trên K8s và ghi prediction idempotent.
-
ais.predictions.hanoi_pm25_forecast_goldlà table duy nhất API đọc cho forecast response. - PM2.5 API chạy trên K8s và không chạy Spark/HYSPLIT/model inference trong request.
- Airflow orchestrates K8s compute workloads hoặc có gap chính xác về provider/kubeconfig/RBAC.
- Freshness/schema/readiness checks tồn tại và có exit code rõ.
- Smoke tests trong section 17 được document.
- TODO1/TODO2 behavior vẫn compatible, với Docker Compose Spark chỉ là dev fallback.