「ローカルでは動く」と「本番で無停止運用できる」の間には、深い谷があります。デプロイのたびに 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.Timeout はv5 で廃止されました。代わりに、コネクションレベルのタイムアウトを 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
},
}
| タイムアウト | 守るもの |
|---|---|
ReadHeaderTimeout | slowloris(ヘッダをだらだら送って接続を占有する攻撃) |
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 / Fargate | Google Cloud Run |
|---|---|---|
| ポート | タスク定義のポートマッピング | PORT 環境変数を必ず読む |
| 終了シグナル | SIGTERM → stopTimeout(既定30秒) | SIGTERM → 猶予(既定10秒、延長可) |
| 猶予との整合 | GracefulTimeout < stopTimeout | GracefulTimeout < 終了猶予 |
| クライアント IP | ALB 経由→ 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-lintとgovulncheck(Go の既知脆弱性検査)も合わせる。 - 鍵レス CI/CD:デプロイ認証は長期キーではなく **OIDC(GitHub Actions → AWS/GCP の一時credential)**で。長期アクセスキーをリポジトリ Secrets に置かない。
このイメージ衛生・鍵レス CI/CD の思想は、AI駆動開発の品質ゲートで扱う「秘密情報・脆弱性スキャンを CI で機械強制する」設計と同じです。
まとめ:無停止デプロイの7原則
- 設定は 12-factor で環境変数。必須欠如は起動時 fail-fast。
- CGO_ENABLED=0 のマルチステージ + distroless/nonroot で極小・最小権限イメージに。
- v5 はサーバータイムアウトを
BeforeServeFuncで設定(ReadHeaderTimeoutで slowloris 対策)。 StartConfig{GracefulTimeout}+signal.NotifyContext、ErrServerClosedは正常終了。GracefulTimeoutはオーケストレータ猶予より短く。readiness を先に落として流入を止める。- liveness と readiness を分ける。依存は readiness だけで見る。
- シークレットはイメージに焼かず、OIDC の鍵レス CI/CD とイメージスキャンをゲートに。
デプロイ設計は、コードの正しさを**本番の現実(再起動・スケール・攻撃・障害)**に耐えさせる最後の工程です。Echo の単一バイナリ + v5 の StartConfig は、この工程を素直に・正確に組めるように設計されています。クラスタ全体はGo Echo 本番運用ガイドから辿れます。