iT邦幫忙

2025 iThome 鐵人賽

DAY 21
0
AI & Data

論文流浪記:我與AI 探索工具、組合流程、挑戰完整平台系列 第 22

Day 21|Makefile 活到 2025!懶人工程師的 AI 專案「萬能遙控器」

  • 分享至 

  • xImage
  •  

前言

有些工具,一開始你覺得它就是個花瓶,擺在那邊,感覺有用但又懶得學。結果某一天,你被專案的複雜性狠狠揍了一拳,才跪下來喊:「大哥,原來你這麼香。」

Makefile 就是這種存在。

老實說,我以前看到 Makefile 這三個字母,腦袋裡第一個浮現的畫面是:某個 1980 年代穿吊帶褲的老工程師,邊抽煙邊敲 CRT 螢幕前的黑底綠字,然後很酷地打一行 make build。我心裡只想:好啦好啦,這東西應該跟 DOS 一樣,應該早就進博物館了吧?

結果呢?2025 年了,Docker 滿天飛,Kubernetes 把大家搞到 PTSD,CI/CD 自動到你以為自己會失業,Makefile 居然還活著。活得比誰都頑強。
它簡直是工程師界的小強:打不死、掃不掉,甚至還會在你最狼狽的時候拍拍你肩膀,淡淡地說:「要不要我幫你跑一下環境?」

人生就這樣,有些東西比你的戀情還穩定。


為什麼要用 Makefile

講什麼「團隊標準化流程」啦、「協作一致性」啦,都是漂亮話。其實大家用 Makefile 的原因很簡單:

舉個例子,這串:

docker compose -f docker-compose.dev.yml -f docker-compose.dev.gpu.yml up -d

我跟你講,這指令長到像咒語,一個 - 打錯就會讓整個環境崩掉,崩得比你晚餐掉地上還慘。那一刻你會懷疑人生,想說:「幹,不能有個東西幫我把這串咒語,變成一個短短的指令嗎?」

結果 Makefile出場了。

你只要打 make up,就像呼叫神燈。環境一秒起飛。爽。

Makefile 其實是一個「咒語字典」

它的結構很簡單:

  • 目標 target:你想幹嘛(build、deploy、clean)。
  • 依賴 prerequisites:在幹嘛之前,要先幹嘛。
  • 命令 commands:實際上要幹嘛。(一定要用 TAB 開頭,不然 Make 會發飆)。

一切都很直白,像極了你媽在你出門前給你的待辦清單:

  • 先穿衣服。
  • 再戴口罩。
  • 最後才可以滾去便利商店。

如果你沒照做?對不起,make 會翻臉:

no rule to make target

語氣比你媽還兇,冷酷到讓你懷疑自己是不是親生的。

Makefile 的基礎三寶

除了 target 和 command,你還要懂三個基本概念:

1️⃣ 延遲賦值 =

  • 技術:變數在真正使用時才求值(lazy evaluation)
  • 範例:
DOCKER_COMPOSE = docker compose -f docker-compose.dev.yml -f docker-compose.dev.gpu.yml

2️⃣ 立即賦值 :=

  • 技術:定義時立即求值(simply expanded)
  • 範例:
HAS_GPU := $(shell command -v nvidia-smi >/dev/null 2>&1 && echo 1 || echo 0)

3️⃣ .PHONY

  • 技術:宣告 target 不是檔案,而是動作,避免與同名檔案衝突
  • 範例:
.PHONY: clean
clean:
	rm -rf build/

這三寶學會了,你就能少掉一半的白頭髮。

Makefile 的魔法:把懶惰變成「自動化」

範例一:Lint + Test + Build + Deploy

你可以這樣寫:

deploy: lint test build
    @echo "Deploying..."

這就像一個「人生流程表」:

  1. 先檢查程式碼有沒有亂七八糟(lint)。
  2. 再跑一下測試(test)。
  3. 確認沒炸掉再打包(build)。
  4. 最後才丟上去(deploy)。

整個過程像去醫院做全身檢查:驗血 → X 光 → 心電圖 → 醫生蓋章。
如果中間某一步爆掉?對不起,不能出院,繼續躺床上 debug。


範例二:判斷有沒有 GPU
有些人用 MacBook Air,硬要跑 CUDA,就像拿湯匙去敲石頭。這時候 Makefile 很懂事:

HAS_GPU := $(shell command -v nvidia-smi >/dev/null 2>&1 && echo 1 || echo 0)

ifeq ($(HAS_GPU),1)
  DOCKER_COMPOSE = docker compose -f docker-compose.dev.yml -f docker-compose.dev.gpu.yml
else
  DOCKER_COMPOSE = docker compose -f docker-compose.dev.yml
endif

結果就是:

  • 有 GPU?恭喜,跑全餐。
  • 沒 GPU?也別哭,還有平民套餐。

Makefile 就像那種貼心朋友,不會因為你窮就不跟你吃飯,只會拍拍你說:「來啦,咱們吃滷肉飯也行。」

$():Makefile 的「變數展開魔法」

在 Makefile 裡,$() 代表「去把變數的值拿出來」。
有點像你家冰箱上的便條紙寫著「牛奶在下層門板」,每次看到 $(),Make 就會乖乖去冰箱拿牛奶給你。
範例:

DOCKER_COMPOSE = docker compose -f docker-compose.dev.yml

up:
	$(DOCKER_COMPOSE) up -d

這裡的 $(DOCKER_COMPOSE) 就會被換成完整的 docker compose -f docker-compose.dev.yml。
所以當你打 make up,實際執行的指令就是:

docker compose -f docker-compose.dev.yml up -d

👉 簡單來說:$() 就是「把變數填進去」的意思,避免你一直重複貼長長的指令。


Docker + Makefile:天生一對,像雞排配珍奶

起容器

up:
	$(DOCKER_COMPOSE) up -d

make up,所有容器乖乖起來。比養寵物還聽話。
要是容器跑不動?別擔心,錯誤訊息會像你的前任一樣,毫不留情地提醒你:「你哪裡搞錯了。」

停止與重啟

down:
	$(DOCKER_COMPOSE) down

restart:
	$(DOCKER_COMPOSE) down
	$(DOCKER_COMPOSE) up -d

工程師的信仰指令:restart
不管發生什麼,先重啟。
人生要是也能這樣多好,失戀了就 make restart,乾乾淨淨回到初始狀態。

一鍵品質檢查:工程師的出門三連動作

quality_checks: format lint

format:
	isort $(PY_DIRS)
	black $(PY_DIRS)
	python -m ruff check $(PY_DIRS) --fix

這裡做了三件事:

  • isort → 把 import 排序好,不會有人 import os 跑到檔案最下面。
  • black → 把程式碼縮排、換行、括號格式統一。風格爭議直接解決,因為 black 說了算。
  • ruff --fix → 檢查並修一些小錯誤,像漏掉的逗號或沒用到的變數。
    打 make format,等於找了一個「強迫症小幫手」幫你自動整理房間:
    書一定放書架。
    鞋一定放鞋櫃。
    程式碼一定照規矩排好。
    這樣團隊裡每個人寫的程式碼看起來都像同一個人寫的,不會有人左括號靠左、右括號靠右,像是不同人裝潢的拼裝屋。

想像一下:專案是一個家。

  • Docker:小孩,常常在家裡亂跑亂叫。
  • 測試:媽媽,永遠在碎念你有沒有好好做事。
  • Makefile:老爸,不常講話,但一開口就是「全部照規矩來」。

有時候你覺得 Makefile 很煩,限制一堆。但沒有它,整個家瞬間變成無政府狀態。小孩會炸鍋,媽媽會崩潰,而你可能會直接搬出去(也就是 rage quit 這個專案)。


Docker / AI 專案實例心得:Makefile 是專案的「統一遙控器」

一個 專案的 Makefile,大概會長這樣:

.PHONY: lint test build deploy

lint:
	@echo "🔍 Linting..."
	black --check $(PY_DIRS)
	isort --check-only $(PY_DIRS)

test:
	@echo "🧪 Running tests..."
	pytest tests

build:
	@echo "📦 Building..."
	docker build -t $(DOCKER_IMAGE) .

deploy: lint test build
	@echo "🚀 Deploying..."
	docker push $(DOCKER_IMAGE)
	@echo "✅ Done!"

執行 make deploy 時,流程如下:

  1. lint → 程式碼檢查
  2. test → 單元測試
  3. build → 建置 Docker 映像
  4. deploy → 推送映像到 registry,印出完成訊息

輸出示例:

🔍 Linting...
🧪 Running tests...
📦 Building...
🚀 Deploying...
✅ Done!

💡重點:

  • target 的依賴會先被執行,依賴本身的 command 也會依序執行
  • target 自身的 command 也是逐行執行
  • 使用 @ 可以 suppress command 輸出,只印出結果

常見坑:Makefile 這老兄也不是沒脾氣

  1. TAB vs 空白地獄
    Makefile 最陰險的坑就是:命令要用 TAB,不是四個空白。IDE 自動轉換?炸。你以為是 bug,結果是縮排。這坑害死過一代又一代工程師。

  2. 變數遞迴展開*
    =:= 搞錯,你可能得到一個遞迴到懷疑人生的變數值。常常跑到一半發現 $() 變數展開變成鬼打牆。

  3. 錯誤不顯示
    command 前加了 @,輸出被抑制,然後 build 爆炸,你還看不到哪行掛了。人生就像跟對象冷戰一樣,對方什麼都不講,你只能自己猜哪裡做錯。

專案中的實務應用

啟動所有容器

up:
    $(OBS_COMPOSE) up -d
    $(STORAGE_COMPOSE) up -d
    $(DOCKER_COMPOSE) up -d
  • -d 表示背景執行
  • 不同 Compose 指令依照服務類別拆分,提高可維護性

停止與重啟

down:
    $(DOCKER_COMPOSE) down
    $(STORAGE_COMPOSE) down

restart:
    $(DOCKER_COMPOSE) down
    $(DOCKER_COMPOSE) up -d

測試與品質檢查

quality_checks: format lint

format:
    isort $(PY_DIRS)
    black $(PY_DIRS)
    python -m ruff check $(PY_DIRS) --fix

lint:
    pylint --rcfile=.pylintrc $(PY_DIRS) || true
    python -m bandit -r $(PY_DIRS) || true
  • 一鍵完成格式化、程式碼檢查、靜態安全掃描
  • 避免開發過程中疏漏

我發現,Makefile 就是工程師的「萬能遙控器」。
以前你要開電視、開冷氣、開電燈,每個要用不同的遙控器。現在只要一個 Makefile,全包。

  • make up:環境起來。
  • make down:環境收掉。
  • make test:測試跑完。
  • make deploy:專案上線。
  • make clean:資料全炸掉(像按下「人生重置」)。

最重要的是:這些東西團隊成員都能共用。大家不用再在 Slack 上問:「欸,要跑測試的指令是什麼來著?」你只要冷冷丟一句:「make test」。
瞬間變成團隊裡的智者,或者說,懶得講廢話的智者。


實務心得

  • 分層管理 Docker Compose
    前端、後端、存儲、監控等 Compose 拆開,方便局部重啟或 CI/CD 測試

  • 利用變數與條件簡化操作
    GPU 判斷、目標組合、網路建立等用變數管理,方便多人協作

  • 容器化測試
    測試命令直接進入容器,避免 host 環境干擾,CI/CD 無痛集成

  • 品質檢查一鍵化
    make quality_checks 立即完成格式化 + lint + Bandit 安全掃描,減少人為疏漏


結論 Makefile 沒有很潮,但超耐用

Makefile 不會幫你寫程式碼,不會幫你 debug,更不會幫你升職加薪。
但它能讓你少掉一堆無聊動作,幫你留點精力,去焦慮人生更大的麻煩。

而且每次我敲下 make deploy,看到「🚀 Deploying… ✅ Done!」,內心都會冒出一種小確幸。
就像在這個充滿 bug、KPI、情感債務的世界裡,終於有一件事是「一鍵完成」的。

可惜,這世界上其他麻煩事,比如戀愛、健身、存錢,都沒有 make 指令。
不然我一定天天敲 make happy,然後坐在那裡等結果。

👉 好啦,說到這裡,你是不是也開始想回專案裡,加一個 Makefile 了?
不加也沒差,等你哪天被一堆 Docker 指令搞到懷疑人生,你自然會懂。

到時候別謝我,謝那個在 1976 年發明 make 的祖師爺吧。


附錄:專案 Makefile 範例

完整 Makefile 內容如專案實例,涵蓋 network 建立、容器啟停、測試、建置、部署與 pipeline 操作,可直接參考附錄程式碼。

# .env 檔案會自動載入環境變數
ENV_FILE=.env
HAS_GPU := $(shell command -v nvidia-smi >/dev/null 2>&1 && echo 1 || echo 0)


OBS_COMPOSE = docker compose -f docker-compose.obs.yml
# 動態決定 docker compose 指令
ifeq ($(HAS_GPU),1)
  DOCKER_COMPOSE = docker compose -f docker-compose.dev.yml -f docker-compose.dev.gpu.yml
else
  DOCKER_COMPOSE = docker compose -f docker-compose.dev.yml
endif

STORAGE_COMPOSE = docker compose -f docker-compose.storage.yml
MONITOR_DEV_COMPOSE = docker compose -f docker-compose.monitor.dev.yml
MONITOR_COMPOSE = docker compose -f docker-compose.monitor.yml
DOCKER_FRONTEND_COMPOSE = docker compose -f docker-compose.frontend.yml

PY_DIRS = note apiGateway email arxiv

NETWORKS = monitor-net app-net langfuse-otel-net


.PHONY: test



net-create:
	@for net in $(NETWORKS); do \
		echo "🔌 檢查/建立 network $$net"; \
		if ! docker network inspect $$net >/dev/null 2>&1; then \
			docker network create $$net --driver bridge; \
			echo "✅ 建立 $$net 完成"; \
		else \
			echo "✅ $$net 已存在"; \
		fi \
	done
# 啟動所有容器(背景執行)
up:
	$(OBS_COMPOSE) up -d
	$(STORAGE_COMPOSE) up -d
	$(DOCKER_COMPOSE) up -d
	$(MONITOR_DEV_COMPOSE) up -d
	$(MONITOR_COMPOSE) up -d
	$(DOCKER_FRONTEND_COMPOSE) up -d


up-front:
	cd frontend && npm i && npm run dev

# 停止所有容器
down:
	$(DOCKER_FRONTEND_COMPOSE) down
	$(STORAGE_COMPOSE) down
	$(MONITOR_DEV_COMPOSE) down
	$(MONITOR_COMPOSE) down
	$(DOCKER_COMPOSE) down
	$(OBS_COMPOSE) down

# 重啟所有容器
restart:
	$(DOCKER_COMPOSE) down
	$(DOCKER_COMPOSE) up -d

# 查看容器日誌(預設看 apiGateway)
logs:
	$(DOCKER_COMPOSE) logs -f apiGateway

# 查看所有容器日誌
logs-all:
	$(DOCKER_COMPOSE) logs -f


# 重建全部服務
build:
	$(MAKE) net-create
	$(DOCKER_COMPOSE) build
	$(MONITOR_DEV_COMPOSE) build
	$(DOCKER_COMPOSE) exec ollama /bin/bash -c "ollama pull gpt-oss:20b"

# 進入 apiGateway 容器
shell:
	$(DOCKER_COMPOSE) exec apiGateway bash

# 測試 (需先裝 pytest)
test-note:
	$(DOCKER_COMPOSE) exec noteserver /bin/sh -c "PYTHONPATH=/app pytest tests"

test-apiGateway:
	$(DOCKER_COMPOSE) exec apiGateway /bin/sh -c "PYTHONPATH=/app pytest tests"

# integration_test:
# 	$(DOCKER_COMPOSE) exec apiGateway /bin/sh -c "PYTHONPATH=/app pytest -v tests/integration"


# sample job
lanhchain:
	$(DOCKER_COMPOSE) exec apiGateway /bin/bash -c "PYTHONPATH=/app python services/langchain_client.py"


ollama-client:
	$(DOCKER_COMPOSE) exec noteserver /bin/bash -c "PYTHONPATH=/app python services/ollama/client.py"

sample_qdrant:
	$(DOCKER_COMPOSE) exec noteserver /bin/bash -c "PYTHONPATH=/app python services/qdrant/sample_qdrant.py"

email-trial:
	$(DOCKER_COMPOSE) exec email-flow /bin/bash -c "/opt/conda/envs/prefect/bin/python trial.py"

# pipeline
ingest-arxiv:
	$(DOCKER_COMPOSE) exec arxiv-flow /bin/bash -c "/opt/conda/envs/prefect/bin/python arxiv_pipeline.py"

email-subscribe:
	$(DOCKER_COMPOSE) exec email-flow /bin/bash -c "/opt/conda/envs/prefect/bin/python pipeline.py"

rag:
	$(DOCKER_COMPOSE) exec noteserver /bin/bash -c "PYTHONPATH=/app python arxiv_rag_pipeline.py"



# 移除所有 volumes (⚠️會清除資料)
clean:
	$(MAKE) down
	sudo rm -rf ./data ./obs_data


up-dev:
	$(DOCKER_COMPOSE) up -d note-qdrant noteserver



# 1️⃣ 一鍵檢查品質
quality_checks: format lint

# 2️⃣ 格式化程式碼
format:
	isort $(PY_DIRS)
	black $(PY_DIRS)
	python -m ruff check $(PY_DIRS) --fix


# 3️⃣ 代碼檢查
lint:
	pylint --rcfile=.pylintrc $(PY_DIRS) || true
	python -m bandit -r $(PY_DIRS) || true


down-monitor:
	$(MONITOR_COMPOSE) down
up-monitor:
	$(MONITOR_COMPOSE) up -次


上一篇
Day 20|FastAPI 安全大公開!沒 CORS、Trusted Host、Rate Limiting,你的 API 就像無人看管的自助餐廳
下一篇
Day22|系統健康全攻略:Monitoring → Observability,一篇搞懂
系列文
論文流浪記:我與AI 探索工具、組合流程、挑戰完整平台23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言