Skip to content

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 -f

ExecStart 里的配置路径要和实际安装路径一致。路径错了时,服务可能启动后立刻退出,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.service

timer 触发的是 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 10001app 用户。程序如果要监听 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-config

selector.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/healthz

Service 的 selector 要匹配 Pod label。Service 有地址但访问不通时,先看 kubectl get endpointslices -n ops,确认后端 Pod 是否被选中。

十、发布排查

部署失败时按层看:

层级现象排查入口
镜像ImagePullBackOff镜像名、tag、仓库凭据、节点架构
启动参数Pod 反复重启kubectl logs、容器 args、配置路径
配置挂载程序提示找不到文件ConfigMap 名称、volumeMount 路径
探针Pod Running 但 NotReadyreadinessProbe 路径、端口、服务日志
ServicePod Ready 但访问不到Service selector、EndpointSlice、端口
业务检查/api/checks 返回 503targets.json、下游目标、检查日志

CrashLoopBackOff 不等于 Kubernetes 本身坏了,通常是容器进程启动后很快退出。先看上一轮日志:

bash
kubectl logs pod/<pod-name> -n ops --previous

--previous 能看到上一个已经退出容器的日志。配置路径写错、参数解析失败、端口绑定失败这类问题经常只出现在 previous 日志里。

十一、部署形态

ops-checkerops-checker-api 可以保持两条发布线:

程序运行形态适合场景
ops-checker二进制、systemd timer、Kubernetes CronJob周期性检查,靠退出码表达结果
ops-checker-apisystemd service、Kubernetes Deployment常驻 HTTP 服务,外部按需触发
ops-checker-operatorKubernetes Deployment用 CRD 声明检查任务,Controller 维护子资源

同一套检查逻辑放在 internal/checker,不同入口只负责运行形态。这样新增一种检查类型时,不需要在 CLI、API 和 Operator 里分别重写判断逻辑。