🎉

Kubernetes上の異なるPodにFastAPIサーバを立てて通信させてみた

に公開

今回は、Kubernetes上で異なるFastAPIサーバを立てて、片方のFastAPIサーバにアクセスすると別のFastAPIサーバにアクセスされる環境を作ってみました。まだマイクロサービスを構築した経験が少ないので、まずはシンプルな構成から始めてみます。

今回のシステム構成

今回は以下のような構成でシステムを構築します。

  • Google CloudのGKE上でシステムを構成
  • DockerイメージはArtifact Registryに保存
  • FastAPIサーバは二種類建てる
    • パブリックアクセスをするためのサーバ
    • パブリックサーバからアクセスされるが、クラスタ内に閉じているプライベートサーバ
  • ロードバランサーサービスを使ってパブリックサーバへアクセスする
  • パブリックサーバからプライベートサーバへのアクセスはClusterIPを利用

それでは実装していく

FastAPIサーバの用意

まずはパブリックサーバのコードとDockerfileを載せます。

public_server.py
import os
import requests
from fastapi import FastAPI

app = FastAPI()


PRIVATE_SERVER_ENDPOINT = os.environ["PRIVATE_SERVER_ENDPOINT"]
PRIVATE_SERVER_PORT = os.environ["PRIVATE_SERVER_PORT"]

@app.get("/hello")
def hello():
    print(f"http://{PRIVATE_SERVER_ENDPOINT}:{PRIVATE_SERVER_PORT}/hello")
    private_server_response = requests.get(
        f"http://{PRIVATE_SERVER_ENDPOINT}:{PRIVATE_SERVER_PORT}/hello"
    )
    return {
        "message_from_public_server": "Hello from public server",
        "message_from_private_server": private_server_response.json()["message"],
    }
Dockerfile_public
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim

WORKDIR /app
COPY . /app

EXPOSE 80
RUN uv sync

CMD ["uv", "run", "uvicorn", "public_server:app", "--host", "0.0.0.0", "--port", "80"]

パブリックサーバでは/helloに対してGETメソッドを投げると、パブリックサーバで作成したメッセージとプライベートサーバから取得したメッセージを返します。なお、プライベートサーバのエンドポイントとポートはConfigMap経由で環境変数として登録します。Dockerfile_publicではpublic_server.pyを起動してサーバを建てます。

次はプライベートサーバの実装を載せます。

private_server.py
import requests
from fastapi import FastAPI

app = FastAPI()


@app.get("/hello")
def hello():
    return {"message": "Hello from private server."}
Dockerfile_private
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim

WORKDIR /app
COPY . /app

EXPOSE 80
RUN uv sync

CMD ["uv", "run", "uvicorn", "private_server:app", "--host", "0.0.0.0", "--port", "80"]

プライベートサーバの実装も基本的にはパブリックサーバと同じです。/helloに対してGETすることでプライベートサーバで生成した情報を受け渡すことができます。

Kubernetesリソースの定義

まずはConfigMapの定義からです。ConfigMapで定義した情報をパブリックサーバのPodに対して環境変数として指示します。PRIVATE_SERVER_ENDPOINTにはプライベートサーバに対して用意するClusterIPの名前を、PRIVATE_SERVER_PORTは80で指定しております。

configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: env-config
data:
  PRIVATE_SERVER_ENDPOINT: "fastapi-private-server-cluster-ip"
  PRIVATE_SERVER_PORT: "80"

それでは次にPodの定義をします。順番にパブリックサーバ、プライベートサーバの定義となります。今回は両者ともレプリカ数は2つ、ポートは80番を利用します。またパブリックサーバでは先ほど適宜したConfigMapを環境変数として挿入するために、spec.template.spec.containers.envFromに指定しています。

public_server.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: fastapi-public-server-deployment
spec:
  replicas: 2
  selector:
    matchLabels:
      app: fastapi-public-server
  template:
    metadata:
      labels:
        app: fastapi-public-server
    spec:
      containers:
      - name: fastapi-public-server
        image: asia-northeast1-docker.pkg.dev/<PROJECT-ID>/fastapi-repo/fastapi-public-server:latest
        imagePullPolicy: Always
        ports:
        - containerPort: 80
        envFrom:
        - configMapRef:
            name: env-config
private_server.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: fastapi-private-server-deployment
spec:
  replicas: 2
  selector:
    matchLabels:
      app: fastapi-private-server
  template:
    metadata:
      labels:
        app: fastapi-private-server
    spec:
      containers:
      - name: fastapi-private-server
        image: asia-northeast1-docker.pkg.dev/<PROJECT-ID>/fastapi-repo/fastapi-private-server:latest
        imagePullPolicy: Always
        ports:
        - containerPort: 80

次にサービスの定義です。順番にClusterIP、LoadBalancerとなります。ポートは全て80番で統一しております。また、ClusterIPの名前はConfigMapで設定したPRIVATE_SERVER_ENDPOINTと一致させています。

private_cluster_ip.yaml
apiVersion: v1
kind: Service
metadata:
  name: fastapi-private-server-cluster-ip
spec:
  type: ClusterIP
  ports:
    - name: cluster-port
      port: 80
      targetPort: 80
  selector:
    app: fastapi-private-server
loadbalancer.yaml
apiVersion: v1
kind: Service
metadata:
  name: fastapi-public-server-load-balancer
spec:
  type: LoadBalancer
  selector:
    app: fastapi-public-server
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80

最後に、今回はkustomizeを利用して一つのリソースファイルに集約しました。

kustomization.yaml
resources:
  - configmap.yaml
  - public_server.yaml
  - private_server.yaml
  - private_cluster_ip.yaml
  - loadbalancer.yaml
kubectl kustomizee ./ > config.yaml

なお、kustomizeについてはこちらで解説しているのでぜひ参照ください。

https://zenn.dev/akasan/articles/7686dd6d542595

Google Cloudの設定

今回必要なのはArtifact RegistryとGKEになります。

まずは以下のコマンドでArtifact Registryを作成しました。

gcloud artifacts repositories create fastapi-repo --repository-format=docker --location=asia-northeast1

次に、以下のコマンドでクラスタを作成しました。

gcloud container clusters create fastapi-cluster --zone us-central1-a --num-nodes 2 --machine-type n1-standard-4

基本的な設定は以下と同じですね。

https://zenn.dev/akasan/articles/6618da4fe05367#google-cloudの設定

Dockerイメージの作成

今回は二つのDockerイメージを必要とするので、以下のようにして対応しました。

# ビルド
docker build -t fastapi-public-server -f Dockerfile_public . --platform linux/amd64
docker build -t fastapi-private-server -f Dockerfile_private . --platform linux/amd64

# タグづけ
docker tag fastapi-public-server asia-northeast1-docker.pkg.dev/<PROJECT-ID>/fastapi-repo/fastapi-public-server:latest
docker tag fastapi-private-server asia-northeast1-docker.pkg.dev/<PROJECT-ID>/fastapi-repo/fastapi-private-server:latest

# プッシュ
docker asia-northeast1-docker.pkg.dev/<PROJECT-ID>/fastapi-repo/fastapi-public-server:latest
docker asia-northeast1-docker.pkg.dev/<PROJECT-ID>/fastapi-repo/fastapi-private-server:latest

GKEへのデプロイ

いよいよ定義したリソースをデプロイしてみます。kustomizeを使って一つのファイルに定義を集約したので、以下のようにしてデプロイしました。

kubectl appy -f config.yaml

少し待つとデプロイが完了します。kdashを利用して確認すると以下のようになりました。Podがそれぞれ2つずつ作成されており、ClusterIP、LoadBalancerによるExternalIPの払い出しも確認できます。

また、Swaggerから実行すると以下のようにプライベートサーバからのメッセージを取得した上でパブリックサーバから返答がきていることを確認できました。

最後に、検証が終わったら環境を閉じます。以下のコマンドで環境をクローズします。

kubectl delete -f config.yaml
gcloud container clusters delete fastapi-cluster --zone us-central1-a

まとめ

今回は二つのFastAPIサーバを建てて相互に連携させてレスポンスを返す構成を作ってみました。だいぶKubernetesには慣れてきたので今回の範囲は割とすんなり対応できて成長を感じていますw。次回は今回作った環境のObservabilityを導入して、どのようにサービスが動いているか可視化してみたいと思っています。

Discussion