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 サブドメインからアクセスできるようにすることでした。サービス自体を直接 public にするのではなく、既存の internal ALB の後ろに置き、Kubernetes Ingress が Host header を見て該当 Service へ転送します。

最終的なヘルスチェックは次の形です。

https://app-api-dev.example.com/actuator/health/readiness

レスポンス:

{"status":"UP"}

以下は、実際に確認した順番での記録です。

最初の状態

既存のバックエンドには、すでにいくつかのサービスがありました。

サービスアクセス方法
Main APIdev API ドメイン経由
workerクラスター内部のみ
新しい app service別の dev API ドメインからアクセス

新サービスは独立した Spring Boot module で、ポートは 8082 です。Main API はクラスター内部で次の URL を使って呼び出します。

http://app-service:8082

そのため、最初にやるべきことは DNS ではありません。まず EKS 内でサービスを起動し、安定した Kubernetes Service 名を用意することです。

Step 1: ECR repository を追加する

CI が新しい image を push できるように、dev 環境の ECR repository list に 1 つ追加します。

ecr_repos = [
  "dev-api",
  "dev-app-service",
  "dev-worker-core",
  "dev-worker-growth"
]

dev ECR stack に対して plan を実行します。

AWS_PROFILE=<profile> terragrunt plan -no-color

重要なのは次の結果です。

Plan: 2 to add, 0 to change, 0 to destroy.

この結果により、Terraform の変更が ECR repository と lifecycle policy の追加だけで、既存サービスを変更しないことが分かります。

確認後に apply します。

AWS_PROFILE=<profile> terragrunt apply -auto-approve -no-color

Step 2: GitOps の deployment directory を追加する

既存環境は 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

Kubernetes Service も、Main API が期待する DNS 名に合わせます。

apiVersion: v1
kind: Service
metadata:
  name: app-service
spec:
  ports:
    - port: 8082
      targetPort: 8082

ここで Service port を安易に 80 にしないことが重要です。アプリケーション設定では次を呼び出します。

http://app-service:8082

Service が 80 だけを公開していると、Pod が起動していてもクラスター内部の呼び出しは失敗します。

Step 3: 既存の backend configuration を再利用する

最初は、新サービス用に別の Secrets Manager entry を作る案がありました。

dev/app-service/application-secrets.yaml

しかし Java 側の設定を確認すると、Main API と同じ runtime configuration を使う方が自然でした。ExternalSecret は別の Kubernetes Secret を作りますが、remoteRef は既存の backend configuration を参照します。

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 は同じ database variables を読みます。

spring:
  datasource:
    url: ${DB_URL}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}

これにより環境ごとの設定を重複させずに済み、Main API と新サービスの設定差分も小さくできます。

Step 4: GitLab CI を更新する

新しい module はコード repository に存在していましたが、CI は Main API と worker image だけを build していました。必要な変更は 3 つです。

  1. app service 用の ECR repository variable を追加する。
  2. Docker build と push を追加する。
  3. dev/app-service/kustomization.yaml の image tag も更新する。

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 更新にも新しい path を含めます。

update_newtag(
  f"{prefix}/app-service/kustomization.yaml",
  os.environ["APP_SERVICE_IMAGE"],
  tag
)

これを忘れると、ECR repository と Kubernetes manifest が正しくても、新しい image はデプロイされません。

Step 5: ArgoCD Application を作成する

app-of-apps ファイルに ArgoCD Application を 1 つ追加します。

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

適用後に ArgoCD と Kubernetes を確認します。

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

ここまでで、クラスター内部のサービスは動いています。

Step 6: ALB 経由で新しい Host を公開する

既存の dev API は internal ALB を使っていました。新サービスも同じ Ingress に host rule を 1 つ追加します。

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 が出ていれば OK です。

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 が揃っていることを確認できます。

Step 7: DNS record を違う場所に追加していた

一番誤解しやすかったのは DNS です。

最初は Route53 に次の record を追加しました。

app-api-dev.example.com CNAME <internal-alb-dns-name>

しかしローカルからのアクセスはまだ失敗しました。

Could not resolve host

まず名前解決を確認します。

dig +short app-api-dev.example.com

結果は空でした。

この時点で ALB や Kubernetes を疑い続けるのではなく、root domain の authoritative name server を確認します。

dig +short NS example.com

結果として、この domain は Route53 ではなく Cloudflare に委任されていました。

つまり、Route53 に同名の hosted zone があっても、それが実際の domain を管理しているとは限りません。Public DNS は authoritative NS の委任に従います。

正しい修正は Cloudflare に record を追加することでした。

Type: CNAME
Name: app-api-dev
Target: <internal-alb-dns-name>
Proxy status: DNS only

ここは必ず DNS only にします。internal ALB は Cloudflare の orange-cloud proxy では到達できません。

Step 8: internal ALB には internal network access が必要

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 内、またはその private address に route できる network 上にいる必要があります。Public machine からは、DNS が解決できても internal ALB には接続できません。

Step 9: 200、401、network failure を分ける

最終的な health check:

curl -i https://app-api-dev.example.com/actuator/health/readiness

レスポンス:

HTTP/2 200

本文:

{"status":"UP"}

root path:

curl -i https://app-api-dev.example.com/

レスポンス:

HTTP/2 401

これは service outage ではありません。リクエストはアプリケーションに届いており、認証が必要という意味です。

現象意味
Could not resolve hostDNS が解決できていない
Connection timed outinternal ALB への network path がない
502ALB が backend target に到達できない、または app が失敗している
401アプリケーションには届いているが認証が必要
/actuator/health/readinessUP を返すbackend readiness は正常

主な学び

ブラウザのエラーだけを見て推測しない方がよいです。より安定した順序は次の通りです。

  1. Pod、Service、ExternalSecret を確認する。
  2. Ingress が ALB rule を生成しているか確認する。
  3. Target group が healthy か確認する。
  4. Certificate が新しい host をカバーしているか確認する。
  5. Authoritative DNS と実際の record を確認する。

Route53 と Cloudflare が両方ある環境では、最初に authoritative DNS を確認するべきです。Route53 に record が見えることと、その record が使われていることは別です。

最終的な経路

動作する経路は次の通りです。

browser
  -> Cloudflare DNS (DNS only)
  -> internal ALB
  -> Kubernetes Ingress host rule
  -> app-service Service:8082
  -> app-service Pod:8082

Health check:

{"status":"UP"}

ここまで確認できれば、backend path は通っています。Business endpoint が 401 を返す場合、次に見るべきなのは認証、token、authorization scope であり、ALB や DNS ではありません。