マイクロサービス戦略の実践:BFF・API管理・認証基盤を支える技術スタック(AWS, Keycloak, gRPC, Kafka)

  • aws
    aws
  • expressjs
    expressjs
  • keycloak
    keycloak
  • grpc
    grpc
  • kafka
    kafka
  • rabbitmq
    rabbitmq
  • kong
    kong
2024/03/22に公開

はじめに

近年、多くの企業がマイクロサービスアーキテクチャを採用し、システムのスケーラビリティや開発効率を向上させています。しかし、単にサービスを分割するだけではなく、適切な API 設計、通信方法の選定、認証・認可の統合、非同期処理の最適化 など、多くの課題に対応する必要があります。

本記事では、BFF(バックエンド・フォー・フロントエンド)の導入、API Gateway を活用したエンドポイント管理、gRPC によるサービス間通信、Kafka や RabbitMQ を用いた非同期メッセージング、Keycloak を利用した認証基盤の整備、Swagger を活用した API ドキュメントの標準化 など、実際のシステム設計で活用できる技術を紹介します。

マイクロサービスを導入・運用する際の課題と、その解決策を具体的なサンプルとともに解説していきます。これからマイクロサービス化を進める方や、より洗練された設計を目指すエンジニアの方にとって、役立つ内容になれば幸いです。

マイクロサービス化戦略の策定

マイクロサービスアーキテクチャを導入することで、スケーラビリティや開発スピードの向上が期待できます。しかし、適切な戦略なしに導入すると、運用や管理の複雑性が増し、逆に開発効率を下げる可能性もあります。

マイクロサービスアーキテクチャの概要

マイクロサービスアーキテクチャ(MSA: Microservices Architecture)は、単一の大きなシステム(モノリス)を複数の独立したサービスに分割するアーキテクチャスタイルです。各サービスは独立してデプロイ可能であり、異なる技術スタックを採用できます。

  • メリット
    • 開発スピードの向上
      • 各サービスが独立しているため、複数のチームが並行して開発可能。
    • スケーラビリティ
      • 必要なサービスだけをスケールアップ/ダウンできる。
    • 技術の多様化
      • サービスごとに最適な技術を選択可能(例: 一部はNode.js、別の部分はGoなど)。
  • デメリット
    • 運用管理の複雑化
      • サービス間通信(API GatewayやgRPCなど)の設計が必要。
    • データ管理の分散化
      • 各サービスがデータをどのように共有するかの戦略が必要。

マイクロサービス導入の目的を明確化

導入の目的を明確にすることで、適切な設計を行いやすくなります。

開発のスピード向上

  • 独立したデプロイが可能(CI/CDで自動化)

  • 異なるチームが並行して開発できる

  • 例: REST API を独立したサービスに分ける
    例えば、ECサイトの「注文管理」と「ユーザー管理」を分けることで、別々の開発チームが独立してリリースできます。

// orders-service (注文管理API)
import express from "express";

const app = express();
app.get("/orders", (req, res) => {
  res.json([{ id: 1, item: "Laptop", status: "Shipped" }]);
});

app.listen(3001, () => console.log("Orders Service running on port 3001"));
// users-service (ユーザー管理API)
import express from "express";

const app = express();
app.get("/users", (req, res) => {
  res.json([{ id: 1, name: "John Doe" }]);
});

app.listen(3002, () => console.log("Users Service running on port 3002"));

スケーラビリティ

  • 負荷の高いサービスを個別にスケール可能(水平スケール)
  • 例: 注文処理APIのトラフィックが多いなら、注文サービスのみスケールアップ

Kubernetesの例

Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: orders-service
spec:
  replicas: 2  # DesiredCapacityに相当(HPAが上書きする前の初期値)
  selector:
    matchLabels:
      app: orders
  template:
    metadata:
      labels:
        app: orders
    spec:
      containers:
        - name: orders
          image: ghcr.io/example/orders:1.2.3
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: "200m"
              memory: "256Mi"
            limits:
              cpu: "1"
              memory: "512Mi"
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8080
          livenessProbe:
            httpGet:
              path: /livez
              port: 8080

Service

apiVersion: v1
kind: Service
metadata:
  name: orders-service
spec:
  selector:
    app: orders
  ports:
    - port: 80
      targetPort: 8080
  type: ClusterIP

HPA

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: orders-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: orders-service
  minReplicas: 1   # MinSize
  maxReplicas: 5   # MaxSize
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70  # 例: CPU平均70%を目標
  • DesiredCapacity → Deployment.spec.replicas(初期値)。実際の自動増減はHPAが担当。
  • MinSize/MaxSize → HPA.spec.minReplicas / maxReplicas。
  • LaunchTemplate → コンテナイメージ+resources設定(Pod仕様)。ノード側の起動定義はk8sではクラスタ/ノードプール側の責務。

技術の多様化

  • フロントエンドは Next.js(React)、バックエンドは NestJS(TypeScript)
  • 一部のサービスは Go や Python で実装可能
    • 例: ML処理は Python で、API は Go で開発
from flask import Flask, jsonify

app = Flask(__name__)

@app.route("/predict", methods=["GET"])
def predict():
    return jsonify({"prediction": "Positive"})

app.run(port=5000)

モノリスからの移行戦略

マイクロサービスをいきなり導入すると運用が難しくなるため、段階的な移行が重要です。

ストラングラーパターンの適用

  • 既存のモノリスシステムの一部をマイクロサービスに分割
  • 古いモノリスの機能を新しいマイクロサービスに徐々に移行

移行イメージ

  1. モノリスのAPIにProxyを追加
  2. 新しいマイクロサービスが処理を担当
  3. 段階的にモノリスを小さくする

データ管理戦略

  • モノリスの単一DB → マイクロサービスごとのDBへ移行
  • 各サービスごとに独立したデータストアを持つ(Database per service)

移行期間の策定

フェーズ 作業内容
1. 調査 既存のシステム分析
2. 分割設計 サービス分割計画
3. 実装 API開発、データ分割
4. 検証 負荷テスト、監視設定
5. 移行完了 本番環境での適用

BFFの導入計画

BFF(Backend for Frontend)は、フロントエンドごとに最適化されたバックエンドを用意するアーキテクチャパターンです。モバイルアプリ、Webアプリ、管理画面など、異なるクライアントごとに専用のAPIを設計できるため、開発の自由度が向上し、APIのレスポンスを最適化できます。

BFF導入のメリット

BFFを導入することで、以下のような利点が得られます。

  1. クライアントごとに最適化されたAPI設計
    • フロントエンドの要件に合わせたAPIを設計できるため、不要なデータを省略し、必要なデータを適切に提供できる。
    • モバイルアプリ用のAPIではデータサイズを最小化し、Webアプリ用のAPIではリッチなデータを提供するなど、用途に応じた最適化が可能。
  2. フロントエンドとバックエンドの独立性向上
    • フロントエンド側の変更が直接バックエンドの変更を強制しないため、開発の柔軟性が向上する。
    • BFFがクライアントとバックエンドの仲介役となるため、バックエンドのAPI変更の影響を吸収できる。
  3. バックエンドの負荷軽減
    • 複数のAPIリクエストをBFF側で集約し、バックエンドへのリクエスト回数を削減。
    • BFFでデータの加工やキャッシュを行うことで、バックエンドの負担を軽減できる。

BFFの実装ポイント

  1. GraphQL または REST API の採用検討
    • GraphQL の場合
      • クライアントが取得するデータを柔軟に指定でき、オーバーフェッチ(不要なデータ取得)やアンダーフェッチ(データ不足)を防げる。
      • BFFの役割として複数のバックエンドAPIを統合する際に適している。
    • REST API の場合
      • クライアントごとにエンドポイントを適切に設計すれば、シンプルなAPI管理が可能。
      • 既存のREST APIと連携しやすく、GraphQLに比べて導入コストが低い。
  2. API Gateway との連携
    • BFFをAPI Gatewayと組み合わせることで、セキュリティや認証を一元管理し、運用負担を減らせる。
    • API GatewayでBFFのリクエストルーティングを制御すれば、異なるフロントエンド向けのBFFを簡単に分けられる。
  3. キャッシュ戦略の導入
    • フロントエンドが頻繁にアクセスするデータをBFF側でキャッシュし、バックエンドへのリクエストを削減。
    • Redisやメモリキャッシュを活用することで、レスポンス速度を向上させる。

BFFを導入する際の注意点

  1. メンテナンス負担の増加
    • フロントエンドごとにBFFを設計すると、APIが増えすぎて管理が難しくなる可能性がある。
    • APIの設計を統一しつつ、共通化できる部分は共通化する工夫が必要。
  2. デプロイやスケールの考慮
    • フロントエンドごとに異なるBFFをデプロイする必要があるため、CI/CDの管理を適切に行うことが重要。
    • スケール戦略として、トラフィックの多いBFFを個別にスケールアップできる仕組みを検討する。

API Gateway を使用したエンドポイント管理の最適化

マイクロサービスアーキテクチャでは、多数のサービスがAPIを提供するため、API Gatewayを活用してエンドポイント管理を最適化することが重要です。

API Gateway の役割

API Gateway は、マイクロサービスアーキテクチャにおいて、クライアントと複数のマイクロサービスの間に立ち、エンドポイントの統合管理を行う役割を持ちます。これにより、複雑な API の管理をシンプルにし、セキュリティやパフォーマンスを向上させることができます。

  1. リクエストのルーティング
    • クライアントからのリクエストを適切なマイクロサービスへ転送する。
    • パスベース(例:/users は User サービスへ)やホストベース(例:api.example.com から異なるバックエンドへ)のルーティングが可能。
  2. 認証・認可の一元管理
    • API Gateway で認証(Authentication)を実施し、不正なリクエストをブロック。
    • OAuth2、JWT(JSON Web Token)、APIキー認証、IP 制限などを適用可能。
    • マイクロサービスごとに認証処理を実装する負担を軽減。
  3. レスポンスの集約(フェデレーション)
    • 複数のマイクロサービスからのレスポンスを統合し、一つのレスポンスとして返す。
    • 例えば、/user-info のリクエストで、ユーザー情報と注文履歴をまとめて取得できるようにする。
    • GraphQL のような API アプローチと組み合わせることで、フロントエンドのリクエスト回数を削減。
  4. 負荷分散
    • バックエンドサービスへの負荷を分散し、パフォーマンスを向上。
    • クライアントの接続数を制御し、オートスケールしやすい環境を提供。
  5. セキュリティ強化
    • DDoS 対策として、レートリミット(Rate Limiting)を設定可能。
    • CORS(Cross-Origin Resource Sharing)設定を管理し、外部からのリクエスト制限が可能。

API Gateway の実装ポイント

API Gateway の導入にあたって、適切なツールの選定や設定が重要になります。

API Gateway の選定

  • AWS API Gateway:
    • フルマネージドで、サーバーレスアーキテクチャ(Lambda 連携)に最適。
    • 認証(Cognito、IAM)、キャッシュ、レートリミットなどを統合可能。
  • NGINX:
    • 軽量で高速なリバースプロキシとして動作し、API Gateway の役割を果たせる。
    • OpenResty を使えば、さらに高度な制御が可能。

キャッシュ機能の活用

  • 頻繁にリクエストされるデータをキャッシュし、レスポンスタイムを短縮。
  • API Gateway のキャッシュ機能(AWS API Gateway のキャッシュ、Kong の Proxy Cache)を利用。
  • CDN(CloudFront, Fastly)と組み合わせると、さらにパフォーマンスが向上。

レートリミットの設定

  • 1秒間に一定回数以上のリクエストを受け付けないよう制限をかけ、DDoS 攻撃や過負荷を防ぐ。
  • 例: Kong では rate-limiting プラグイン、AWS API Gateway では「スロットリング」機能を使用。

ログと監視の導入

  • API Gateway を通るリクエストをログに記録し、異常を検出。
  • AWS CloudWatch、Datadog、Prometheus などを活用。
  • リクエストごとのレイテンシ(遅延時間)を計測し、ボトルネックを特定。

サービス分割とそのシーケンスの定義

マイクロサービスアーキテクチャでは、適切なサービスの分割が非常に重要です。分割が適切でないと、開発の複雑性が増し、システムの拡張性や運用が困難になる可能性があります。

サービス分割の基準

サービスを分割する際に考慮すべき主な基準としては下記が挙げられます。

  1. ビジネスドメインごとの分割(ドメイン駆動設計, DDD)
    • ドメイン駆動設計(DDD) の考え方を活用し、ビジネスのユースケースごとにサービスを分割するのが基本です。
    • 各サービスは 「境界づけられたコンテキスト(Bounded Context)」 を持ち、それぞれが独立したビジネスルールを管理します。
    • 例:
      • ECサイトの場合
        • 注文管理サービス
        • 在庫管理サービス
        • 決済サービス
        • ユーザー管理サービス
  2. データ所有権の明確化(1サービス1データ)
    • 1つのサービスが特定のデータを所有する形にすることで、データ整合性を管理しやすくする。
    • 「サービスAが書き込んだデータを、サービスBが直接更新できる」ような構造を避ける。
    • データが共有される場合は、API経由 や イベント通知 を活用する。
    • 例:
      • ユーザー管理サービス は ユーザー情報(email, addressなど)を管理
      • 注文管理サービス は 注文情報を管理 し、必要なときに ユーザー管理サービス に問い合わせる
  3. 開発チームごとの独立性
    • 各サービスが独立して開発・デプロイできる状態を作ることで、開発の生産性を向上 させる。
    • サービスごとに独立したリポジトリを持ち、異なる技術スタックを採用することも可能。
    • 例:
      • 注文管理サービス は Java + Spring Boot
      • 決済サービス は Node.js + Express
      • 分析サービス は Python + FastAPI

サービス間のデータ整合性

サービスを分割すると、データが分散するため、データの整合性を保つ方法が重要になります。

  1. イベント駆動アーキテクチャ(Event Sourcing)
    • 「あるデータの変更が発生したら、そのイベントを他のサービスに通知する」 方式。
    • Kafka, RabbitMQ, Amazon SNS/SQS などのメッセージキューを活用することが多い。
    • 例:
      • 注文管理サービス が「注文完了」のイベントを発行
      • 在庫管理サービス はそのイベントを受け取り、在庫を減らす
      • 決済サービス は注文を処理し、支払いを完了させる
  2. データの複製と同期戦略
    • データベースを共有せず、サービス間でデータの一部をコピーする戦略。
    • 最終的な整合性(Eventual Consistency) を考慮し、リアルタイムのデータ更新を行うか、バッチ処理を行うかを決める。
    • キャッシュやCQRS(Command Query Responsibility Segregation)を活用することも有効。
    • 例:
      • ユーザー管理サービス で変更があった場合、注文管理サービス に一部のデータを同期
      • RedisElasticsearch に一時的なキャッシュを保存し、レイテンシを減らす

各サービス間の通信方法の検証

マイクロサービスアーキテクチャでは、サービス間の通信方式を適切に選定することが非常に重要です。適切な通信方式を選ぶことで、システムのパフォーマンス、スケーラビリティ、耐障害性を向上させることができます。

通信方式の選定

サービス間通信の方式は、大きく分けて以下の 3 つに分類されます。

同期通信

クライアントがリクエストを送り、レスポンスを受け取るまで待機する方式。

  1. REST(HTTP API)
    • シンプルで広く採用されている
    • JSON や XML 形式でデータをやり取り
    • HTTP メソッド(GET, POST, PUT, DELETE など)を活用

Express + Axios(REST API)

user-service.ts
import express, { Request, Response } from "express";
import axios from "axios";

const app = express();
const PORT = 3000;

app.get("/users/:id", async (req: Request, res: Response) => {
    const { id } = req.params;

    try {
        // 別のマイクロサービス(order-service)にHTTPリクエスト
        const orderResponse = await axios.get(`http://localhost:4000/orders/${id}`);
        res.json({ userId: id, orders: orderResponse.data });
    } catch (error) {
        res.status(500).json({ error: "Failed to fetch orders" });
    }
});

app.listen(PORT, () => console.log(`User Service running on port ${PORT}`));
order-service.ts
import express, { Request, Response } from "express";

const app = express();
const PORT = 4000;

app.get("/orders/:userId", (req: Request, res: Response) => {
    const { userId } = req.params;
    const orders = [{ id: 1, item: "Laptop" }, { id: 2, item: "Phone" }];
    
    res.json(orders);
});

app.listen(PORT, () => console.log(`Order Service running on port ${PORT}`));
  • user-serciceからorder-serviceヘアクセス

gRPC

  • メリット: 高速な通信が可能(バイナリフォーマット)。
  • デメリット: 設定が少し複雑。
order.proto
syntax = "proto3";

package orderService;

service OrderService {
  rpc GetOrder (OrderRequest) returns (OrderResponse);
}

message OrderRequest {
  string id = 1;
}

message OrderResponse {
  string id = 1;
  string item = 2;
}

gRPC サーバー

order-service.ts
import grpc from "@grpc/grpc-js";
import protoLoader from "@grpc/proto-loader";

const PROTO_PATH = "order.proto";
const packageDefinition = protoLoader.loadSync(PROTO_PATH);
const orderProto = grpc.loadPackageDefinition(packageDefinition) as any;

const orders = [{ id: "1", item: "Laptop" }, { id: "2", item: "Phone" }];

const server = new grpc.Server();
server.addService(orderProto.orderService.OrderService.service, {
    GetOrder: (call: any, callback: any) => {
        const order = orders.find(o => o.id === call.request.id);
        callback(null, order || { id: "", item: "Not Found" });
    },
});

server.bindAsync("0.0.0.0:50051", grpc.ServerCredentials.createInsecure(), () => {
    console.log("gRPC Server running on port 50051");
});

gRPC クライアント

client.ts
import grpc from "@grpc/grpc-js";
import protoLoader from "@grpc/proto-loader";

const PROTO_PATH = "order.proto";
const packageDefinition = protoLoader.loadSync(PROTO_PATH);
const orderProto = grpc.loadPackageDefinition(packageDefinition) as any;

const client = new orderProto.orderService.OrderService("localhost:50051", grpc.credentials.createInsecure());

client.GetOrder({ id: "1" }, (err: any, response: any) => {
    if (err) console.error(err);
    else console.log(response);
});

非同期通信

サービスがリクエストを送信した後、レスポンスを待たずに処理を継続できる方式。
RabbitMQ と Kafka は、どちらも メッセージブローカー(Message Broker)として機能し、非同期メッセージ処理を実現します。しかし、それぞれの設計思想や用途が異なります。

RabbitMQ(メッセージキュー)

特徴

  • 軽量なメッセージキュー(Message Queue)システム
  • プロデューサー(送信側) がメッセージを送信し、コンシューマー(受信側) が受け取る
  • AMQP(Advanced Message Queuing Protocol) を使用
  • メッセージのルーティング や 優先度管理 が可能
  • エンタープライズ向けの柔軟なキュー制御

利用シーン

  • リクエスト・レスポンス型の処理
  • メッセージを確実に処理する必要がある場合
  • トランザクション管理が必要なワークフロー
  • 少量のメッセージを低遅延で送信する場合

RabbitMQ のサンプルコード(TypeScript)

プロデューサー

producer.ts
import amqplib from "amqplib";

async function sendToQueue() {
    const connection = await amqplib.connect("amqp://localhost");
    const channel = await connection.createChannel();
    const queue = "orderQueue";

    await channel.assertQueue(queue, { durable: true });
    channel.sendToQueue(queue, Buffer.from(JSON.stringify({ orderId: 123 })));

    console.log("Sent order 123 to queue");
    setTimeout(() => connection.close(), 500);
}

sendToQueue();

コンシューマー

consumer.ts
import amqplib from "amqplib";

async function receiveFromQueue() {
    const connection = await amqplib.connect("amqp://localhost");
    const channel = await connection.createChannel();
    const queue = "orderQueue";

    await channel.assertQueue(queue, { durable: true });

    channel.consume(queue, (msg) => {
        if (msg) {
            const order = JSON.parse(msg.content.toString());
            console.log("Received order:", order);
            channel.ack(msg);
        }
    });
}

receiveFromQueue();

Kafka

特徴

  • 大規模データストリーム処理向けの分散型メッセージブローカー
  • イベント駆動アーキテクチャ(EDA) に最適
  • ログベースのメッセージ保存
  • Pub/Sub モデルを採用
  • 超高スループット

利用シーン

  • イベント駆動アーキテクチャ(EDA)
  • ストリームデータ処理(ログ解析、リアルタイム処理)
  • マイクロサービス間の非同期通信
  • 分散システムでのデータ共有

Kafka のサンプルコード(TypeScript)

プロデューサー

import { Kafka } from "kafkajs";

const kafka = new Kafka({
    clientId: "order-service",
    brokers: ["localhost:9092"]
});

async function produceMessage() {
    const producer = kafka.producer();
    await producer.connect();
    await producer.send({
        topic: "order-events",
        messages: [{ value: JSON.stringify({ orderId: "123", status: "Created" }) }],
    });
    await producer.disconnect();
}

produceMessage();

コンシューマー

import { Kafka } from "kafkajs";

const kafka = new Kafka({
    clientId: "order-service",
    brokers: ["localhost:9092"]
});

async function consumeMessage() {
    const consumer = kafka.consumer({ groupId: "order-group" });
    await consumer.connect();
    await consumer.subscribe({ topic: "order-events", fromBeginning: true });

    await consumer.run({
        eachMessage: async ({ message }) => {
            console.log("Received:", message.value.toString());
        },
    });
}

consumeMessage();

複雑な認証・認可の統合

認証・認可の統合が必要な理由

マイクロサービスアーキテクチャにおいて、各サービスが独自に認証・認可を管理すると、以下のような問題が発生します。

  • 認証情報の分散管理
    • 各サービスが個別に認証機構を持つと、ユーザー情報の一貫性を保つのが難しくなる。
    • 例:あるサービスでパスワードを変更したのに、別のサービスでは反映されない。
  • 認可ルールの重複と管理の煩雑化
    • 各サービスが独自の認可ルールを実装すると、同じルールを複数のサービスで管理することになり、一貫性を保つのが難しい。
    • 例:あるサービスで「管理者のみ編集可能」というルールを変更したが、別のサービスでは古いルールのままになってしまう。
  • セキュリティリスク
    • 認証・認可の実装が統一されていないと、脆弱性のあるサービスを狙われる可能性が高まる。
    • 例:特定のマイクロサービスだけが古い認証方式(Basic 認証など)を使っており、攻撃のターゲットになる。

このような課題を解決するために、認証(Authentication)と認可(Authorization)を統一的に管理する仕組みが必要になります。

OAuth 2.0 + OpenID Connect の採用

OAuth 2.0 と OpenID Connect(OIDC)を組み合わせることで、安全かつシンプルに認証・認可を統一できます。

OAuth 2.0 の役割

OAuth 2.0 は、第三者アプリがリソースにアクセスする権限をユーザーから委譲して受け取るためのプロトコルです。これにより、ユーザーは自分の認証情報(ID・パスワード)をアプリに直接渡す必要がなく、トークンを使って安全に認可を行えます。
特に Authorization Code Flow(認可コードフロー) は、アクセストークンが直接ブラウザに渡らずサーバー経由で取得されるため、Web アプリやモバイルアプリで広く利用されています。

  • Authorization Code Flow(認可コードフロー)
    • Web アプリやモバイルアプリでよく利用される。
    • クライアント(アプリ)が ID プロバイダー(IdP)に認証を委託し、トークンを取得する。
    • メリット:認証情報(ユーザーの ID・パスワード)をアプリ側で扱わずに済むため、安全性が高い。

OpenID Connect(OIDC)の役割

OpenID Connect(OIDC)は、OAuth 2.0 に「ユーザーの認証機能」を拡張した仕様です。OIDC では ID トークンを通じてユーザーの識別情報を取得できるため、SSO(シングルサインオン)や標準化されたプロフィール情報の取得が可能になります。

  • シングルサインオン(SSO)の実現
    • ユーザーは 1 度ログインすれば、すべてのサービスにアクセス可能。
    • 例:Google アカウントで YouTube や Gmail にログインするイメージ。
  • 標準化されたユーザー情報の取得
    • OIDC では、ID トークンにユーザー情報(メールアドレス、名前など)が含まれるため、各サービスで統一的にユーザー情報を扱える。

導入する IdP(Identity Provider)の例

  • Keycloak
    • OSS で無料利用可能。
    • カスタマイズ性が高く、自社サービスに適した認証基盤を構築しやすい。
  • Auth0
    • クラウド型の認証プラットフォームで、簡単に導入可能。
    • ソーシャルログインや多要素認証(MFA)などの機能が豊富。
  • Okta
    • 大規模企業向けの認証基盤。
    • 高いセキュリティ要件に対応可能。

認可管理の一元化

認可を一元的に管理することで、各サービスが個別に認可ロジックを持つ必要がなくなり、一貫性を確保できます。
代表的な認可モデルとして、RBAC(ロールベースアクセス制御) と ABAC(属性ベースアクセス制御) があります。

RBAC(Role-Based Access Control)

  • ロール(役割) に基づいてアクセス制御を行う方式。
      • 「管理者」 はすべてのデータを編集可能。
      • 「一般ユーザー」 はデータの閲覧のみ可能。
  • メリット
    • 直感的でわかりやすい。
    • 企業の組織構造にマッチしやすい。
  • デメリット
    • 権限の細かい設定が難しい(柔軟性に欠ける)。
    • ユーザーごとのカスタム権限設定が難しい。

ABAC(Attribute-Based Access Control)

ユーザーやリソースの属性(例:部署、デバイスの種類、アクセス時間など)に基づいてアクセス制御を行う方式。

  • 「マーケティング部のメンバーは、営業データにはアクセス不可」
  • 「午前 9 時〜午後 6 時のみ特定の API にアクセス可能」

メリット

  • より細かいアクセス制御が可能。
  • ダイナミックなポリシー設定が可能。

デメリット

  • RBAC よりも設定が複雑

JWT(JSON Web Token)の活用

JWT は、認証・認可に関する情報を署名付きで安全にやり取りできるトークン形式。

  • 役割

    • RBAC/ABAC で使う「ロール」や「属性」情報を claim として保持できる。
    • 各マイクロサービスがこの情報を検証して、適切なアクセス制御を行う。
  • メリット

    • 改ざん防止(署名付き)
    • ステートレス認証(セッション管理不要)
    • 各サービス間で一貫した情報共有が可能
  • 使い方

    • ユーザーがログインすると、IdP から JWT(アクセストークンや ID トークン)を取得。
    • マイクロサービス間のリクエストに Authorization: Bearer <JWT> を付与。
    • 各サービスが JWT を検証し、RBAC や ABAC のルールに基づきアクセス制御を実施。
  • 実装例

    • RBACでの利用
    {
      "sub": "user-123",
      "roles": ["admin", "editor"],   // RBAC用: ユーザーのロール
      "exp": 1724150400
    }
    
    • ABAC での利用
    {
      "sub": "user-456",
      "department": "marketing",       // ABAC用: 部署
      "device": "mobile",              // ABAC用: デバイス属性
      "time": "2025-08-20T10:00:00Z"
    }
    

マイクロサービスのデプロイメント戦略の検討

マイクロサービスのデプロイメントは、スケール・デプロイ頻度・耐障害性 を考慮し、適切な戦略を選択することが重要です。本記事では、代表的な Blue-Green デプロイメント と Canary リリース、さらに GitOps の活用 について詳しく解説します。

Blue-Green デプロイメント

Blue-Green デプロイメントは、2つの環境(Blue / Green)を切り替えながらリリースする手法 です。本番環境 (Blue) を稼働させたまま、新バージョン (Green) をデプロイし、問題がなければトラフィックを切り替えます。

  • メリット
    • ダウンタイムなし → トラフィックの切り替えのみでリリース完了。
    • ロールバックが容易 → 問題発生時に旧バージョンへ即時戻せる。
    • 一貫した環境でのテスト → Green 環境で本番同様のテストが可能。
  • デメリット
    • 2つの環境を用意する必要がある → リソースコスト増。
    • データの互換性を考慮する必要がある → DB スキーマの変更時に注意。

Canary リリース

Canary リリースは、新バージョンへのトラフィックを段階的に増やす手法 です。初めは 5% のリクエストを新バージョンへ流し、問題がなければ 20% → 50% → 100% と段階的にリリースを進めます。

  • メリット
    • 段階的なリリースが可能 → 影響範囲を限定できる。
    • リアルなトラフィックで新バージョンを検証 → 実際のユーザー行動を見ながら安全に展開。
    • ロールバックが容易 → 問題が発生したら即座に旧バージョンへ戻せる。
  • デメリット
    • モニタリングが必須 → ログ収集、エラーレート監視が必要。
    • トラフィック制御が必要 → Istio や Argo Rollouts などのツールが必要。

GitOps の活用

GitOps は、Git リポジトリをデプロイメントの唯一のソースとする手法 です。ArgoCD や Flux を利用して、Git の変更がそのまま Kubernetes 環境に反映される ように構成します。

  • メリット
    • デプロイの一貫性を確保 → すべての変更が Git に記録されるため、環境差分を防げる。
    • 自動ロールバックが可能 → 問題が発生した場合、直前のコミットへ戻すだけでロールバックできる。
    • セキュリティ向上 → 手動での kubectl apply を排除し、不正な変更を防止。

サービス間のトランザクション管理

マイクロサービスアーキテクチャでは、各サービスが独立して動作し、データベースを分離していることが一般的です。そのため、単一のデータベースに対するトランザクションのように、簡単にACIDトランザクション(原子性、一貫性、分離性、持続性)を保証することができません。この問題に対処するためのアプローチについて詳しく解説します。

データ一貫性の問題

  • 異なるデータベースをまたぐトランザクション
    • 例: 注文サービス で注文を作成し、決済サービス で支払いを処理する。
    • 途中で 決済サービス が失敗すると 注文サービス に不整合が生じる(支払いが完了していないのに注文が確定)。
  • 分散トランザクションの管理が複雑
    • 一般的なデータベースのトランザクション機能(BEGIN、COMMIT、ROLLBACK)は1つのDB内でしか機能しない。
    • 2フェーズコミット(2PC) のような従来の分散トランザクション管理は複雑でパフォーマンスの問題もある。

Saga パターンの導入

Saga パターンは、分散システムでのデータ一貫性を確保するために使われるパターンです。2PC(2フェーズコミット)を避け、一連のローカルトランザクションの組み合わせ によってデータの整合性を担保します。

Saga の 2 つの種類

  1. Choreography(イベント駆動型)

    • 各サービスが他のサービスのイベントをリッスンし、自律的に次の処理を実行。
    • メリット:
      • 単純なフローの場合、管理コストが低い。
    • デメリット:
      • サービス間の連携が密接になり、変更が難しくなる(カスケード変更リスク)。
    • 適用例: 小規模なサービス、シンプルなワークフロー。
  2. Orchestration(管理型)

    • 中央の Saga Coordinator(オーケストレーター) が、各サービスに対する処理の制御を行う。
    • メリット:
      • ワークフローの可視化がしやすい。
      • 変更の影響範囲を局所化しやすい。
    • デメリット:
      • コーディネーターがボトルネックになる可能性がある。
    • 適用例: 複雑なワークフロー、大規模なサービス。

Saga パターンのサンプルコード(Orchestration)
以下のコードでは、注文の作成→決済の完了→在庫の確保を Saga オーケストレーターで管理しています。

import { Kafka } from "kafkajs";

const kafka = new Kafka({ clientId: "saga-coordinator", brokers: ["localhost:9092"] });
const producer = kafka.producer();
const consumer = kafka.consumer({ groupId: "order-group" });

async function startSaga() {
  await consumer.connect();
  await producer.connect();

  consumer.subscribe({ topic: "order_created", fromBeginning: true });

  consumer.run({
    eachMessage: async ({ topic, message }) => {
      const { orderId, userId, amount } = JSON.parse(message.value.toString());

      try {
        // Step 1: 決済処理
        await producer.send({ topic: "payment_request", messages: [{ value: JSON.stringify({ orderId, userId, amount }) }] });
        
        // Step 2: 在庫確保
        await producer.send({ topic: "inventory_reserve", messages: [{ value: JSON.stringify({ orderId }) }] });

        // Step 3: 注文確定
        await producer.send({ topic: "order_confirmed", messages: [{ value: JSON.stringify({ orderId }) }] });
      } catch (error) {
        console.error("Saga transaction failed, initiating rollback", error);
        
        // Step 4: ロールバック(補償トランザクション)
        await producer.send({ topic: "order_rollback", messages: [{ value: JSON.stringify({ orderId }) }] });
      }
    },
  });
}

startSaga();

API ドキュメンテーションの標準化と整備

API ドキュメンテーションの標準化は、開発チーム内外のコミュニケーションを円滑にし、保守性と拡張性を高めるために重要です。本記事では、API ドキュメンテーションの課題とその解決策について詳しく解説し、実装のサンプルも紹介します。

課題

  • API 仕様の統一がされていない
    • チームごとに異なる形式で API を設計すると、クライアント側の実装が煩雑になり、誤解やバグの原因になる。
  • バージョン管理が曖昧
    • API の変更に伴う影響範囲が不明確になり、既存の利用者に不具合を引き起こす可能性がある。

OpenAPI(Swagger)を活用した API ドキュメントの自動生成

OpenAPI(旧 Swagger)を活用することで、API 仕様を一元管理し、最新の状態に保つことが可能になります。

  • 手順
    • OpenAPI 仕様(openapi.yaml)を定義。
    • Swagger UI や Redoc を使用してドキュメントを自動生成。
    • CI/CD に組み込み、ドキュメントの最新化を自動化。
openapi: 3.0.0
info:
  title: Sample API
  version: 1.0.0
paths:
  /users:
    get:
      summary: ユーザー一覧の取得
      description: 登録されているすべてのユーザーを取得する
      responses:
        '200':
          description: 成功
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: integer
                    name:
                      type: string

Swagger UIのイメージ

Image from Gyazo

参考:
https://swagger.io/tools/swagger-ui/

API バージョニング戦略の策定

API のバージョニングが適切でないと、変更の影響を最小限に抑えることができず、クライアントの互換性が壊れてしまう可能性があります。以下の戦略を採用すると、スムーズなバージョン管理が可能です。

手法 メリット デメリット
URL ベース (/v1/users) シンプルでわかりやすい URL が変更されるため、クライアント側の修正が必要
ヘッダーベース (Accept: application/vnd.myapi.v1+json) エンドポイントが変わらない クライアント側でヘッダーを適切に管理する必要がある
リクエストパラメータ (?version=1) 柔軟なバージョン指定が可能 セキュリティの観点から適切な管理が必要

バージョン管理のサンプル

import express from "express";

const app = express();

app.get("/v1/users", (req, res) => {
  res.json({ message: "This is API v1" });
});

app.get("/v2/users", (req, res) => {
  res.json({ message: "This is API v2 with additional fields" });
});

app.listen(3000, () => console.log("API Server running on port 3000"));
  • /v1/users/v2/users で異なるレスポンスを返し、バージョニングの管理が容易になります。

さいごに

マイクロサービスアーキテクチャを成功させるには、単にサービスを分割するだけでなく、適切な技術選定と統合戦略が欠かせません。本記事では、BFF、API Gateway、gRPC、Kafka、RabbitMQ、Keycloak、Swagger などの技術を活用した具体的なアプローチを紹介しました。

導入する技術はプロジェクトの要件によって異なりますが、スケーラビリティ、セキュリティ、メンテナンス性を考慮した設計を行うことで、持続可能なシステムを構築することが可能 です。

これからマイクロサービスの導入を進める方は、自社の要件に合わせた技術選定を行い、適切なアーキテクチャを設計してください。本記事がその一助となれば幸いです。

Xでシェア
Facebookでシェア
LinkedInでシェア

記事に関するお問い合わせ📝

記事の内容に関するご質問、ご意見などは、下記よりお気軽にお問い合わせください。
ご質問フォームへ

技術支援などお仕事に関するお問い合わせ📄

技術支援やお仕事のご依頼に関するお問い合わせは、下記よりお気軽にお問い合わせください。
お問い合わせフォームへ

関連する技術ブログ