bashupload.app 的 K8s 部署基建
本文记录 bashupload.app 在 Kubernetes 上的部署基建方案。
背景
bashupload.app 是一个基于 bashupload-r2 的快捷终端文件文件上传服务,最初部署在单台 VPS 上,随着用户量的增加,单机部署的局限性逐渐显现出来:
- 全球速度低:单机部署在某一地区,其他地区访问速度较慢。
- 高可用性不足:单点故障风险高,服务稳定性较差。
- 高流量风险:单机承载高流量时容易出现性能瓶颈,也容易受到云服务商的限制/风控。
为了解决这些问题,决定将 bashupload.app 部署在 Kubernetes 集群上,利用 K8s 的弹性伸缩和多节点分布式部署能力,实现全球加速和高可用性。
方案设计
节点信息
一台主节点,部署k3s server,仅仅用于集群管理,不承载业务流量。
多台工作节点,部署k3s agent,承载 bashupload.app 业务流量。
注意,部署k3s server时,会自动安装一个内置的 Traefik Ingress Controller,并且添加iptables转发规则,但由于本项目不使用中心 Ingress Controller 进行流量转发,因此不需要配置 Traefik。如果server上还跑着其他HTTP服务,会导致流量被发送到Traefik,返回404 NOT FOUND,需要禁用 Traefik:
1 2 3
| kubectl -n kube-system delete helmchart traefik traefik-crd 2>/dev/null kubectl -n kube-system delete svc traefik traefik-web-ui 2>/dev/null kubectl -n kube-system delete deploy traefik 2>/dev/null
|
节点与项目特点
节点来自不同云服务商账号和地区,没有服务商提供的VPC组网方案,需要自行组网
节点都包含自己的公网IP,可以直接对外提供服务;项目涉及文件上传,流量大,因此不需要也不应当使用中心 Ingress Controller 进行流量转发,否则会导致大量不必要的通信流量产生
节点稳定性低,如果单点长时间大流量,可能受到AWS风控而下线
节点资源有限,单节点配置大约为 2C2G,承载能力有限,应当采用轻量级的 K3s
每个节点上都应当运行一个 bashupload.app 实例,提供服务,所以采用 DaemonSet 进行部署
网络方案
由于节点分布在不同云服务商账号和地区,且 Lightsail 没有VPC组网方案,传统的K8s集群网络方案(如Calico、Flannel等)无法使用。为此,采用 Zerotier 进行节点间的 VPN 组网,实现跨地域、跨服务商的集群网络连接。
细节可参见:使用 Zerotier 构建跨云服务商的 K8s 集群网络
对于公网入口,每个节点都直接暴露自己的公网IP,使用多条 DNS A记录,均摊流量到不同的节点,使用 ClusterIP hostPort 和 nginx sidecar 反代方式对外提供 HTTP 和 HTTPS 服务,避免了中心 Ingress Controller 带来的流量瓶颈和额外成本。
如果可以使用GeoDNS,则可以智能匹配较近的节点,进一步提升全球访问速度。
部署架构
部署架构设计如下:

- 外部接入层 (Internet & Public DNS)
- 外部用户通过域名访问服务。
- 图顶部的“Public DNS”配置了 多 A 记录轮询 (Round Robin DNS)。这意味着同一个域名被解析到三个不同的 Worker 节点的公网 IP 地址上。
- 用户的请求流量(绿色箭头)会被 DNS 服务器以轮询的方式分发到三个 Worker 节点中的任意一个。这提供了最基础的网络层负载均衡和高可用性——即使某个节点宕机,DNS 仍会将流量导向其他健康的节点(依赖于客户端或 DNS 解析器的重试机制)。
- K3s 集群数据平面 (Worker Nodes)
集群包含三个 Worker 节点,它们是处理实际业务流量的地方。
- 节点间通信 (WireGuard隧道):
- 三个 Worker 节点分布在公网环境中(拥有各自的公网 IP)。
- 为了安全地进行 Kubernetes 内部通信(例如 Pod 跨节点访问、CNI 网络流量、集群控制信号),节点之间建立了一个全互联的 WireGuard VPN 隧道网格。这确保了节点间的流量是加密的,不会在公网上明文传输。
- 应用部署方式 (DaemonSet):
- 业务应用以 DaemonSet 的方式部署。这保证了集群中的每一个 Worker 节点上都会运行且仅运行一个同样的 Pod 副本。
- 流量处理 Pod (Nginx + App):
- 每个 DaemonSet Pod 内部包含两个容器(Sidecar 模式):
- Nginx 容器(入口与 TLS 终止):
- hostPort:Nginx 并没有通过常规的 Kubernetes Service (如 NodePort 或 LoadBalancer) 暴露服务,而是直接使用了 hostPort 80/443。这意味着 Nginx 容器直接监听宿主机(Worker 节点)网络接口的 80 和 443 端口。外部流量到达节点公网 IP 的这两个端口时,会直达这个 Nginx 容器。
- TLS 终止:Nginx 负责处理 HTTPS 加密流量,它解密传入的流量。为了实现这一点,它挂载了包含 SSL 证书的 Kubernetes Secret (wildcard-tls-secret)。
- 反向代理:解密后的 HTTP 流量通过 Pod 内部网络 (localhost/127.0.0.1) 转发给后端的 App 容器。
- App 容器 (业务逻辑):
- 实际的业务应用(图中为 bashupload),监听在本地的 3000 端口,只接收来自同一个 Pod 内 Nginx 的纯 HTTP 请求。
- 证书管理控制平面 (cert-manager Namespace)
这一部分负责自动化 PKI(公钥基础设施)生命周期管理,确保所有节点的 Nginx 都有有效的 SSL 证书。
- 核心组件 (cert-manager):
- 部署在独立的命名空间中,是整个证书自动化流程的大脑。
- 证书颁发者 (ClusterIssuer):
- 配置了一个集群级别的颁发者,类型为 ACME (用于对接 Let’s Encrypt)。
- 指定使用 DNS-01 挑战方式来验证域名的所有权。这是申请通配符证书的必要条件。
- 外部验证服务 (Cloudflare & Let’s Encrypt):
- Cloudflare API Secret:集群里存储了访问 Cloudflare API 的凭证。
- 验证流程 (紫色箭头路径):
- 当需要颁发或续期证书时,cert-manager 向 Let’s Encrypt 发起申请。
- Let’s Encrypt 要求在域名的 DNS 记录中添加特定的 TXT 记录以证明所有权。
- cert-manager 使用存储的 API 凭证,自动调用 Cloudflare 的接口,在 Cloudflare DNS 中添加相应的 TXT 验证记录。
- Let’s Encrypt 验证 Cloudflare 上的 TXT 记录成功后,签发证书。
- 证书存储与分发 (wildcard-tls-secret):
- 签发成功的证书文件(tls.crt 和 tls.key)被保存在一个名为 wildcard-tls-secret 的 Kubernetes Secret 对象中。
- 分发 (黄色虚线箭头):由于 DaemonSet Pod 配置了挂载这个 Secret,Kubernetes 会自动将最新的证书文件投递并挂载到所有 Worker 节点上的 Nginx 容器中。当证书续期后,Secret 内容更新,Nginx 也能读取到新证书。
yaml 配置
需要安装 cert-manager 来管理 TLS 证书,使用 Cloudflare DNS-01 验证方式从 Let’s Encrypt 获取通配符证书。
1
| kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml
|
cloudflare-dns-token-secret.yaml
1 2 3 4 5 6 7 8
| apiVersion: v1 kind: Secret metadata: name: cloudflare-api-token-secret namespace: cert-manager type: Opaque stringData: api-token: "<your_cloudflare_api_token>"
|
cluster-issuer.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt-prod spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: example@example.com privateKeySecretRef: name: letsencrypt-prod-account-key solvers: - dns01: cloudflare: apiTokenSecretRef: name: cloudflare-api-token-secret key: api-token selector: dnsZones: - "bashupload.app"
|
bashupload-wildcard-cert.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13
| apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: wildcard-cert namespace: bashupload spec: secretName: wildcard-tls-secret issuerRef: name: letsencrypt-prod kind: ClusterIssuer dnsNames: - "bashupload.app" - "*.bashupload.app"
|
bashupload-deployment.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
| apiVersion: v1 kind: Namespace metadata: name: bashupload --- apiVersion: v1 kind: Secret metadata: name: bashupload-secret namespace: bashupload type: Opaque stringData: R2_ACCOUNT_ID: "<your_r2_account_id>" R2_ACCESS_KEY_ID: "<your_r2_access_key_id>" R2_SECRET_ACCESS_KEY: "<your_r2_secret_access_key>" R2_BUCKET_NAME: "bashupload" --- apiVersion: v1 kind: ConfigMap metadata: name: bashupload-config namespace: bashupload data: MAX_UPLOAD_SIZE: "5368709120" MAX_AGE: "3600" MAX_AGE_FOR_MULTIDOWNLOAD: "86400" ENABLE_SHORT_URL: "false" ALLOW_LIFETIME_OVER_MAX_AGE: "false" SHORT_URL_SERVICE: "https://suosuo.de/short" NO_CLEANUP: "true" --- apiVersion: v1 kind: ConfigMap metadata: name: bashupload-nginx-conf namespace: bashupload data: nginx.conf: | worker_processes auto; events { worker_connections 1024; } http { sendfile on; client_max_body_size 5G;
server { listen 80; listen [::]:80; listen 443 ssl http2; listen [::]:443 ssl http2; server_name _;
ssl_certificate /etc/nginx/certs/tls.crt; ssl_certificate_key /etc/nginx/certs/tls.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m;
location = /servads.txt { default_type text/plain; return 200 'S6GVLTLHQPLW1RLTWUKOY3I8JI46Y5HO'; }
location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_request_buffering off; } } } --- apiVersion: apps/v1 kind: DaemonSet metadata: name: bashupload namespace: bashupload spec: selector: matchLabels: app: bashupload template: metadata: labels: app: bashupload spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: NotIn values: - server6127 containers: - name: nginx image: nginx:1.27-alpine ports: - name: http containerPort: 80 hostPort: 80 - name: https containerPort: 443 hostPort: 443 volumeMounts: - name: nginx-conf mountPath: /etc/nginx/nginx.conf subPath: nginx.conf - name: tls-cert mountPath: /etc/nginx/certs readOnly: true livenessProbe: httpGet: path: /servads.txt port: http initialDelaySeconds: 10 periodSeconds: 20 timeoutSeconds: 5 failureThreshold: 3 readinessProbe: httpGet: path: /servads.txt port: http initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 - name: bashupload image: dulljz/bashupload-go:dev ports: - containerPort: 3000 name: app env: - name: PORT value: "3000" envFrom: - secretRef: name: bashupload-secret - configMapRef: name: bashupload-config resources: requests: cpu: "0.5" memory: "128Mi" livenessProbe: httpGet: path: / port: app initialDelaySeconds: 40 periodSeconds: 30 timeoutSeconds: 10 failureThreshold: 3 readinessProbe: httpGet: path: / port: app initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 volumes: - name: nginx-conf configMap: name: bashupload-nginx-conf - name: tls-cert secret: secretName: wildcard-tls-secret restartPolicy: Always --- apiVersion: v1 kind: Service metadata: name: bashupload namespace: bashupload spec: type: ClusterIP selector: app: bashupload ports: - name: http port: 80 targetPort: http - name: https port: 443 targetPort: https
|
效果

查看详情可以看到,访问流量通过 DNS 轮询分发到了不同的节点IP上,达到了预期的负载均衡效果。