前言
K3s 里的 crawlab-worker 要连数据库,密码一直是写死在 ConfigMap 里的,上线后改密码就要重新部署,太蠢了。
正好借这个机会把 Vault 搭起来,顺便用 Vault Agent 的 Sidecar 模式把密钥自动注入进去,以后轮换密码只要在 Vault 改一次,应用侧什么都不用动。
Vault 官方推荐用 Helm 安装 Agent Injector,但它本质上是一个 MutatingWebhookConfiguration + 一套 Helm Chart,在国内网络环境下拉取镜像和 Chart 都很麻烦,而且引入了额外的 Webhook 组件。
实际上 Vault Agent 就是 hashicorp/vault 镜像里的一个子命令,完全可以直接在 Deployment 里写一个 Sidecar 容器,效果和 Injector 完全一样,还更透明。
本文操作环境:两台虚拟机,
<Vault机器IP>运行 Docker(Vault),<K3s机器IP>运行 K3s,请自行替换。
方案概述
- 使用 Docker 部署 Vault(文件存储持久化)
- 准备目录与配置文件
- 编写
docker-compose.yml - 初始化与解封
- 配置 Vault
- 启用 KV v2 密钥引擎
- 写入
crawlab-worker的密钥 - 创建访问策略
- 在 K3s 中配置 Kubernetes Auth
- 创建 ServiceAccount 与 RBAC
- 在 Vault 中启用并配置 Kubernetes Auth
- 创建 Vault Role 绑定策略
- 手动注入 Vault Agent Sidecar(不用 Helm)
- 创建 ConfigMap(Vault Agent 配置文件)
- 修改
crawlab-workerDeployment - 验证注入效果
- 添加新的密钥并验证
- 删除密钥并验证
操作步骤
一、使用 Docker 部署 Vault
1、准备目录与配置文件
mkdir -vp /opt/vault/{config,data,logs}
cd /opt/vault
# hashicorp/vault 镜像以 vault 用户(UID 100)运行,需要赋予 data 目录写权限
sudo chown -R 100:1000 /opt/vault/data
nano config/vault.hcl
在 /opt/vault/config/vault.hcl 中写入以下内容:
storage "file" {
path = "/vault/data"
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_disable = true
}
api_addr = "http://0.0.0.0:8200"
ui = true
storage "file"使用本地文件系统持久化,数据写入挂载卷,容器重启后不丢失tls_disable = true内网使用,生产环境建议配置证书ui = true开启 Web UI,可通过http://<Vault机器IP>:8200/ui访问
2、编写 docker-compose.yml 并启动
在 /opt/vault/ 目录下创建 docker-compose.yml:
services:
vault:
image: hashicorp/vault:1.21
container_name: vault
restart: unless-stopped
ports:
- "8200:8200"
volumes:
- ./config:/vault/config
- ./data:/vault/data
- ./logs:/vault/logs
environment:
VAULT_ADDR: "http://0.0.0.0:8200"
cap_add:
- IPC_LOCK
command: server
IPC_LOCK是防止 Vault 内存中的密钥被操作系统换出到磁盘。
docker compose up -d
docker logs vault
3、初始化与解封
Vault 首次启动后处于密封(Sealed)状态,必须先初始化再解封:
# 初始化,生成 5 个 Key Share,解封需要其中任意 3 个
docker exec -it vault vault operator init \
-key-shares=5 \
-key-threshold=3
输出示例:
Unseal Key 1: xxxx...
Unseal Key 2: xxxx...
Unseal Key 3: xxxx...
Unseal Key 4: xxxx...
Unseal Key 5: xxxx...
Initial Root Token: hvs.xxxx...
Unseal Key 和 Root Token 只显示一次,立即保存好。
# 解封,需要执行 3 次,每次提供不同的 Key
docker exec -it vault vault operator unseal <Unseal Key 1>
docker exec -it vault vault operator unseal <Unseal Key 2>
docker exec -it vault vault operator unseal <Unseal Key 3>
Sealed 变为 false 说明解封成功。
注意:容器重启后 Vault 会重新进入密封状态,需要再次执行上面 3 条
unseal命令。如果不想每次重启都手动操作,可以后续配置 Auto Unseal(接入云 KMS)。
二、配置 Vault
Vault 跑在 Docker 容器里,所有操作都通过 docker exec 在容器内执行,不需要在宿主机上额外安装 vault CLI。
为了少打字,先进入容器并设好环境变量:
docker exec -it vault sh
export VAULT_ADDR="http://127.0.0.1:8200"
export VAULT_TOKEN="<Initial Root Token>"
后面的
vault命令都在这个容器 shell 里执行。如果中途退出了容器,重新docker exec -it vault sh进来再export一次即可。
1、启用 KV v2 密钥引擎
vault secrets enable -path=secret kv-v2
2、写入测试密钥
先写一条测试用的密钥,整个流程跑通后再换成真实业务数据:
vault kv put secret/crawlab/worker \
test_message="hello-from-vault"
# 验证写入
vault kv get secret/crawlab/worker
3、创建访问策略
策略内容比较短,直接用 heredoc 在容器内写入临时文件再应用:
cat > /tmp/crawlab-worker-policy.hcl <<'EOF'
path "secret/data/crawlab/worker" {
capabilities = ["read"]
}
EOF
vault policy write crawlab-worker /tmp/crawlab-worker-policy.hcl
KV v2 的实际数据路径是
secret/data/<path>,不是secret/<path>,这里要注意。
操作完成后可以 exit 退出容器 shell。
三、在 K3s 中配置 Kubernetes Auth
Vault Kubernetes Auth 的工作原理:Pod 用自身的 ServiceAccount Token 向 Vault 登录,Vault 收到请求后调用 K3s 的 TokenReview API 验证 Token 是否合法,验证通过后下发一个有时效的 Vault Token。
因此需要在 K3s 里准备两类 ServiceAccount:
vault-auth:专供 Vault 调用 K3s TokenReview API 时使用(相当于 Vault 的”验证通道”)crawlab-worker:Pod 实际使用的身份,用来向 Vault 登录
1、创建 ServiceAccount 与 RBAC
新建 vault-auth-sa.yaml:
---
apiVersion: v1
kind: Namespace
metadata:
name: vault
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault-auth
namespace: vault
---
# K3s 1.24+ 不再自动创建长期 Token,需显式声明
apiVersion: v1
kind: Secret
metadata:
name: vault-auth-token
namespace: vault
annotations:
kubernetes.io/service-account.name: vault-auth
type: kubernetes.io/service-account-token
---
# 授予 vault-auth 调用 TokenReview API 的权限
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: vault-auth-tokenreview
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: vault-auth
namespace: vault
---
# crawlab-worker Pod 使用的 ServiceAccount,命名空间和 Deployment 保持一致
apiVersion: v1
kind: ServiceAccount
metadata:
name: crawlab-worker
namespace: crawlab
kubectl apply -f vault-auth-sa.yaml
2、在 Vault 中启用并配置 Kubernetes Auth
两台机器都要用到,分开操作:先在 K3s 机器上取出集群信息,再到 Vault 机器上通过 docker exec 写入配置。
在 K3s 机器上执行,取出两个值并打印备用:
# vault-auth 的长期 JWT Token
kubectl get secret vault-auth-token -n vault \
-o jsonpath='{.data.token}' | base64 -d
echo ""
# K3s 集群的 CA 证书,保存为文件(证书内容有换行,不能直接粘贴进命令)
kubectl config view \
--raw --minify --flatten \
-o jsonpath='{.clusters[].cluster.certificate-authority-data}' | base64 -d > /tmp/k8s-ca.crt
把 JWT 输出复制好,CA 证书通过 scp 传到 Vault 机器:
scp /tmp/k8s-ca.crt <Vault机器IP>:/tmp/k8s-ca.crt
切换到 Vault 机器,先把 CA 证书文件复制进容器,再进入容器写入配置:
docker cp /tmp/k8s-ca.crt vault:/tmp/k8s-ca.crt
docker exec -it vault sh
export VAULT_ADDR="http://127.0.0.1:8200"
export VAULT_TOKEN="<Initial Root Token>"
vault auth enable kubernetes
vault write auth/kubernetes/config \
token_reviewer_jwt="<粘贴JWT Token>" \
kubernetes_host="https://<K3s机器IP>:6443" \
kubernetes_ca_cert=@/tmp/k8s-ca.crt
kubernetes_host必须填写 K3s 安装时使用的 IP 地址(即 K3s Agent 加入集群时--server参数里的地址),因为 K3s 的 TLS 证书 SANs 里只包含了这个 IP。如果填写 Tailscale 域名等其他地址,TLS 验证会失败,Vault 会将其统一报告为 403 permission denied。@/tmp/k8s-ca.crt是 vault CLI 的文件读取语法,会自动读取文件内容传入。
# 验证配置
vault read auth/kubernetes/config
3、创建 Vault Role 绑定策略
继续在 Vault 机器的容器 shell 里执行(沿用上面的 session):
vault write auth/kubernetes/role/crawlab-worker \
bound_service_account_names=crawlab-worker \
bound_service_account_namespaces=crawlab \
policies=crawlab-worker \
ttl=1h
crawlab-worker 这个 ServiceAccount 的 Pod 登录 Vault 后,就会拿到绑定了 crawlab-worker 策略的 Token,有效期 1 小时,Vault Agent 会在过期前自动续期。
四、手动注入 Vault Agent Sidecar
1、创建 ConfigMap(Vault Agent 配置文件)
我们准备两份配置,分别挂给不同的容器:
vault-agent-init.hcl:给 Init 容器用,认证成功并渲染密钥文件后立即退出,确保主容器启动前密钥已就绪vault-agent-sidecar.hcl:给 Sidecar 容器用,持续运行,监听密钥变化并自动续期
新建 vault-agent-configmap.yaml:
apiVersion: v1
kind: ConfigMap
metadata:
name: vault-agent-config
namespace: crawlab
data:
vault-agent-init.hcl: |
exit_after_auth = true
pid_file = "/tmp/.vault-agent.pid"
vault {
address = "http://<Vault机器IP>:8200"
}
auto_auth {
method "kubernetes" {
mount_path = "auth/kubernetes"
config = {
role = "crawlab-worker"
}
}
sink "file" {
config = {
path = "/home/vault/.vault-token"
}
}
}
template_config {
static_secret_render_interval = "1m"
}
template {
destination = "/vault/secrets/config.env"
contents = <<EOT
{{- with secret "secret/data/crawlab/worker" -}}
{{- range $k, $v := .Data.data }}
{{ $k }}={{ $v }}
{{- end }}
{{- end }}
EOT
}
vault-agent-sidecar.hcl: |
exit_after_auth = false
pid_file = "/tmp/.vault-agent.pid"
vault {
address = "http://<Vault机器IP>:8200"
}
auto_auth {
method "kubernetes" {
mount_path = "auth/kubernetes"
config = {
role = "crawlab-worker"
}
}
sink "file" {
config = {
path = "/home/vault/.vault-token"
}
}
}
template_config {
static_secret_render_interval = "1m"
}
template {
destination = "/vault/secrets/config.env"
contents = <<EOT
{{- with secret "secret/data/crawlab/worker" -}}
{{- range $k, $v := .Data.data }}
{{ $k }}={{ $v }}
{{- end }}
{{- end }}
EOT
}
kubectl apply -f vault-agent-configmap.yaml
2、修改 crawlab-worker Deployment
在原有 Deployment 基础上新增 serviceAccountName、volumes、initContainers,以及给 worker 容器加 volumeMounts,再追加 vault-agent sidecar 容器:
---
apiVersion: v1
kind: Namespace
metadata:
name: crawlab
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: crawlab-worker
namespace: crawlab
spec:
replicas: 2
strategy:
type: Recreate
selector:
matchLabels:
app: crawlab-worker
template:
metadata:
labels:
app: crawlab-worker
spec:
serviceAccountName: crawlab-worker # 新增
imagePullSecrets:
- name: harbor-auth
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-role.kubernetes.io/control-plane
operator: DoesNotExist
- key: node-role.kubernetes.io/master
operator: DoesNotExist
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: crawlab-worker
topologyKey: "kubernetes.io/hostname"
volumes: # 新增
- name: vault-secrets
emptyDir: {}
- name: vault-agent-config
configMap:
name: vault-agent-config
- name: vault-token
emptyDir: {}
initContainers: # 新增
- name: vault-agent-init
image: hashicorp/vault:1.21
args:
- agent
- -config=/vault/config/vault-agent-init.hcl
volumeMounts:
- name: vault-agent-config
mountPath: /vault/config
- name: vault-secrets
mountPath: /vault/secrets
- name: vault-token
mountPath: /home/vault
containers:
- name: worker
image: <Harbor地址>/crawlab-custom:latest
imagePullPolicy: Always
env:
- name: CRAWLAB_NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: CRAWLAB_NODE_MASTER
value: 'N'
- name: CRAWLAB_GRPC_ADDRESS
value: '<Master节点地址>:9666'
- name: CRAWLAB_FS_FILER_URL
value: 'http://<Master节点地址>:8080/api/filer'
- name: CRAWLAB_TASK_HANDLER_MAXRUNNERS
value: '12'
- name: CONSUL_HOST
value: '<Master节点地址>'
- name: CONSUL_PORT
value: '8500'
volumeMounts: # 新增
- name: vault-secrets
mountPath: /vault/secrets
# 新增:Vault Agent Sidecar,持续运行负责密钥续期
- name: vault-agent
image: hashicorp/vault:1.21
args:
- agent
- -config=/vault/config/vault-agent-sidecar.hcl
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 100m
memory: 128Mi
volumeMounts:
- name: vault-agent-config
mountPath: /vault/config
- name: vault-secrets
mountPath: /vault/secrets
- name: vault-token
mountPath: /home/vault
restartPolicy: Always
kubectl apply -f crawlab-worker-deployment.yaml
3、验证注入效果
# READY 列应为 2/2(worker + vault-agent),Init 容器完成后消失
kubectl get pod -n crawlab -l app=crawlab-worker
# 查看 Pod 列表并获取 Pod 名称
kubectl get pod -n crawlab -l app=crawlab-worker
# 查看 Init 容器日志,确认认证和模板渲染成功
kubectl logs -n crawlab <pod-name> -c vault-agent-init
正常的话可以看到:
[INFO] auth.handler: authenticating
[INFO] auth.handler: authentication successful, sending token to sinks
[INFO] template.server: template rendered successfully
[INFO] agent: exit_after_auth set, exiting
Pod 正常运行后,在 Crawlab 中新建一个爬虫,写一段读取注入文件并打印的脚本:
with open('/vault/secrets/config.env') as f:
print(f.read())
运行该爬虫,在日志中看到以下输出说明密钥注入成功:
test_message=hello-from-vault

主容器里的应用直接读 /vault/secrets/config.env 这个文件就行,完全不需要知道 Vault 的存在。
后续如果要换成真实业务密钥,只需要 vault kv patch 更新对应字段,Sidecar 会在 1 分钟内自动重新渲染文件。
4、添加新的密钥并验证
ConfigMap 里的模板已使用 range 动态遍历所有字段,后续无论增删多少字段,只需在 Vault 侧操作,不需要改 ConfigMap:
# vault kv patch 只追加/更新指定字段,已有字段不受影响
vault kv patch secret/crawlab/worker \
another_key="another-value"
# 确认两个字段都存在
vault kv get secret/crawlab/worker
等待约 1 分钟后,再次执行爬虫:

5、删除密钥并验证
删除某个字段
KV v2 把一个路径下的所有字段当作一个版本化的整体文档来存,没有”只删文档内某个字段”的原子操作。
想删字段,只能重写整个文档(只写保留的字段,不写的自然就没了):
# 只保留 test_message,不写 another_key 即为将其删除
vault kv put secret/crawlab/worker \
test_message="hello-from-vault"
# 确认只剩一个字段
vault kv get secret/crawlab/worker

删除整条密钥路径
KV v2 区分软删除(可恢复)和永久删除:
# 软删除:标记最新版本为已删除,可通过 vault kv undelete 恢复
vault kv delete secret/crawlab/worker
# 永久删除全部版本和元数据(不可恢复)
vault kv metadata delete secret/crawlab/worker
软删除后,Vault Agent 下一次轮询时读到已删除版本,with secret 条件不成立,模板渲染为空文件:
kubectl exec -n crawlab <pod-name> -c worker -- cat /vault/secrets/config.env
# (空输出)
生产环境删除密钥前,务必确认应用侧已做好降级处理,否则主容器读取到空配置文件会引发异常。
结束。