Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

This article is a Private article. Only a writer and users who know the URL can access it.
Please change open range to public in publish setting if you want to share this article with other users.

マイクロサービス

Posted at

第1章:基本概念の理解 - Go、gRPC、K8sの世界

1.1 なぜGoを選ぶのか?

Goの特徴とメリット

Goは2009年にGoogleで開発されたプログラミング言語である。マイクロサービスアーキテクチャにおいて、Goが選ばれる理由は以下の通りだ。

1. 高いパフォーマンス

  • コンパイル型言語:実行時のオーバーヘッドが少ない
  • ガベージコレクション:メモリ管理が自動化されている
  • 並行処理:ゴルーチンによる軽量な並行処理

2. シンプルな構文

  • 学習コストが低い:C言語ライクなシンプルな構文
  • 明確なエラーハンドリング:例外処理ではなく、明示的なエラー処理
  • パッケージ管理:go modulesによる依存関係管理

3. マイクロサービスに適した特性

  • 小さなバイナリサイズ:Dockerイメージが軽量になる
  • クロスコンパイル:異なるプラットフォーム向けビルドが容易
  • 標準ライブラリの充実:HTTPサーバー、JSONパース等が標準搭載

Goコードの例

// シンプルなHTTPサーバーの例
package main

import (
    "fmt"
    "net/http"
    "log"
)

func main() {
    http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Service is healthy")
    })

    log.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

1.2 gRPCとは何か?

gRPCの基本概念

gRPC(gRPC Remote Procedure Calls)は、Googleが開発したRPCフレームワークである。マイクロサービス間の通信において、RESTful APIに代わる選択肢として注目されている。

gRPCの特徴

  1. Protocol Buffersの使用

    • バイナリシリアライゼーション
    • 型安全性の保証
    • 言語間の互換性
  2. HTTP/2ベース

    • ストリーミング通信のサポート
    • 多重化による効率的な通信
    • フロー制御とバックプレッシャー
  3. コード生成

    • .protoファイルからクライアント・サーバーコードを自動生成
    • 複数言語対応

gRPC vs REST比較

特徴 gRPC REST
プロトコル HTTP/2 HTTP/1.1
データ形式 Protocol Buffers JSON
型安全性 あり なし
ストリーミング 双方向サポート 限定的
ブラウザサポート 限定的 完全
可読性 バイナリ テキスト

Protocol Buffersの例

// user.proto
syntax = "proto3";

package user;

option go_package = "github.com/example/ecommerce/shared/proto/user";

service UserService {
    rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
    rpc GetUser(GetUserRequest) returns (GetUserResponse);
    rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse);
    rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse);
}

message User {
    string id = 1;
    string email = 2;
    string name = 3;
    string created_at = 4;
    string updated_at = 5;
}

message CreateUserRequest {
    string email = 1;
    string name = 2;
    string password = 3;
}

message CreateUserResponse {
    User user = 1;
    string message = 2;
}

1.3 Kubernetesとは何か?

Kubernetesの基本概念

Kubernetes(K8s)は、コンテナオーケストレーションプラットフォームである。マイクロサービスアーキテクチャにおいて、複数のサービスを効率的に管理・運用するために不可欠なツールだ。

Kubernetesの主要コンポーネント

  1. Pod:最小のデプロイ単位
  2. Service:ポッド間の通信を管理
  3. Deployment:アプリケーションのデプロイメント管理
  4. ConfigMap/Secret:設定情報の管理
  5. Ingress:外部からのアクセス制御

Kubernetes vs Dockerの違い

項目 Docker Kubernetes
目的 コンテナ化 コンテナオーケストレーション
スケール 単一ホスト クラスター
ネットワーク 基本的な機能 高度なネットワーキング
自動復旧 なし あり
負荷分散 限定的 高度な負荷分散
設定管理 環境変数 ConfigMap/Secret

Dockerが得意なこと

  • コンテナの作成・実行:アプリケーションの隔離と実行
  • イメージ管理:アプリケーションのパッケージング
  • 開発環境:ローカル開発での一貫性

Kubernetesが得意なこと

  • スケーリング:負荷に応じた自動スケール
  • サービスディスカバリ:サービス間の自動検出
  • ヘルスチェック:異常なコンテナの自動復旧
  • ローリングアップデート:無停止でのアップデート

Kubernetesマニフェストの例

# user-service-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
  labels:
    app: user-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
    spec:
      containers:
      - name: user-service
        image: user-service:latest
        ports:
        - containerPort: 8080
        env:
        - name: DB_HOST
          value: "postgres-service"
        - name: DB_PORT
          value: "5432"
---
apiVersion: v1
kind: Service
metadata:
  name: user-service
spec:
  selector:
    app: user-service
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
  type: ClusterIP

1.4 マイクロサービスアーキテクチャの基本

マイクロサービスとは

マイクロサービスアーキテクチャは、アプリケーションを小さな独立したサービスの集合として構築するアプローチである。各サービスは特定のビジネス機能を担当し、独立してデプロイ・スケール可能だ。

マイクロサービスの特徴

  1. ビジネス機能による分割

    • ドメイン駆動設計に基づいた境界
    • 単一責任の原則
  2. 独立性

    • 独自のデータベース
    • 独立したデプロイメント
    • 技術スタックの選択自由度
  3. 分散システム

    • ネットワーク通信
    • 障害の分離
    • データの一貫性課題

マイクロサービスの利点

  1. スケーラビリティ:個別サービスの独立スケール
  2. 技術多様性:サービス毎の最適技術選択
  3. チーム自律性:小さなチームでの開発
  4. 障害の分離:一つのサービス障害が全体に影響しない

マイクロサービスの課題

  1. 分散システムの複雑性:ネットワーク遅延、パーシャルフェイラー
  2. データ一貫性:トランザクション管理の複雑化
  3. 運用の複雑性:多数のサービスの監視・ログ管理
  4. テストの複雑性:サービス間結合テスト

1.5 本プロジェクトでの技術選択理由

なぜこの組み合わせなのか?

Go + gRPC + K8sの組み合わせは、現代のマイクロサービス開発において非常に強力である理由を説明する。

1. パフォーマンス最適化

  • Go:高速な実行速度とメモリ効率
  • gRPC:バイナリプロトコルによる高速通信
  • K8s:効率的なリソース管理

2. 開発者体験

  • Go:シンプルで学習しやすい
  • gRPC:型安全な通信
  • K8s:宣言的な設定管理

3. 運用効率

  • Go:小さなバイナリサイズ
  • gRPC:双方向ストリーミング
  • K8s:自動化された運用

ECサイトを例に選んだ理由

ECサイトは、マイクロサービスアーキテクチャの学習に最適な例である。

  1. 明確なドメイン境界:ユーザー、商品、注文、決済など
  2. 現実的な要件:スケーラビリティ、可用性、一貫性
  3. 実装の多様性:CRUD操作、イベント処理、外部連携

次の章では、具体的なアーキテクチャ設計について詳しく見ていく。

第2章:アーキテクチャ概要 - ECサイトマイクロサービス設計

2.1 システム全体像

本プロジェクトでは、ECサイトを5つのマイクロサービスに分割して設計する。各サービスはドメイン駆動設計の原則に従い、明確なビジネス境界を持つ。

2.1.1 サービス構成図

┌─────────────────────────────────────────────────────────────┐
│                        API Gateway                          │
│                    (Kong/Istio/Envoy)                      │
└─────────────────────┬───────────────────────────────────────┘
                      │
    ┌─────────────────┼─────────────────┐
    │                 │                 │
    ▼                 ▼                 ▼
┌─────────┐    ┌─────────────┐    ┌─────────────┐
│  User   │    │   Product   │    │    Order    │
│ Service │    │   Service   │    │   Service   │
└─────────┘    └─────────────┘    └─────────────┘
    │                 │                 │
    │                 │                 │
    ▼                 ▼                 ▼
┌─────────┐    ┌─────────────┐    ┌─────────────┐
│UserDB   │    │ ProductDB   │    │  OrderDB    │
│(Postgres│    │ (Postgres)  │    │ (Postgres)  │
└─────────┘    └─────────────┘    └─────────────┘

    ┌─────────────────┬─────────────────┐
    │                 │                 │
    ▼                 ▼                 ▼
┌─────────────┐    ┌─────────────────────────────┐
│   Payment   │    │      Notification           │
│   Service   │    │        Service              │
└─────────────┘    └─────────────────────────────┘
    │                         │
    ▼                         ▼
┌─────────────┐    ┌─────────────────────────────┐
│ PaymentDB   │    │    Kafka Message Broker     │
│ (Postgres)  │    │   (Event Streaming)         │
└─────────────┘    └─────────────────────────────┘

2.1.2 サービス間通信パターン

Communication Patterns:

1. Synchronous (gRPC)
   User Service ←→ Order Service
   Order Service ←→ Product Service
   Order Service ←→ Payment Service

2. Asynchronous (Kafka Events)
   Order Created → Notification Service
   Payment Completed → Order Service
   User Registered → Notification Service

3. Data Access Pattern
   Each Service → Own Database (Database per Service)

2.2 各サービスの責務

2.2.1 User Service (ユーザーサービス)

主要責務

  • ユーザー登録・認証・認可
  • プロフィール管理
  • パスワード管理

提供API

  • CreateUser: 新規ユーザー登録
  • GetUser: ユーザー情報取得
  • UpdateUser: ユーザー情報更新
  • DeleteUser: ユーザー削除
  • AuthenticateUser: 認証

データベース

  • ユーザー基本情報
  • 認証情報(ハッシュ化パスワード)
  • ユーザー設定

2.2.2 Product Service (商品サービス)

主要責務

  • 商品カタログ管理
  • 在庫管理
  • 商品カテゴリ管理

提供API

  • CreateProduct: 商品登録
  • GetProduct: 商品情報取得
  • UpdateProduct: 商品情報更新
  • ListProducts: 商品一覧取得
  • UpdateInventory: 在庫更新

データベース

  • 商品情報(名前、説明、価格)
  • カテゴリ情報
  • 在庫情報

2.2.3 Order Service (注文サービス)

主要責務

  • 注文処理
  • 注文履歴管理
  • 注文状態管理

提供API

  • CreateOrder: 注文作成
  • GetOrder: 注文情報取得
  • UpdateOrderStatus: 注文状態更新
  • ListOrders: 注文履歴取得
  • CancelOrder: 注文キャンセル

データベース

  • 注文情報
  • 注文明細
  • 注文状態履歴

2.2.4 Payment Service (決済サービス)

主要責務

  • 決済処理
  • 決済履歴管理
  • 返金処理

提供API

  • ProcessPayment: 決済処理
  • GetPayment: 決済情報取得
  • RefundPayment: 返金処理
  • ListPayments: 決済履歴取得

データベース

  • 決済情報
  • 決済状態
  • 返金情報

2.2.5 Notification Service (通知サービス)

主要責務

  • メール通知
  • プッシュ通知
  • SMS通知(将来拡張)

提供API

  • SendEmail: メール送信
  • SendPushNotification: プッシュ通知送信
  • GetNotificationHistory: 通知履歴取得

イベント処理

  • ユーザー登録完了
  • 注文完了
  • 決済完了

2.3 ドメイン駆動設計の適用

2.3.1 Bounded Context (境界づけられたコンテキスト)

各マイクロサービスは独立したBounded Contextを形成する。

User Context:
- User (Entity)
- UserProfile (Value Object)
- UserRepository (Repository)
- AuthenticationService (Domain Service)

Product Context:
- Product (Entity)
- Category (Entity)
- Inventory (Value Object)
- ProductCatalog (Aggregate Root)

Order Context:
- Order (Aggregate Root)
- OrderItem (Entity)
- OrderStatus (Value Object)
- OrderRepository (Repository)

Payment Context:
- Payment (Aggregate Root)
- PaymentMethod (Value Object)
- PaymentStatus (Value Object)
- PaymentProcessor (Domain Service)

Notification Context:
- Notification (Entity)
- NotificationTemplate (Value Object)
- NotificationChannel (Value Object)
- NotificationService (Domain Service)

2.3.2 Aggregate設計

各サービス内でのAggregate設計を示す。

Order Aggregate

type Order struct {
    ID          OrderID
    UserID      UserID
    Items       []OrderItem
    Status      OrderStatus
    TotalAmount Money
    CreatedAt   time.Time
    UpdatedAt   time.Time
}

type OrderItem struct {
    ProductID ProductID
    Quantity  int
    Price     Money
}

// ビジネスルール例
func (o *Order) AddItem(productID ProductID, quantity int, price Money) error {
    if o.Status != OrderStatusDraft {
        return errors.New("cannot modify confirmed order")
    }
    // アイテム追加ロジック
    return nil
}

func (o *Order) Confirm() error {
    if len(o.Items) == 0 {
        return errors.New("cannot confirm empty order")
    }
    o.Status = OrderStatusConfirmed
    o.UpdatedAt = time.Now()
    return nil
}

2.4 クリーンアーキテクチャの適用

2.4.1 レイヤー構成

各マイクロサービスは以下のレイヤーで構成される:

┌─────────────────────────────────────────┐
│             Interface Layer              │
│  (gRPC Handlers, HTTP Handlers)        │
└─────────────────┬───────────────────────┘
                  │
┌─────────────────▼───────────────────────┐
│             UseCase Layer                │
│    (Business Logic, Application Services) │
└─────────────────┬───────────────────────┘
                  │
┌─────────────────▼───────────────────────┐
│              Domain Layer                │
│   (Entities, Value Objects, Repositories) │
└─────────────────┬───────────────────────┘
                  │
┌─────────────────▼───────────────────────┐
│         Infrastructure Layer            │
│  (Database, External APIs, Messaging)   │
└─────────────────────────────────────────┘

2.4.2 依存関係の方向

Interface ──→ UseCase ──→ Domain ←── Infrastructure
                            ▲
                            │
                    依存関係の逆転
                   (Dependency Inversion)

2.4.3 ディレクトリ構造例 (User Service)

user-service/
├── cmd/
│   └── server/
│       └── main.go                 # エントリーポイント
├── internal/
│   ├── domain/                     # ドメイン層
│   │   ├── user.go                # User Entity
│   │   ├── repository.go          # Repository Interface
│   │   └── service.go             # Domain Service
│   ├── usecase/                   # ユースケース層
│   │   ├── create_user.go         # ユーザー作成ユースケース
│   │   ├── get_user.go            # ユーザー取得ユースケース
│   │   └── update_user.go         # ユーザー更新ユースケース
│   ├── interface/                 # インターフェース層
│   │   ├── grpc/
│   │   │   ├── handler.go         # gRPCハンドラー
│   │   │   └── server.go          # gRPCサーバー
│   │   └── http/
│   │       └── handler.go         # HTTPハンドラー(ヘルスチェック等)
│   └── infrastructure/            # インフラストラクチャ層
│       ├── postgres/
│       │   ├── user_repository.go # Repository実装
│       │   └── migrations/        # DBマイグレーション
│       └── config/
│           └── config.go          # 設定管理
├── proto/
│   └── user.proto                 # Protocol Buffers定義
├── go.mod
└── go.sum

2.5 イベント駆動アーキテクチャ

2.5.1 イベントの種類

システム内で発生する主要なイベントを定義する。

Domain Events

type UserRegistered struct {
    UserID    string    `json:"user_id"`
    Email     string    `json:"email"`
    Name      string    `json:"name"`
    CreatedAt time.Time `json:"created_at"`
}

type OrderCreated struct {
    OrderID     string    `json:"order_id"`
    UserID      string    `json:"user_id"`
    TotalAmount float64   `json:"total_amount"`
    Items       []Item    `json:"items"`
    CreatedAt   time.Time `json:"created_at"`
}

type PaymentCompleted struct {
    PaymentID string    `json:"payment_id"`
    OrderID   string    `json:"order_id"`
    Amount    float64   `json:"amount"`
    Status    string    `json:"status"`
    CreatedAt time.Time `json:"created_at"`
}

2.5.2 イベントフロー

User Registration Flow:
1. User Service → UserRegistered Event
2. Notification Service ← UserRegistered Event
3. Welcome Email Sent

Order Processing Flow:
1. Order Service → OrderCreated Event
2. Payment Service ← OrderCreated Event
3. Payment Service → PaymentCompleted Event
4. Order Service ← PaymentCompleted Event
5. Notification Service ← PaymentCompleted Event
6. Confirmation Email Sent

2.6 データベース設計戦略

2.6.1 Database per Service Pattern

各マイクロサービスは独自のデータベースを持つ。

利点

  • サービス間の疎結合
  • 技術スタックの選択自由度
  • スケーラビリティの向上

課題

  • データ一貫性の管理
  • 結合クエリの困難さ
  • トランザクション管理の複雑化

2.6.2 データ一貫性戦略

Eventual Consistency(結果整合性)

  • イベント駆動による非同期更新
  • Sagaパターンによる分散トランザクション

Saga Pattern実装例

type OrderSaga struct {
    OrderID   string
    UserID    string
    ProductID string
    Amount    float64
    State     SagaState
}

type SagaState int

const (
    OrderCreated SagaState = iota
    PaymentProcessing
    PaymentCompleted
    OrderConfirmed
    OrderFailed
)

func (s *OrderSaga) ProcessPayment() error {
    // 決済処理
    if paymentSuccess {
        s.State = PaymentCompleted
        // OrderConfirmedイベント発行
    } else {
        s.State = OrderFailed
        // OrderCancelledイベント発行(補償処理)
    }
    return nil
}

2.7 セキュリティ設計

2.7.1 認証・認可アーキテクチャ

┌─────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Client    │────│   API Gateway   │────│  User Service   │
│ (Frontend)  │    │  (JWT Validation) │    │ (Authentication)│
└─────────────┘    └─────────────────┘    └─────────────────┘
                            │
                            ▼
                   ┌─────────────────┐
                   │  Other Services │
                   │ (Authorization) │
                   └─────────────────┘

2.7.2 JWT実装例

type JWTClaims struct {
    UserID string `json:"user_id"`
    Email  string `json:"email"`
    Role   string `json:"role"`
    jwt.StandardClaims
}

func GenerateJWT(user *User) (string, error) {
    claims := JWTClaims{
        UserID: user.ID,
        Email:  user.Email,
        Role:   user.Role,
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
            IssuedAt:  time.Now().Unix(),
        },
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString([]byte(secretKey))
}

2.8 監視・観測性

2.8.1 Three Pillars of Observability

1. Metrics(メトリクス)

  • Prometheus + Grafana
  • レスポンス時間、エラー率、スループット

2. Logs(ログ)

  • 構造化ログ(JSON形式)
  • 分散トレーシングID

3. Traces(トレース)

  • Jaegerによる分散トレーシング
  • サービス間呼び出しの可視化

2.8.2 ヘルスチェック実装

func (s *Server) HealthCheck(ctx context.Context, req *healthpb.HealthCheckRequest) (*healthpb.HealthCheckResponse, error) {
    // データベース接続チェック
    if err := s.db.Ping(); err != nil {
        return &healthpb.HealthCheckResponse{
            Status: healthpb.HealthCheckResponse_NOT_SERVING,
        }, nil
    }

    return &healthpb.HealthCheckResponse{
        Status: healthpb.HealthCheckResponse_SERVING,
    }, nil
}

この章では、ECサイトマイクロサービスの全体アーキテクチャを概観した。次の章では、具体的なプロジェクト構造について詳しく見ていく。

第3章:プロジェクト構造詳解 - ディレクトリとファイルの役割

3.1 全体プロジェクト構造

本章では、Go + gRPC + K8sマイクロサービスプロジェクトの詳細な構造を解説する。各ディレクトリとファイルの役割、命名規則、設計思想について詳しく説明する。

3.1.1 ルートディレクトリ構造

ecommerce-microservices/
├── README.md                          # プロジェクト概要とセットアップ手順
├── Makefile                           # ビルド・テスト・デプロイ自動化
├── docker-compose.yml                 # ローカル開発環境構築
├── .gitignore                         # Git無視ファイル設定
├── .github/                           # GitHub Actions CI/CD設定
│   └── workflows/
│       ├── ci.yml                     # 継続的インテグレーション
│       └── cd.yml                     # 継続的デプロイメント
├── services/                          # 各マイクロサービス
│   ├── user/                          # ユーザーサービス
│   ├── product/                       # 商品サービス
│   ├── order/                         # 注文サービス
│   ├── payment/                       # 決済サービス
│   └── notification/                  # 通知サービス
├── shared/                            # 共有コンポーネント
│   ├── proto/                         # Protocol Buffers定義
│   ├── common/                        # 共通ライブラリ
│   └── events/                        # イベント定義
├── deployments/                       # デプロイメント設定
│   ├── k8s/                          # Kubernetes manifest
│   ├── docker/                       # Dockerファイル
│   └── helm/                         # Helmチャート
├── scripts/                          # 運用スクリプト
│   ├── setup.sh                     # 初期セットアップ
│   ├── build-all.sh                 # 全サービスビルド
│   └── deploy.sh                    # デプロイスクリプト
├── docs/                             # ドキュメント(本ファイル群)
│   ├── 01_introduction.md
│   ├── 02_architecture_overview.md
│   ├── 03_project_structure.md
│   ├── 04_services_deep_dive.md
│   ├── 05_deployment.md
│   └── 06_monorepo_vs_microservices.md
└── tools/                            # 開発ツール
    ├── proto-gen.sh                  # Protocol Buffers生成スクリプト
    └── db-migration.sh               # データベースマイグレーション

3.2 各ディレクトリの詳細解説

3.2.1 servicesディレクトリ - マイクロサービス群

servicesディレクトリは、各マイクロサービスの実装を格納する。各サービスは独立したGoモジュールとして管理される。

User Service構造

services/user/
├── go.mod                            # Goモジュール定義
├── go.sum                            # 依存関係ハッシュ
├── Dockerfile                        # コンテナイメージ定義
├── cmd/                              # アプリケーションエントリーポイント
│   └── server/
│       └── main.go                   # メイン関数(サーバー起動)
├── internal/                         # 内部パッケージ(外部非公開)
│   ├── domain/                       # ドメイン層
│   │   ├── user.go                   # Userエンティティ
│   │   ├── user_repository.go        # リポジトリインターフェース
│   │   ├── auth_service.go           # 認証ドメインサービス
│   │   └── value_objects.go          # 値オブジェクト群
│   ├── usecase/                      # ユースケース層
│   │   ├── create_user.go            # ユーザー作成ユースケース
│   │   ├── get_user.go               # ユーザー取得ユースケース
│   │   ├── update_user.go            # ユーザー更新ユースケース
│   │   ├── delete_user.go            # ユーザー削除ユースケース
│   │   └── authenticate_user.go      # 認証ユースケース
│   ├── interface/                    # インターフェース層
│   │   ├── grpc/                     # gRPC関連
│   │   │   ├── handler.go            # gRPCハンドラー実装
│   │   │   ├── server.go             # gRPCサーバー設定
│   │   │   └── interceptor.go        # gRPCインターセプター
│   │   └── http/                     # HTTP関連(ヘルスチェック等)
│   │       ├── health_handler.go     # ヘルスチェックハンドラー
│   │       └── metrics_handler.go    # メトリクス公開ハンドラー
│   └── infrastructure/               # インフラストラクチャ層
│       ├── postgres/                 # PostgreSQL関連
│       │   ├── user_repository.go    # リポジトリ実装
│       │   ├── connection.go         # DB接続管理
│       │   └── migrations/           # DBマイグレーション
│       │       ├── 001_create_users_table.up.sql
│       │       └── 001_create_users_table.down.sql
│       ├── kafka/                    # Kafka関連
│       │   ├── producer.go           # イベント発行
│       │   └── consumer.go           # イベント購読
│       └── config/                   # 設定管理
│           ├── config.go             # 設定構造体
│           └── env.go                # 環境変数読み込み
├── proto/                            # Protocol Buffers定義(サービス固有)
│   └── user_service.proto            # ユーザーサービスAPI定義
├── pkg/                              # 外部公開パッケージ
│   └── client/                       # クライアントライブラリ
│       └── user_client.go            # gRPCクライアント
├── test/                             # テストファイル
│   ├── integration/                  # 結合テスト
│   │   └── user_service_test.go      # サービス結合テスト
│   └── unit/                         # 単体テスト
│       ├── domain_test.go            # ドメインロジックテスト
│       └── usecase_test.go           # ユースケーステスト
└── docs/                             # サービス固有ドキュメント
    ├── api.md                        # API仕様
    └── database.md                   # データベース設計

3.2.2 sharedディレクトリ - 共有コンポーネント

マイクロサービス間で共有するコンポーネントを格納する。

shared/
├── proto/                            # 共有Protocol Buffers定義
│   ├── common/                       # 共通メッセージ定義
│   │   ├── common.proto              # 共通型定義
│   │   ├── error.proto               # エラー定義
│   │   └── pagination.proto          # ページネーション定義
│   ├── user/                         # ユーザー関連Protocol Buffers
│   │   └── user.proto                # ユーザーサービスAPI定義
│   ├── product/                      # 商品関連Protocol Buffers
│   │   └── product.proto             # 商品サービスAPI定義
│   └── generated/                    # 自動生成コード
│       ├── go/                       # Go用生成コード
│       └── docs/                     # API仕様書自動生成
├── common/                           # 共通ライブラリ
│   ├── auth/                         # 認証関連
│   │   ├── jwt.go                    # JWT処理
│   │   └── middleware.go             # 認証ミドルウェア
│   ├── database/                     # データベース関連
│   │   ├── postgres.go               # PostgreSQL接続管理
│   │   └── migration.go              # マイグレーション共通処理
│   ├── logging/                      # ログ関連
│   │   ├── logger.go                 # 構造化ログ
│   │   └── context.go                # コンテキスト情報
│   ├── metrics/                      # メトリクス関連
│   │   ├── prometheus.go             # Prometheusメトリクス
│   │   └── middleware.go             # メトリクス収集ミドルウェア
│   └── tracing/                      # 分散トレーシング
│       ├── jaeger.go                 # Jaeger設定
│       └── middleware.go             # トレーシングミドルウェア
└── events/                           # イベント定義
    ├── user_events.go                # ユーザー関連イベント
    ├── order_events.go               # 注文関連イベント
    ├── payment_events.go             # 決済関連イベント
    └── event_handler.go              # イベントハンドラー共通処理

3.2.3 deploymentsディレクトリ - デプロイメント設定

Kubernetes、Docker、Helmなどのデプロイメント関連ファイルを格納する。

deployments/
├── k8s/                              # Kubernetesマニフェスト
│   ├── namespace.yaml                # ネームスペース定義
│   ├── configmap.yaml                # 設定マップ
│   ├── secrets.yaml                  # シークレット
│   ├── services/                     # 各サービスのK8sリソース
│   │   ├── user-service/
│   │   │   ├── deployment.yaml       # デプロイメント
│   │   │   ├── service.yaml          # サービス
│   │   │   ├── configmap.yaml        # サービス固有設定
│   │   │   └── hpa.yaml              # 水平スケーリング設定
│   │   ├── product-service/
│   │   ├── order-service/
│   │   ├── payment-service/
│   │   └── notification-service/
│   ├── databases/                    # データベース関連
│   │   ├── postgres/                 # PostgreSQL
│   │   │   ├── statefulset.yaml      # ステートフルセット
│   │   │   ├── service.yaml          # サービス
│   │   │   ├── pvc.yaml              # 永続ボリューム要求
│   │   │   └── secret.yaml           # データベース認証情報
│   │   └── redis/                    # Redis(キャッシュ)
│   ├── messaging/                    # メッセージング
│   │   └── kafka/                    # Apache Kafka
│   │       ├── kafka.yaml            # Kafkaクラスター
│   │       ├── zookeeper.yaml        # Zookeeper
│   │       └── topics.yaml           # トピック定義
│   ├── monitoring/                   # 監視システム
│   │   ├── prometheus/               # Prometheus
│   │   │   ├── deployment.yaml       # Prometheusデプロイ
│   │   │   ├── configmap.yaml        # 設定
│   │   │   └── rbac.yaml             # RBAC設定
│   │   ├── grafana/                  # Grafana
│   │   └── jaeger/                   # Jaeger
│   └── ingress/                      # 外部アクセス制御
│       ├── ingress.yaml              # Ingressコントローラー
│       └── gateway.yaml              # API Gateway
├── docker/                           # Docker関連
│   ├── Dockerfile.user               # ユーザーサービス用
│   ├── Dockerfile.product            # 商品サービス用
│   ├── Dockerfile.order              # 注文サービス用
│   ├── Dockerfile.payment            # 決済サービス用
│   ├── Dockerfile.notification       # 通知サービス用
│   └── docker-compose.dev.yml        # 開発環境用
└── helm/                             # Helmチャート
    ├── Chart.yaml                    # チャート情報
    ├── values.yaml                   # デフォルト値
    ├── values-dev.yaml               # 開発環境用値
    ├── values-prod.yaml              # 本番環境用値
    └── templates/                    # テンプレート
        ├── deployment.yaml           # デプロイメントテンプレート
        ├── service.yaml              # サービステンプレート
        └── ingress.yaml              # Ingressテンプレート

3.3 クリーンアーキテクチャの実装詳細

3.3.1 Domain層の実装

ドメイン層は、ビジネスロジックの核心部分である。外部依存を持たず、純粋なビジネスルールを表現する。

User Entity実装例

// services/user/internal/domain/user.go
package domain

import (
    "errors"
    "time"
    "golang.org/x/crypto/bcrypt"
)

// User エンティティ - ユーザーのビジネスルールを表現
type User struct {
    id        UserID
    email     Email
    name      string
    password  HashedPassword
    profile   UserProfile
    createdAt time.Time
    updatedAt time.Time
}

// UserID 値オブジェクト - ユーザーIDの不変性を保証
type UserID struct {
    value string
}

// Email 値オブジェクト - メールアドレスの検証ルールを含む
type Email struct {
    value string
}

// HashedPassword 値オブジェクト - パスワードハッシュ化ルールを含む
type HashedPassword struct {
    value string
}

// UserProfile 値オブジェクト - ユーザープロフィール情報
type UserProfile struct {
    firstName string
    lastName  string
    avatar    string
}

// コンストラクタ - ビジネスルールを適用してユーザーを作成
func NewUser(email string, name string, password string) (*User, error) {
    // メールアドレス検証
    validEmail, err := NewEmail(email)
    if err != nil {
        return nil, err
    }

    // パスワードハッシュ化
    hashedPassword, err := NewHashedPassword(password)
    if err != nil {
        return nil, err
    }

    return &User{
        id:        NewUserID(),
        email:     validEmail,
        name:      name,
        password:  hashedPassword,
        createdAt: time.Now(),
        updatedAt: time.Now(),
    }, nil
}

// メソッド - ビジネスルールを実装
func (u *User) ChangePassword(oldPassword, newPassword string) error {
    // 現在のパスワード検証
    if !u.password.Verify(oldPassword) {
        return errors.New("invalid current password")
    }

    // 新しいパスワードのハッシュ化
    newHashedPassword, err := NewHashedPassword(newPassword)
    if err != nil {
        return err
    }

    u.password = newHashedPassword
    u.updatedAt = time.Now()
    return nil
}

// UpdateProfile プロフィール更新
func (u *User) UpdateProfile(firstName, lastName, avatar string) {
    u.profile = UserProfile{
        firstName: firstName,
        lastName:  lastName,
        avatar:    avatar,
    }
    u.updatedAt = time.Now()
}

Repository Interface

// services/user/internal/domain/user_repository.go
package domain

import "context"

// UserRepository ドメイン層のリポジトリインターフェース
// インフラストラクチャ層で実装される
type UserRepository interface {
    Save(ctx context.Context, user *User) error
    FindByID(ctx context.Context, id UserID) (*User, error)
    FindByEmail(ctx context.Context, email Email) (*User, error)
    Update(ctx context.Context, user *User) error
    Delete(ctx context.Context, id UserID) error
    List(ctx context.Context, limit, offset int) ([]*User, error)
}

// DomainService ドメインサービスインターフェース
type AuthenticationService interface {
    GenerateToken(user *User) (string, error)
    ValidateToken(token string) (*UserID, error)
    IsEmailUnique(ctx context.Context, email Email) (bool, error)
}

3.3.2 UseCase層の実装

ユースケース層は、アプリケーションの具体的な使用シナリオを実装する。

// services/user/internal/usecase/create_user.go
package usecase

import (
    "context"
    "errors"
    "github.com/example/ecommerce/user-service/internal/domain"
)

// CreateUserUseCase ユーザー作成ユースケース
type CreateUserUseCase struct {
    userRepo domain.UserRepository
    authService domain.AuthenticationService
    eventPublisher EventPublisher
}

// CreateUserRequest 入力データ
type CreateUserRequest struct {
    Email    string `json:"email" validate:"required,email"`
    Name     string `json:"name" validate:"required,min=2,max=50"`
    Password string `json:"password" validate:"required,min=8"`
}

// CreateUserResponse 出力データ
type CreateUserResponse struct {
    UserID string `json:"user_id"`
    Email  string `json:"email"`
    Name   string `json:"name"`
    Token  string `json:"token"`
}

// Execute ユースケース実行
func (uc *CreateUserUseCase) Execute(ctx context.Context, req CreateUserRequest) (*CreateUserResponse, error) {
    // 入力検証
    if err := uc.validateInput(req); err != nil {
        return nil, err
    }

    // メールアドレス重複チェック
    email, err := domain.NewEmail(req.Email)
    if err != nil {
        return nil, err
    }

    isUnique, err := uc.authService.IsEmailUnique(ctx, email)
    if err != nil {
        return nil, err
    }
    if !isUnique {
        return nil, errors.New("email already exists")
    }

    // ユーザー作成
    user, err := domain.NewUser(req.Email, req.Name, req.Password)
    if err != nil {
        return nil, err
    }

    // 永続化
    if err := uc.userRepo.Save(ctx, user); err != nil {
        return nil, err
    }

    // 認証トークン生成
    token, err := uc.authService.GenerateToken(user)
    if err != nil {
        return nil, err
    }

    // ドメインイベント発行
    event := &UserRegisteredEvent{
        UserID: user.ID().String(),
        Email:  user.Email().String(),
        Name:   user.Name(),
    }
    if err := uc.eventPublisher.Publish(ctx, event); err != nil {
        // ログに記録してエラーを隠蔽(イベント発行失敗は致命的でない)
        logger.Error("Failed to publish UserRegistered event", err)
    }

    return &CreateUserResponse{
        UserID: user.ID().String(),
        Email:  user.Email().String(),
        Name:   user.Name(),
        Token:  token,
    }, nil
}

func (uc *CreateUserUseCase) validateInput(req CreateUserRequest) error {
    // バリデーション実装
    // github.com/go-playground/validator等を使用
    return nil
}

3.3.3 Interface層の実装

gRPCハンドラーとHTTPハンドラーを実装する。

// services/user/internal/interface/grpc/handler.go
package grpc

import (
    "context"
    "github.com/example/ecommerce/shared/proto/user"
    "github.com/example/ecommerce/user-service/internal/usecase"
)

// UserServiceHandler gRPCハンドラー実装
type UserServiceHandler struct {
    user.UnimplementedUserServiceServer
    createUserUC *usecase.CreateUserUseCase
    getUserUC    *usecase.GetUserUseCase
    updateUserUC *usecase.UpdateUserUseCase
    deleteUserUC *usecase.DeleteUserUseCase
}

// CreateUser gRPCメソッド実装
func (h *UserServiceHandler) CreateUser(ctx context.Context, req *user.CreateUserRequest) (*user.CreateUserResponse, error) {
    // gRPCリクエストをユースケースリクエストに変換
    ucReq := usecase.CreateUserRequest{
        Email:    req.Email,
        Name:     req.Name,
        Password: req.Password,
    }

    // ユースケース実行
    ucResp, err := h.createUserUC.Execute(ctx, ucReq)
    if err != nil {
        return nil, err
    }

    // ユースケースレスポンスをgRPCレスポンスに変換
    return &user.CreateUserResponse{
        User: &user.User{
            Id:    ucResp.UserID,
            Email: ucResp.Email,
            Name:  ucResp.Name,
        },
        Token: ucResp.Token,
    }, nil
}

// GetUser gRPCメソッド実装
func (h *UserServiceHandler) GetUser(ctx context.Context, req *user.GetUserRequest) (*user.GetUserResponse, error) {
    ucReq := usecase.GetUserRequest{
        UserID: req.UserId,
    }

    ucResp, err := h.getUserUC.Execute(ctx, ucReq)
    if err != nil {
        return nil, err
    }

    return &user.GetUserResponse{
        User: &user.User{
            Id:        ucResp.User.ID,
            Email:     ucResp.User.Email,
            Name:      ucResp.User.Name,
            CreatedAt: ucResp.User.CreatedAt,
            UpdatedAt: ucResp.User.UpdatedAt,
        },
    }, nil
}

3.3.4 Infrastructure層の実装

データベースアクセス、外部API呼び出しなどを実装する。

// services/user/internal/infrastructure/postgres/user_repository.go
package postgres

import (
    "context"
    "database/sql"
    "github.com/example/ecommerce/user-service/internal/domain"
    _ "github.com/lib/pq"
)

// UserRepository PostgreSQL実装
type UserRepository struct {
    db *sql.DB
}

// Save ユーザー保存
func (r *UserRepository) Save(ctx context.Context, user *domain.User) error {
    query := `
        INSERT INTO users (id, email, name, password_hash, created_at, updated_at)
        VALUES ($1, $2, $3, $4, $5, $6)
    `

    _, err := r.db.ExecContext(ctx, query,
        user.ID().String(),
        user.Email().String(),
        user.Name(),
        user.Password().String(),
        user.CreatedAt(),
        user.UpdatedAt(),
    )

    return err
}

// FindByID ID検索
func (r *UserRepository) FindByID(ctx context.Context, id domain.UserID) (*domain.User, error) {
    query := `
        SELECT id, email, name, password_hash, created_at, updated_at
        FROM users
        WHERE id = $1
    `

    row := r.db.QueryRowContext(ctx, query, id.String())

    var userID, email, name, passwordHash string
    var createdAt, updatedAt time.Time

    err := row.Scan(&userID, &email, &name, &passwordHash, &createdAt, &updatedAt)
    if err != nil {
        if err == sql.ErrNoRows {
            return nil, domain.ErrUserNotFound
        }
        return nil, err
    }

    // ドメインオブジェクトの復元
    user, err := domain.RestoreUser(userID, email, name, passwordHash, createdAt, updatedAt)
    if err != nil {
        return nil, err
    }

    return user, nil
}

3.4 Protocol Buffers構造

3.4.1 共通型定義

// shared/proto/common/common.proto
syntax = "proto3";

package common;

option go_package = "github.com/example/ecommerce/shared/proto/common";

// 共通メッセージ型
message Timestamp {
    int64 seconds = 1;
    int32 nanos = 2;
}

message Money {
    string currency = 1;  // ISO 4217 currency code
    int64 amount = 2;     // Amount in smallest currency unit
}

message Address {
    string street = 1;
    string city = 2;
    string state = 3;
    string postal_code = 4;
    string country = 5;
}

// ページネーション
message PageRequest {
    int32 page_number = 1;
    int32 page_size = 2;
}

message PageResponse {
    int32 total_elements = 1;
    int32 total_pages = 2;
    int32 current_page = 3;
    int32 page_size = 4;
}

3.4.2 サービス固有Protocol Buffers

// shared/proto/user/user.proto
syntax = "proto3";

package user;

import "common/common.proto";

option go_package = "github.com/example/ecommerce/shared/proto/user";

// ユーザーサービス定義
service UserService {
    rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
    rpc GetUser(GetUserRequest) returns (GetUserResponse);
    rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse);
    rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse);
    rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
    rpc AuthenticateUser(AuthenticateUserRequest) returns (AuthenticateUserResponse);
}

// メッセージ定義
message User {
    string id = 1;
    string email = 2;
    string name = 3;
    UserProfile profile = 4;
    common.Timestamp created_at = 5;
    common.Timestamp updated_at = 6;
}

message UserProfile {
    string first_name = 1;
    string last_name = 2;
    string avatar = 3;
    common.Address address = 4;
}

// リクエスト・レスポンス定義
message CreateUserRequest {
    string email = 1;
    string name = 2;
    string password = 3;
    UserProfile profile = 4;
}

message CreateUserResponse {
    User user = 1;
    string token = 2;
}

message GetUserRequest {
    string user_id = 1;
}

message GetUserResponse {
    User user = 1;
}

3.5 設定管理とコマンド

3.5.1 環境設定構造

// services/user/internal/infrastructure/config/config.go
package config

import (
    "os"
    "strconv"
    "time"
)

// Config アプリケーション設定
type Config struct {
    Server   ServerConfig
    Database DatabaseConfig
    JWT      JWTConfig
    Kafka    KafkaConfig
    Redis    RedisConfig
    Logging  LoggingConfig
}

type ServerConfig struct {
    Host         string
    Port         int
    GRPCPort     int
    ReadTimeout  time.Duration
    WriteTimeout time.Duration
}

type DatabaseConfig struct {
    Host         string
    Port         int
    User         string
    Password     string
    Database     string
    MaxOpenConns int
    MaxIdleConns int
}

type JWTConfig struct {
    SecretKey      string
    ExpirationTime time.Duration
}

// LoadConfig 環境変数から設定読み込み
func LoadConfig() (*Config, error) {
    config := &Config{
        Server: ServerConfig{
            Host:         getEnvOrDefault("SERVER_HOST", "0.0.0.0"),
            Port:         getEnvAsIntOrDefault("SERVER_PORT", 8080),
            GRPCPort:     getEnvAsIntOrDefault("GRPC_PORT", 50051),
            ReadTimeout:  getEnvAsDurationOrDefault("READ_TIMEOUT", 10*time.Second),
            WriteTimeout: getEnvAsDurationOrDefault("WRITE_TIMEOUT", 10*time.Second),
        },
        Database: DatabaseConfig{
            Host:         getEnvOrDefault("DB_HOST", "localhost"),
            Port:         getEnvAsIntOrDefault("DB_PORT", 5432),
            User:         getEnvOrDefault("DB_USER", "postgres"),
            Password:     getEnvOrDefault("DB_PASSWORD", ""),
            Database:     getEnvOrDefault("DB_NAME", "user_service"),
            MaxOpenConns: getEnvAsIntOrDefault("DB_MAX_OPEN_CONNS", 25),
            MaxIdleConns: getEnvAsIntOrDefault("DB_MAX_IDLE_CONNS", 5),
        },
        JWT: JWTConfig{
            SecretKey:      getEnvOrDefault("JWT_SECRET", "default-secret"),
            ExpirationTime: getEnvAsDurationOrDefault("JWT_EXPIRATION", 24*time.Hour),
        },
    }

    return config, nil
}

func getEnvOrDefault(key, defaultValue string) string {
    if value := os.Getenv(key); value != "" {
        return value
    }
    return defaultValue
}

func getEnvAsIntOrDefault(key string, defaultValue int) int {
    if value := os.Getenv(key); value != "" {
        if intValue, err := strconv.Atoi(value); err == nil {
            return intValue
        }
    }
    return defaultValue
}

3.5.2 プロジェクト生成コマンド

#!/bin/bash
# scripts/setup.sh - プロジェクト初期セットアップスクリプト

set -e

PROJECT_NAME="ecommerce-microservices"
SERVICES=("user" "product" "order" "payment" "notification")

echo "🚀 Setting up $PROJECT_NAME project..."

# ルートディレクトリ作成
mkdir -p $PROJECT_NAME
cd $PROJECT_NAME

# 基本ディレクトリ構造作成
echo "📁 Creating directory structure..."
mkdir -p {services,shared/{proto,common,events},deployments/{k8s,docker,helm},scripts,docs,tools}

# 各サービスディレクトリ作成
for service in "${SERVICES[@]}"; do
    echo "🔧 Setting up $service service..."
    mkdir -p services/$service/{cmd/server,internal/{domain,usecase,interface/{grpc,http},infrastructure/{postgres,kafka,config}},proto,pkg/client,test/{integration,unit},docs}

    # Go module初期化
    cd services/$service
    go mod init github.com/example/ecommerce/$service-service
    cd ../..
done

# 共有Protocol Buffers構造作成
echo "📋 Setting up shared proto structure..."
mkdir -p shared/proto/{common,user,product,order,payment,notification,generated/{go,docs}}

# Kubernetes manifests構造作成
echo "☸️ Setting up Kubernetes manifests..."
mkdir -p deployments/k8s/{services/{user-service,product-service,order-service,payment-service,notification-service},databases/{postgres,redis},messaging/kafka,monitoring/{prometheus,grafana,jaeger},ingress}

# Docker構造作成
echo "🐳 Setting up Docker structure..."
mkdir -p deployments/docker

# 設定ファイル作成
echo "⚙️ Creating configuration files..."

# ルートMakefile作成
cat > Makefile << 'EOF'
.PHONY: build test clean proto docker-build docker-push deploy

# Build all services
build:
	@echo "Building all services..."
	@for service in user product order payment notification; do \
		echo "Building $$service service..."; \
		cd services/$$service && go build -o bin/$$service cmd/server/main.go && cd ../..; \
	done

# Run tests
test:
	@echo "Running tests..."
	@for service in user product order payment notification; do \
		echo "Testing $$service service..."; \
		cd services/$$service && go test ./... && cd ../..; \
	done

# Generate Protocol Buffers
proto:
	@echo "Generating Protocol Buffers..."
	@./tools/proto-gen.sh

# Clean build artifacts
clean:
	@echo "Cleaning..."
	@find . -name "bin" -type d -exec rm -rf {} +
	@find . -name "*.log" -type f -delete

# Docker build
docker-build:
	@echo "Building Docker images..."
	@for service in user product order payment notification; do \
		echo "Building $$service Docker image..."; \
		docker build -f deployments/docker/Dockerfile.$$service -t $$service-service:latest .; \
	done

# Deploy to Kubernetes
deploy:
	@echo "Deploying to Kubernetes..."
	@kubectl apply -f deployments/k8s/
EOF

# Docker Compose for development
cat > docker-compose.yml << 'EOF'
version: '3.8'

services:
  postgres:
    image: postgres:14
    environment:
      POSTGRES_DB: ecommerce
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

  kafka:
    image: confluentinc/cp-kafka:latest
    environment:
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
    ports:
      - "9092:9092"
    depends_on:
      - zookeeper

  zookeeper:
    image: confluentinc/cp-zookeeper:latest
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
      ZOOKEEPER_TICK_TIME: 2000
    ports:
      - "2181:2181"

volumes:
  postgres_data:
EOF

# Protocol Buffers生成スクリプト
cat > tools/proto-gen.sh << 'EOF'
#!/bin/bash
set -e

echo "🔄 Generating Protocol Buffers..."

# Go生成用ディレクトリ作成
mkdir -p shared/proto/generated/go

# 各protoファイルの生成
find shared/proto -name "*.proto" -not -path "*/generated/*" | while read proto_file; do
    echo "Generating: $proto_file"
    protoc --go_out=shared/proto/generated/go \
           --go-grpc_out=shared/proto/generated/go \
           --proto_path=shared/proto \
           "$proto_file"
done

echo "✅ Protocol Buffers generation completed!"
EOF

chmod +x tools/proto-gen.sh

echo "✅ Project setup completed!"
echo "📖 Next steps:"
echo "   1. cd $PROJECT_NAME"
echo "   2. Install dependencies: go mod tidy"
echo "   3. Generate Protocol Buffers: make proto"
echo "   4. Start development environment: docker-compose up -d"
echo "   5. Build services: make build"

この章では、プロジェクト構造の詳細と各ファイルの役割について説明した。次の章では、各サービスの具体的な実装について深く掘り下げる。

第4章:サービス詳細実装 - 各マイクロサービスの深堀り

4.1 User Service (ユーザーサービス) 詳細

4.1.1 ドメインモデル設計

User Serviceは、ユーザー管理の中核となるサービスである。認証、認可、プロフィール管理を担当する。

エンティティ設計

// User エンティティ - 集約ルート
type User struct {
    id        UserID
    email     Email
    profile   UserProfile
    auth      AuthInfo
    settings  UserSettings
    createdAt time.Time
    updatedAt time.Time
}

// UserProfile 値オブジェクト
type UserProfile struct {
    name      string
    firstName string
    lastName  string
    avatar    string
    bio       string
}

// AuthInfo 値オブジェクト
type AuthInfo struct {
    passwordHash HashedPassword
    lastLoginAt  *time.Time
    loginCount   int
    isActive     bool
}

ビジネスルール実装

// ユーザー登録時のビジネスルール
func (u *User) Register() error {
    if u.auth.isActive {
        return ErrUserAlreadyActive
    }

    // アクティベーション処理
    u.auth.isActive = true
    u.updatedAt = time.Now()

    // ドメインイベント発行
    u.addDomainEvent(NewUserRegisteredEvent(u.id, u.email))
    return nil
}

// ログイン処理
func (u *User) Login(password string) error {
    if !u.auth.isActive {
        return ErrUserNotActive
    }

    if !u.auth.passwordHash.Verify(password) {
        return ErrInvalidPassword
    }

    // ログイン情報更新
    now := time.Now()
    u.auth.lastLoginAt = &now
    u.auth.loginCount++
    u.updatedAt = now

    return nil
}

4.1.2 ユースケース実装

ユーザー登録ユースケース

type CreateUserUseCase struct {
    userRepo       domain.UserRepository
    emailValidator domain.EmailValidator
    eventBus       EventBus
}

func (uc *CreateUserUseCase) Execute(ctx context.Context, req CreateUserRequest) (*CreateUserResponse, error) {
    // 入力バリデーション
    if err := uc.validateRequest(req); err != nil {
        return nil, NewValidationError(err)
    }

    // メール重複チェック
    exists, err := uc.userRepo.ExistsByEmail(ctx, req.Email)
    if err != nil {
        return nil, err
    }
    if exists {
        return nil, ErrEmailAlreadyExists
    }

    // ユーザー作成
    user, err := domain.NewUser(req.Email, req.Name, req.Password)
    if err != nil {
        return nil, err
    }

    // 永続化
    if err := uc.userRepo.Save(ctx, user); err != nil {
        return nil, err
    }

    // イベント発行
    events := user.GetDomainEvents()
    for _, event := range events {
        if err := uc.eventBus.Publish(ctx, event); err != nil {
            // ログに記録、処理は継続
            log.Error("Failed to publish event", "event", event, "error", err)
        }
    }

    return &CreateUserResponse{
        UserID: user.ID().String(),
        Email:  user.Email().String(),
        Name:   user.Profile().Name(),
    }, nil
}

4.1.3 gRPCインターフェース実装

type UserServiceServer struct {
    userpb.UnimplementedUserServiceServer
    createUserUC *usecase.CreateUserUseCase
    getUserUC    *usecase.GetUserUseCase
}

func (s *UserServiceServer) CreateUser(ctx context.Context, req *userpb.CreateUserRequest) (*userpb.CreateUserResponse, error) {
    // リクエスト変換
    ucReq := usecase.CreateUserRequest{
        Email:    req.Email,
        Name:     req.Name,
        Password: req.Password,
    }

    // ユースケース実行
    ucResp, err := s.createUserUC.Execute(ctx, ucReq)
    if err != nil {
        return nil, status.Errorf(codes.Internal, "failed to create user: %v", err)
    }

    // レスポンス変換
    return &userpb.CreateUserResponse{
        User: &userpb.User{
            Id:    ucResp.UserID,
            Email: ucResp.Email,
            Name:  ucResp.Name,
        },
    }, nil
}

4.2 Product Service (商品サービス) 詳細

4.2.1 ドメインモデル設計

商品カタログと在庫管理を担当するサービスである。

集約設計

// Product 集約ルート
type Product struct {
    id          ProductID
    name        string
    description string
    price       Money
    category    Category
    inventory   Inventory
    images      []ProductImage
    attributes  ProductAttributes
    status      ProductStatus
    createdAt   time.Time
    updatedAt   time.Time
}

// Inventory 値オブジェクト
type Inventory struct {
    quantity     int
    reservedQty  int
    threshold    int
    isInStock    bool
}

// Category エンティティ
type Category struct {
    id       CategoryID
    name     string
    parent   *CategoryID
    path     string
}

在庫管理ビジネスルール

// 在庫予約
func (p *Product) ReserveStock(quantity int) error {
    if !p.inventory.CanReserve(quantity) {
        return ErrInsufficientStock
    }

    p.inventory.reservedQty += quantity
    p.inventory.updateStockStatus()
    p.updatedAt = time.Now()

    // 在庫僅少イベント
    if p.inventory.isLowStock() {
        p.addDomainEvent(NewLowStockEvent(p.id, p.inventory.quantity))
    }

    return nil
}

// 在庫確定
func (p *Product) ConfirmStock(quantity int) error {
    if p.inventory.reservedQty < quantity {
        return ErrInsufficientReservedStock
    }

    p.inventory.quantity -= quantity
    p.inventory.reservedQty -= quantity
    p.inventory.updateStockStatus()
    p.updatedAt = time.Now()

    return nil
}

4.2.2 商品検索機能

type ProductSearchUseCase struct {
    productRepo domain.ProductRepository
    searchIndex SearchIndex
}

func (uc *ProductSearchUseCase) Execute(ctx context.Context, req SearchProductsRequest) (*SearchProductsResponse, error) {
    // 検索条件構築
    criteria := domain.SearchCriteria{
        Query:      req.Query,
        CategoryID: req.CategoryID,
        PriceRange: domain.PriceRange{
            Min: req.MinPrice,
            Max: req.MaxPrice,
        },
        InStockOnly: req.InStockOnly,
        Pagination: domain.Pagination{
            Page: req.Page,
            Size: req.Size,
        },
    }

    // 検索実行
    results, err := uc.searchIndex.Search(ctx, criteria)
    if err != nil {
        return nil, err
    }

    // 商品詳細取得
    products := make([]*domain.Product, 0, len(results.ProductIDs))
    for _, productID := range results.ProductIDs {
        product, err := uc.productRepo.FindByID(ctx, productID)
        if err != nil {
            continue // エラーログ記録、処理継続
        }
        products = append(products, product)
    }

    return &SearchProductsResponse{
        Products:    products,
        TotalCount:  results.TotalCount,
        CurrentPage: req.Page,
        TotalPages:  (results.TotalCount + req.Size - 1) / req.Size,
    }, nil
}

4.3 Order Service (注文サービス) 詳細

4.3.1 注文集約の設計

注文処理の中核となる複雑な集約である。

// Order 集約ルート
type Order struct {
    id           OrderID
    userID       UserID
    items        []OrderItem
    status       OrderStatus
    totalAmount  Money
    shippingAddr Address
    payment      PaymentInfo
    timeline     OrderTimeline
    createdAt    time.Time
    updatedAt    time.Time
}

// OrderItem エンティティ
type OrderItem struct {
    productID ProductID
    name      string
    price     Money
    quantity  int
    total     Money
}

// OrderStatus 値オブジェクト
type OrderStatus int

const (
    OrderStatusDraft OrderStatus = iota
    OrderStatusConfirmed
    OrderStatusPaid
    OrderStatusShipped
    OrderStatusDelivered
    OrderStatusCancelled
)

注文状態遷移

// 注文確定
func (o *Order) Confirm() error {
    if o.status != OrderStatusDraft {
        return ErrInvalidOrderStatus
    }

    if len(o.items) == 0 {
        return ErrEmptyOrder
    }

    o.status = OrderStatusConfirmed
    o.timeline.confirmedAt = time.Now()
    o.updatedAt = time.Now()

    // 在庫予約イベント
    o.addDomainEvent(NewOrderConfirmedEvent(o.id, o.items))

    return nil
}

// 決済完了
func (o *Order) MarkPaid(paymentID PaymentID) error {
    if o.status != OrderStatusConfirmed {
        return ErrInvalidOrderStatus
    }

    o.status = OrderStatusPaid
    o.payment.paymentID = paymentID
    o.timeline.paidAt = time.Now()
    o.updatedAt = time.Now()

    o.addDomainEvent(NewOrderPaidEvent(o.id, paymentID))

    return nil
}

4.3.2 Sagaパターン実装

分散トランザクション制御のためのSagaパターンを実装する。

type OrderSaga struct {
    sagaID    SagaID
    orderID   OrderID
    userID    UserID
    items     []OrderItem
    state     SagaState
    steps     []SagaStep
    createdAt time.Time
    updatedAt time.Time
}

type SagaStep struct {
    stepType        StepType
    status          StepStatus
    compensateType  CompensateType
    executedAt      *time.Time
    compensatedAt   *time.Time
}

func (s *OrderSaga) Execute(ctx context.Context) error {
    for i, step := range s.steps {
        if step.status == StepStatusCompleted {
            continue
        }

        if err := s.executeStep(ctx, i); err != nil {
            // 補償処理実行
            if err := s.compensate(ctx, i-1); err != nil {
                return fmt.Errorf("compensation failed: %w", err)
            }
            return err
        }

        s.steps[i].status = StepStatusCompleted
        s.steps[i].executedAt = &time.Time{}
    }

    s.state = SagaStateCompleted
    return nil
}

4.4 Payment Service (決済サービス) 詳細

4.4.1 決済処理設計

// Payment 集約ルート
type Payment struct {
    id            PaymentID
    orderID       OrderID
    userID        UserID
    amount        Money
    method        PaymentMethod
    status        PaymentStatus
    providerTxnID string
    timeline      PaymentTimeline
    createdAt     time.Time
    updatedAt     time.Time
}

// PaymentMethod 値オブジェクト
type PaymentMethod struct {
    methodType PaymentMethodType
    details    map[string]interface{}
}

type PaymentMethodType int

const (
    PaymentMethodCreditCard PaymentMethodType = iota
    PaymentMethodDebitCard
    PaymentMethodBankTransfer
    PaymentMethodDigitalWallet
)

決済プロセス実装

type ProcessPaymentUseCase struct {
    paymentRepo domain.PaymentRepository
    gateway     PaymentGateway
    eventBus    EventBus
}

func (uc *ProcessPaymentUseCase) Execute(ctx context.Context, req ProcessPaymentRequest) (*ProcessPaymentResponse, error) {
    // 決済オブジェクト作成
    payment, err := domain.NewPayment(req.OrderID, req.UserID, req.Amount, req.Method)
    if err != nil {
        return nil, err
    }

    // 外部決済プロバイダー呼び出し
    gatewayReq := &GatewayRequest{
        Amount: req.Amount,
        Method: req.Method,
        OrderRef: req.OrderID.String(),
    }

    gatewayResp, err := uc.gateway.ProcessPayment(ctx, gatewayReq)
    if err != nil {
        payment.MarkFailed(err.Error())
        uc.paymentRepo.Save(ctx, payment)
        return nil, err
    }

    // 決済成功処理
    payment.MarkSucceeded(gatewayResp.TransactionID)

    if err := uc.paymentRepo.Save(ctx, payment); err != nil {
        return nil, err
    }

    // 決済完了イベント発行
    event := NewPaymentCompletedEvent(payment.ID(), payment.OrderID(), payment.Amount())
    uc.eventBus.Publish(ctx, event)

    return &ProcessPaymentResponse{
        PaymentID:     payment.ID().String(),
        Status:        payment.Status().String(),
        TransactionID: gatewayResp.TransactionID,
    }, nil
}

4.5 Notification Service (通知サービス) 詳細

4.5.1 通知システム設計

// Notification エンティティ
type Notification struct {
    id        NotificationID
    userID    UserID
    template  NotificationTemplate
    channel   NotificationChannel
    status    NotificationStatus
    sentAt    *time.Time
    createdAt time.Time
}

// NotificationTemplate 値オブジェクト
type NotificationTemplate struct {
    templateType TemplateType
    subject      string
    body         string
    variables    map[string]interface{}
}

// NotificationChannel 値オブジェクト
type NotificationChannel int

const (
    NotificationChannelEmail NotificationChannel = iota
    NotificationChannelSMS
    NotificationChannelPush
    NotificationChannelInApp
)

4.5.2 イベントハンドラー実装

type EmailNotificationHandler struct {
    notificationRepo domain.NotificationRepository
    emailService     EmailService
    templateEngine   TemplateEngine
}

func (h *EmailNotificationHandler) HandleUserRegistered(ctx context.Context, event *UserRegisteredEvent) error {
    // テンプレート作成
    template := domain.NotificationTemplate{
        TemplateType: domain.TemplateTypeWelcome,
        Subject:      "Welcome to our service!",
        Body:         "welcome_email.html",
        Variables: map[string]interface{}{
            "UserName": event.Name,
            "Email":    event.Email,
        },
    }

    // 通知作成
    notification := domain.NewNotification(
        event.UserID,
        template,
        domain.NotificationChannelEmail,
    )

    // メール送信
    emailContent, err := h.templateEngine.Render(template)
    if err != nil {
        return err
    }

    emailReq := &EmailRequest{
        To:      event.Email,
        Subject: template.Subject,
        Body:    emailContent,
    }

    if err := h.emailService.SendEmail(ctx, emailReq); err != nil {
        notification.MarkFailed(err.Error())
    } else {
        notification.MarkSent()
    }

    return h.notificationRepo.Save(ctx, notification)
}

4.6 サービス間通信パターン

4.6.1 同期通信 (gRPC)

// Order Service から Product Service への在庫確認
type OrderService struct {
    productClient productpb.ProductServiceClient
}

func (s *OrderService) ValidateOrder(ctx context.Context, items []OrderItem) error {
    for _, item := range items {
        req := &productpb.CheckStockRequest{
            ProductId: item.ProductID.String(),
            Quantity:  int32(item.Quantity),
        }

        resp, err := s.productClient.CheckStock(ctx, req)
        if err != nil {
            return fmt.Errorf("failed to check stock for product %s: %w", item.ProductID, err)
        }

        if !resp.Available {
            return fmt.Errorf("insufficient stock for product %s", item.ProductID)
        }
    }

    return nil
}

4.6.2 非同期通信 (Kafka)

type EventPublisher struct {
    producer *kafka.Producer
}

func (p *EventPublisher) PublishOrderCreated(ctx context.Context, event *OrderCreatedEvent) error {
    data, err := json.Marshal(event)
    if err != nil {
        return err
    }

    msg := &kafka.Message{
        Topic: "order.created",
        Key:   []byte(event.OrderID),
        Value: data,
        Headers: []kafka.Header{
            {Key: "event-type", Value: []byte("OrderCreated")},
            {Key: "event-version", Value: []byte("v1")},
        },
    }

    return p.producer.Produce(msg, nil)
}

// イベント購読側
type PaymentEventHandler struct {
    processPaymentUC *usecase.ProcessPaymentUseCase
}

func (h *PaymentEventHandler) HandleOrderCreated(ctx context.Context, msg *kafka.Message) error {
    var event OrderCreatedEvent
    if err := json.Unmarshal(msg.Value, &event); err != nil {
        return err
    }

    // 決済処理実行
    req := ProcessPaymentRequest{
        OrderID: event.OrderID,
        UserID:  event.UserID,
        Amount:  event.TotalAmount,
    }

    _, err := h.processPaymentUC.Execute(ctx, req)
    return err
}

この章では、各マイクロサービスの詳細実装について説明した。次の章では、Kubernetesでのデプロイメントについて詳しく解説する。

第5章:Kubernetesデプロイメント - 本番環境への展開

5.1 デプロイメント戦略概要

本章では、Go + gRPC + K8sマイクロサービスの実際のデプロイメント方法について詳しく解説する。開発環境から本番環境まで、段階的なデプロイメント戦略を実践的に説明する。

5.1.1 デプロイメント環境構成

Development → Staging → Production

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Development   │    │     Staging     │    │   Production    │
│                 │    │                 │    │                 │
│ • Local K8s     │───→│ • Dev Cluster   │───→│ • Prod Cluster  │
│ • Docker Desktop│    │ • AWS EKS       │    │ • AWS EKS       │
│ • Kind/Minikube │    │ • 1-2 Nodes     │    │ • 3+ Nodes      │
│                 │    │ • Basic Monitor │    │ • Full Monitor  │
└─────────────────┘    └─────────────────┘    └─────────────────┘

5.1.2 デプロイメント原則

Blue-Green Deployment: ゼロダウンタイムデプロイメント
Rolling Update: 段階的な更新でリスクを最小化
Canary Deployment: 一部のトラフィックで新バージョンをテスト
Infrastructure as Code: 全ての設定をコード化

5.2 Dockerイメージ作成

5.2.1 マルチステージDockerfile

各マイクロサービス用の最適化されたDockerfileを作成する。

# deployments/docker/Dockerfile.user
# Build stage
FROM golang:1.21-alpine AS builder

WORKDIR /app

# 依存関係のキャッシュ最適化
COPY services/user/go.mod services/user/go.sum ./
RUN go mod download

# ソースコードをコピー
COPY services/user/ ./
COPY shared/ ../shared/

# バイナリをビルド
RUN CGO_ENABLED=0 GOOS=linux go build \
    -a -installsuffix cgo \
    -ldflags '-extldflags "-static"' \
    -o user-service cmd/server/main.go

# Runtime stage
FROM alpine:3.18

# セキュリティのためのCA証明書追加
RUN apk --no-cache add ca-certificates tzdata

WORKDIR /root/

# 実行ユーザーを作成(セキュリティ強化)
RUN addgroup -g 1001 appgroup && \
    adduser -D -s /bin/sh -u 1001 -G appgroup appuser

# バイナリをコピー
COPY --from=builder /app/user-service .

# 権限設定
RUN chown appuser:appgroup user-service

# 実行ユーザーを切り替え
USER appuser

# ポート公開
EXPOSE 50051 8080

# ヘルスチェック
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
    CMD ./user-service --health-check || exit 1

# 実行コマンド
CMD ["./user-service"]

5.2.2 Docker Compose for Development

# docker-compose.dev.yml
version: '3.8'

services:
  # インフラストラクチャサービス
  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: ecommerce_dev
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./deployments/sql/init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 30s
      timeout: 10s
      retries: 5

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data

  kafka:
    image: confluentinc/cp-kafka:7.4.0
    environment:
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
    ports:
      - "9092:9092"
    depends_on:
      - zookeeper
    volumes:
      - kafka_data:/var/lib/kafka/data

  zookeeper:
    image: confluentinc/cp-zookeeper:7.4.0
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
      ZOOKEEPER_TICK_TIME: 2000
    ports:
      - "2181:2181"
    volumes:
      - zookeeper_data:/var/lib/zookeeper

  # マイクロサービス
  user-service:
    build:
      context: .
      dockerfile: deployments/docker/Dockerfile.user
    environment:
      - DB_HOST=postgres
      - DB_PORT=5432
      - DB_USER=postgres
      - DB_PASSWORD=postgres
      - DB_NAME=user_service
      - REDIS_HOST=redis
      - REDIS_PORT=6379
      - KAFKA_BROKERS=kafka:9092
    ports:
      - "50051:50051"
      - "8081:8080"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
      kafka:
        condition: service_started
    restart: unless-stopped

  product-service:
    build:
      context: .
      dockerfile: deployments/docker/Dockerfile.product
    environment:
      - DB_HOST=postgres
      - DB_PORT=5432
      - DB_USER=postgres
      - DB_PASSWORD=postgres
      - DB_NAME=product_service
      - REDIS_HOST=redis
      - REDIS_PORT=6379
    ports:
      - "50052:50051"
      - "8082:8080"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started

  # API Gateway
  api-gateway:
    build:
      context: .
      dockerfile: deployments/docker/Dockerfile.gateway
    environment:
      - USER_SERVICE_HOST=user-service
      - USER_SERVICE_PORT=50051
      - PRODUCT_SERVICE_HOST=product-service
      - PRODUCT_SERVICE_PORT=50051
      - ORDER_SERVICE_HOST=order-service
      - ORDER_SERVICE_PORT=50051
    ports:
      - "8080:8080"
    depends_on:
      - user-service
      - product-service

volumes:
  postgres_data:
  redis_data:
  kafka_data:
  zookeeper_data:

networks:
  default:
    name: ecommerce-network

5.3 Kubernetes Manifests

5.3.1 Namespace and ConfigMap

# deployments/k8s/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: ecommerce
  labels:
    name: ecommerce
    environment: production

---
# 共通設定
apiVersion: v1
kind: ConfigMap
metadata:
  name: common-config
  namespace: ecommerce
data:
  # ログレベル
  LOG_LEVEL: "info"
  LOG_FORMAT: "json"

  # トレーシング設定
  JAEGER_AGENT_HOST: "jaeger-agent.monitoring.svc.cluster.local"
  JAEGER_AGENT_PORT: "6831"

  # メトリクス設定
  METRICS_PORT: "8080"
  METRICS_PATH: "/metrics"

  # gRPC設定
  GRPC_PORT: "50051"
  GRPC_MAX_RECV_MSG_SIZE: "4194304"
  GRPC_MAX_SEND_MSG_SIZE: "4194304"

---
# 環境別設定
apiVersion: v1
kind: ConfigMap
metadata:
  name: env-config
  namespace: ecommerce
data:
  ENVIRONMENT: "production"

  # Redis設定
  REDIS_HOST: "redis-service.ecommerce.svc.cluster.local"
  REDIS_PORT: "6379"

  # Kafka設定
  KAFKA_BROKERS: "kafka-service.ecommerce.svc.cluster.local:9092"

  # 外部サービス
  EMAIL_SERVICE_URL: "https://api.sendgrid.com"
  PAYMENT_GATEWAY_URL: "https://api.stripe.com"

5.3.2 Secrets Management

# deployments/k8s/secrets.yaml
apiVersion: v1
kind: Secret
metadata:
  name: database-secrets
  namespace: ecommerce
type: Opaque
data:
  # base64エンコードされた値
  user-db-password: cG9zdGdyZXM=
  product-db-password: cG9zdGdyZXM=
  order-db-password: cG9zdGdyZXM=
  payment-db-password: cG9zdGdyZXM=

---
apiVersion: v1
kind: Secret
metadata:
  name: jwt-secrets
  namespace: ecommerce
type: Opaque
data:
  jwt-secret-key: bXktc2VjcmV0LWtleS1mb3ItcHJvZHVjdGlvbg==

---
apiVersion: v1
kind: Secret
metadata:
  name: external-api-secrets
  namespace: ecommerce
type: Opaque
data:
  sendgrid-api-key: U0cuZXhhbXBsZWtleQ==
  stripe-secret-key: c2tfdGVzdF9leGFtcGxla2V5

5.3.3 User Service Deployment

# deployments/k8s/services/user-service/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
  namespace: ecommerce
  labels:
    app: user-service
    version: v1
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
        version: v1
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "8080"
        prometheus.io/path: "/metrics"
    spec:
      serviceAccountName: user-service-sa
      securityContext:
        runAsNonRoot: true
        runAsUser: 1001
        fsGroup: 1001
      containers:
      - name: user-service
        image: user-service:latest
        imagePullPolicy: Always
        ports:
        - name: grpc
          containerPort: 50051
          protocol: TCP
        - name: http
          containerPort: 8080
          protocol: TCP
        env:
        # ConfigMapからの設定
        - name: LOG_LEVEL
          valueFrom:
            configMapKeyRef:
              name: common-config
              key: LOG_LEVEL
        - name: GRPC_PORT
          valueFrom:
            configMapKeyRef:
              name: common-config
              key: GRPC_PORT
        - name: REDIS_HOST
          valueFrom:
            configMapKeyRef:
              name: env-config
              key: REDIS_HOST

        # Secretからの設定
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: database-secrets
              key: user-db-password
        - name: JWT_SECRET
          valueFrom:
            secretKeyRef:
              name: jwt-secrets
              key: jwt-secret-key

        # 固有設定
        - name: DB_HOST
          value: "user-postgres-service.ecommerce.svc.cluster.local"
        - name: DB_PORT
          value: "5432"
        - name: DB_USER
          value: "postgres"
        - name: DB_NAME
          value: "user_service"

        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"

        # ヘルスチェック
        livenessProbe:
          grpc:
            port: 50051
          initialDelaySeconds: 30
          periodSeconds: 10
          timeoutSeconds: 5
          failureThreshold: 3

        readinessProbe:
          grpc:
            port: 50051
          initialDelaySeconds: 5
          periodSeconds: 5
          timeoutSeconds: 3
          failureThreshold: 3

        # 起動時プローブ
        startupProbe:
          grpc:
            port: 50051
          initialDelaySeconds: 10
          periodSeconds: 10
          timeoutSeconds: 5
          failureThreshold: 30

        # セキュリティコンテキスト
        securityContext:
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: true
          capabilities:
            drop:
            - ALL

        # ボリュームマウント
        volumeMounts:
        - name: tmp-volume
          mountPath: /tmp
        - name: logs-volume
          mountPath: /app/logs

      volumes:
      - name: tmp-volume
        emptyDir: {}
      - name: logs-volume
        emptyDir: {}

      # 初期化コンテナ
      initContainers:
      - name: db-migration
        image: user-service:latest
        command: ['./user-service', '--migrate']
        env:
        - name: DB_HOST
          value: "user-postgres-service.ecommerce.svc.cluster.local"
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: database-secrets
              key: user-db-password

---
# Service
apiVersion: v1
kind: Service
metadata:
  name: user-service
  namespace: ecommerce
  labels:
    app: user-service
spec:
  type: ClusterIP
  ports:
  - name: grpc
    port: 50051
    targetPort: grpc
    protocol: TCP
  - name: http
    port: 8080
    targetPort: http
    protocol: TCP
  selector:
    app: user-service

---
# ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
  name: user-service-sa
  namespace: ecommerce
  labels:
    app: user-service

5.3.4 Horizontal Pod Autoscaler

# deployments/k8s/services/user-service/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: user-service-hpa
  namespace: ecommerce
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: user-service
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80
  - type: Pods
    pods:
      metric:
        name: grpc_requests_per_second
      target:
        type: AverageValue
        averageValue: "100"
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
      - type: Percent
        value: 10
        periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
      - type: Percent
        value: 50
        periodSeconds: 60
      - type: Pods
        value: 2
        periodSeconds: 60
      selectPolicy: Max

5.4 データベースデプロイメント

5.4.1 PostgreSQL StatefulSet

# deployments/k8s/databases/postgres/user-postgres.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: user-postgres
  namespace: ecommerce
  labels:
    app: user-postgres
spec:
  serviceName: user-postgres-headless
  replicas: 1
  selector:
    matchLabels:
      app: user-postgres
  template:
    metadata:
      labels:
        app: user-postgres
    spec:
      containers:
      - name: postgres
        image: postgres:15-alpine
        env:
        - name: POSTGRES_DB
          value: user_service
        - name: POSTGRES_USER
          value: postgres
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: database-secrets
              key: user-db-password
        - name: PGDATA
          value: /var/lib/postgresql/data/pgdata
        ports:
        - name: postgres
          containerPort: 5432
        resources:
          requests:
            memory: "512Mi"
            cpu: "500m"
          limits:
            memory: "1Gi"
            cpu: "1"
        volumeMounts:
        - name: postgres-storage
          mountPath: /var/lib/postgresql/data
        - name: postgres-config
          mountPath: /etc/postgresql/postgresql.conf
          subPath: postgresql.conf
        livenessProbe:
          exec:
            command:
            - pg_isready
            - -U
            - postgres
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          exec:
            command:
            - pg_isready
            - -U
            - postgres
          initialDelaySeconds: 5
          periodSeconds: 5
      volumes:
      - name: postgres-config
        configMap:
          name: postgres-config
  volumeClaimTemplates:
  - metadata:
      name: postgres-storage
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: "gp2"
      resources:
        requests:
          storage: 10Gi

---
# Headless Service
apiVersion: v1
kind: Service
metadata:
  name: user-postgres-headless
  namespace: ecommerce
spec:
  clusterIP: None
  ports:
  - name: postgres
    port: 5432
    targetPort: postgres
  selector:
    app: user-postgres

---
# Regular Service
apiVersion: v1
kind: Service
metadata:
  name: user-postgres-service
  namespace: ecommerce
spec:
  type: ClusterIP
  ports:
  - name: postgres
    port: 5432
    targetPort: postgres
  selector:
    app: user-postgres

5.5 監視とログ

5.5.1 Prometheus設定

# deployments/k8s/monitoring/prometheus/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: prometheus
  namespace: monitoring
spec:
  replicas: 1
  selector:
    matchLabels:
      app: prometheus
  template:
    metadata:
      labels:
        app: prometheus
    spec:
      serviceAccountName: prometheus
      containers:
      - name: prometheus
        image: prom/prometheus:v2.45.0
        args:
        - '--config.file=/etc/prometheus/prometheus.yml'
        - '--storage.tsdb.path=/prometheus/'
        - '--web.console.libraries=/etc/prometheus/console_libraries'
        - '--web.console.templates=/etc/prometheus/consoles'
        - '--storage.tsdb.retention.time=200h'
        - '--web.enable-lifecycle'
        ports:
        - containerPort: 9090
        resources:
          requests:
            memory: "512Mi"
            cpu: "500m"
          limits:
            memory: "1Gi"
            cpu: "1"
        volumeMounts:
        - name: prometheus-config
          mountPath: /etc/prometheus/
        - name: prometheus-storage
          mountPath: /prometheus/
      volumes:
      - name: prometheus-config
        configMap:
          name: prometheus-config
      - name: prometheus-storage
        persistentVolumeClaim:
          claimName: prometheus-pvc

---
# Prometheus設定
apiVersion: v1
kind: ConfigMap
metadata:
  name: prometheus-config
  namespace: monitoring
data:
  prometheus.yml: |
    global:
      scrape_interval: 15s
      evaluation_interval: 15s

    rule_files:
    - "/etc/prometheus/alert_rules.yml"

    alerting:
      alertmanagers:
      - static_configs:
        - targets:
          - alertmanager:9093

    scrape_configs:
    # Kubernetes API Server
    - job_name: 'kubernetes-apiserver'
      kubernetes_sd_configs:
      - role: endpoints
        namespaces:
          names:
          - default
      scheme: https
      tls_config:
        ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
      bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
      relabel_configs:
      - source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name]
        action: keep
        regex: default;kubernetes;https

    # マイクロサービス
    - job_name: 'ecommerce-services'
      kubernetes_sd_configs:
      - role: pod
        namespaces:
          names:
          - ecommerce
      relabel_configs:
      - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
        action: keep
        regex: true
      - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
        action: replace
        target_label: __metrics_path__
        regex: (.+)
      - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
        action: replace
        regex: ([^:]+)(?::\d+)?;(\d+)
        replacement: $1:$2
        target_label: __address__
      - action: labelmap
        regex: __meta_kubernetes_pod_label_(.+)
      - source_labels: [__meta_kubernetes_namespace]
        action: replace
        target_label: kubernetes_namespace
      - source_labels: [__meta_kubernetes_pod_name]
        action: replace
        target_label: kubernetes_pod_name

5.5.2 Grafanaダッシュボード

# deployments/k8s/monitoring/grafana/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: grafana
  namespace: monitoring
spec:
  replicas: 1
  selector:
    matchLabels:
      app: grafana
  template:
    metadata:
      labels:
        app: grafana
    spec:
      containers:
      - name: grafana
        image: grafana/grafana:10.0.0
        env:
        - name: GF_SECURITY_ADMIN_PASSWORD
          valueFrom:
            secretKeyRef:
              name: grafana-secrets
              key: admin-password
        ports:
        - containerPort: 3000
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        volumeMounts:
        - name: grafana-storage
          mountPath: /var/lib/grafana
        - name: grafana-datasources
          mountPath: /etc/grafana/provisioning/datasources
        - name: grafana-dashboards
          mountPath: /etc/grafana/provisioning/dashboards
      volumes:
      - name: grafana-storage
        persistentVolumeClaim:
          claimName: grafana-pvc
      - name: grafana-datasources
        configMap:
          name: grafana-datasources
      - name: grafana-dashboards
        configMap:
          name: grafana-dashboards

5.6 CI/CDパイプライン

5.6.1 GitHub Actions Workflow

# .github/workflows/deploy.yml
name: Deploy to Kubernetes

on:
  push:
    branches:
    - main
    - develop
  pull_request:
    branches:
    - main

env:
  REGISTRY: ghcr.io
  CLUSTER_NAME: ecommerce-cluster
  CLUSTER_REGION: us-west-2

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Set up Go
      uses: actions/setup-go@v4
      with:
        go-version: '1.21'

    - name: Run tests
      run: |
        for service in user product order payment notification; do
          echo "Testing $service service..."
          cd services/$service
          go test -v -race -coverprofile=coverage.out ./...
          go tool cover -html=coverage.out -o coverage.html
          cd ../..
        done

    - name: Upload coverage reports
      uses: codecov/codecov-action@v3
      with:
        files: services/*/coverage.out
        flags: unittests

  build:
    needs: test
    runs-on: ubuntu-latest
    strategy:
      matrix:
        service: [user, product, order, payment, notification]
    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3

    - name: Log in to Container Registry
      uses: docker/login-action@v3
      with:
        registry: ${{ env.REGISTRY }}
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}

    - name: Extract metadata
      id: meta
      uses: docker/metadata-action@v5
      with:
        images: ${{ env.REGISTRY }}/${{ github.repository }}/${{ matrix.service }}-service
        tags: |
          type=ref,event=branch
          type=ref,event=pr
          type=sha,prefix=sha-
          type=raw,value=latest,enable={{is_default_branch}}

    - name: Build and push Docker image
      uses: docker/build-push-action@v5
      with:
        context: .
        file: deployments/docker/Dockerfile.${{ matrix.service }}
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}
        cache-from: type=gha
        cache-to: type=gha,mode=max

  deploy-staging:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/develop'
    environment: staging
    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ${{ env.CLUSTER_REGION }}

    - name: Update kubeconfig
      run: |
        aws eks update-kubeconfig --region ${{ env.CLUSTER_REGION }} --name ${{ env.CLUSTER_NAME }}-staging

    - name: Deploy to staging
      run: |
        # Helmチャートを使用してデプロイ
        helm upgrade --install ecommerce ./deployments/helm \
          --namespace ecommerce-staging \
          --create-namespace \
          --values ./deployments/helm/values-staging.yaml \
          --set image.tag=sha-${{ github.sha }} \
          --wait --timeout=600s

    - name: Run smoke tests
      run: |
        kubectl wait --for=condition=ready pod -l app.kubernetes.io/instance=ecommerce -n ecommerce-staging --timeout=300s
        ./scripts/smoke-tests.sh ecommerce-staging

  deploy-production:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment: production
    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ${{ env.CLUSTER_REGION }}

    - name: Update kubeconfig
      run: |
        aws eks update-kubeconfig --region ${{ env.CLUSTER_REGION }} --name ${{ env.CLUSTER_NAME }}-prod

    - name: Deploy to production
      run: |
        # Blue-Green デプロイメント
        helm upgrade --install ecommerce ./deployments/helm \
          --namespace ecommerce \
          --values ./deployments/helm/values-production.yaml \
          --set image.tag=sha-${{ github.sha }} \
          --wait --timeout=600s

    - name: Run health checks
      run: |
        kubectl wait --for=condition=ready pod -l app.kubernetes.io/instance=ecommerce -n ecommerce --timeout=300s
        ./scripts/health-checks.sh ecommerce

    - name: Notify deployment
      uses: 8398a7/action-slack@v3
      with:
        status: ${{ job.status }}
        channel: '#deployments'
        webhook_url: ${{ secrets.SLACK_WEBHOOK }}

5.7 Helmチャート

5.7.1 Chart.yaml

# deployments/helm/Chart.yaml
apiVersion: v2
name: ecommerce-microservices
description: A Helm chart for E-commerce Microservices
type: application
version: 0.1.0
appVersion: "1.0.0"

dependencies:
- name: postgresql
  version: "12.x.x"
  repository: "https://charts.bitnami.com/bitnami"
  condition: postgresql.enabled
- name: redis
  version: "17.x.x"
  repository: "https://charts.bitnami.com/bitnami"
  condition: redis.enabled
- name: kafka
  version: "22.x.x"
  repository: "https://charts.bitnami.com/bitnami"
  condition: kafka.enabled

maintainers:
- name: DevOps Team
  email: devops@example.com

keywords:
- microservices
- ecommerce
- golang
- grpc
- kubernetes

5.7.2 Values.yaml

# deployments/helm/values.yaml
global:
  imageRegistry: "ghcr.io"
  imagePullPolicy: IfNotPresent
  storageClass: "gp2"

# 環境設定
environment: production
namespace: ecommerce

# イメージ設定
image:
  registry: ghcr.io
  repository: example/ecommerce
  tag: "latest"
  pullPolicy: IfNotPresent

# サービス設定
services:
  user:
    enabled: true
    replicaCount: 3
    resources:
      requests:
        memory: "256Mi"
        cpu: "250m"
      limits:
        memory: "512Mi"
        cpu: "500m"
    autoscaling:
      enabled: true
      minReplicas: 2
      maxReplicas: 10
      targetCPUUtilizationPercentage: 70

  product:
    enabled: true
    replicaCount: 5
    resources:
      requests:
        memory: "512Mi"
        cpu: "500m"
      limits:
        memory: "1Gi"
        cpu: "1"
    autoscaling:
      enabled: true
      minReplicas: 3
      maxReplicas: 15
      targetCPUUtilizationPercentage: 70

  order:
    enabled: true
    replicaCount: 3
    resources:
      requests:
        memory: "256Mi"
        cpu: "250m"
      limits:
        memory: "512Mi"
        cpu: "500m"

# データベース設定
postgresql:
  enabled: true
  auth:
    postgresPassword: "postgres"
    database: "ecommerce"
  primary:
    persistence:
      enabled: true
      size: 10Gi
      storageClass: "gp2"
    resources:
      requests:
        memory: "512Mi"
        cpu: "500m"
      limits:
        memory: "1Gi"
        cpu: "1"

# Redis設定
redis:
  enabled: true
  auth:
    enabled: false
  master:
    persistence:
      enabled: true
      size: 5Gi
      storageClass: "gp2"

# Kafka設定
kafka:
  enabled: true
  replicaCount: 3
  persistence:
    enabled: true
    size: 10Gi
    storageClass: "gp2"

# 監視設定
monitoring:
  prometheus:
    enabled: true
  grafana:
    enabled: true
  jaeger:
    enabled: true

# セキュリティ設定
security:
  networkPolicies:
    enabled: true
  podSecurityPolicies:
    enabled: true
  rbac:
    enabled: true

# Ingress設定
ingress:
  enabled: true
  className: "nginx"
  annotations:
    kubernetes.io/ingress.class: nginx
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
  hosts:
  - host: api.ecommerce.example.com
    paths:
    - path: /
      pathType: Prefix
  tls:
  - secretName: ecommerce-tls
    hosts:
    - api.ecommerce.example.com

5.8 セキュリティ設定

5.8.1 Network Policies

# deployments/k8s/security/network-policies.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: ecommerce-network-policy
  namespace: ecommerce
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress
  ingress:
  # API Gatewayからの通信を許可
  - from:
    - namespaceSelector:
        matchLabels:
          name: ingress-nginx
    ports:
    - protocol: TCP
      port: 8080

  # サービス間の内部通信を許可
  - from:
    - podSelector:
        matchLabels:
          app: user-service
    - podSelector:
        matchLabels:
          app: order-service
    ports:
    - protocol: TCP
      port: 50051

  egress:
  # DNS解決を許可
  - to: []
    ports:
    - protocol: UDP
      port: 53

  # データベースへの通信を許可
  - to:
    - podSelector:
        matchLabels:
          app: postgres
    ports:
    - protocol: TCP
      port: 5432

  # 外部APIへの通信を許可(HTTPS)
  - to: []
    ports:
    - protocol: TCP
      port: 443

5.8.2 Pod Security Policy

# deployments/k8s/security/pod-security-policy.yaml
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: ecommerce-psp
spec:
  privileged: false
  allowPrivilegeEscalation: false
  requiredDropCapabilities:
  - ALL
  volumes:
  - 'configMap'
  - 'emptyDir'
  - 'projected'
  - 'secret'
  - 'downwardAPI'
  - 'persistentVolumeClaim'
  runAsUser:
    rule: 'MustRunAsNonRoot'
  seLinux:
    rule: 'RunAsAny'
  fsGroup:
    rule: 'RunAsAny'
  readOnlyRootFilesystem: true

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: ecommerce-psp-use
rules:
- apiGroups: ['policy']
  resources: ['podsecuritypolicies']
  verbs: ['use']
  resourceNames:
  - ecommerce-psp

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: ecommerce-psp-use
roleRef:
  kind: ClusterRole
  name: ecommerce-psp-use
  apiGroup: rbac.authorization.k8s.io
subjects:
- kind: ServiceAccount
  name: default
  namespace: ecommerce

この章では、Go + gRPC + K8sマイクロサービスの包括的なデプロイメント戦略について説明した。次の章では、モノレポとマイクロサービスの具体的な比較について詳しく見ていく。

第6章:アーキテクチャ比較 - モノレポ vs マイクロサービス

6.1 同じECサイトでの構造比較

本章では、同じECサイトを「モノレポ」と「マイクロサービス」で実装した場合の違いを詳しく比較する。実際のコード例を交えながら、それぞれのメリット・デメリットを解説する。

6.1.1 プロジェクト構造の違い

モノレポ構造

ecommerce-monorepo/
├── cmd/
│   └── server/
│       └── main.go                  # 単一エントリーポイント
├── internal/
│   ├── user/                        # ユーザードメイン
│   │   ├── domain/
│   │   ├── usecase/
│   │   └── repository/
│   ├── product/                     # 商品ドメイン
│   │   ├── domain/
│   │   ├── usecase/
│   │   └── repository/
│   ├── order/                       # 注文ドメイン
│   │   ├── domain/
│   │   ├── usecase/
│   │   └── repository/
│   ├── payment/                     # 決済ドメイン
│   │   ├── domain/
│   │   ├── usecase/
│   │   └── repository/
│   └── shared/                      # 共通コンポーネント
│       ├── database/
│       ├── auth/
│       └── logging/
├── api/                             # 単一API層
│   ├── user/
│   ├── product/
│   ├── order/
│   └── payment/
├── migrations/                      # 単一データベース
├── config/
└── deployments/
    └── k8s/
        └── monolith.yaml           # 単一デプロイメント

マイクロサービス構造

ecommerce-microservices/
├── services/                        # 独立したサービス群
│   ├── user/                        # 完全に独立
│   │   ├── cmd/server/main.go
│   │   ├── internal/
│   │   ├── deployments/
│   │   └── go.mod                   # 独立したモジュール
│   ├── product/
│   ├── order/
│   ├── payment/
│   └── notification/
├── shared/                          # 最小限の共有
│   ├── proto/
│   └── common/
└── deployments/
    └── k8s/
        ├── user-service/
        ├── product-service/
        ├── order-service/
        ├── payment-service/
        └── notification-service/

6.2 コード実装の違い

6.2.1 データベースアクセスパターン

モノレポ(単一データベース)

// モノレポでの注文作成(同一トランザクション内)
type OrderService struct {
    db           *sql.DB
    userRepo     user.Repository
    productRepo  product.Repository
    orderRepo    order.Repository
}

func (s *OrderService) CreateOrder(ctx context.Context, req CreateOrderRequest) error {
    // 単一トランザクションで全操作を実行
    tx, err := s.db.BeginTx(ctx, nil)
    if err != nil {
        return err
    }
    defer tx.Rollback()

    // ユーザー存在確認
    user, err := s.userRepo.FindByIDWithTx(ctx, tx, req.UserID)
    if err != nil {
        return err
    }

    // 商品存在確認と在庫確認
    var totalAmount decimal.Decimal
    for _, item := range req.Items {
        product, err := s.productRepo.FindByIDWithTx(ctx, tx, item.ProductID)
        if err != nil {
            return err
        }

        // 在庫更新(同一トランザクション内)
        if err := s.productRepo.UpdateStockWithTx(ctx, tx, item.ProductID, -item.Quantity); err != nil {
            return err
        }

        totalAmount = totalAmount.Add(product.Price.Mul(decimal.NewFromInt(int64(item.Quantity))))
    }

    // 注文作成
    order := &order.Order{
        UserID:      req.UserID,
        Items:       req.Items,
        TotalAmount: totalAmount,
        Status:      order.StatusCreated,
    }

    if err := s.orderRepo.SaveWithTx(ctx, tx, order); err != nil {
        return err
    }

    // 全て成功したらコミット
    return tx.Commit()
}

マイクロサービス(分散データベース)

// マイクロサービスでの注文作成(Sagaパターン)
type OrderService struct {
    orderRepo     domain.OrderRepository
    userClient    userpb.UserServiceClient
    productClient productpb.ProductServiceClient
    eventBus      EventBus
}

func (s *OrderService) CreateOrder(ctx context.Context, req CreateOrderRequest) error {
    // 1. ユーザー存在確認(他サービスへのgRPC呼び出し)
    userResp, err := s.userClient.GetUser(ctx, &userpb.GetUserRequest{
        UserId: req.UserID,
    })
    if err != nil {
        return fmt.Errorf("user validation failed: %w", err)
    }

    // 2. 商品情報と在庫確認(他サービスへのgRPC呼び出し)
    var totalAmount decimal.Decimal
    for _, item := range req.Items {
        productResp, err := s.productClient.GetProduct(ctx, &productpb.GetProductRequest{
            ProductId: item.ProductID,
        })
        if err != nil {
            return fmt.Errorf("product validation failed: %w", err)
        }

        // 在庫予約(別サービス)
        stockResp, err := s.productClient.ReserveStock(ctx, &productpb.ReserveStockRequest{
            ProductId: item.ProductID,
            Quantity:  int32(item.Quantity),
        })
        if err != nil {
            // 既に予約した在庫をロールバック
            s.compensateStockReservations(ctx, req.Items[:len(req.Items)])
            return fmt.Errorf("stock reservation failed: %w", err)
        }

        totalAmount = totalAmount.Add(decimal.NewFromFloat(productResp.Product.Price).Mul(decimal.NewFromInt(int64(item.Quantity))))
    }

    // 3. 注文作成(自サービス内)
    order := &domain.Order{
        UserID:      req.UserID,
        Items:       req.Items,
        TotalAmount: totalAmount,
        Status:      domain.OrderStatusPending,
    }

    if err := s.orderRepo.Save(ctx, order); err != nil {
        // 在庫予約をロールバック
        s.compensateStockReservations(ctx, req.Items)
        return err
    }

    // 4. 注文作成イベント発行(非同期処理開始)
    event := &OrderCreatedEvent{
        OrderID:     order.ID(),
        UserID:      order.UserID(),
        Items:       order.Items(),
        TotalAmount: order.TotalAmount(),
    }

    return s.eventBus.Publish(ctx, event)
}

// 補償処理(Saga Compensation)
func (s *OrderService) compensateStockReservations(ctx context.Context, items []OrderItem) {
    for _, item := range items {
        s.productClient.ReleaseStock(ctx, &productpb.ReleaseStockRequest{
            ProductId: item.ProductID,
            Quantity:  int32(item.Quantity),
        })
    }
}

6.2.2 API設計の違い

モノレポ(RESTful API)

// モノレポでの統一API
type APIServer struct {
    userService    *user.Service
    productService *product.Service
    orderService   *order.Service
    paymentService *payment.Service
}

// 単一エンドポイントで複数ドメインのデータを返却可能
func (s *APIServer) GetOrderDetails(w http.ResponseWriter, r *http.Request) {
    orderID := mux.Vars(r)["id"]

    // 同一プロセス内での呼び出し
    order, err := s.orderService.GetOrder(r.Context(), orderID)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    // ユーザー情報取得(同一プロセス内)
    user, err := s.userService.GetUser(r.Context(), order.UserID)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    // 商品情報取得(同一プロセス内)
    var products []Product
    for _, item := range order.Items {
        product, err := s.productService.GetProduct(r.Context(), item.ProductID)
        if err != nil {
            continue // エラーをスキップして続行
        }
        products = append(products, product)
    }

    // 決済情報取得(同一プロセス内)
    payment, err := s.paymentService.GetPaymentByOrderID(r.Context(), orderID)
    if err != nil {
        // 決済情報がない場合もある
        payment = nil
    }

    // 統合レスポンス
    response := OrderDetailsResponse{
        Order:    order,
        User:     user,
        Products: products,
        Payment:  payment,
    }

    json.NewEncoder(w).Encode(response)
}

マイクロサービス(API Gateway + gRPC)

// API Gatewayでの統合
type APIGateway struct {
    userClient    userpb.UserServiceClient
    productClient productpb.ProductServiceClient
    orderClient   orderpb.OrderServiceClient
    paymentClient paymentpb.PaymentServiceClient
}

func (g *APIGateway) GetOrderDetails(w http.ResponseWriter, r *http.Request) {
    orderID := mux.Vars(r)["id"]

    // 注文サービスから基本情報取得
    orderResp, err := g.orderClient.GetOrder(r.Context(), &orderpb.GetOrderRequest{
        OrderId: orderID,
    })
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    // 並行して関連データを取得
    var wg sync.WaitGroup
    var user *userpb.User
    var products []*productpb.Product
    var payment *paymentpb.Payment

    // ユーザー情報取得(並行処理)
    wg.Add(1)
    go func() {
        defer wg.Done()
        userResp, err := g.userClient.GetUser(r.Context(), &userpb.GetUserRequest{
            UserId: orderResp.Order.UserId,
        })
        if err == nil {
            user = userResp.User
        }
    }()

    // 商品情報取得(並行処理)
    wg.Add(1)
    go func() {
        defer wg.Done()
        for _, item := range orderResp.Order.Items {
            productResp, err := g.productClient.GetProduct(r.Context(), &productpb.GetProductRequest{
                ProductId: item.ProductId,
            })
            if err == nil {
                products = append(products, productResp.Product)
            }
        }
    }()

    // 決済情報取得(並行処理)
    wg.Add(1)
    go func() {
        defer wg.Done()
        paymentResp, err := g.paymentClient.GetPaymentByOrderId(r.Context(), &paymentpb.GetPaymentByOrderIdRequest{
            OrderId: orderID,
        })
        if err == nil {
            payment = paymentResp.Payment
        }
    }()

    wg.Wait()

    // 統合レスポンス
    response := OrderDetailsResponse{
        Order:    orderResp.Order,
        User:     user,
        Products: products,
        Payment:  payment,
    }

    json.NewEncoder(w).Encode(response)
}

6.3 デプロイメントとスケーリングの違い

6.3.1 Kubernetesデプロイメント

モノレポデプロイメント

# モノレポの単一デプロイメント
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ecommerce-monolith
spec:
  replicas: 3
  selector:
    matchLabels:
      app: ecommerce-monolith
  template:
    metadata:
      labels:
        app: ecommerce-monolith
    spec:
      containers:
      - name: ecommerce
        image: ecommerce:latest
        ports:
        - containerPort: 8080
        env:
        - name: DB_HOST
          value: "postgres-service"
        - name: DB_PORT
          value: "5432"
        resources:
          requests:
            memory: "1Gi"
            cpu: "500m"
          limits:
            memory: "2Gi"
            cpu: "1"
---
apiVersion: v1
kind: Service
metadata:
  name: ecommerce-service
spec:
  selector:
    app: ecommerce-monolith
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
  type: LoadBalancer

マイクロサービスデプロイメント

# ユーザーサービス
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
spec:
  replicas: 2
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
    spec:
      containers:
      - name: user-service
        image: user-service:latest
        ports:
        - containerPort: 50051
        env:
        - name: DB_HOST
          value: "user-postgres-service"
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
---
# 商品サービス(高負荷想定でより多くのレプリカ)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: product-service
spec:
  replicas: 5  # 商品検索の高負荷に対応
  selector:
    matchLabels:
      app: product-service
  template:
    metadata:
      labels:
        app: product-service
    spec:
      containers:
      - name: product-service
        image: product-service:latest
        ports:
        - containerPort: 50052
        env:
        - name: DB_HOST
          value: "product-postgres-service"
        resources:
          requests:
            memory: "512Mi"
            cpu: "500m"
          limits:
            memory: "1Gi"
            cpu: "1"
---
# 水平スケーリング設定
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: product-service-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: product-service
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

6.3.2 スケーリング戦略の比較

モノレポのスケーリング

# 全体をスケール(効率が悪い)
kubectl scale deployment ecommerce-monolith --replicas=10

# CPUベースの自動スケーリング
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: ecommerce-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: ecommerce-monolith
  minReplicas: 3
  maxReplicas: 20
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 80

マイクロサービスの細粒度スケーリング

# 商品サービスのみ高負荷時にスケール
kubectl scale deployment product-service --replicas=15

# ユーザーサービスは少ないまま
kubectl scale deployment user-service --replicas=2

# 注文サービスは中程度
kubectl scale deployment order-service --replicas=5

6.4 開発・運用の比較

6.4.1 開発チーム構成

モノレポでの開発

開発チーム構成:
┌─────────────────────────────────┐
│        Full-Stack Team          │
│  (8-12 developers)             │
├─────────────────────────────────┤
│ • Backend Developers (4-6人)    │
│ • Frontend Developers (2-3人)   │
│ • DevOps Engineer (1人)        │
│ • QA Engineer (1-2人)          │
└─────────────────────────────────┘

メリット:
- チーム間コミュニケーションが容易
- 統一された技術スタック
- 機能横断的な変更が容易

デメリット:
- 大きなチームでの並行開発が困難
- 単一の技術スタックに制約
- 全体のビルド・テスト時間が長い

マイクロサービスでの開発

開発チーム構成:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│   User Team     │ │  Product Team   │ │   Order Team    │
│   (2-3人)       │ │   (3-4人)       │ │   (2-3人)       │
├─────────────────┤ ├─────────────────┤ ├─────────────────┤
│• Backend Dev    │ │• Backend Devs   │ │• Backend Dev    │
│• Frontend Dev   │ │• Search Eng     │ │• Frontend Dev   │
│                 │ │• Frontend Dev   │ │                 │
└─────────────────┘ └─────────────────┘ └─────────────────┘

┌─────────────────┐ ┌─────────────────┐
│  Payment Team   │ │ Platform Team   │
│   (2-3人)       │ │   (3-4人)       │
├─────────────────┤ ├─────────────────┤
│• Backend Dev    │ │• DevOps Eng     │
│• Security Eng   │ │• SRE            │
│                 │ │• Infrastructure │
└─────────────────┘ └─────────────────┘

メリット:
- 小さなチームでの高速開発
- 独立した技術選択
- 並行開発が容易

デメリット:
- チーム間調整コスト
- 技術スタック乱立リスク
- サービス間連携の複雑性

6.4.2 CI/CDパイプライン

モノレポのCI/CD

# .github/workflows/monolith-ci.yml
name: Monolith CI/CD

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3

    - name: Set up Go
      uses: actions/setup-go@v3
      with:
        go-version: 1.21

    - name: Run all tests
      run: |
        go test ./...  # 全てのテストを実行

    - name: Build application
      run: |
        go build -o bin/ecommerce cmd/server/main.go

    - name: Build Docker image
      run: |
        docker build -t ecommerce:${{ github.sha }} .

    - name: Deploy to staging
      if: github.ref == 'refs/heads/develop'
      run: |
        kubectl set image deployment/ecommerce-monolith \
          ecommerce=ecommerce:${{ github.sha }}

    - name: Deploy to production
      if: github.ref == 'refs/heads/main'
      run: |
        kubectl set image deployment/ecommerce-monolith \
          ecommerce=ecommerce:${{ github.sha }}

マイクロサービスのCI/CD

# .github/workflows/user-service-ci.yml
name: User Service CI/CD

on:
  push:
    paths:
    - 'services/user/**'  # ユーザーサービスの変更のみ
    branches: [main, develop]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3

    - name: Set up Go
      uses: actions/setup-go@v3
      with:
        go-version: 1.21

    - name: Test user service only
      run: |
        cd services/user
        go test ./...

    - name: Build user service
      run: |
        cd services/user
        go build -o bin/user-service cmd/server/main.go

    - name: Build Docker image
      run: |
        docker build -f deployments/docker/Dockerfile.user \
          -t user-service:${{ github.sha }} .

    - name: Deploy user service only
      if: github.ref == 'refs/heads/main'
      run: |
        kubectl set image deployment/user-service \
          user-service=user-service:${{ github.sha }}

# 他のサービスも同様に独立したパイプライン

6.5 まとめ:適用場面の指針

6.5.1 モノレポが適している場面

プロジェクト特性:
✅ 小〜中規模チーム(10人以下)
✅ 機能間の結合が強い
✅ 一貫したユーザー体験が重要
✅ 技術スタック統一が望ましい
✅ 初期の開発速度を重視

技術要件:
✅ トランザクション整合性が重要
✅ 低レイテンシが要求される
✅ シンプルなデプロイメント
✅ 運用コストを抑えたい

ビジネス要件:
✅ 迅速な機能リリース
✅ 統一されたUIUX
✅ データ整合性が最優先
✅ 初期コストを抑えたい

6.5.2 マイクロサービスが適している場面

プロジェクト特性:
✅ 大規模チーム(15人以上)
✅ 異なるドメインが独立している
✅ 各機能の要件が大きく異なる
✅ 技術選択の自由度が欲しい
✅ 長期的な拡張性を重視

技術要件:
✅ 高いスケーラビリティが必要
✅ 部分的な障害許容性
✅ 異なる技術スタックが最適
✅ 独立したデプロイメント

ビジネス要件:
✅ 各チームの自律性
✅ 段階的なスケール
✅ 特定機能の高可用性
✅ 長期的なROI重視

6.5.3 移行戦略

モノレポからマイクロサービスへの段階的移行

// Phase 1: ドメイン分離(同一プロセス内)
type ECommerceApp struct {
    userModule    *user.Module
    productModule *product.Module
    orderModule   *order.Module
}

// Phase 2: APIの分離
type UserAPI struct {
    userModule *user.Module
}

type ProductAPI struct {
    productModule *product.Module
}

// Phase 3: プロセス分離
// 各モジュールを独立したサービスとして分離

// Phase 4: データベース分離
// 各サービスに専用データベースを割り当て

この比較により、プロジェクトの特性に応じた適切なアーキテクチャ選択が可能である。重要なのは、現在の要件だけでなく、将来の成長性や組織の変化も考慮することである。

0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?