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 サブドメインからアクセスできるようにすることでした。サービス自体を直接 public にするのではなく、既存の internal ALB の後ろに置き、Kubernetes Ingress が Host header を見て該当 Service へ転送します。
最終的なヘルスチェックは次の形です。
https://app-api-dev.example.com/actuator/health/readiness
レスポンス:
{"status":"UP"}
以下は、実際に確認した順番での記録です。
最初の状態
既存のバックエンドには、すでにいくつかのサービスがありました。
| サービス | アクセス方法 |
|---|---|
| Main API | dev 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 つです。
- app service 用の ECR repository variable を追加する。
- Docker build と push を追加する。
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 host | DNS が解決できていない |
Connection timed out | internal ALB への network path がない |
502 | ALB が backend target に到達できない、または app が失敗している |
401 | アプリケーションには届いているが認証が必要 |
/actuator/health/readiness が UP を返す | backend readiness は正常 |
主な学び
ブラウザのエラーだけを見て推測しない方がよいです。より安定した順序は次の通りです。
- Pod、Service、ExternalSecret を確認する。
- Ingress が ALB rule を生成しているか確認する。
- Target group が healthy か確認する。
- Certificate が新しい host をカバーしているか確認する。
- 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 ではありません。