Go言語マイクロサービス本番導入チェックリスト
Go言語でマイクロサービスを開発すると、軽量なバイナリ、起動の速さ、並行処理の扱いやすさ、標準ライブラリの充実によって、比較的短い期間でAPIサービスを構築できます。しかし、本番環境へ導入する段階では、単に「ローカルで動いた」「検証環境で一度成功した」という状態では不十分です。本番では、利用者からの予測不能なアクセス、下流サービスの遅延、データベースの負荷、コンテナの再起動、ノード障害、ネットワーク断、設定ミス、認証認可の不備、監視不足など、開発中には見えにくい問題が一気に表面化します。そのため、Go言語マイクロサービスを本番へ出す前には、コード、設定、コンテナ、Kubernetes、CI/CD、監視、セキュリティ、運用体制をまとめて確認するチェックリストが必要になります。
このチェックリストは、単なる作業表ではなく、本番環境でサービスを安全に動かし続けるための判断基準です。GoのHTTPサーバーには適切なタイムアウトと正常終了処理が必要であり、Docker画像は不要なファイルを含まず、Kubernetesではresources、probe、RollingUpdate、HorizontalPodAutoscalerを適切に設定する必要があります。また、マイクロサービスでは一つのサービスだけが正しく動いても十分ではなく、サービス間の契約、下流API、データベース移行、監視、トレース、障害時の戻し方まで含めて確認しなければなりません。以下の20項目は、Go言語マイクロサービスを本番へ導入する前に、開発チーム、運用チーム、セキュリティ担当、プロダクト責任者が共通の基準として確認できるように整理したものです。
1. サービス境界を本番前に明確にする
Go言語でマイクロサービスを作るとき、本番導入前に最初に確認すべきなのは、サービス境界が明確かどうかです。サービス境界とは、そのサービスがどの業務責任を持ち、どのデータを所有し、どのAPIを公開し、どのサービスに依存し、障害時にどこまで影響するのかを示すものです。ここが曖昧なまま本番へ出すと、障害時に「どのチームが直すべきか」「どのデータが正しいのか」「どのAPIを変更してよいのか」が分からなくなります。特にマイクロサービスでは、サービスを細かく分けること自体が目的ではなく、それぞれのサービスが独立して変更、配備、運用できることが重要です。
本番前には、サービス名、所有チーム、公開エンドポイント、内部エンドポイント、所有データベース、利用している外部API、下流サービス、上流サービス、障害時の利用者影響を文書化します。たとえば注文サービスが決済サービス、在庫サービス、通知サービスに依存している場合、どの依存先が落ちると注文作成を止めるのか、どの依存先は非同期で後から処理できるのかを決めておく必要があります。サービス境界が明確であれば、CI/CD、監視、アラート、権限管理、データベース移行、障害対応の設計も一貫しやすくなります。
2. Go HTTPサーバーに本番用タイムアウトを設定する
Goの net/http は非常に便利ですが、本番環境で http.ListenAndServe(":8080", mux) だけを使う実装は避けるべきです。理由は、読み取りタイムアウト、書き込みタイムアウト、ヘッダー読み取りタイムアウト、アイドル接続タイムアウトを明示しないと、遅いクライアントや不完全な接続によってサーバー資源が長く占有される可能性があるからです。開発環境では問題にならなくても、本番ではロードバランサー、プロキシ、モバイル回線、外部連携、攻撃的なアクセスなどが混在するため、接続管理を明確にする必要があります。
Goマイクロサービスでは、サーバー側タイムアウトだけでなく、下流サービスを呼ぶHTTPクライアントにもタイムアウトを設定します。要求を受けたハンドラーは r.Context() を使い、データベースや外部API呼び出しへキャンセルを伝えるべきです。これにより、利用者が接続を切った後も裏側で処理が走り続ける問題を減らせます。本番前には、全てのHTTPサーバーとHTTPクライアントに期限があるか、長時間処理は非同期化されているか、タイムアウト時のエラーがログとメトリクスに記録されるかを確認します。
package main
import ( "log" "net/http" "time")
func main() { mux := http.NewServeMux()
mux.HandleFunc("/orders", func(w http.ResponseWriter, r *http.Request) { ctx := r.Context()
select { case <-time.After(100 * time.Millisecond): w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"status":"ok"}`)) case <-ctx.Done(): log.Printf("request canceled: %v", ctx.Err()) return } })
server := &http.Server{ Addr: ":8080", Handler: mux, ReadHeaderTimeout: 2 * time.Second, ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 60 * time.Second, }
log.Println("server started on :8080") if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatal(err) }}
3. 正常終了を実装してローリング更新に耐える
本番環境では、コンテナは永遠に動き続けるものではありません。Kubernetesのローリング更新、ノードメンテナンス、オートスケーリング、障害復旧、手動再起動などによって、Podは頻繁に終了します。このときGoアプリケーションが終了信号を無視して即座に落ちると、処理中のHTTP要求、データベース更新、メッセージ処理、外部API呼び出しが中断され、利用者に失敗が返ったり、データの整合性が崩れたりします。そのため、本番用のGoサービスでは、SIGTERM や SIGINT を受け取り、新規要求を止め、処理中の要求を一定時間待ち、接続を閉じる正常終了が必要です。
正常終了を実装すると、KubernetesのRollingUpdateと相性が良くなります。readiness probeを失敗させて新規トラフィックを止め、既存処理を終わらせ、その後にプロセスを終了する流れを作れます。本番前には、終了時にHTTPサーバーが Shutdown されるか、データベース接続が閉じられるか、メッセージキューの購読が止まるか、終了猶予時間がKubernetesの terminationGracePeriodSeconds と合っているかを確認します。
package main
import ( "context" "log" "net/http" "os" "os/signal" "syscall" "time")
func main() { mux := http.NewServeMux() mux.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })
server := &http.Server{ Addr: ":8080", Handler: mux, ReadHeaderTimeout: 2 * time.Second, }
go func() { log.Println("server started") if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("server failed: %v", err) } }()
stop := make(chan os.Signal, 1) signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT) <-stop
log.Println("shutdown started")
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel()
if err := server.Shutdown(ctx); err != nil { log.Printf("graceful shutdown failed: %v", err) }
log.Println("shutdown completed")}
4. 環境別設定と秘密情報を分離する
Goマイクロサービスを本番へ導入する前に、設定管理が整理されているかを確認します。設定には、HTTPポート、データベースURL、Redis接続先、外部APIの基底URL、タイムアウト、ログレベル、機能切り替え、認可サーバーURLなどがあります。一方で、秘密情報には、データベースパスワード、APIトークン、JWT署名鍵、OAuthクライアント秘密値、暗号鍵などがあります。この二つを混ぜると、通常設定を共有したいだけなのに秘密値まで広がってしまう危険があります。
本番前の合格条件は、通常設定はConfigMapや環境変数で管理し、秘密情報はSecretや秘密情報管理サービスで管理していることです。また、Goアプリケーションは起動時に必須設定を検証し、不足している場合は明確なエラーで起動失敗するべきです。曖昧なデフォルト値で本番起動すると、検証環境のDBへ接続したり、外部APIのテスト環境へ誤送信したりする危険があります。設定管理は地味ですが、本番事故の原因になりやすい領域です。
package config
import ( "fmt" "os" "time")
type Config struct { HTTPAddr string DatabaseURL string RedisURL string LogLevel string RequestTimeout time.Duration}
func Load() (Config, error) { databaseURL := os.Getenv("DATABASE_URL") if databaseURL == "" { return Config{}, fmt.Errorf("DATABASE_URL is required") }
redisURL := os.Getenv("REDIS_URL") if redisURL == "" { return Config{}, fmt.Errorf("REDIS_URL is required") }
return Config{ HTTPAddr: env("HTTP_ADDR", ":8080"), DatabaseURL: databaseURL, RedisURL: redisURL, LogLevel: env("LOG_LEVEL", "info"), RequestTimeout: 3 * time.Second, }, nil}
func env(key string, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback}
5. Docker画像を小さく安全に作る
Go言語はコンパイル型言語なので、本番用Docker画像にはGoコンパイラ、ソースコード、テストファイル、ビルドキャッシュを含める必要がありません。複数段階ビルドを使えば、ビルド段階ではGo公式画像を使い、最終段階では実行バイナリだけを軽量画像へコピーできます。これにより、画像サイズを小さくし、配備時間を短縮し、不要なツールやファイルが本番環境に入ることを避けられます。
本番前には、Dockerfileが複数段階ビルドになっているか、非rootユーザーで実行しているか、画像タグが latest だけになっていないか、ビルド結果がコミットSHAやリリース番号で追跡できるかを確認します。また、依存関係取得を go.mod と go.sum のコピー後に行うことで、Dockerレイヤーキャッシュを効かせやすくなります。不要なファイルを .dockerignore に入れることも重要です。
FROM golang:1.24 AS builder
WORKDIR /src
COPY go.mod go.sum ./RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ go build -trimpath -ldflags="-s -w" \ -o /out/order-api ./cmd/order-api
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /out/order-api /order-api
USER nonroot:nonrootEXPOSE 8080
ENTRYPOINT ["/order-api"]
6. Kubernetesのrequestsとlimitsを設定する
本番Kubernetes環境では、GoマイクロサービスのPodにCPUとメモリのrequests、limitsを設定します。requestsはスケジューラーがPodをどのノードへ配置するか判断するための基準になり、limitsはコンテナが使える資源の上限になります。これを設定しないと、あるGoサービスが予想以上にメモリを使って他のPodへ影響したり、逆に必要なCPUが確保されず応答時間が不安定になったりします。
Goサービスでは、CPU上限とGoランタイムの並列実行、メモリ上限とガベージコレクション、接続数と下流サービスの負荷を合わせて考える必要があります。本番前には、負荷試験を行い、通常時、ピーク時、障害時のCPU使用率、メモリ使用量、GC頻度、応答時間を確認します。最初から極端に小さいresourcesにすると、コストは下がるかもしれませんが、ピーク時に不安定なサービスになります。
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-api
spec:
replicas: 3
selector:
matchLabels:
app: order-api
template:
metadata:
labels:
app: order-api
spec:
containers:
- name: order-api
image: registry.example.com/order-api:1.0.0
ports:
- containerPort: 8080
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "1"
memory: "512Mi"
7. liveness、readiness、startup probeを分ける
Kubernetesのprobeは、本番運用で非常に重要です。liveness probeはコンテナを再起動すべきかを判断し、readiness probeはPodへトラフィックを流してよいかを判断し、startup probeは起動に時間がかかるアプリケーションが初期化完了するまで待つために使います。この三つを同じ意味で扱うと、単なる一時的なDB遅延でコンテナが再起動されたり、まだ準備できていないPodへトラフィックが流れたりします。
Goアプリケーションでは、/livez はプロセスが生きていることだけを返し、/readyz はデータベース、Redis、重要な下流サービス、設定読み込みなど、実際にリクエストを処理できる条件を確認します。startupが必要な場合は、マイグレーション、キャッシュウォームアップ、鍵読み込みなどが完了するまで失敗させます。本番前には、probeが落ちたときに何が起こるかを検証環境で確認するべきです。
mux.HandleFunc("/livez", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("live"))
})
mux.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 500*time.Millisecond)
defer cancel()
if err := db.PingContext(ctx); err != nil {
http.Error(w, "database not ready", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ready"))
})
livenessProbe:
httpGet:
path: /livez
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /readyz
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
startupProbe:
httpGet:
path: /livez
port: 8080
failureThreshold: 30
periodSeconds: 2
8. ローリング更新と戻し手順を確認する
マイクロサービスを本番へ導入するとき、配備戦略を決めていない状態は非常に危険です。KubernetesのDeploymentではRollingUpdateを使って、新しいPodを少しずつ増やしながら古いPodを減らすことができます。しかし、ローリング更新中は新旧バージョンが同時に動くため、API形式、データベーススキーマ、メッセージ形式が後方互換でなければ障害になります。
本番前には、maxSurge、maxUnavailable、readiness probe、正常終了、データベース移行、Feature Flag、戻し手順をまとめて確認します。特に戻し手順は、障害が起きてから考えるのでは遅すぎます。直前の画像タグへ戻せるか、DB変更は戻せるか、戻せない変更なら前方互換の設計になっているか、戻し後に監視で正常化を確認できるかを事前に決めます。
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
kubectl rollout status deployment/order-api -n production
kubectl rollout undo deployment/order-api -n production
9. CI/CDに品質門を入れる
Goマイクロサービスを本番へ出す前に、CI/CDが単なるビルド自動化ではなく、品質門として機能しているかを確認します。最低限、gofmt、go vet、単体試験、競合検出、依存関係検査、Docker画像作成、画像脆弱性検査、検証環境への配備、配備後確認を自動化します。人が手元でコマンドを実行する運用は、最初は楽に見えても、チームやサービス数が増えるほど漏れが発生します。
本番品質のCI/CDでは、失敗した品質門を無視して配備できないようにします。ただし、全ての重い試験を毎回実行すると開発速度が落ちるため、軽い試験は全ての変更で実行し、重い結合試験や負荷試験は主要分岐、夜間、リリース前に実行する設計が現実的です。CI/CDの目的は、配備を速くするだけでなく、同じ基準で安全に配備できる状態を作ることです。
stages:
- test
- build
- scan
- deploy
test:
stage: test
script:
- test -z "$(gofmt -l .)"
- go vet ./...
- go test ./... -race
build:
stage: build
script:
- docker build -t registry.example.com/order-api:${CI_COMMIT_SHORT_SHA} .
- docker push registry.example.com/order-api:${CI_COMMIT_SHORT_SHA}
deploy_staging:
stage: deploy
script:
- kubectl set image deployment/order-api order-api=registry.example.com/order-api:${CI_COMMIT_SHORT_SHA} -n staging
- kubectl rollout status deployment/order-api -n staging
10. サービス間契約をテストする
マイクロサービスでは、Goの単体試験が全て成功しても、本番で別サービスとの通信が壊れることがあります。たとえば、注文サービスが決済サービスへ送るJSON項目を変更した、在庫サービスの応答から必須項目が消えた、エラー形式が変わった、HTTPステータスが変わった、メッセージキューのイベント名が変更された、という問題は単体試験だけでは検出できません。これを防ぐために、サービス間契約を定義し、契約試験をCI/CDへ組み込みます。
本番前には、公開API、内部API、イベント、スキーマ、エラー形式、認証方式、必須ヘッダーを契約として管理しているかを確認します。特にローリング更新では、新旧サービスが同時に動くため、破壊的変更をいきなり入れてはいけません。削除ではなく追加を基本にし、古い項目を一定期間残し、利用側が移行したことを確認してから削除する流れを作ります。
| 契約対象 | 確認内容 | 本番前の条件 |
|---|---|---|
| HTTP API | URL、メソッド、ステータス、JSON形式 | 破壊的変更なし |
| イベント | イベント名、必須項目、型、バージョン | 古い購読者と互換 |
| 認証 | Bearer Token、scope、audience | 必要権限が明確 |
| エラー | エラーコード、メッセージ形式 | 呼び出し側が処理可能 |
| タイムアウト | 上流と下流の期限 | 呼び出し連鎖が詰まらない |
11. データベース移行を後方互換で設計する
Goマイクロサービスの本番配備で最も危険な変更の一つがデータベース移行です。コードは古い画像へ戻せても、データベースの列削除、型変更、一括更新、制約追加、索引作成は簡単に戻せない場合があります。特にマイクロサービスでは、アプリケーションの新旧バージョンがローリング更新中に同時稼働するため、データベーススキーマは一時的に両方のバージョンに対応できる必要があります。
安全な移行では、まず新しい列やテーブルを追加し、アプリケーションを新旧両対応にし、データを移行し、利用状況を監視し、古い列やコードを最後に削除します。これを一回のリリースでまとめて行うと危険が大きくなります。本番前には、移行スクリプトがレビュー済みか、実行時間を検証環境で確認したか、バックアップがあるか、戻し方があるか、ロック影響を評価したかを確認します。
-- 1回目のリリース: 後方互換な列追加 ALTER TABLE orders ADD COLUMN external_reference TEXT;
-- 2回目のリリース: Goアプリを新旧両方の列へ対応させる
-- 3回目のリリース: データ移行を実行する UPDATE orders
SET external_reference = old_reference
WHERE external_reference IS NULL;
-- 4回目のリリース: 利用がなくなったことを確認してから削除する -- ALTER TABLE orders DROP COLUMN old_reference;
12. 下流呼び出しにタイムアウトと再試行制御を入れる
マイクロサービスでは、自サービスのコードが速くても、下流サービスや外部APIが遅いと全体の応答時間が悪化します。GoではHTTPクライアント、データベース接続、Redis、メッセージ処理など、あらゆるI/Oにタイムアウトを設定する必要があります。タイムアウトがない処理は、障害時にゴルーチンや接続を占有し続け、最終的にはサービス全体を詰まらせます。
再試行も注意が必要です。一時的なネットワーク失敗には有効ですが、無制限の再試行は下流サービスへさらに負荷をかけ、障害を拡大します。本番前には、再試行回数、待機時間、指数バックオフ、再試行対象のエラー、冪等性、Contextキャンセルを確認します。POSTのような副作用のある操作を安易に再試行すると、重複注文や重複決済につながる可能性があります。
package client
import (
"context"
"fmt"
"net/http"
"time"
)
type InventoryClient struct {
client *http.Client
baseURL string
}
func NewInventoryClient(baseURL string) *InventoryClient {
return &InventoryClient{
client: &http.Client{
Timeout: 3 * time.Second,
},
baseURL: baseURL,
}
}
func (c *InventoryClient) Check(ctx context.Context, itemID string) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/items/"+itemID, nil)
if err != nil {
return err
}
res, err := c.client.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return fmt.Errorf("inventory failed: status=%d", res.StatusCode)
}
return nil
}
13. 構造化ログで障害調査できる状態にする
本番環境では、ログは単なる文字列ではなく、障害調査、監査、メトリクス補助、トレース連携に使われる重要な運用データです。Goサービスでは、要求ID、トレースID、利用者ID、サービス名、エンドポイント、HTTPステータス、処理時間、エラー種別を構造化して出す必要があります。自由な文章だけのログでは、障害時に検索や集計が難しくなり、複数サービスをまたいだ調査に時間がかかります。
一方で、ログには出してはいけない情報があります。アクセストークン、リフレッシュトークン、パスワード、認可コード、個人情報、決済情報、秘密鍵をログに出すと、ログ基盤そのものが漏えい経路になります。本番前には、ログ出力の項目、マスキング規則、保存期間、アクセス権限、削除ポリシーを確認します。障害調査に必要な情報を残しながら、秘密情報を守る設計が必要です。
logger.Info("request completed",
"service", "order-api",
"method", r.Method,
"path", r.URL.Path,
"status", statusCode,
"duration_ms", time.Since(start).Milliseconds(),
"request_id", requestID,
"trace_id", traceID,
)
14. メトリクスでSLOを確認できるようにする
Goマイクロサービスの本番導入では、メトリクスがなければ安全な運用はできません。最低限、要求数、エラー数、応答時間、p95、p99、CPU使用率、メモリ使用量、ゴルーチン数、データベース接続数、下流API失敗率を観測できるようにします。これらが見えない状態で本番へ出すと、障害が起きたときに「遅い気がする」「たぶんDBが原因」という曖昧な判断しかできません。
メトリクスは、ただ出すだけではなく、SLOやアラートへつなげる必要があります。たとえば、p95応答時間が500msを超えたら警告、5xx率が1%を超えたら重大、Pod再起動が急増したら調査、というように基準を決めます。ラベル設計にも注意が必要で、user_idやorder_idのような高カーディナリティ値を入れるとメトリクス基盤に負荷がかかります。
| メトリクス | 意味 | アラート例 |
|---|---|---|
| request_total | リクエスト数 | 急激な増減 |
| request_duration | 応答時間 | p95が基準超過 |
| error_total | エラー数 | 5xx率が上昇 |
| goroutines | ゴルーチン数 | 増え続ける |
| db_connections | DB接続数 | 上限に近い |
| downstream_errors | 下流API失敗 | 特定依存先の障害 |
15. OpenTelemetryでトレースをつなげる
マイクロサービスでは、ひとつの利用者要求が複数サービスを通過します。注文作成だけでも、注文API、在庫API、決済API、通知API、監査ログAPIなどを通ることがあります。このとき、全体の応答が遅いとしても、ログだけではどのサービスが遅いのか分かりにくくなります。OpenTelemetryを使ってトレースをつなげると、要求がどのサービスを通り、どこでどれだけ時間を使ったかを可視化できます。
本番前には、HTTP受信、HTTP送信、データベース問い合わせ、メッセージ発行、外部API呼び出しにスパンを付けます。また、ログにもtrace_idを含めることで、トレース画面とログ検索を行き来できるようにします。トレースは便利ですが、全てを細かく取りすぎるとコストやノイズが増えるため、重要な境界に絞って設計します。
ctx, span := tracer.Start(r.Context(), "CreateOrder")
defer span.End()
orderID, err := service.CreateOrder(ctx, input)
if err != nil {
span.RecordError(err)
http.Error(w, "failed to create order", http.StatusInternalServerError)
return
}
span.SetAttributes(attribute.String("order.id", orderID))
w.WriteHeader(http.StatusCreated)
16. pprofを安全に利用できるようにする
Goサービスの本番性能問題を調査するうえで、pprofは非常に強力です。CPU使用率が高い、メモリが増え続ける、ゴルーチンが減らない、ロック競合が疑われる、応答時間が突然悪化する、といった状況では、pprofを使うことで原因箇所を具体的に調べられます。しかし、pprofエンドポイントには内部情報が含まれるため、外部に公開してはいけません。
本番前には、pprofを有効にする場合のアクセス経路を制限します。たとえば、localhostのみで起動する、管理ネットワークだけからアクセス可能にする、認証付きの内部プロキシ経由にする、一時的にだけ有効化する、などの運用を決めます。障害が起きてから慌ててビルドし直すのではなく、安全に調査できる導線を事前に用意しておくことが重要です。
package diagnostics
import (
"log"
"net/http"
_ "net/http/pprof"
)
func StartPrivatePprof() {
go func() {
log.Println("pprof started on 127.0.0.1:6060")
if err := http.ListenAndServe("127.0.0.1:6060", nil); err != nil {
log.Printf("pprof failed: %v", err)
}
}()
}
17. 認証認可と通信の安全性を確認する
本番のGoマイクロサービスでは、認証と認可を曖昧にしてはいけません。外部公開APIでは利用者認証、内部APIではサービス間認証、管理APIでは強い権限確認が必要です。Bearer Tokenを受け取るAPIでは、単にトークン文字列があるかどうかではなく、署名、有効期限、発行者、対象者、scopeを確認します。マイクロサービス内部だから安全という前提は危険であり、内部ネットワークに侵入された場合の被害を小さくする設計が必要です。
通信の安全性も同時に確認します。外部通信はTLSを使い、内部通信でも必要に応じてmTLSやサービスメッシュを検討します。CORS、CSRF、Cookie属性、JWT検証、OAuth/OIDC設定、Secret管理、RBAC、ServiceAccount権限も本番前チェックに含めます。セキュリティは最後に一度だけ見るものではなく、CI/CD、コードレビュー、運用監視に組み込むべきです。
func RequireScope(required string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims, ok := ClaimsFromContext(r.Context())
if !ok {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
if !claims.HasScope(required) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
18. オートスケーリングと容量計画を検証する
Goサービスは軽量に動くことが多いですが、マイクロサービス全体としてはスケーリング設計が必要です。Pod数を増やせば必ず処理量が増えるとは限りません。下流サービス、データベース、キャッシュ、メッセージブローカー、外部APIが先に限界を迎える場合があります。そのため、本番前にはHorizontalPodAutoscalerの設定だけでなく、負荷試験によってどの程度まで処理量が伸びるかを確認します。
容量計画では、通常時、ピーク時、障害時、キャンペーン時、バッチ処理時の負荷を考えます。CPU使用率だけでスケールさせると、I/O待ちが多いサービスでは適切に増えない場合があります。応答時間、キュー長、リクエスト数、カスタムメトリクスを使ったスケーリングも検討します。また、Podを増やしたときにDB接続数が増えすぎないか、下流APIのレート制限に当たらないかも確認します。
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-api
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-api
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
19. リリース判定と戻し条件を明文化する
本番導入で最後に重要なのは、リリースしてよい条件と、止める条件と、戻す条件を明文化することです。試験が通った、画像が作れた、Kubernetesへ配備できた、というだけではリリース成功とは言えません。配備後にエラー率、応答時間、ログ、トレース、メトリクス、利用者影響を確認し、基準を満たして初めて成功と判断します。
戻し条件も事前に決めておきます。たとえば、5xx率が1%を超える、p95応答時間が基準を超える、決済失敗が増える、Pod再起動が続く、下流サービスへのエラーが増える、という条件があれば、誰が判断し、どの手順で戻すかを決めます。戻し手順が文書化されていないと、障害時に議論が長引き、被害が広がります。
| 判定項目 | 成功条件 | 戻し条件 |
|---|---|---|
| エラー率 | 5xx率が基準以内 | 5xx率が継続的に上昇 |
| 応答時間 | p95がSLO以内 | p95またはp99が大幅悪化 |
| Pod状態 | 再起動なし | CrashLoopBackOff発生 |
| 業務指標 | 注文、決済、通知が正常 | 重要処理の失敗増加 |
| ログ | 新規重大エラーなし | 同一エラーが急増 |
| トレース | 下流遅延なし | 特定依存先で遅延急増 |
20. 本番導入後もチェックリストを更新する
Go言語マイクロサービスの本番導入チェックリストは、一度作って終わりではありません。実際に運用を始めると、想定していなかった障害、監視不足、ログ不足、スケール不足、配備手順の不備、データベース移行の危険性が見えてきます。これらを次回のチェックリストへ反映しないと、同じ種類の問題を繰り返します。本番運用の成熟度は、失敗しないことではなく、失敗から仕組みを改善できることに表れます。
導入後には、リリース振り返り、障害振り返り、SLOレビュー、セキュリティレビュー、容量レビューを定期的に行います。Goのバージョン、依存ライブラリ、コンテナ基盤、Kubernetes設定、監視基盤、認証基盤は時間とともに変わるため、チェックリストも更新する必要があります。サービスが増えるほど、個別チームの経験だけに頼らず、組織共通のproduction readiness基準として整備することが重要です。
| 更新対象 | 見直すタイミング | 追加すべき内容 |
|---|---|---|
| CI/CD | リリース失敗後 | 足りなかった品質門 |
| 監視 | 障害検知遅れ後 | 新しいメトリクスとアラート |
| ログ | 調査困難だった後 | request_id、trace_id、業務ID |
| DB移行 | 移行トラブル後 | ロック確認、戻し手順 |
| セキュリティ | 監査後 | 権限、Secret、依存関係検査 |
| スケール | 負荷増加後 | HPA、接続数、容量計画 |
おわりに
Go言語マイクロサービスを本番環境へ導入するには、アプリケーションコードが正しく動くだけでは不十分です。HTTPサーバーのタイムアウト、正常終了、設定管理、秘密情報、Docker画像、Kubernetesのresources、probe、RollingUpdate、CI/CD、契約試験、データベース移行、監視、トレース、pprof、セキュリティ、スケーリング、障害対応まで、複数の観点をまとめて確認する必要があります。特にマイクロサービスでは、一つのサービスの不備が他サービスへ連鎖しやすいため、個別サービスの品質と全体システムの安全性を両方見ることが重要です。
このチェックリストは、本番導入前の最終確認として使うだけでなく、チームの運用成熟度を上げるための継続的な道具として使うべきです。リリースのたびに確認し、障害のたびに改善し、新しい技術や組織の変化に合わせて更新することで、Go言語マイクロサービスを長く安全に運用できます。本番環境で求められるのは、一度だけ成功する配備ではなく、何度も安全に変更を届けられる仕組みです。
EN
JP
KR