cloud-sre

一次 EKS 新服务上线:从 GitOps 部署到 internal ALB 和 Cloudflare DNS 排查

记录一个 Spring Boot 服务在 EKS dev 环境上线的完整路径:ECR、GitLab CI、ArgoCD、Ingress、internal ALB、Route53 与 Cloudflare DNS 的排查。

Jun 18, 2026
AWSEKSALBRoute53CloudflareGitOpstroubleshooting

这是一篇部署记录,也是一篇 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 镜像。于是需要补三件事:

  1. 增加 app service 的 ECR repo 变量。
  2. 增加 Docker build 和 push。
  3. 在更新 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 hostDNS 没解析
Connection timed out网络到 internal ALB 不通
502ALB 到后端 target 不通,或应用异常
401请求到了应用,但接口需要认证
/actuator/health/readiness 返回 UP后端 readiness 正常

这次排查的核心经验

这类问题不要从浏览器错误开始猜。更稳的顺序是:

  1. 先确认 Pod、Service、ExternalSecret。
  2. 再确认 Ingress 是否生成了 ALB rule。
  3. 再确认 target group 是否 healthy。
  4. 再确认证书是否覆盖新域名。
  5. 最后确认 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。