cloud-sre
一次 EKS 新服务上线:从 GitOps 部署到 internal ALB 和 Cloudflare DNS 排查
记录一个 Spring Boot 服务在 EKS dev 环境上线的完整路径:ECR、GitLab CI、ArgoCD、Ingress、internal ALB、Route53 与 Cloudflare DNS 的排查。
这是一篇部署记录,也是一篇 DNS 排查记录。
目标是在已有 EKS dev 环境里新增一个 Spring Boot 服务,并通过一个新的 dev 子域名访问它。服务本身不直接暴露公网,而是挂在已有的 internal ALB 后面,由 Kubernetes Ingress 根据 Host header 转发到对应的 Service。
最终结果是:
https://app-api-dev.example.com/actuator/health/readiness
返回:
{"status":"UP"}
下面按实际排查顺序记录。
初始目标
已有后端系统里已经有几个服务:
| 服务 | 访问方式 |
|---|---|
| 主 API | 通过 dev API 域名访问 |
| worker | 只在集群内部运行 |
| 新 app service | 需要通过新的 dev API 域名访问 |
新服务是一个独立 Spring Boot module,端口是 8082。主 API 会通过集群内地址调用它:
http://app-service:8082
所以第一阶段不是先改 DNS,而是先让服务在 EKS 里跑起来,并保证集群内部服务名稳定。
第一步:新增 ECR 仓库
CI 需要把新服务镜像推到 ECR,所以先在 dev 环境的 ECR repo 列表里增加一项:
ecr_repos = [
"dev-api",
"dev-app-service",
"dev-worker-core",
"dev-worker-growth"
]
然后对 dev ECR stack 运行 plan:
AWS_PROFILE=<profile> terragrunt plan -no-color
确认结果只包含新增仓库和 lifecycle policy:
Plan: 2 to add, 0 to change, 0 to destroy.
这个结果很关键。它说明这次 Terraform 变更只会新增 ECR 资源,不会改动已有服务。
确认后 apply:
AWS_PROFILE=<profile> terragrunt apply -auto-approve -no-color
第二步:新增 GitOps 部署目录
已有环境使用 ArgoCD + Kustomize 管理应用部署,所以新服务也按同样模式新增一个目录:
dev/app-service/
application.yaml
external-secret.yaml
k8s-app-service-dev.yaml
kustomization.yaml
Deployment 里几个点需要和服务本身对齐:
containers:
- name: app-service
image: <ecr>/dev-app-service:dev-REPLACE_ME
ports:
- containerPort: 8082
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8082
Service 使用同名 DNS,方便主 API 调用:
apiVersion: v1
kind: Service
metadata:
name: app-service
spec:
ports:
- port: 8082
targetPort: 8082
这里不能把 Service port 随手改成 80。因为主 API 的配置里明确使用:
http://app-service:8082
如果 Service 只暴露 80,应用层调用就会失败。
第三步:复用已有后端配置
一开始我准备给 app service 单独建 Secrets Manager key,例如:
dev/app-service/application-secrets.yaml
后来确认这个 Java 项目的运行配置和主 API 一样,所以改为复用主 API 的配置文件。ExternalSecret 仍然生成独立 Kubernetes Secret,但 remoteRef 指向同一个 Secrets Manager key:
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: app-service-secrets
spec:
target:
name: app-service-secrets
data:
- secretKey: application.yaml
remoteRef:
key: dev/api/application-secrets.yaml
app-service 的 dev config 读取同一套变量:
spring:
datasource:
url: ${DB_URL}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
这比复制一份新 secret 更稳。数据库连接、账号、环境变量来源保持一致,后续维护成本也更低。
第四步:补 GitLab CI
新 module 已经在代码仓库里,但 CI 原来只构建主 API 和 worker 镜像。于是需要补三件事:
- 增加 app service 的 ECR repo 变量。
- 增加 Docker build 和 push。
- 在更新 GitOps image tag 时同时更新
dev/app-service/kustomization.yaml。
CI 中新增类似逻辑:
variables:
DEV_ECR_APP_SERVICE_REPO: "dev-app-service"
build_push:
script:
- docker build -f app-service-app/Dockerfile \
-t "$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$APP_SERVICE_REPO:$TAG" .
- docker push "$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$APP_SERVICE_REPO:$TAG"
GitOps tag 更新也要覆盖新路径:
update_newtag(
f"{prefix}/app-service/kustomization.yaml",
os.environ["APP_SERVICE_IMAGE"],
tag
)
否则 ECR 仓库和 Kubernetes manifest 都准备好了,最终还是没有新镜像被部署。
第五步:创建 ArgoCD Application
在 app-of-apps 文件里增加一个 ArgoCD Application:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: dev-app-service
spec:
source:
repoURL: <gitops-repo>
targetRevision: HEAD
path: dev/app-service
destination:
server: https://kubernetes.default.svc
namespace: dev
syncPolicy:
automated:
prune: true
selfHeal: true
应用后检查:
kubectl -n argocd get application dev-app-service
kubectl -n dev get deploy,svc,pod -l app=app-service -o wide
正常状态应该类似:
dev-app-service Synced Healthy
deployment/app-service 1/1
pod/app-service-... Running
service/app-service 8082/TCP
再确认 ExternalSecret:
kubectl -n dev get externalsecret app-service-secrets
期望状态:
SecretSynced True
到这里,集群内部服务已经完成。
第六步:通过 ALB 暴露新 Host
已有 dev API 使用一个 internal ALB。新服务也挂到同一个 Ingress 上,只新增一个 host rule:
rules:
- host: api-dev.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api
port:
number: 80
- host: app-api-dev.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app-service
port:
number: 8082
应用后确认 Ingress:
kubectl -n dev describe ingress dev-apps
重点看 Rules 里是否出现新 host:
app-api-dev.example.com
/ app-service:8082 (<pod-ip>:8082)
然后确认 ALB target group:
aws elbv2 describe-target-health \
--target-group-arn <app-service-target-group-arn>
期望结果:
State: healthy
这一步可以证明:Ingress rule、ALB listener rule、target group、Pod readiness 都没问题。
第七步:DNS 记录加错地方
最容易误判的是 DNS。
一开始在 Route53 里加了记录:
app-api-dev.example.com CNAME <internal-alb-dns-name>
但本机访问仍然报:
Could not resolve host
先查域名:
dig +short app-api-dev.example.com
没有任何结果。
这时不要继续查 ALB,也不要怀疑 Kubernetes。应该先查根域名的权威 DNS:
dig +short NS example.com
结果发现根域名的权威 DNS 不在 Route53,而在 Cloudflare。
这意味着:即使 Route53 控制台里有一个同名 hosted zone,也不代表它对当前域名生效。公网解析只会去权威 NS 指向的 DNS 服务查记录。
正确做法是在 Cloudflare 里添加记录:
Type: CNAME
Name: app-api-dev
Target: <internal-alb-dns-name>
Proxy status: DNS only
这里必须是 DNS only。internal ALB 不能通过 Cloudflare 橙云代理访问。
第八步:internal ALB 只能在内网访问
DNS 生效后,再查:
dig +short app-api-dev.example.com
应该返回:
<internal-alb-dns-name>.
10.x.x.x
10.x.x.x
看到 10.x.x.x 是正常的,因为 ALB 是 internal。
这也意味着:只有在 VPN、VPC 或能访问该私网网段的网络里才能访问。公网机器即使解析到了这个域名,也连不上。
第九步:区分 200、401 和网络失败
最终验证健康检查:
curl -i https://app-api-dev.example.com/actuator/health/readiness
返回:
HTTP/2 200
正文:
{"status":"UP"}
访问根路径:
curl -i https://app-api-dev.example.com/
返回:
HTTP/2 401
这不是服务不可用,而是应用鉴权生效。判断时要区分:
| 现象 | 含义 |
|---|---|
Could not resolve host | DNS 没解析 |
Connection timed out | 网络到 internal ALB 不通 |
502 | ALB 到后端 target 不通,或应用异常 |
401 | 请求到了应用,但接口需要认证 |
/actuator/health/readiness 返回 UP | 后端 readiness 正常 |
这次排查的核心经验
这类问题不要从浏览器错误开始猜。更稳的顺序是:
- 先确认 Pod、Service、ExternalSecret。
- 再确认 Ingress 是否生成了 ALB rule。
- 再确认 target group 是否 healthy。
- 再确认证书是否覆盖新域名。
- 最后确认 DNS 权威 NS 和实际记录。
尤其是 Route53 和 Cloudflare 同时存在时,必须先确认根域名的权威 DNS。Route53 控制台里有记录,不代表这个记录正在被使用。
最终状态
最终链路是:
browser
-> Cloudflare DNS (DNS only)
-> internal ALB
-> Kubernetes Ingress host rule
-> app-service Service:8082
-> app-service Pod:8082
健康检查返回:
{"status":"UP"}
这说明后端链路已经打通。后续如果业务接口返回 401,应该进入认证、token、权限范围的排查,而不是继续排查 ALB 或 DNS。