Appearance
Go部署与容器化
Go 程序最终通常以两种形态运行:一个二进制文件,或者一个包含二进制的容器镜像。ops-checker 这种命令行工具可以放进 cron、systemd timer、Kubernetes CronJob;ops-checker-api 这种 HTTP 服务更适合 systemd service 或 Kubernetes Deployment。
部署要记录清楚四件事:二进制怎么构建,配置从哪里来,进程怎么运行,失败时从哪里看日志和退出码。
一、构建二进制
本机开发时常用 go run。部署时通常先构建二进制:
bash
go build -o bin/ops-checker ./cmd/ops-checker
go build -o bin/ops-checker-api ./cmd/ops-checker-api查看产物:
bash
bin/ops-checker -config configs/targets.json
bin/ops-checker-api -addr :8080 -config configs/targets.json跨平台构建 Linux 产物:
bash
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \
go build -trimpath -ldflags="-s -w" \
-o bin/ops-checker-api-linux-amd64 \
./cmd/ops-checker-api几个参数:
| 参数 | 作用 |
|---|---|
GOOS=linux | 构建 Linux 可执行文件 |
GOARCH=amd64 | 构建 x86_64 架构 |
CGO_ENABLED=0 | 生成不依赖 glibc 的静态链接产物,容器里更容易运行 |
-trimpath | 去掉本机源码路径,避免产物里带开发机路径 |
-ldflags="-s -w" | 去掉符号表和调试信息,减小体积 |
CGO_ENABLED=0 适合大多数纯 Go 程序。如果程序依赖 SQLite、某些 DNS 行为或 C 库能力,关闭 CGO 前要单独验证。
二、版本信息
发布后排查问题时,经常需要知道当前二进制来自哪个版本。可以在代码里留几个变量,再在构建时注入。
新增文件:internal/version/version.go
go
package version
var (
Version = "dev"
Commit = "unknown"
Date = "unknown"
)入口里打印版本:
go
fmt.Printf("ops-checker-api version=%s commit=%s date=%s\n",
version.Version,
version.Commit,
version.Date,
)构建时注入:
bash
VERSION=v0.1.0
COMMIT=$(git rev-parse --short HEAD)
DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
go build \
-ldflags="-X example.com/ops-checker/internal/version.Version=$VERSION -X example.com/ops-checker/internal/version.Commit=$COMMIT -X example.com/ops-checker/internal/version.Date=$DATE" \
-o bin/ops-checker-api \
./cmd/ops-checker-api-X 修改的是字符串变量。变量路径要写完整包名和变量名,包名不对时不会按预期注入。
三、systemd服务
HTTP 服务适合用 systemd service 常驻运行。先准备目录:
bash
install -m 0755 bin/ops-checker-api /usr/local/bin/ops-checker-api
install -d -m 0755 /etc/ops-checker
install -m 0644 configs/targets.json /etc/ops-checker/targets.json新增文件:/etc/systemd/system/ops-checker-api.service
ini
[Unit]
Description=Ops Checker API
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=ops-checker
Group=ops-checker
ExecStart=/usr/local/bin/ops-checker-api -addr :8080 -config /etc/ops-checker/targets.json
Restart=on-failure
RestartSec=5s
# 服务只读配置并监听端口,不写系统目录。
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
[Install]
WantedBy=multi-user.target启动:
bash
systemctl daemon-reload
systemctl enable --now ops-checker-api
systemctl status ops-checker-api
journalctl -u ops-checker-api -fExecStart 里的配置路径要和实际安装路径一致。路径错了时,服务可能启动后立刻退出,journalctl 里会看到配置读取失败。
四、命令行工具定时运行
CLI 版本可以配 systemd timer。service 负责执行一次,timer 负责定时触发。
新增文件:/etc/systemd/system/ops-checker.service
ini
[Unit]
Description=Run Ops Checker Once
[Service]
Type=oneshot
ExecStart=/usr/local/bin/ops-checker -config /etc/ops-checker/targets.json -output json新增文件:/etc/systemd/system/ops-checker.timer
ini
[Unit]
Description=Run Ops Checker Every 5 Minutes
[Timer]
OnBootSec=1min
OnUnitActiveSec=5min
Unit=ops-checker.service
[Install]
WantedBy=timers.target启用:
bash
systemctl daemon-reload
systemctl enable --now ops-checker.timer
systemctl list-timers ops-checker.timer
journalctl -u ops-checker.servicetimer 触发的是 service。排查执行结果时看 ops-checker.service 的日志,而不是只看 timer 本身。
五、多阶段镜像
容器镜像里保留运行时二进制,编译器留在构建阶段。多阶段构建把编译环境和运行环境分开。
新增文件:Dockerfile
dockerfile
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags="-s -w" \
-o /out/ops-checker-api \
./cmd/ops-checker-api
FROM alpine:3.20
RUN adduser -D -H -u 10001 app
USER app
WORKDIR /app
COPY --from=builder /out/ops-checker-api /app/ops-checker-api
EXPOSE 8080
ENTRYPOINT ["/app/ops-checker-api"]
CMD ["-addr", ":8080", "-config", "/etc/ops-checker/targets.json"]golang:1.22-alpine 示例里写的是 Go 1.22。实际项目按 go.mod 里的 Go 版本选择对应镜像。构建镜像和本地 Go 版本差太多时,可能出现依赖不兼容或新语法无法编译。
构建:
bash
docker build -t registry.example.com/ops/ops-checker-api:v0.1.0 .
docker run --rm -p 8080:8080 \
-v "$PWD/configs:/etc/ops-checker:ro" \
registry.example.com/ops/ops-checker-api:v0.1.0配置用只读挂载。容器里路径 /etc/ops-checker/targets.json 要和启动参数一致。
六、镜像体积和用户
Go 静态二进制可以放进很小的运行镜像。常见选择:
| 运行镜像 | 特点 |
|---|---|
alpine | 有 shell 和包管理器,排查方便 |
distroless/static | 更小,攻击面更少,没有 shell |
scratch | 空镜像,只适合完全静态二进制 |
基础阶段用 alpine 更容易排查路径、证书和启动参数。镜像稳定后再考虑 distroless 或 scratch。
容器尽量不要用 root 用户运行。上面的 Dockerfile 创建了 uid 10001 的 app 用户。程序如果要监听 80 端口、写系统目录或读取受限文件,非 root 用户会暴露权限问题,这些问题应该通过端口映射、目录权限和配置挂载解决,而不是直接改回 root。
七、Kubernetes配置
目标配置放进 ConfigMap。
新增文件:deploy/configmap.yaml
yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: ops-checker-api-config
namespace: ops
data:
targets.json: |
[
{
"type": "http",
"name": "api",
"url": "https://example.com/health",
"timeout_seconds": 3
}
]ConfigMap 适合普通配置,不适合放 token、密码、证书私钥。敏感内容放 Secret,并控制 RBAC。
八、Deployment
HTTP 服务用 Deployment 管副本和滚动更新。
新增文件:deploy/deployment.yaml
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: ops-checker-api
namespace: ops
spec:
replicas: 2
selector:
matchLabels:
app: ops-checker-api
template:
metadata:
labels:
app: ops-checker-api
spec:
containers:
- name: api
image: registry.example.com/ops/ops-checker-api:v0.1.0
args:
- "-addr"
- ":8080"
- "-config"
- "/etc/ops-checker/targets.json"
ports:
- name: http
containerPort: 8080
readinessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 3
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 10
periodSeconds: 20
volumeMounts:
- name: config
mountPath: /etc/ops-checker
readOnly: true
volumes:
- name: config
configMap:
name: ops-checker-api-configselector.matchLabels 和 Pod 模板里的 labels 要一致。Deployment 通过这个选择器找到自己管理的 Pod,写错后会出现副本创建异常或无法接管 Pod。
探针路径使用 /healthz。服务增加 /readyz 后,readinessProbe 可以改成 /readyz,让 Pod 只有在配置加载、依赖就绪后才接流量。
九、Service
Deployment 只管理 Pod,不提供固定访问入口。Service 负责给 Pod 提供稳定地址。
新增文件:deploy/service.yaml
yaml
apiVersion: v1
kind: Service
metadata:
name: ops-checker-api
namespace: ops
spec:
selector:
app: ops-checker-api
ports:
- name: http
port: 8080
targetPort: http验证:
bash
kubectl apply -f deploy/configmap.yaml
kubectl apply -f deploy/deployment.yaml
kubectl apply -f deploy/service.yaml
kubectl get deploy,pod,svc -n ops
kubectl logs deploy/ops-checker-api -n ops
kubectl port-forward svc/ops-checker-api -n ops 8080:8080
curl -i http://127.0.0.1:8080/healthzService 的 selector 要匹配 Pod label。Service 有地址但访问不通时,先看 kubectl get endpointslices -n ops,确认后端 Pod 是否被选中。
十、发布排查
部署失败时按层看:
| 层级 | 现象 | 排查入口 |
|---|---|---|
| 镜像 | ImagePullBackOff | 镜像名、tag、仓库凭据、节点架构 |
| 启动参数 | Pod 反复重启 | kubectl logs、容器 args、配置路径 |
| 配置挂载 | 程序提示找不到文件 | ConfigMap 名称、volumeMount 路径 |
| 探针 | Pod Running 但 NotReady | readinessProbe 路径、端口、服务日志 |
| Service | Pod Ready 但访问不到 | Service selector、EndpointSlice、端口 |
| 业务检查 | /api/checks 返回 503 | targets.json、下游目标、检查日志 |
CrashLoopBackOff 不等于 Kubernetes 本身坏了,通常是容器进程启动后很快退出。先看上一轮日志:
bash
kubectl logs pod/<pod-name> -n ops --previous--previous 能看到上一个已经退出容器的日志。配置路径写错、参数解析失败、端口绑定失败这类问题经常只出现在 previous 日志里。
十一、部署形态
ops-checker 和 ops-checker-api 可以保持两条发布线:
| 程序 | 运行形态 | 适合场景 |
|---|---|---|
ops-checker | 二进制、systemd timer、Kubernetes CronJob | 周期性检查,靠退出码表达结果 |
ops-checker-api | systemd service、Kubernetes Deployment | 常驻 HTTP 服务,外部按需触发 |
ops-checker-operator | Kubernetes Deployment | 用 CRD 声明检查任务,Controller 维护子资源 |
同一套检查逻辑放在 internal/checker,不同入口只负责运行形态。这样新增一种检查类型时,不需要在 CLI、API 和 Operator 里分别重写判断逻辑。