第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の特徴
-
Protocol Buffersの使用
- バイナリシリアライゼーション
- 型安全性の保証
- 言語間の互換性
-
HTTP/2ベース
- ストリーミング通信のサポート
- 多重化による効率的な通信
- フロー制御とバックプレッシャー
-
コード生成
- .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の主要コンポーネント
- Pod:最小のデプロイ単位
- Service:ポッド間の通信を管理
- Deployment:アプリケーションのデプロイメント管理
- ConfigMap/Secret:設定情報の管理
- 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.5 本プロジェクトでの技術選択理由
なぜこの組み合わせなのか?
Go + gRPC + K8sの組み合わせは、現代のマイクロサービス開発において非常に強力である理由を説明する。
1. パフォーマンス最適化
- Go:高速な実行速度とメモリ効率
- gRPC:バイナリプロトコルによる高速通信
- K8s:効率的なリソース管理
2. 開発者体験
- Go:シンプルで学習しやすい
- gRPC:型安全な通信
- K8s:宣言的な設定管理
3. 運用効率
- Go:小さなバイナリサイズ
- gRPC:双方向ストリーミング
- K8s:自動化された運用
ECサイトを例に選んだ理由
ECサイトは、マイクロサービスアーキテクチャの学習に最適な例である。
- 明確なドメイン境界:ユーザー、商品、注文、決済など
- 現実的な要件:スケーラビリティ、可用性、一貫性
- 実装の多様性: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: データベース分離
// 各サービスに専用データベースを割り当て
この比較により、プロジェクトの特性に応じた適切なアーキテクチャ選択が可能である。重要なのは、現在の要件だけでなく、将来の成長性や組織の変化も考慮することである。