Contents
スポンサードリンク
1️⃣ サービス粒度と境界の決め方
1.1 DDD と Bounded Context の基本
| 項目 | 内容 |
|---|---|
| 概念 | Bounded Context は、ドメインモデルとその整合性を保つために「データ所有権」と「ビジネスルール」の境界を明確化する設計単位です。 |
| 利点 | 境界がはっきりすれば、チーム間の所有責任が分離され、変更が他サービスへ波及しにくくなります(《Domain‑Driven Design, Evans 2003》)。 |
| 実務での指標 | - ユビキタス言語 が自然に切り替わる場所 - トランザクション境界 が跨がらないこと - データ更新頻度 が大きく異なる領域 |
1.2 サービス例:User と Order
| 項目 | User Service | Order Service |
|---|---|---|
| 主なエンティティ | User, Profile, Credential |
Order, Cart, Payment |
| データストア | PostgreSQL(認証情報) | MySQL(取引履歴)+Redis(カート) |
| 主要 API | CreateUser, GetProfile |
PlaceOrder, CancelOrder |
| 所有チーム | AuthTeam | OrderTeam |
| 境界根拠 | 認可ロジックは ユーザー単位 に集中させ、注文フローは在庫・決済と独立させることで相互依存を排除 |
ポイント:サービス粒度は「ビジネス機能の独立性」だけでなく、「データ所有権」と「チーム責任」の分離でも測ります。
2️⃣ Protocol Buffers と gRPC の基礎
2.1 .proto ファイル設計のベストプラクティス
| 項目 | 推奨 |
|---|---|
| syntax | proto3(非推奨な required/optional が無く、将来拡張しやすい) |
| パッケージ | yourdomain.service.v1 のようにバージョンを明示 |
| go_package オプション | "github.com/yourorg/project/internal/<pkg>;pb" で名前空間衝突防止 |
| フィールド番号 | 1 〜 15 はタグが 1 バイト に収まります((field_number << 3) | wire_type が 0x08‑0x7F)。16 以上は 2 バイト 必要になるため、将来追加分は 16 以降に割り当てると、既存メッセージのバイナリサイズ増加を抑制できます。※公式ドキュメント: Encoding – Protocol Buffers(Google) |
| コメント | // コメントは protoc-gen-doc で自動生成できるので必ず書く |
フィールド番号とサイズ増加の具体例
|
1 2 3 4 5 6 |
message Sample { int32 id = 1; // tag: 0x08 (1 byte) string name = 2; // tag: 0x12 (1 byte) string description = 16; // tag: 0x82 0x01 (2 bytes) ← 16 以上は 2 バイト } |
idとnameのタグは 1 バイト。descriptionは 2 バイト(0x82, 0x01)になるため、同じペイロード長でもメッセージ全体が 1 バイト多く なります。大量レコードでこの差が積み重なると、ネットワーク帯域やストレージに実質的なコスト増となります(Google のベンチマークで 10 % 程度のサイズ増加が確認されています)。
2.2 gRPC サーバ/クライアント実装の要点
| 項目 | 推奨 |
|---|---|
| Context の受け渡し | CreateUser(ctx context.Context, req *pb.CreateUserRequest) のように必ず context を受け取り、タイムアウト・キャンセルを伝搬 |
| エラーハンドリング | status.Errorf(codes.Internal, "...") で gRPC 標準ステータスコードを返す。クライアント側は status.FromError(err) でコード取得しリトライロジックに活用 |
| バリデーション | proto に必須項目が無いので、サーバ側で入力検証(例: email 正規表現)を実装 |
| oneof の利用 | 複数フォーマットの互換性確保に便利。例: oneof contact { string email = 2; string phone = 3; } |
サーバ側サンプル(User Service)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
package user import ( "context" "log" pb "github.com/yourorg/project/internal/userpb" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) type server struct { pb.UnimplementedUserServiceServer svc *service } func (s *server) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) { if deadline, ok := ctx.Deadline(); ok { log.Printf("deadline set: %v", deadline) } id, err := s.svc.Create(ctx, req.GetUser()) if err != nil { return nil, status.Errorf(codes.Internal, "failed to create user: %w", err) } return &pb.CreateUserResponse{Id: id}, nil } |
クライアント側サンプル
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
package main import ( "context" "log" "time" pb "github.com/yourorg/project/internal/userpb" "google.golang.org/grpc" "google.golang.org/grpc/status" ) func main() { conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatalf("dial error: %v", err) } defer conn.Close() client := pb.NewUserServiceClient(conn) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() resp, err := client.CreateUser(ctx, &pb.CreateUserRequest{ User: &pb.User{Email: "alice@example.com", Name: "Alice"}, }) if err != nil { st, _ := status.FromError(err) log.Fatalf("gRPC error %s: %v", st.Code(), st.Message()) } log.Printf("created user id: %d", resp.Id) } |
3️⃣ Go プロジェクトの構造とモジュール管理
3.1 推奨ディレクトリレイアウト
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
myservice/ ├─ cmd/ # エントリポイント(main.go) │ └─ server/ │ └─ main.go ├─ internal/ # ビジネスロジック・gRPC ハンドラ(外部からは import不可) │ ├─ user/ │ │ ├─ handler.go │ │ ├─ service.go │ │ └─ repository.go │ └─ order/ … ├─ pkg/ # 再利用可能ライブラリ(ロガー、ユーティリティ等) └─ go.mod |
internalは モジュール境界 をコンパイル時に強制し、意図しない依存を防止します(公式 Go 設計ガイド参照)。cmd/server/main.goは設定ロード・サーバ起動だけに留め、ロジックはすべてinternal/*に委譲。
3.2 モジュールと依存関係
|
1 2 3 4 5 6 7 8 9 10 |
# go.mod の例(Go 1.22) module github.com/yourorg/myservice go 1.22 require ( google.golang.org/grpc v1.58.0 google.golang.org/protobuf v1.31.0 ) |
go mod tidy により未使用の依存は自動除去、CI では actions/cache を活用してビルド時間を約30 %短縮できます。
4️⃣ Docker コンテナ化とマルチステージビルド
4.1 軽量イメージ作成のベストプラクティス
| ステップ | 内容 |
|---|---|
| Build Stage | golang:1.22-alpine → ソース取得、buf generate、静的リンクビルド(CGO_ENABLED=0) |
| Runtime Stage | gcr.io/distroless/static:nonroot または scratch → ビルド成果物だけをコピー |
| レイヤーキャッシュ活用 | go.mod/go.sum を最初にコピーし、依存変更が無い限り再ビルドを回避 |
完全サンプル Dockerfile
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
# ---------- Build Stage ---------- FROM golang:1.22-alpine AS builder WORKDIR /src # Go モジュールキャッシュ層 COPY go.mod go.sum ./ RUN go mod download # ソースコードと proto 生成 COPY . . RUN apk add --no-cache protobuf buf && \ buf generate # 静的リンクビルド ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64 RUN go build -ldflags="-s -w" -o /out/usersvc ./cmd/server # ---------- Runtime Stage ---------- FROM gcr.io/distroless/static:nonroot AS runtime WORKDIR /app COPY --from=builder /out/usersvc . EXPOSE 50051 USER nonroot ENTRYPOINT ["./usersvc"] |
- サイズ:
distroless/staticは約 9 MB、scratchなら 5 MB 程度に収まります。 - セキュリティ:glibc が無いため攻撃面が大幅に縮小。
5️⃣ ローカル開発環境と本番デプロイ
5.1 docker‑compose によるローカルマルチサービス構成
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
version: "3.9" services: usersvc: build: . ports: ["50051:50051"] environment: - DB_DSN=postgres://user:pass@db:5432/users?sslmode=disable depends_on: [db] ordersvc: build: context: ./order ports: ["50052:50052"] environment: - DB_DSN=mysql://user:pass@db:3306/orders depends_on: [db, redis] db: image: postgres:15-alpine environment: POSTGRES_USER: user POSTGRES_PASSWORD: pass POSTGRES_DB: users volumes: ["pgdata:/var/lib/postgresql/data"] redis: image: redis:7-alpine volumes: pgdata: |
depends_onは 起動順序 のみ保証します。実運用では各コンテナに healthcheck を追加し、依存サービスが正常になるまで待機させます。
5.2 Kubernetes Manifest(Production 用)
deployment.yaml
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
apiVersion: apps/v1 kind: Deployment metadata: name: usersvc spec: replicas: 3 selector: matchLabels: app: usersvc template: metadata: labels: app: usersvc spec: containers: - name: usersvc image: ghcr.io/yourorg/usersvc:latest ports: - containerPort: 50051 envFrom: - configMapRef: name: usersvc-config readinessProbe: grpc: port: 50051 initialDelaySeconds: 5 periodSeconds: 10 |
service.yaml
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
apiVersion: v1 kind: Service metadata: name: usersvc spec: selector: app: usersvc ports: - protocol: TCP port: 50051 targetPort: 50051 type: ClusterIP |
configmap.yaml
|
1 2 3 4 5 6 7 |
apiVersion: v1 kind: ConfigMap metadata: name: usersvc-config data: DB_DSN: "postgres://user:pass@db-service:5432/users?sslmode=disable" |
- ポイント:
readinessProbeに gRPC ヘルスチェックを設定し、Pod が正常になるまでトラフィックを受け付けません。 - 機密情報は
Secretに分離し、envFrom.secretRefで注入します。
6️⃣ CI/CD パイプラインと Observability
6.1 GitHub Actions(テスト・ビルド・デプロイ)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
name: CI/CD on: push: branches: [main] pull_request: jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.22' - name: Cache Go modules uses: actions/cache@v3 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - run: go test ./... build-and-deploy: needs: test runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - name: Install protobuf tools run: | sudo apt-get update && sudo apt-get install -y protobuf-compiler go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest - name: Generate code run: buf generate - name: Build Docker image run: | docker build -t ghcr.io/${{ github.repository }}/usersvc:${{ github.sha }} . - name: Log in to GHCR uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Push image run: | docker push ghcr.io/${{ github.repository }}/usersvc:${{ github.sha }} - name: Deploy to K8s env: KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG }} run: | echo "$KUBE_CONFIG_DATA" | base64 -d > ~/.kube/config kubectl set image deployment/usersvc usersvc=ghcr.io/${{ github.repository }}/usersvc:${{ github.sha }} --record |
buf generateを CI に組み込むことで proto 変更とコード生成の不整合 を防止。actions/cacheがモジュールキャッシュを共有し、ビルド時間を約30 %短縮。
6.2 OpenTelemetry による可観測性
初期化(Tracer Provider)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
import ( "context" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.17.0" ) func initTracer() (*sdktrace.TracerProvider, error) { exp, err := otlptracegrpc.New(context.Background(), otlptracegrpc.WithEndpoint("otel-collector:4317"), otlptracegrpc.WithInsecure()) if err != nil { return nil, err } res, _ := resource.New(context.Background(), resource.WithAttributes( semconv.ServiceNameKey.String("user-service"), semconv.DeploymentEnvironmentKey.String("production"), )) tp := sdktrace.NewTracerProvider( sdktrace.WithBatcher(exp), sdktrace.WithResource(res), ) otel.SetTracerProvider(tp) return tp, nil } |
- Collector → Jaeger / Prometheus にエクスポートすれば、トレース遅延は 5 ms 未満に抑えられます(Istio + OpenTelemetry の実績参照)。
- gRPC サーバでは
otelgrpc.UnaryServerInterceptorをミドルウェアとして登録し、全 RPC が自動的に計測されます。
6.3 セキュリティ:mTLS と JWT
TLS 設定と認可インターセプタ
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
import ( "context" "os" "github.com/golang-jwt/jwt/v5" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" ) func main() { creds, _ := credentials.NewServerTLSFromFile("certs/server.crt", "certs/server.key") s := grpc.NewServer( grpc.Creds(creds), grpc.UnaryInterceptor(authInterceptor), ) // Register services ... } func authInterceptor( ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, ) (interface{}, error) { md, ok := metadata.FromIncomingContext(ctx) if !ok { return nil, status.Error(codes.Unauthenticated, "missing metadata") } ts := md["authorization"] if len(ts) == 0 { return nil, status.Error(codes.Unauthenticated, "no token") } token, err := jwt.Parse(ts[0], func(t *jwt.Token) (interface{}, error) { return []byte(os.Getenv("JWT_SECRET")), nil }) if err != nil || !token.Valid { return nil, status.Error(codes.Unauthenticated, "invalid JWT") } newCtx := context.WithValue(ctx, "claims", token.Claims) return handler(newCtx, req) } |
- mTLS により通信路が暗号化され、相互認証でサービス間の信頼性を確保。
- JWT はリクエスト単位の認可情報(ロール等)を提供し、インターセプタで一元管理できるため RBAC が容易になる。
7️⃣ まとめと次のアクション
| フェーズ | 主な作業 |
|---|---|
| 設計 | DDD の Bounded Context を基にサービス境界を決定(User / Order) |
| スキーマ | .proto 作成 → buf generate で Go コード生成、フィールド番号は 1‑15 に抑える |
| 実装 | Go モジュール構造 (cmd/, internal/, pkg/) → gRPC ハンドラ・クライアント実装、Context とステータスコード活用 |
| コンテナ化 | マルチステージ Dockerfile(distroless)で 10 MB 未満のイメージを作成 |
| デプロイ | docker‑compose(ローカル)、Kubernetes Manifest(本番)でローリングアップデート |
| CI/CD | GitHub Actions → テスト・コード生成・Docker ビルド・K8s デプロイ |
| 観測性 | OpenTelemetry SDK + Collector → Jaeger / Prometheus |
| セキュリティ | mTLS + JWT インターセプタで暗号化・認可を統合 |
行動チェックリスト
- [ ] リポジトリをクローン
git clone https://github.com/yourorg/go-microservice-sample.git - [ ]
buf generateで proto コード生成、エラーが無いことを確認 - [ ]
go test ./...がすべて成功するかローカルで実行 - [ ]
docker compose up -dで User/Order サービスと DB が起動できるか検証 - [ ] Kubernetes へデプロイ
kubectl apply -k k8s/overlays/staging - [ ] GitHub Actions が自動ビルド・プッシュし、
kubectl set imageが正しく実行されることを確認 - [ ] Jaeger UI と Prometheus のメトリクスが取得できているかブラウザでチェック
- [ ]
curl -v https://usersvc:50051などで mTLS が機能し、JWT 認証が通過するエンドツーエンドテストを実施
これらの手順を完了すれば、Go + gRPC + Protocol Buffers による本格的なマイクロサービス基盤が構築できます。ビジネス要件に合わせてサービスを増減させながら、上記ベストプラクティスを継続的に適用していくことが、長期的な保守性と拡張性の鍵となります。
本稿は執筆時点(2024 年)で確認できた公開情報に基づいています。将来的に公式ドキュメントやツールのバージョンが更新された場合は、適宜内容を見直してください。
スポンサードリンク