make / Makefile —— 任务编排
一句话定义
make 是经典的 任务运行器 —— 你写 Makefile 描述"目标 → 依赖 → 命令",make 按依赖图执行。运维 / DevOps 场景里,Makefile 是替代 shell 脚本 / package.json scripts 的轻量统一入口——make deploy、make test、make logs。
典型场景
- 项目入口:
make deploy/make rollback - K8s 部署套路:
make applymake diffmake port-forward - 替代复杂的
if/else脚本 - CI 流水线的入口(Jenkins / GitHub Actions / GitLab CI 都能
make ...)
Makefile 在运维场景里不为了"增量编译"——是为了"一致的命令入口"。无论 macOS / Linux / Windows WSL 上,
make deploy都跑一样的东西。
最小 Makefile
# Makefile
.PHONY: help install deploy clean
help:
@echo "Targets: install, deploy, clean"
install:
pip install -r requirements.txt
deploy: install
kubectl apply -f manifests/
clean:
rm -rf build/
用法:
make # 默认跑第一个 target(help)
make help
make install
make deploy # 自动先跑依赖 install
make clean
三大元素
target: prerequisites
<TAB>command
<TAB>command
命令前必须是 TAB 不是空格 —— 这是 Makefile 最大坑。
变量
VERSION := v1.2.3 # 立即赋值(=== 一次性求值)
KUBECONFIG ?= ~/.kube/config # 默认值(环境没设才用)
NAMESPACE = production # 延迟赋值(每次用时求值)
deploy:
@echo "Deploying $(VERSION) to $(NAMESPACE)"
kubectl --kubeconfig=$(KUBECONFIG) -n $(NAMESPACE) apply -f deploy.yaml
make deploy # 用默认值
make deploy VERSION=v1.2.4 # 命令行覆盖
NAMESPACE=staging make deploy # 环境变量覆盖
?= 是 make 的一大杀器——配合 K8s 多环境 / 多集群非常顺手。
Make 内置变量
| 变量 | 含义 |
|---|---|
$@ | 当前 target 名 |
$< | 第一个依赖 |
$^ | 所有依赖 |
$? | 比 target 新的依赖 |
$(MAKE) | make 本身(递归用) |
%.png: %.svg # 模式规则
inkscape -e $@ $<
模式规则在编译类场景常用、运维 Makefile 少见。
.PHONY —— 必加
.PHONY: deploy
deploy:
kubectl apply -f deploy.yaml
.PHONY 告诉 make:"这不是个真文件,每次都跑命令"。没 .PHONY 时:
deploy: # 没 .PHONY
kubectl apply -f deploy.yaml
# 如果当前目录恰好有个文件叫 `deploy`:
$ make deploy
make: 'deploy' is up to date. # ❌ 不跑了!
Makefile 里所有任务式 target 都要 .PHONY:
.PHONY: help install deploy clean test apply diff
真实运维 Makefile 模板
# Makefile
.PHONY: help install lint test build push deploy diff rollback logs status \
clean port-forward exec
# === 变量 ===
SHELL := /bin/bash
.SHELLFLAGS := -eu -o pipefail -c
PROJECT := my-app
VERSION ?= $(shell git describe --tags --always --dirty)
REGISTRY ?= registry.example.com
IMAGE := $(REGISTRY)/$(PROJECT):$(VERSION)
NAMESPACE ?= default
KUBECTL := kubectl -n $(NAMESPACE)
# === 帮助 ===
help: ## 列出所有 target
@awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z_-]+:.*##/ {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
# === 开发 ===
install: ## 装依赖
pip install -r requirements.txt
lint: ## 静态检查
flake8 src/
black --check src/
test: ## 跑测试
pytest tests/
# === 容器 ===
build: ## 构建 docker 镜像
docker build -t $(IMAGE) .
push: build ## 推镜像
docker push $(IMAGE)
# === K8s 部署 ===
diff: ## 看部署 diff
helm diff upgrade $(PROJECT) ./chart \
-n $(NAMESPACE) \
--set image.tag=$(VERSION)
deploy: ## 部署
helm upgrade --install $(PROJECT) ./chart \
-n $(NAMESPACE) --create-namespace \
--set image.tag=$(VERSION) \
--wait --timeout 5m
rollback: ## 回滚到上一版
helm rollback $(PROJECT) -n $(NAMESPACE)
# === 运维 ===
status: ## 看 pod 状态
$(KUBECTL) get pod,svc,ing -l app=$(PROJECT)
logs: ## 看日志
$(KUBECTL) logs -l app=$(PROJECT) --tail=200 -f --prefix
port-forward: ## 端口转发到本地 8080
$(KUBECTL) port-forward svc/$(PROJECT) 8080:80
exec: ## 进 pod
$(KUBECTL) exec -it deploy/$(PROJECT) -- bash
clean: ## 清本地构建
rm -rf build/ dist/ *.egg-info
用法:
make help # 列所有
make deploy VERSION=v2.0.0 # 部署 v2.0.0
make deploy NAMESPACE=staging # 部署到 staging
make logs # 看日志
make port-forward # 端口转发
make rollback # 回滚
Makefile 上的几个最佳实践
1. SHELL := /bin/bash + pipefail
SHELL := /bin/bash
.SHELLFLAGS := -eu -o pipefail -c
默认 make 用 /bin/sh 不支持 pipefail 等 bash 特性。这两行让你的命令在出错时立刻退出(set -e 等价)。
2. @ 抑制 echo
deploy:
@echo "deploying ..." # 只打印 "deploying ...",不打印 echo 命令本身
kubectl apply -f ... # 打印命令本身 + 输出
只在 @echo 这种自显示提示性命令前加 @。其它命令保留显示(看得到跑了啥)。
3. 自描述 help
help: ## 列出所有 target
@awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z_-]+:.*##/ {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
deploy: ## 部署应用
...
clean: ## 清理
...
make help 自动列出所有带 ## 注释的 target——一致的项目入口。
4. 默认 target 是 help
把 help 放在第一个 target,或显式:
.DEFAULT_GOAL := help
make 不带参数时跑 help。避免不小心跑了破坏性的 deploy。
多 target 依赖图
.PHONY: ci
ci: lint test build
lint:
...
test:
...
build:
...
make ci # 自动跑 lint → test → build
并行:
make -j4 ci # 并行跑(小心副作用)
-j 加速但要注意 target 之间不能写同一个文件 / 资源。
子 Makefile
build-frontend:
$(MAKE) -C frontend build # 在 frontend/ 目录跑 make build
build-backend:
$(MAKE) -C backend build
build: build-frontend build-backend
-C dir 切目录后跑。monorepo 友好。
实战:K8s 多环境部署
.PHONY: dev staging prod
ENV ?= dev
deploy-%:
@echo "Deploying to $*"
helm upgrade --install $(PROJECT) ./chart \
-n $* --create-namespace \
-f values-base.yaml \
-f values-$*.yaml
dev: deploy-dev
staging: deploy-staging
prod: deploy-prod
make dev # 部署 dev
make staging
make prod # 生产
% 是通配 target,$* 是匹配的部分。
常见踩坑
坑 1:用空格而不是 TAB
deploy:
kubectl apply -f ... # 4 空格
# Makefile:5: *** missing separator. Stop.
命令前必须 TAB。编辑器设:
" .vimrc 给 Makefile 不展开 tab
autocmd FileType make setlocal noexpandtab
# .editorconfig
[Makefile]
indent_style = tab
坑 2:没 .PHONY 又有同名文件
deploy:
kubectl apply -f ...
touch deploy
make deploy
# make: 'deploy' is up to date.
所有非编译 target 加 .PHONY。
坑 3:$ 变量被 make 吃了
list-pods:
for p in $(kubectl get pods -o name); do echo $p; done
$ make list-pods
# 报错:未定义变量 p
shell 里的 $ 在 Makefile 里要 $$:
list-pods:
for p in $$(kubectl get pods -o name); do echo $$p; done
或者写成单行 / 用 here-doc:
list-pods:
@kubectl get pods -o name
坑 4:多行 shell 不在同一个 shell
target:
cd /tmp
ls # ❌ ls 不是在 /tmp 里跑
每行 make 起一个新 shell。多行要用 && 或 ; 或 \ 连:
target:
cd /tmp && ls
# 或
target:
cd /tmp; \
ls
或者用 .ONESHELL(GNU make 4+):
.ONESHELL:
target:
cd /tmp
ls
坑 5:变量 = vs :=
VERSION = $(shell git describe) # 每次用都跑一次 git
VERSION := $(shell git describe) # 一次性跑
:= 求值一次。= 延迟求值。Shell 命令在 = 下会被反复跑——慢且不一致。默认用 :=。
坑 6:?= 没生效
NAMESPACE ?= default
NAMESPACE=prod make deploy
环境变量传给 make 不会自动传给 make 子进程的 shell。要 export:
export NAMESPACE
或者只在命令里用 $(NAMESPACE)(make 变量),不依赖环境传给子 shell。
坑 7:make 没装
$ make
-bash: make: command not found
apt install -y build-essential # Ubuntu
yum groupinstall -y "Development Tools" # CentOS
K8s 节点上一般没装,CI 镜像通常有。
坑 8:循环 / 条件难写
deploy:
if [ "$$ENV" = "prod" ]; then \
echo "deploying prod"; \
else \
echo "non-prod"; \
fi
Makefile 的 shell 嵌入复杂条件反直觉——\ 连行 + $$ 转义。
复杂逻辑外移到 shell 脚本:
deploy:
./scripts/deploy.sh $(ENV)
Makefile 作"入口",shell 脚本做"实际逻辑"。
坑 9:make clean 删了不该删的
clean:
rm -rf $(BUILD_DIR)/*
如果 BUILD_DIR 没设、变成 rm -rf /*,灾难。
防御:
BUILD_DIR := build
clean:
@test -n "$(BUILD_DIR)" || (echo "BUILD_DIR not set"; exit 1)
rm -rf $(BUILD_DIR)
坑 10:make 不显示彩色 / 进度
make 默认输出黑白。脚本里用 ANSI 着色:
deploy:
@echo -e "\033[32m✓ Deployed\033[0m"
或者用 printf。
替代方案
| 工具 | 优点 | 适合 |
|---|---|---|
make | 普及 / 简单 / 无依赖 | 通用入口 |
just | 现代 / 更易写 | 替代 make 的新工具 |
task (Taskfile.yml) | YAML 格式 / 现代 | 不想用 Makefile 语法 |
npm scripts | JS 生态 | Node 项目 |
| 纯 shell 脚本 | 灵活 | 复杂逻辑 |
实际中make 仍是 K8s / DevOps 项目的事实标准入口——所有人都能跑、不需要装额外工具。