マイクロサービス開発におけるKubernetesを活用したローカル結合テスト環境の検証

はじめに

 こんにちは! アスクルのがっしーです!
私は2025年6月より入社しまして、「ASKULの注文領域に関するバックエンド機能開発を担当するチーム」に所属しています。

 普段は次のような業務を行なっています。

  • (1)新機能の要件定義を業務担当チームの皆さんと進めていく
  • (2)開発成果物のレビュー、実装サポート、課題解決をする
  • (3)業務効率化の企画・推進

 今回の本記事では、「(3)業務効率化の企画・推進」として結合テストに関する悩みから始まった部分をご紹介します。

開発成果物をテストするときの悩み…

 当社では、サイトを構成する各機能をマイクロサービスとして開発しています。各機能のテストを行うにあたって、自チームの担当ではないAPIをコールすることも発生します。

 このとき、結合テストの段階で課題が発見されるケースがあります。

  • 結合テスト環境での検証が必要:他チームのAPIと連携するテストは、専用の結合テスト環境にデプロイしないと実行できない
  • 課題発見が遅れる:結合テスト環境の準備や調整に時間を要するため、本来であればもっと早い段階で発見できる課題が後回しになってしまう
  • デバッグ効率の低下:問題が発生した際に、ローカル環境での迅速なデバッグができない

 もし、ローカルでも結合テスト環境を再現できるのであれば、複数のチームが開発するサービスをまとめて検証でき、課題の早期発見が可能になります。また、結合テスト環境の準備に時間を要している間にも、ローカルで手軽に多数のサービスを先に検証できれば、開発サイクル全体の効率化につながるのでは? と考えることがありました。

 しかし、マイクロサービスとして開発する以上、同一ホスト(同じIPアドレス)上でポートが重複してしまい、複数のサービスをPC1台の上では同時起動できない問題が生じます。普段の開発ではDocker Desktopを使用しています。希望の環境を構築するために、今回はKubernetesを用いて検証してみることにしました。

Kubernetesとは

 Kubernetesは、コンテナオーケストレーションツールです。Docker等で作成したコンテナを複数のマシン上で自動的に管理・運用するためのプラットフォームです。

kubernetes.io

主な特徴は次のとおりです。

  • コンテナの自動配置: 複数のコンテナを効率的にマシンに配置
  • 負荷分散: トラフィックを複数のコンテナに分散
  • 自動復旧: 障害が発生したコンテナを自動的に再起動
  • スケーリング: 負荷に応じてコンテナ数を自動調整
  • 名前空間(namespace): リソースを論理的に分離する仕組み

 今回の検証では、特に名前空間(namespace)に注目しています。これにより、同じクラスタ内でも異なるチーム(namespace)が独立してサービスを運用でき、ポート重複の問題を解決できます。

具体的に直面していた課題

 改めて整理すると、PC1台の範囲でテストを行うにあたって、次のような悩みがありました。

  • 複数チームが異なるAPIサーバを開発している
  • 各チームのAPIサーバで同じポート(8080, 8081など)を使用している
  • 多段API呼び出しでAPIサーバのポートが重複している:「フロントサーバ → APIサーバA → APIサーバB」の経路で発生
  • ローカル環境での結合テストが困難である:ポート重複により同時起動不可
  • マイクロサービスの増加により、ポート管理が複雑化している:現実的な管理が困難

検証システム構成

 普段の開発で使用しているDocker Desktop上でKubernetesを有効化しています。なお、本記事ではクラスタの詳しい構築の仕方は割愛しています。

┌─────────────────────────────────────────────────────────┐
│                Docker Desktop                           │
│  ┌───────────────────────────────────────────────────┐  │
│  │                Kubernetes Cluster                 │  │
│  │  ┌─────────────────┐  ┌─────────────────────────┐ │  │
│  │  │   namespace     │  │      namespace          │ │  │
│  │  │     ns-a        │  │        ns-b             │ │  │
│  │  │ ┌─────────────┐ │  │  ┌─────────────────────┐│ │  │
│  │  │ │Front Server │ │  │  │   API Server B      ││ │  │
│  │  │ │(Port 8080)  │ │  │  │   (Port 8081)       ││ │  │
│  │  │ └─────────────┘ │  │  │   (gRPC 9092)       ││ │  │
│  │  │ ┌─────────────┐ │  │  └─────────────────────┘│ │  │
│  │  │ │API Server A │ │  │                         │ │  │
│  │  │ │(Port 8081)  │ │  │                         │ │  │
│  │  │ └─────────────┘ │  │                         │ │  │
│  │  └─────────────────┘  └─────────────────────────┘ │  │
│  └───────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘

 ここでは、API Server AとBのPortで使用しているポートが8081を使用しています。ローカル環境で起動した場合は、どちらか一方のみしか起動できないため、クラスタ内部で別々のIPアドレスを付与することによって、ポート重複の問題を解決していきます。

プロジェクト構成

 今回はPoC段階のため、サンプルプロジェクトを新規で作っています。

MicroServiceIntegrationTest/
├── api-server-a/          # 自チームのAPIサーバ
├── api-server-b/          # 他チームのAPIサーバ
├── front-server/          # フロントサーバ
├── shared/                # 共通モジュール
├── k8s/                   # Kubernetesマニフェスト
│   ├── ns-a/              # namespace ns-a用
│   └── ns-b/              # namespace ns-b用
└── logs/                  # アプリケーションログ

 実際に動いている部分を視覚的に表現するために、ECサイトのカゴ画面をイメージしたモック画面を用意しています。 このモック画面上で、api-server-bからapi-server-aがユーザ情報を取得する、という機能を備えています。 1から構築するのは大変ですので、GitHub Copilot Agentを駆使して、サンプルプロジェクトの構築とk8s環境へ配備するマニフェストの作成を行なっています。

マイクロサービス間の通信フロー

 通信の流れは、「Front Server → API Server A → API Server B」とします。

  • (1)Front Server (ns-a): Webインタフェースを提供
  • (2)API Server A (ns-a): Front Serverからのリクエストを受け、gRPCクライアントとして動作
  • (3)API Server B (ns-b): gRPCサーバとして、ユーザサービスを提供

Kubernetesマニフェストの作成

 次のサンプルコードは、サンプル掲載用に一部のみを抜粋しています。

API Server A のデプロイ(ns-a)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server-a
  namespace: ns-a
spec:
  replicas: 1
  selector:
    matchLabels:
      app: api-server-a
  template:
    metadata:
      labels:
        app: api-server-a
    spec:
      containers:
        - name: api-server-a
          image: api-server-a:latest
          ports:
            - containerPort: 8081
          env:
            - name: GRPC_CLIENT_API_SERVER_B_ADDRESS
              value: 'static://api-server-b.ns-b.svc.cluster.local:9092'
            - name: SPRING_PROFILES_ACTIVE
              value: k8s

API Server B のデプロイ(ns-b)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server-b
  namespace: ns-b
spec:
  replicas: 1
  selector:
    matchLabels:
      app: api-server-b
  template:
    metadata:
      labels:
        app: api-server-b
    spec:
      containers:
        - name: api-server-b
          image: api-server-b:latest
          ports:
            - containerPort: 8081 # API Server Aと同じポート
            - containerPort: 9092 # gRPCポート
          env:
            - name: SPRING_PROFILES_ACTIVE
              value: k8s

デプロイと起動確認

 まず、Kubernetesクラスタにマイクロサービスをデプロイします。手順をシェルスクリプトにして定型作業にします。

  • (1)クラスタの状態確認
  • (2)イメージビルド
  • (3)成果物をクラスタへデプロイ
  • (4)ポートフォワード設定
  • (5)ステータス表示
$ ./deploy-k8s.sh

 ※ シェルスクリプトは少し長いため、一部主要な部分以外はコメント部のみ掲載しています。

#!/bin/bash

# Kubernetesクラスタへのデプロイスクリプト(テスト環境用)

set -e

echo "=== MicroService Integration Test - K8s デプロイ ==="
echo "kubectl port-forward を使用したテスト環境構築"
echo ""

# 関数定義
check_prerequisites() {
    # kubectl の確認
    # Docker の確認
    # Kubernetesクラスタの接続確認
}

build_images() {
    # Gradleビルド
    # Dockerイメージビルド
}

deploy_to_k8s() {
    echo "=== Kubernetesへのデプロイ ==="
    # 既存のリソースがあれば削除
    echo "既存のリソースをクリーンアップ中..."
    kubectl delete -f k8s/ns-b/deployment.yaml --ignore-not-found=true
    kubectl delete -f k8s/ns-a/deployment.yaml --ignore-not-found=true

    # 少し待機
    sleep 5

    # デプロイ実行
    echo "リソースをデプロイ中..."
    kubectl apply -f k8s/ns-b/deployment.yaml
    kubectl apply -f k8s/ns-a/deployment.yaml

    # デプロイ完了まで待機
    echo "デプロイ完了を待機中..."
    kubectl wait --for=condition=ready pod -l app=api-server-b -n ns-b --timeout=120s
    kubectl wait --for=condition=ready pod -l app=api-server-a -n ns-a --timeout=120s
    kubectl wait --for=condition=ready pod -l app=front-server -n ns-a --timeout=120s

    echo "デプロイ完了"
}

setup_port_forward() {
    echo ""
    echo "=== kubectl port-forward 設定 ==="

    # 既存のport-forwardプロセスを停止
    echo "既存のport-forwardプロセスを停止中..."
    pkill -f "kubectl.*port-forward" || true

    # PIDファイルのクリーンアップ
    rm -f logs/k8s-port-forward-*.pid

    # ログディレクトリ作成
    mkdir -p logs

    echo "port-forwardを開始中..."

    # Front Server (8080)
    kubectl port-forward -n ns-a service/front-server 8080:8080 > logs/k8s-port-forward-front.log 2>&1 &
    FRONT_PID=$!
    echo $FRONT_PID > logs/k8s-port-forward-front.pid

    # port-forwardの開始を待機
    sleep 3

    echo ""
    echo "Port-forward設定完了:"
    echo "  Front Server PID: $FRONT_PID (http://localhost:8080)"
    echo ""
    echo "ログファイル:"
    echo "  Front Server: logs/k8s-port-forward-front.log"
    echo "  API Server A: logs/k8s-port-forward-api-a.log"
    echo "  API Server B: logs/k8s-port-forward-api-b.log"
}

show_status() {
    echo ""
    echo "=== デプロイ状況 ==="

    echo "Pods状況:"
    kubectl get pods -n ns-a
    kubectl get pods -n ns-b

    echo ""
    echo "Services状況:"
    kubectl get services -n ns-a
    kubectl get services -n ns-b
}

# メイン処理
main() {
    case "${1:-deploy}" in
        "deploy"|"")
            check_prerequisites
            build_images
            deploy_to_k8s
            setup_port_forward
            show_status
            ;;
        *)
            exit 1
            ;;
    esac
}

main "$@"

 デプロイ後のクラスタ状況を確認していきます。
※ 下記は一例です。IPアドレスはデプロイの度に違う値が割り振られます。

$ kubectl get svc -A
NAMESPACE     NAME            TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                  AGE
default       kubernetes      ClusterIP   10.96.0.1       <none>        443/TCP                  2d
kube-system   kube-dns        ClusterIP   10.96.0.10      <none>        53/UDP,53/TCP,9153/TCP   2d
ns-a          api-server-a    ClusterIP   10.96.252.80    <none>        8081/TCP                 3m4s
ns-a          front-server    ClusterIP   10.96.6.156     <none>        8080/TCP                 3m4s
ns-b          api-server-b    ClusterIP   10.96.213.77    <none>        8081/TCP,9092/TCP        3m5s

$ kubectl get pods -A
NAMESPACE            NAME                                        READY   STATUS    RESTARTS      AGE
ns-a                 api-server-a-69868654dc-mhczz              1/1     Running   0             3m51s
ns-a                 front-server-779966f6cc-l6b2l              1/1     Running   0             3m51s
ns-b                 api-server-b-7b85cb995c-gxxql              1/1     Running   0             3m52s

 ここで、すべてのPod(Kubernetes上で動作するコンテナの最小単位)が Running 状態で正常に起動していることが確認できました。

ポートフォワードの設定

 Front Serverにアクセスするため、ポートフォワード(Kubernetesクラスタ内のサービスにローカルマシンからアクセスする仕組み)を設定します。

$ kubectl port-forward -n ns-a svc/front-server 8080:8080 &
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080

基本動作の確認

 ブラウザで http://localhost:8080/ にアクセスすると、マイクロサービス間連携が正常に動作することが確認できました!

01_PoCサイト_ユーザ取得成功

API Server AとAPI Server Bが両方ともポート8081を使用していても、正常に動作しました!

正常動作の理由

  • コンテナ分離: 各コンテナが独立したネットワーク空間をもつ
  • namespace による組織的分離: ns-a(自チーム)と ns-b(他チーム)でチーム別に管理可能である
  • サービス解決: api-server-b.ns-b.svc.cluster.local:8081 の形式で異なるコンテナのポートにアクセス可能である
  • ポート分離の本質: 同一ホスト上で複数のコンテナが異なるIPアドレスをもつため、ポート重複が回避できる

障害耐性のテスト

 では、システムの障害耐性テストも試しに行ってみましょう。

# API Server Bを停止します
$ kubectl scale deployment api-server-b --replicas=0 -n ns-b

結果:「ユーザ取得中...」とエラーハンドリングができている、というような確認も可能です。

02_PoCサイト_ユーザ取得中

 API Server Bを復旧させるときは次のようなコマンドを実行します。

# API Server Bを復旧します
$ kubectl scale deployment api-server-b --replicas=1 -n ns-b

検証から得られた知見

1. Kubernetesのコンテナ分離機能

 同じポート番号でも、異なるコンテナ(異なるIPアドレス)であれば重複しない形でテストが可能になります。

  • チーム間でのポート調整が不要である
  • 既存のアプリケーションをそのまま利用可能である

2. namespace による組織的な分離

 namespace(Kubernetesリソースを論理的に分離する仕組み)を使用することで、次のようなメリットもあります。

  • チーム別の責任範囲を明確化できる
  • リソースの管理とアクセス制御を効率化する
  • 開発チーム間の独立性を確保できる

3. 開発生産性の向上

 ローカル開発環境でも柔軟なテストが可能であると確認できましたので、実際に開発している環境でも試してみたいですね!

  • ローカル環境での結合テストが可能である
  • チーム間調整の負荷を軽減できる

まとめ

 今回の検証により、Kubernetesを活用したローカル結合テスト環境の有効性を確認できました。
 そして、ここまでの構築・検証作業は、GitHub Copilot Agentを駆使して、スピーディーに確認できました。自分自身のやってみたいことを実現するため、AI Agentを今後とも駆使していきたいですね!

参考情報

  • 使用環境:Spring Boot, Kubernetes, Docker Desktop, GitHub Copilot Agent
  • 参考書籍:Kubernetesマイクロサービス開発の実践

book.impress.co.jp


最後まで読んでいただき、ありがとうございました!

ASKUL Engineering BLOG

2021 © ASKUL Corporation. All rights reserved.