EコマースAPI(第3部):デプロイとモニタリング

アーキテクチャも整い、セキュリティ対策も万全です。さあ、本番運用を始めましょう。

最初の1行のコードから本番環境対応まで:Swaggerドキュメント、グレースフルシャットダウン、Dockerによるデプロイ、pprofによるチューニング、Prometheusによるモニタリング。ボブが開発したeコマースAPIは、実際のユーザーを迎える準備が整いました。

1. 学習内容


2. ストーリー:打ち上げの前夜

(1) 課題:ドキュメントの欠如、監視体制の欠如、および手動によるデプロイ

ボブのEコマースAPIは完全に機能しているが、運用チームはそれを受け入れることを拒否している:

「運用チームはこう言いました。『APIのドキュメントがなければ、フロントエンドを統合できません。ヘルスチェックがなければ、サービスが稼働しているかどうかさえわかりません。モニタリングがなければ、サービスがダウンしたかどうかすら把握できません。しかも、デプロイのためにバイナリを手動でSCP転送しなければならないなんて、あまりにも原始的すぎます』」

立ち上げチェックリスト:

TEXT
❌ API ドキュメント → フロントエンドは毎回尋ねてくる"このインターフェースはどのようなフィールドを返すのか?"
❌ エレガントに閉じる → kill このプロセスにより、処理中の注文が失われた
❌ Docker 展開 → scp バイナリをサーバーへ、手動起動
❌ 性能モニタリング → わからない API どこが遅いのか

(2) 授業の目標:実運用レベル

TEXT
✅ Swagger ドキュメント → フロントエンドでのセルフチェック
✅ Graceful Shutdown → 信号処理 + 接続クローズを待機
✅ Docker Compose → 1つのコマンドでフルスタックを起動
✅ pprof + Prometheus → パフォーマンスの可視化

3. 完全な実装

▶ サンプル:正常なシャットダウン

GO
// cmd/server/main.go
package main

import (
    "context"
    "database/sql"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "ecommerce/internal/handler"
    "ecommerce/internal/repository"
    "ecommerce/internal/service"
    _ "github.com/mattn/go-sqlite3"
)

func main() {
    db, err := sql.Open("sqlite3", "./ecommerce.db")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    db.SetMaxOpenConns(25)
    db.SetMaxIdleConns(10)
    db.SetConnMaxLifetime(5 * time.Minute)

    // 依存性注入
    userRepo := repository.NewUserRepository(db)
    productRepo := repository.NewProductRepository(db)
    orderRepo := repository.NewOrderRepository(db)

    userSvc := service.NewUserService(userRepo)
    productSvc := service.NewProductService(productRepo)
    orderSvc := service.NewOrderService(orderRepo, productRepo, userRepo)

    userHandler := handler.NewUserHandler(userSvc)
    productHandler := handler.NewProductHandler(productSvc)
    orderHandler := handler.NewOrderHandler(orderSvc)

    mux := http.NewServeMux()
    userHandler.Register(mux)
    productHandler.Register(mux)
    orderHandler.Register(mux)

    // ヘルスチェックエンドポイント
    mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
    })

    mux.HandleFunc("GET /ready", func(w http.ResponseWriter, r *http.Request) {
        if err := db.Ping(); err != nil {
            w.WriteHeader(http.StatusServiceUnavailable)
            json.NewEncoder(w).Encode(map[string]string{"status": "not ready"})
            return
        }
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(map[string]string{"status": "ready"})
    })

    server := &http.Server{
        Addr:    ":8080",
        Handler: mux,
        // タイムアウト設定(スロー接続攻撃の防止)
        ReadTimeout:       10 * time.Second,
        WriteTimeout:      10 * time.Second,
        IdleTimeout:       60 * time.Second,
        ReadHeaderTimeout: 5 * time.Second,
    }

    // Graceful Shutdown
    go func() {
        sigCh := make(chan os.Signal, 1)
        signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
        sig := <-sigCh
        log.Printf("信号を受信しました %v、閉じようとしています...", sig)

        // 処理中のリクエストに最大 30 秒の猶予
        ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
        defer cancel()

        if err := server.Shutdown(ctx); err != nil {
            log.Printf("強制終了: %v", err)
        }
    }()

    log.Println("Eコマース API 開始日: :8080")
    if err := server.ListenAndServe(); err != http.ErrServerClosed {
        log.Fatal(err)
    }
    log.Println("サービスは正常に終了しました")
}
▶ 試してみよう

▶ サンプル:pprof と Prometheus の連携

GO
// cmd/server/main.go(pprof と Prometheus の一部)

import (
    "net/http/pprof"  // 標準ライブラリ pprof
    "runtime"
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

// Prometheus メトリクス
var (
    httpRequestsTotal = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "http_requests_total",
            Help: "Total HTTP requests",
        },
        []string{"method", "path", "status"},
    )

    httpRequestDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "http_request_duration_seconds",
            Help:    "HTTP request duration in seconds",
            Buckets: []float64{.001, .005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5},
        },
        []string{"method", "path"},
    )

    activeGoroutines = prometheus.NewGaugeFunc(
        prometheus.GaugeOpts{
            Name: "go_goroutines_active",
            Help: "Current number of goroutines",
        },
        func() float64 {
            return float64(runtime.NumGoroutine())
        },
    )
)

func init() {
    prometheus.MustRegister(httpRequestsTotal)
    prometheus.MustRegister(httpRequestDuration)
    prometheus.MustRegister(activeGoroutines)
}

// Prometheus ミドルウェア
func prometheusMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        // ResponseWriter をラップしてステータスコードを取得
        wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
        next.ServeHTTP(wrapped, r)

        duration := time.Since(start)
        httpRequestsTotal.WithLabelValues(
            r.Method, r.URL.Path, strconv.Itoa(wrapped.statusCode),
        ).Inc()
        httpRequestDuration.WithLabelValues(
            r.Method, r.URL.Path,
        ).Observe(duration.Seconds())
    })
}

type responseWriter struct {
    http.ResponseWriter
    statusCode int
}

func (rw *responseWriter) WriteHeader(code int) {
    rw.statusCode = code
    rw.ResponseWriter.WriteHeader(code)
}

// main で pprof と Prometheus エンドポイントを登録
func registerMonitoring(mux *http.ServeMux) {
    // pprof エンドポイント
    mux.HandleFunc("/debug/pprof/", pprof.Index)
    mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
    mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
    mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
    mux.HandleFunc("/debug/pprof/trace", pprof.Trace)

    // Prometheus メトリクス
    mux.Handle("/metrics", promhttp.Handler())
}
▶ 試してみよう

▶ サンプル:Dockerによるデプロイ

DOCKERFILE
# Dockerfile(Multi-stage Build)
FROM golang:1.22-alpine AS builder
WORKDIR /app

# 依存関係をキャッシュ
COPY go.mod go.sum ./
RUN go mod download

# ソースコード + コンパイル
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server ./cmd/server

# === 実行ステージ ===
FROM alpine:3.19
RUN apk --no-cache add ca-certificates tzdata

COPY --from=builder /app/server /server

EXPOSE 8080
EXPOSE 6060  # pprof

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD wget -qO- http://localhost:8080/health || exit 1

CMD ["/server"]
YAML
# docker-compose.yml
version: '3.8'

services:
  api:
    build: .
    ports:
      - "8080:8080"
      - "6060:6060"   # pprof
    environment:
      - DB_PATH=/data/ecommerce.db
      - GIN_MODE=release
    volumes:
      - data:/data
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:8080/health"]
      interval: 30s
      timeout: 3s
      retries: 3
      start_period: 5s

  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:
      - grafana_data:/var/lib/grafana

volumes:
  data:
  prometheus_data:
  grafana_data:
YAML
# prometheus.yml
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'ecommerce-api'
    static_configs:
      - targets: ['api:8080']
    metrics_path: '/metrics'

▶ サンプル:Swaggerのドキュメント

GO
// コメントから OpenAPI ドキュメントを生成(swag ツール連携)

// Package handler handles HTTP requests.
//
// Eコマース API
//
//     Schemes: http
//     Host: localhost:8080
//     BasePath: /api/v1
//     Version: 1.0.0
//
//     Consumes:
//     - application/json
//
//     Produces:
//     - application/json
//
// swagger:meta
パッケージ handler

import "ecommerce/pkg/model"

// User ユーザー情報
// swagger:model
type User struct {
    ID    int    `json:"id"`
    Email 文字列 `json:"email"`
    Name  文字列 `json:"name"`
}

// RegisterUser ユーザー登録
// swagger:ルート POST /api/v1/register users registerUser
//
// 新規ユーザー登録
//
//     Consumes:
//     - application/json
//
//     Produces:
//     - application/json
//
//     Responses:
//       201: User
//       400: ErrorResponse
//       409: ErrorResponse
func (h *UserHandler) RegisterUser(w http.ResponseWriter, r *http.Request) {
    // handler implementation
}

// swagger:parameters registerUser
type RegisterUserParams struct {
    // in: body
    Body model.RegisterRequest
}

// ErrorResponse エラー応答
// swagger:レスポンス
type ErrorResponse struct {
    // in: body
    Body struct {
        Error 文字列 `json:"エラー"`
    }
}
▶ 試してみよう
BASH
# インストール swag CLI
$ go install github.com/swaggo/swag/cmd/swag@latest

# ドキュメントを生成
$ swag init -g cmd/server/main.go

# ドキュメントにアクセス
# http://localhost:8080/swagger/index.html

(4) 打ち上げチェックリスト

TEXT
✅ Graceful Shutdown — kill 処理終了後、リクエストの完了を待つ
✅ Health Check — /health と /ready エンドポイント
✅ pprof — /debug/pprof/ 性能分析
✅ Prometheus — /metrics 指標の開示
✅ Swagger — API ドキュメントの自動生成
✅ Docker — Multi-stage build、イメージ < 20 MB
✅ docker-compose — 1つのコマンドでフルスタックを起動
✅ ReadTimeout / WriteTimeout — スロー接続攻撃の防止
✅ DB 接続プール — MaxOpenConns=25, MaxIdleConns=10
✅ server.Shutdown — SIGINT/SIGTERM 処理
💡 ヒント: 本番環境では、以下のものも必要です:(1) TLS およびドメイン名を処理するためのリバースプロキシ(Nginx / Caddy);(2) ビルドとデプロイを自動化する CI/CD パイプライン;(3) 一元化されたログ管理(Loki や Elasticsearch など); (4) アラートルール(例:5xxエラー率が1%を超えた際にアラートを発動するなど)。これらのトピックは本レッスンの範囲外ですが、デプロイ前に設定しておくことをお勧めします。


❓ よくある質問

Q なぜ正常なシャットダウンが重要なのでしょうか?
A SIGTERMシグナルを受信した場合(KubernetesがPodを終了させる場合など)、server.Shutdownは、すべてのアクティブなHTTPリクエストが完了するまで(指定されたタイムアウトまで)待機してから、リスナーを閉じます。プロセスが即座に強制終了されると、現在処理中のリクエストが中断され、データの順序に不整合が生じることになります。
Q サーバーのタイムアウト設定にはどのようなものがありますか?
A (1) ReadTimeout:リクエスト全体(本文を含む)を読み込むためのタイムアウト; (2) WriteTimeout:レスポンスを送信するためのタイムアウト; (3) IdleTimeout:Keep-Alive接続のアイドルタイムアウト; (4) ReadHeaderTimeout:リクエストヘッダーを読み込むためのタイムアウト。低速接続攻撃を防ぐため、これらの設定をすべて適切に構成することを推奨します。
Q pprofは本番環境で安全ですか?
A pprofエンドポイントは外部に公開すべきではありません。解決策:(1) 業務用ポートと共有されていない内部ポート(例::6060)で提供すること;(2) /debug/pprof/ パスを認証ミドルウェアで保護すること;(3) ステージング環境でのみ有効にすること。本番環境では、必要な場合にのみ一時的にプロファイリングを有効にすることを推奨します。
Q Prometheusのメトリクスを公開するにはどうすればよいですか?
A promhttp.Handler() を使用して、/metrics エンドポイントでメトリクスを公開します。Prometheusサービスは、このエンドポイントを定期的にスクレイピングします。GrafanaはPrometheusのデータソースに接続し、可視化を作成します。標準的なメトリクスには、リクエスト数、レイテンシの分布、エラー率、goroutineの数、GCイベントの数が含まれます。
Q OpenAPIドキュメントを自動的に生成するにはどうすればよいですか?
A swaggo/swag ツールを使用します。ハンドラーコード内に特定の形式でコメントを記述し、swag init を実行して docs/ ディレクトリを生成します。その後、swaggo/http-swagger を使用して、ドキュメントを /swagger/ エンドポイントにマウントします。コメントの形式:// swagger:route// swagger:model// swagger:parameters
Q Docker Compose を使って複数のサービスをどのようにオーケストレーションしますか?
A 複数のサービス(API、Prometheus、Grafana)を定義し、depends_on を使って起動順序を制御し、volumes を使ってデータを永続化します。docker-compose up -d を実行すると、1つのコマンドですべてのサービスを起動できます。本番環境では、Docker Stack または Kubernetes を使用してください。
Q ヘルスチェックには3つの詳細度レベルがありますか?
A (1) /health (alive) — 最も単純なチェック。200のレスポンスが返されれば、プロセスが実行中であることを示します。(2) /ready (ready) — 依存関係(データベース、キャッシュ)にアクセスできるかどうかを確認します。(3) /status (詳細) — すべての依存関係のステータスとレイテンシを返します。

📖 まとめ


📝 練習問題

  1. 基本演習(難易度:⭐):このレッスンで扱ったeコマースAPIに、グレースフルシャットダウンとタイムアウトの設定を追加してください。curl を使用して、/health および /ready エンドポイントをテストしてください。kill -SIGTERM <pid> を使用して、グレースフルシャットダウンをテストしてください。

  2. 上級演習(難易度:⭐⭐)Docker ベースの完全なデプロイを実装してください。要件:(1) マルチステージビルドに対応した Dockerfile;(2) docker-compose.yml(API + Prometheus + Grafana); (3) HEALTHCHECK;(4) pprofエンドポイント(内部ポートのみ);(5) docker-compose upを実行した後、すべてのサービスが正常に起動することを確認する。

  3. 課題(難易度:⭐⭐⭐)Prometheusによる監視とGrafanaダッシュボードを実装する。要件:(1) APIにPrometheusミドルウェアを追加する(リクエスト数、レイテンシ、エラー率);(2) ビジネス指標を追加する(例:作成された注文数、登録ユーザー数); (3) Docker Compose ファイルに Prometheus と Grafana を組み込む;(4) QPS、P99 レイテンシ、エラー率、および goroutine の数を表示するための Grafana ダッシュボードをインポートまたは作成する;(5) 負荷テスト後に Grafana を使用して、メトリクスが正しいことを確認する。

🎉 おめでとうございます!Goチュートリアルの全30レッスンを修了しました!「Hello World」から本番環境対応のEC APIまで、Goによるバックエンド開発に必要なスキルをすべて習得しました。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%