メインコンテンツへスキップ
友田 陽大
Go・Echo 本番運用
Go
Echo
アーキテクチャ設計
可観測性
セキュリティ
コスト最適化

Echo 本番デプロイ完全ガイド:マルチステージ Docker・distroless・サーバータイムアウト・グレースフルシャットダウンで無停止運用する

Go Echo(v5)を本番にデプロイする実装ガイド。12-factor の環境変数設定、CGO_ENABLED=0 のマルチステージ Docker と distroless/nonroot、StartConfig.BeforeServeFunc による http.Server タイムアウト(v5でTimeoutミドルウェア廃止)、SIGTERM を取りこぼさないグレースフルシャットダウン、liveness/readiness ヘルスチェック、ECS Fargate / Cloud Run の作法、シークレット管理、コスト最適化までを実コードで解説します。

公開日
読了時間
9分
著者
友田 陽大
シェア

「ローカルでは動く」と「本番で無停止運用できる」の間には、深い谷があります。デプロイのたびに 502 が出る、巨大リクエストでメモリが溢れる、遅いクライアントが接続を握り続ける(slowloris)、コンテナイメージに認証情報が焼かれている——これらはデプロイ設計の不備であって、アプリのバグではありません。

この記事は、Go Echo 本番運用ガイドのデプロイ編です。Go の「単一バイナリ」という強みを最大限に活かし、極小・安全・無停止なデプロイを、Echo v5 の正確な API で実装します。

この記事のルール:Echo の API は 公式ドキュメント / ソース(v5・2026年6月時点) に基づきます。特に v5 は middleware.Timeout が廃止され、サーバータイムアウトの設定方法が変わっています(第3章)。シークレット(DB URL・署名鍵)は環境変数/シークレットマネージャ前提で、コードにもイメージにも絶対に焼き込みません。


1. 設定は 12-factor で:環境変数から読む

設定値(ポート・DB URL・ログレベル・鍵)は環境変数から読みます。これにより、同じイメージを dev/staging/prod でビルドし直さずに使い回せます。

type Config struct {
	Port        string
	DatabaseURL string
	JWTKey      []byte
	LogLevel    string
}

func LoadConfig() (*Config, error) {
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080" // 既定値(Cloud Run は PORT を注入してくる)
	}
	dsn := os.Getenv("DATABASE_URL")
	if dsn == "" {
		return nil, errors.New("DATABASE_URL is required") // 起動時に fail-fast
	}
	return &Config{
		Port:        port,
		DatabaseURL: dsn,
		JWTKey:      []byte(os.Getenv("JWT_SIGNING_KEY")),
		LogLevel:    os.Getenv("LOG_LEVEL"),
	}, nil
}

必須設定が無ければ**起動時に即エラー(fail-fast)**にします。「動き始めてから設定不足で落ちる」より、「起動を拒否する」方が遥かに安全です。


2. マルチステージ Docker:極小・最小権限イメージ

Go の単一バイナリを活かし、ビルド環境と実行環境を分離します。実行イメージは distroless(シェル・パッケージマネージャなし)+ nonroot

# --- build stage ---
FROM golang:1.25 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download                       # 依存だけ先に取りレイヤキャッシュを効かせる
COPY . .
# CGO 無効で静的リンク、-trimpath とシンボル除去で軽量化・再現性向上
RUN CGO_ENABLED=0 GOOS=linux go build \
      -trimpath -ldflags="-s -w" \
      -o /app ./cmd/server

# --- runtime stage ---
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /app /app
USER nonroot:nonroot                       # 非 root 実行(最小権限)
EXPOSE 8080
ENTRYPOINT ["/app"]
設定効果
CGO_ENABLED=0静的リンク。distroless/static で動く・libc 依存を断つ
-trimpathバイナリからローカルパスを除去(情報漏洩防止・再現性)
-ldflags="-s -w"デバッグ情報除去でサイズ削減
distroless/staticシェルもパッケージも無い=攻撃面が最小
nonroot侵害時の被害を限定(最小権限の原則)
依存を先に COPYレイヤキャッシュでビルド高速化(コスト効率)

コスト最適化:本番ターゲットが arm64(AWS Graviton / Cloud Run)なら、GOARCH=arm64 でビルドして arm 系インスタンスに載せると、同等性能で単価が安くなります。Go のクロスコンパイルは容易なので、マルチアーキビルドの恩恵を受けやすい言語です。


3. サーバータイムアウト:v5 は StartConfig.BeforeServeFunc で設定する

ここが v5 の重要な落とし穴です。 v4 にあった middleware.Timeoutv5 で廃止されました。代わりに、コネクションレベルのタイムアウトを http.Server に設定します。v5 では StartConfig.BeforeServeFunc(サーバー起動直前に *http.Server を触れるコールバック)がその正規の場所です。

sc := echo.StartConfig{
	Address:         ":" + cfg.Port,
	GracefulTimeout: 10 * time.Second,
	BeforeServeFunc: func(s *http.Server) error {
		// slowloris 等の遅延攻撃・リソース占有を防ぐ
		s.ReadHeaderTimeout = 5 * time.Second   // ヘッダ読み取りの上限(最重要)
		s.ReadTimeout = 15 * time.Second        // ボディ含むリクエスト全体
		s.WriteTimeout = 30 * time.Second       // レスポンス書き込み
		s.IdleTimeout = 120 * time.Second       // keep-alive 接続のアイドル上限
		return nil
	},
}
タイムアウト守るもの
ReadHeaderTimeoutslowloris(ヘッダをだらだら送って接続を占有する攻撃)
ReadTimeout巨大/低速なリクエストボディによる占有
WriteTimeout応答を受け取らないクライアントによる占有
IdleTimeoutアイドルな keep-alive 接続の滞留

ハンドラ単位のタイムアウト(重いクエリの打ち切り)は、context.WithTimeout で表現します(DB 層の記事)。サーバーレベル(接続)とハンドラレベル(処理)は別物で、両方を設定します。リクエストボディはBodyLimitでサイズ上限も掛けます。


4. グレースフルシャットダウン:SIGTERM を取りこぼさない

デプロイ・スケールインのたびに、オーケストレータはまず SIGTERM を送ります。これを無視すると処理中のリクエストが切れて 502 になります。v5 は StartConfig{GracefulTimeout}signal.NotifyContext を渡す形に一本化されています。http.ErrServerClosed は正常終了として扱うのが正しい作法です。

func main() {
	cfg, err := LoadConfig()
	if err != nil {
		slog.Error("config error", "error", err)
		os.Exit(1)
	}

	e := buildEcho(cfg) // ルート・ミドルウェア・DI 済みの Echo

	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	sc := echo.StartConfig{
		Address:         ":" + cfg.Port,
		GracefulTimeout: 10 * time.Second,
		BeforeServeFunc: applyServerTimeouts, // 第3章
		OnShutdownError: func(err error) {
			e.Logger.Error("graceful shutdown error", "error", err)
		},
	}

	// ErrServerClosed は SIGTERM での正常停止。エラー扱いしない
	if err := sc.Start(ctx, e); err != nil && !errors.Is(err, http.ErrServerClosed) {
		e.Logger.Error("server failed", "error", err)
		os.Exit(1)
	}
	slog.Info("server stopped gracefully")
}

SIGTERM 受信後の流れ:①新規接続の受付停止 → ②GracefulTimeout(既定10秒)の範囲で処理中リクエストの完了を待つ → ③DB プール等のクリーンアップ(defer pool.Close())→ ④終了

致命的な設定整合GracefulTimeout は、オーケストレータの終了猶予より必ず短くします(ECS の stopTimeout、Kubernetes の terminationGracePeriodSeconds 等)。逆だと、片付けの途中で SIGKILL され、結局リクエストが切れます。例:猶予 30 秒なら GracefulTimeout は 10〜20 秒に。


5. ヘルスチェック:liveness と readiness を分ける

オーケストレータはヘルスチェックで「生きているか」「トラフィックを流してよいか」を判断します。この2つを混同しないのが無停止運用の鍵です。

// liveness: プロセスが生きているか(依存はチェックしない。落ちていれば再起動される)
e.GET("/healthz", func(c *echo.Context) error {
	return c.NoContent(http.StatusOK)
})

// readiness: トラフィックを受けられるか(DB 等の依存も確認)
e.GET("/readyz", func(c *echo.Context) error {
	ctx, cancel := context.WithTimeout(c.Request().Context(), 2*time.Second)
	defer cancel()
	if err := pool.Ping(ctx); err != nil {
		return c.JSON(http.StatusServiceUnavailable, map[string]string{"db": "down"})
	}
	return c.NoContent(http.StatusOK)
})
  • liveness(/healthz:依存を見ない。DB が一時的に落ちてもプロセスは再起動すべきでない(再起動しても直らない)。
  • readiness(/readyz:依存を見る。DB が落ちている間はトラフィックを流さない(が、プロセスは殺さない)。
  • グレースフル停止との連携:理想は、SIGTERM 受信時にまず readiness を 503 にしてロードバランサからの新規流入を止め、その後に既存リクエストを捌き切ること。

これらのヘルスチェックやRequestLogger(slog)、相関 ID を組み合わせると、本番の可観測性が一段上がります(OpenTelemetry 連携)。


6. ECS Fargate / Cloud Run の作法

Echo は常駐コンテナとして、どのコンテナ基盤にも素直に載ります。プラットフォーム別の要点だけ押さえます。

項目AWS ECS / FargateGoogle Cloud Run
ポートタスク定義のポートマッピングPORT 環境変数を必ず読む
終了シグナルSIGTERMstopTimeout(既定30秒)SIGTERM → 猶予(既定10秒、延長可)
猶予との整合GracefulTimeout < stopTimeoutGracefulTimeout < 終了猶予
クライアント IPALB 経由→ Echo.IPExtractor を設定プロキシ経由→同上
シークレットSecrets Manager / SSM → 環境変数注入Secret Manager → 環境変数/ボリューム
オートスケールTarget Tracking 等リクエスト並行数ベース(ゼロスケール可)

c.RealIP()レート制限や監査ログで使うなら、ロードバランサ背後では Echo.IPExtractor で信頼できる前段を設定しないと、ヘッダ偽装で誤った IP を掴みます。

ECS/Fargate のオートスケールや Blue/Green、コスト最適化はECS Fargate 本番ガイド、Cloud Run のゼロスケール・並行性・課金はAzure Container Apps ガイドのサーバーレスコンテナの考え方とも地続きです。


7. シークレット管理とイメージ衛生

  • イメージに焼かないENV JWT_KEY=....env のコミットは厳禁。鍵はランタイムでシークレットマネージャ → 環境変数注入。
  • .dockerignore.git.env・テスト・ローカル設定をイメージから除外(漏洩・肥大化防止)。
  • イメージスキャン:CI でコンテナ脆弱性スキャン(Trivy 等)をゲートに。golangci-lintgovulncheck(Go の既知脆弱性検査)も合わせる。
  • 鍵レス CI/CD:デプロイ認証は長期キーではなく **OIDC(GitHub Actions → AWS/GCP の一時credential)**で。長期アクセスキーをリポジトリ Secrets に置かない。

このイメージ衛生・鍵レス CI/CD の思想は、AI駆動開発の品質ゲートで扱う「秘密情報・脆弱性スキャンを CI で機械強制する」設計と同じです。


まとめ:無停止デプロイの7原則

  1. 設定は 12-factor で環境変数。必須欠如は起動時 fail-fast。
  2. CGO_ENABLED=0 のマルチステージ + distroless/nonroot で極小・最小権限イメージに。
  3. v5 はサーバータイムアウトを BeforeServeFunc で設定(ReadHeaderTimeout で slowloris 対策)。
  4. StartConfig{GracefulTimeout} + signal.NotifyContextErrServerClosed は正常終了。
  5. GracefulTimeout はオーケストレータ猶予より短く。readiness を先に落として流入を止める。
  6. liveness と readiness を分ける。依存は readiness だけで見る。
  7. シークレットはイメージに焼かず、OIDC の鍵レス CI/CD とイメージスキャンをゲートに。

デプロイ設計は、コードの正しさを**本番の現実(再起動・スケール・攻撃・障害)**に耐えさせる最後の工程です。Echo の単一バイナリ + v5 の StartConfig は、この工程を素直に・正確に組めるように設計されています。クラスタ全体はGo Echo 本番運用ガイドから辿れます。

友田

友田 陽大

経済産業大臣賞 受賞プロダクト開発者。TypeScript + Python + AWS で、SaaS・業界DX・ 実用レベルの生成AI(RAG)を、要件定義からインフラ・運用まで一人で完遂します。

この記事の実装を、案件として承ります

Go / Echo のバックエンドを、設計から本番運用まで承ります

Echo v5 へのAPI設計・移行、クリーンアーキテクチャ(Controller/UseCase/Repository + DI)、ミドルウェアとセキュリティ、集中エラー処理、グレースフルシャットダウン、テストとCIまで。Go/Echo + google/wire で実際にクリーンアーキのバックエンドを構築した知見で、落ちない・追える・変更しやすいAPIを実装します。

プロジェクト単位(請負)・技術顧問のどちらにも対応可能です。まずは30分の無料技術相談から。

あわせて読みたい