EコマースAPI(第3部):デプロイとモニタリング
アーキテクチャも整い、セキュリティ対策も万全です。さあ、本番運用を始めましょう。
最初の1行のコードから本番環境対応まで:Swaggerドキュメント、グレースフルシャットダウン、Dockerによるデプロイ、pprofによるチューニング、Prometheusによるモニタリング。ボブが開発したeコマースAPIは、実際のユーザーを迎える準備が整いました。
1. 学習内容
- Swagger / OpenAPI 向けの自動ドキュメント生成
サーバー.Shutdown正常なシャットダウン- Dockerのマルチステージビルド + Docker Composeによるフルスタック
- pprof パフォーマンス分析のエンドポイント統合
- プロメテウス・メトリクスの公開
- ヘルスチェックのエンドポイントの設計
2. ストーリー:打ち上げの前夜
(1) 課題:ドキュメントの欠如、監視体制の欠如、および手動によるデプロイ
ボブのEコマースAPIは完全に機能しているが、運用チームはそれを受け入れることを拒否している:
「運用チームはこう言いました。『APIのドキュメントがなければ、フロントエンドを統合できません。ヘルスチェックがなければ、サービスが稼働しているかどうかさえわかりません。モニタリングがなければ、サービスがダウンしたかどうかすら把握できません。しかも、デプロイのためにバイナリを手動でSCP転送しなければならないなんて、あまりにも原始的すぎます』」
立ち上げチェックリスト:
❌ API ドキュメント → フロントエンドは毎回尋ねてくる"このインターフェースはどのようなフィールドを返すのか?"
❌ エレガントに閉じる → kill このプロセスにより、処理中の注文が失われた
❌ Docker 展開 → scp バイナリをサーバーへ、手動起動
❌ 性能モニタリング → わからない API どこが遅いのか
(2) 授業の目標:実運用レベル
✅ Swagger ドキュメント → フロントエンドでのセルフチェック
✅ Graceful Shutdown → 信号処理 + 接続クローズを待機
✅ Docker Compose → 1つのコマンドでフルスタックを起動
✅ pprof + Prometheus → パフォーマンスの可視化
3. 完全な実装
▶ サンプル:正常なシャットダウン
// 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 の連携
// 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(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"]
# 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:
# prometheus.yml
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'ecommerce-api'
static_configs:
- targets: ['api:8080']
metrics_path: '/metrics'
▶ サンプル:Swaggerのドキュメント
// コメントから 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:"エラー"`
}
}
# インストール 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) 打ち上げチェックリスト
✅ 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 処理
❓ よくある質問
server.Shutdownは、すべてのアクティブなHTTPリクエストが完了するまで(指定されたタイムアウトまで)待機してから、リスナーを閉じます。プロセスが即座に強制終了されると、現在処理中のリクエストが中断され、データの順序に不整合が生じることになります。/debug/pprof/ パスを認証ミドルウェアで保護すること;(3) ステージング環境でのみ有効にすること。本番環境では、必要な場合にのみ一時的にプロファイリングを有効にすることを推奨します。promhttp.Handler() を使用して、/metrics エンドポイントでメトリクスを公開します。Prometheusサービスは、このエンドポイントを定期的にスクレイピングします。GrafanaはPrometheusのデータソースに接続し、可視化を作成します。標準的なメトリクスには、リクエスト数、レイテンシの分布、エラー率、goroutineの数、GCイベントの数が含まれます。swaggo/swag ツールを使用します。ハンドラーコード内に特定の形式でコメントを記述し、swag init を実行して docs/ ディレクトリを生成します。その後、swaggo/http-swagger を使用して、ドキュメントを /swagger/ エンドポイントにマウントします。コメントの形式:// swagger:route、// swagger:model、// swagger:parameters。depends_on を使って起動順序を制御し、volumes を使ってデータを永続化します。docker-compose up -d を実行すると、1つのコマンドですべてのサービスを起動できます。本番環境では、Docker Stack または Kubernetes を使用してください。📖 まとめ
- 正常なシャットダウン:
server.Shutdown(ctx)+signal.Notify - タイムアウト設定:ReadTimeout / WriteTimeout / IdleTimeout
- pprof:
/debug/pprof/エンドポイント(外部には公開されていない) - Prometheus:
/metricsエンドポイント + カスタムメトリクス - Swagger:
swaggo/swagはコメントから OpenAPI を生成します - Docker:マルチステージビルド + HEALTHCHECK
- Docker Compose:API + Prometheus + Grafana
- ヘルスチェック:
/health(稼働中)+/ready(準備完了)
📝 練習問題
-
基本演習(難易度:⭐):このレッスンで扱ったeコマースAPIに、グレースフルシャットダウンとタイムアウトの設定を追加してください。
curlを使用して、/healthおよび/readyエンドポイントをテストしてください。kill -SIGTERM <pid>を使用して、グレースフルシャットダウンをテストしてください。 -
上級演習(難易度:⭐⭐):Docker ベースの完全なデプロイを実装してください。要件:(1) マルチステージビルドに対応した Dockerfile;(2) docker-compose.yml(API + Prometheus + Grafana); (3) HEALTHCHECK;(4) pprofエンドポイント(内部ポートのみ);(5)
docker-compose upを実行した後、すべてのサービスが正常に起動することを確認する。 -
課題(難易度:⭐⭐⭐):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によるバックエンド開発に必要なスキルをすべて習得しました。



