Go デプロイガイド
Docker、クロスコンパイルを駆使してGoアプリケーションを効率的にビルド・デプロイする
87 分で読めます43,449 文字
Go デプロイガイド
Docker、クロスコンパイルを駆使してGoアプリケーションを効率的にビルド・デプロイする
この章で学ぶこと
- Docker マルチステージビルド で最小のコンテナイメージを構築する方法
- クロスコンパイル によるマルチプラットフォーム対応バイナリの生成
- CI/CDパイプライン でのビルド・テスト・デプロイ自動化
- Kubernetes デプロイ -- マニフェスト設計、ヘルスチェック、リソース管理
- サーバーレスデプロイ -- AWS Lambda、Google Cloud Run
- Graceful Shutdown -- 安全なプロセス停止と接続管理
- 設定管理 -- 環境変数、設定ファイル、シークレット管理
前提知識
このガイドを読む前に、以下の知識があると理解が深まります:
- 基本的なプログラミングの知識
- 関連する基礎概念の理解
- Go プロファイリングガイド の内容を理解していること
1. Goバイナリの特性とデプロイ戦略
デプロイ方式の選択
Go アプリをデプロイしたい
|
+-- シングルバイナリ配布
| |
| +-- クロスコンパイル → GitHub Releases
| +-- GoReleaser で自動化
| +-- Homebrew tap で配布
|
+-- コンテナデプロイ
| |
| +-- Docker マルチステージビルド
| +-- distroless / scratch ベース
| +-- Kubernetes / ECS / Cloud Run
|
+-- サーバーレス
| |
| +-- AWS Lambda (provided.al2023)
| +-- Google Cloud Functions (Go 1.22+)
| +-- Google Cloud Run (コンテナ)
| +-- Azure Functions
|
+-- PaaS
|
+-- Google App Engine
+-- Heroku (Container Stack)
+-- Fly.io
+-- Railway
Goバイナリの特徴
+------------------------------------------+
| Go バイナリ (静的リンク) |
+------------------------------------------+
| |
| +----------------+ +-----------------+ |
| | アプリコード | | Go ランタイム | |
| | ビジネスロジック| | GC, scheduler | |
| +----------------+ +-----------------+ |
| |
| +----------------+ +-----------------+ |
| | 標準ライブラリ | | 依存ライブラリ | |
| | net/http, etc | | 全て埋め込み | |
| +----------------+ +-----------------+ |
| |
| → 外部依存なし、単体で実行可能 |
| → CGO_ENABLED=0 で完全静的リンク |
| → 起動時間: 数ミリ秒 |
| → 典型的なサイズ: 10-30 MB |
+------------------------------------------+
CGO_ENABLED=0 の場合:| Go Runtime + App Code |
|---|
| すべてGoで実装(C依存なし) |
| → scratch/distroless で実行可能 |
CGO_ENABLED=1 の場合:| Go Runtime + App Code |
|---|
| + libc (glibc/musl) |
| → alpine (musl) or debian (glibc) |
| のベースイメージが必要 |
2. Docker マルチステージビルド
コード例1: 本番用 Dockerfile
# ============================================
# Stage 1: ビルドステージ
# ============================================
FROM golang:1.22-alpine AS builder
# セキュリティアップデートとビルドツール
RUN apk add --no-cache git ca-certificates tzdata
# 非rootユーザーでビルド(セキュリティ向上)
RUN adduser -D -g '' appuser
# 依存のキャッシュ層
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download && go mod verify
# ソースコードのコピーとビルド
COPY . .
# ビルド引数
ARG VERSION=dev
ARG BUILD_TIME=unknown
ARG GIT_COMMIT=unknown
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build \
-trimpath \
-ldflags="-w -s \
-X main.version=${VERSION} \
-X main.buildTime=${BUILD_TIME} \
-X main.gitCommit=${GIT_COMMIT}" \
-o /app/server ./cmd/server
# ============================================
# Stage 2: 実行ステージ(最小イメージ)
# ============================================
FROM gcr.io/distroless/static-debian12
# タイムゾーンデータと証明書をコピー
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# 非rootユーザー情報をコピー
COPY --from=builder /etc/passwd /etc/passwd
# 非rootユーザーで実行
USER appuser
# バイナリをコピー
COPY --from=builder /app/server /server
# 設定ファイルやマイグレーションも必要に応じてコピー
# COPY --from=builder /app/migrations /migrations
# COPY --from=builder /app/configs /configs
EXPOSE 8080
# ヘルスチェック用エンドポイント
# HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
# CMD ["/server", "healthcheck"] || exit 1
ENTRYPOINT ["/server"]コード例2: Docker Compose(開発環境)
# docker-compose.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
args:
VERSION: dev
BUILD_TIME: "2024-01-01T00:00:00Z"
ports:
- "8080:8080"
environment:
- DATABASE_URL=postgres://user:pass@db:5432/mydb?sslmode=disable
- REDIS_URL=redis://redis:6379/0
- LOG_LEVEL=debug
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: mydb
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./migrations/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
# 開発用: ホットリロード
app-dev:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "8080:8080"
volumes:
- .:/app
environment:
- DATABASE_URL=postgres://user:pass@db:5432/mydb?sslmode=disable
depends_on:
- db
profiles:
- dev
volumes:
postgres_data:
redis_data:# Dockerfile.dev -- 開発用(ホットリロード対応)
FROM golang:1.22-alpine
RUN go install github.com/air-verse/air@latest
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
CMD ["air", "-c", ".air.toml"]# .air.toml
root = "."
tmp_dir = "tmp"
[build]
cmd = "go build -o ./tmp/server ./cmd/server"
bin = "tmp/server"
full_bin = "./tmp/server"
include_ext = ["go", "tpl", "tmpl", "html", "sql"]
exclude_dir = ["assets", "tmp", "vendor", "node_modules"]
delay = 1000
[log]
time = false
[color]
main = "magenta"
watcher = "cyan"
build = "yellow"
runner = "green"Dockerイメージサイズ比較
+----------------------------------------------+
| ベースイメージ別サイズ比較 |
+----------------------------------------------+
| |
| golang:1.22 |████████████| 850 MB |
| golang:1.22-alpine |██████| 350 MB |
| alpine:3.19 |█| 7 MB |
| distroless/static |░| 2 MB |
| scratch |░| 0 MB |
| |
| 最終イメージ (distroless + Go binary) |
| |██| 15-20 MB |
| |
| 最終イメージ (scratch + Go binary) |
| |█| 10-15 MB |
+----------------------------------------------+
コード例3: scratch ベースの最小イメージ
FROM golang:1.22-alpine AS builder
# TLS用の証明書を取得
RUN apk add --no-cache ca-certificates tzdata
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags="-w -s" -o /app/server ./cmd/server
# scratch: 完全に空のイメージ
FROM scratch
# TLS通信に必要
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# タイムゾーン情報
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
# バイナリ
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]ベースイメージ比較表
| ベースイメージ | サイズ | シェル | パッケージマネージャ | デバッグ | セキュリティ | 用途 |
|---|---|---|---|---|---|---|
| golang:1.22 | 850MB | bash | apt | 容易 | 攻撃対象面大 | 開発のみ |
| golang:1.22-alpine | 350MB | ash | apk | 可能 | 良好 | ビルドステージ |
| alpine:3.19 | 7MB | ash | apk | 可能 | 良好 | CGO必要時 |
| distroless/static | 2MB | なし | なし | 困難 | 非常に良好 | 本番推奨 |
| scratch | 0MB | なし | なし | 非常に困難 | 最高 | 最小構成 |
コード例4: デバッグ可能なイメージ
# 本番イメージにデバッグツールを追加したバリエーション
FROM gcr.io/distroless/static-debian12:debug AS debug
COPY --from=builder /app/server /server
# debug タグにはbusyboxシェルが含まれる
# kubectl exec -it <pod> -- /busybox/sh
ENTRYPOINT ["/server"]
# 使い分け:
# 本番: gcr.io/distroless/static-debian12 (シェルなし、最小攻撃面)
# デバッグ: gcr.io/distroless/static-debian12:debug (busybox付き)3. クロスコンパイル
コード例5: マルチプラットフォームビルド
# Linux AMD64 (サーバー、CI/CD)
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o myapp-linux-amd64 ./cmd/myapp
# Linux ARM64 (AWS Graviton, Raspberry Pi 4)
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o myapp-linux-arm64 ./cmd/myapp
# Linux ARM v7 (Raspberry Pi 3, 古いARM)
GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=0 go build -o myapp-linux-armv7 ./cmd/myapp
# macOS Intel
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -o myapp-darwin-amd64 ./cmd/myapp
# macOS Apple Silicon
GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -o myapp-darwin-arm64 ./cmd/myapp
# Windows
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o myapp-windows-amd64.exe ./cmd/myapp
# サポートされるOS/ARCH一覧
go tool dist listコード例6: Makefile でのビルド管理
APP_NAME := myapp
VERSION := $(shell git describe --tags --always --dirty)
BUILD_TIME := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
GIT_COMMIT := $(shell git rev-parse --short HEAD)
LDFLAGS := -ldflags "-w -s \
-X main.version=$(VERSION) \
-X main.buildTime=$(BUILD_TIME) \
-X main.gitCommit=$(GIT_COMMIT)"
# Go のビルドフラグ
GO_BUILD := CGO_ENABLED=0 go build -trimpath $(LDFLAGS)
# ターゲット
.PHONY: build build-all test lint clean docker docker-push help
## help: ヘルプを表示
help:
@grep -E '^## ' $(MAKEFILE_LIST) | sed 's/## //'
## build: ローカルビルド
build:
$(GO_BUILD) -o bin/$(APP_NAME) ./cmd/$(APP_NAME)
## build-all: 全プラットフォーム向けビルド
build-all:
GOOS=linux GOARCH=amd64 $(GO_BUILD) -o bin/$(APP_NAME)-linux-amd64 ./cmd/$(APP_NAME)
GOOS=linux GOARCH=arm64 $(GO_BUILD) -o bin/$(APP_NAME)-linux-arm64 ./cmd/$(APP_NAME)
GOOS=darwin GOARCH=amd64 $(GO_BUILD) -o bin/$(APP_NAME)-darwin-amd64 ./cmd/$(APP_NAME)
GOOS=darwin GOARCH=arm64 $(GO_BUILD) -o bin/$(APP_NAME)-darwin-arm64 ./cmd/$(APP_NAME)
GOOS=windows GOARCH=amd64 $(GO_BUILD) -o bin/$(APP_NAME)-windows-amd64.exe ./cmd/$(APP_NAME)
## test: テスト実行
test:
go test -race -cover -coverprofile=coverage.out ./...
## test-integration: インテグレーションテスト
test-integration:
go test -race -tags=integration -cover ./...
## lint: リンターチェック
lint:
golangci-lint run ./...
## fmt: コードフォーマット
fmt:
gofmt -w .
goimports -w .
## vet: 静的解析
vet:
go vet ./...
## docker: Dockerイメージビルド
docker:
docker build \
--build-arg VERSION=$(VERSION) \
--build-arg BUILD_TIME=$(BUILD_TIME) \
--build-arg GIT_COMMIT=$(GIT_COMMIT) \
-t $(APP_NAME):$(VERSION) \
-t $(APP_NAME):latest .
## docker-push: Dockerイメージプッシュ
docker-push: docker
docker tag $(APP_NAME):$(VERSION) ghcr.io/myorg/$(APP_NAME):$(VERSION)
docker push ghcr.io/myorg/$(APP_NAME):$(VERSION)
## docker-multi: マルチアーキテクチャビルド
docker-multi:
docker buildx build \
--platform linux/amd64,linux/arm64 \
--build-arg VERSION=$(VERSION) \
-t ghcr.io/myorg/$(APP_NAME):$(VERSION) \
--push .
## migrate-up: マイグレーション実行
migrate-up:
migrate -path ./migrations -database $(DATABASE_URL) up
## migrate-down: マイグレーションロールバック
migrate-down:
migrate -path ./migrations -database $(DATABASE_URL) down 1
## migrate-create: マイグレーションファイル作成
migrate-create:
@read -p "Migration name: " name; \
migrate create -ext sql -dir ./migrations -seq $$name
## clean: ビルド成果物削除
clean:
rm -rf bin/ tmp/ coverage.out
## coverage: カバレッジレポート表示
coverage: test
go tool cover -html=coverage.out -o coverage.html
open coverage.htmlコード例7: ビルド時の変数埋め込み
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
"runtime"
"runtime/debug"
)
// ビルド時に -ldflags で注入
var (
version = "dev"
buildTime = "unknown"
gitCommit = "unknown"
)
// BuildInfo はビルド情報を表す
type BuildInfo struct {
Version string `json:"version"`
BuildTime string `json:"build_time"`
GitCommit string `json:"git_commit"`
GoVersion string `json:"go_version"`
OS string `json:"os"`
Arch string `json:"arch"`
Compiler string `json:"compiler"`
}
// GetBuildInfo はビルド情報を取得する
func GetBuildInfo() BuildInfo {
info := BuildInfo{
Version: version,
BuildTime: buildTime,
GitCommit: gitCommit,
GoVersion: runtime.Version(),
OS: runtime.GOOS,
Arch: runtime.GOARCH,
Compiler: runtime.Compiler,
}
// debug.ReadBuildInfo() でモジュール情報も取得可能
if bi, ok := debug.ReadBuildInfo(); ok {
for _, s := range bi.Settings {
switch s.Key {
case "vcs.revision":
if info.GitCommit == "unknown" {
info.GitCommit = s.Value
}
}
}
}
return info
}
// HandleVersion はバージョン情報をJSONで返すHTTPハンドラ
func HandleVersion(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(GetBuildInfo())
}
// PrintVersion はバージョン情報を標準出力に表示する
func PrintVersion() {
info := GetBuildInfo()
fmt.Printf("%s version %s\n", os.Args[0], info.Version)
fmt.Printf(" Built: %s\n", info.BuildTime)
fmt.Printf(" Commit: %s\n", info.GitCommit)
fmt.Printf(" Go: %s\n", info.GoVersion)
fmt.Printf(" OS/Arch: %s/%s\n", info.OS, info.Arch)
}
func main() {
// --version フラグ対応
if len(os.Args) > 1 && (os.Args[1] == "--version" || os.Args[1] == "-v") {
PrintVersion()
return
}
// サーバー起動
http.HandleFunc("/version", HandleVersion)
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
})
fmt.Printf("Starting server %s on :8080\n", version)
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Fprintf(os.Stderr, "Server error: %v\n", err)
os.Exit(1)
}
}4. Graceful Shutdown
コード例8: 本番対応の Graceful Shutdown
package main
import (
"context"
"database/sql"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
// App はアプリケーション全体を管理する
type App struct {
httpServer *http.Server
db *sql.DB
wg sync.WaitGroup
}
// NewApp はアプリケーションを初期化する
func NewApp(db *sql.DB) *App {
mux := http.NewServeMux()
app := &App{
db: db,
httpServer: &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
},
}
mux.HandleFunc("/healthz", app.handleHealth)
mux.HandleFunc("/readyz", app.handleReady)
mux.HandleFunc("/api/", app.handleAPI)
return app
}
// Run はサーバーを起動し、シグナルを待ってGraceful Shutdownする
func (app *App) Run() error {
// サーバー起動
errCh := make(chan error, 1)
go func() {
log.Printf("Server starting on %s", app.httpServer.Addr)
if err := app.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
errCh <- err
}
}()
// シグナル待ち
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
select {
case err := <-errCh:
return fmt.Errorf("server error: %w", err)
case sig := <-quit:
log.Printf("Received signal: %s", sig)
}
// Graceful Shutdown
return app.Shutdown()
}
// Shutdown はアプリケーションを安全に停止する
func (app *App) Shutdown() error {
log.Println("Starting graceful shutdown...")
// Phase 1: 新しいリクエストの受付を停止
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// HTTPサーバーのシャットダウン(進行中のリクエスト完了を待つ)
if err := app.httpServer.Shutdown(ctx); err != nil {
log.Printf("HTTP server shutdown error: %v", err)
}
// Phase 2: バックグラウンドタスクの完了を待つ
done := make(chan struct{})
go func() {
app.wg.Wait()
close(done)
}()
select {
case <-done:
log.Println("All background tasks completed")
case <-ctx.Done():
log.Println("Timeout waiting for background tasks")
}
// Phase 3: リソースのクリーンアップ
if app.db != nil {
if err := app.db.Close(); err != nil {
log.Printf("DB close error: %v", err)
}
}
log.Println("Graceful shutdown completed")
return nil
}
// handleHealth はLiveness probe用
func (app *App) handleHealth(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}
// handleReady はReadiness probe用
func (app *App) handleReady(w http.ResponseWriter, r *http.Request) {
if err := app.db.PingContext(r.Context()); err != nil {
http.Error(w, "db not ready", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("ready"))
}
// handleAPI はビジネスロジックのハンドラ
func (app *App) handleAPI(w http.ResponseWriter, r *http.Request) {
// バックグラウンドタスクのトラッキング
app.wg.Add(1)
defer app.wg.Done()
// 処理...
w.Write([]byte("ok"))
}5. CI/CD パイプライン
コード例9: GitHub Actions ワークフロー(フル構成)
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: golangci-lint
uses: golangci/golangci-lint-action@v4
with:
version: latest
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Run tests
env:
DATABASE_URL: postgres://test:test@localhost:5432/testdb?sslmode=disable
run: |
go test -race -coverprofile=coverage.out -covermode=atomic ./...
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.out
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Run govulncheck
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
- name: Run gosec
uses: securego/gosec@master
with:
args: ./...
build:
needs: [lint, test, security]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Build
run: |
CGO_ENABLED=0 go build -trimpath -ldflags="-w -s" -o bin/server ./cmd/server
- uses: actions/upload-artifact@v4
with:
name: server
path: bin/server# .github/workflows/release.yml
name: Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
packages: write
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- run: go test -race -cover ./...
release:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- uses: goreleaser/goreleaser-action@v5
with:
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
docker:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
push: true
platforms: linux/amd64,linux/arm64
build-args: |
VERSION=${{ github.ref_name }}
BUILD_TIME=${{ github.event.head_commit.timestamp }}
GIT_COMMIT=${{ github.sha }}
tags: |
ghcr.io/${{ github.repository }}:${{ github.ref_name }}
ghcr.io/${{ github.repository }}:latest
cache-from: type=gha
cache-to: type=gha,mode=maxCI/CDパイプラインフロー
Pull Request → main へマージ
│
▼| CI Pipeline (on push / PR) | ||||||
|---|---|---|---|---|---|---|
| ┌──────────┐ ┌──────────┐ ┌─────────────┐ | ||||||
| Lint | Test | Security | ||||
| golangci | go test | govulncheck | ||||
| -lint | -race | gosec | ||||
| └──────────┘ └──────────┘ └─────────────┘ | ||||||
| └────────────┼──────────────┘ | ||||||
| ▼ | ||||||
| ┌──────────┐ | ||||||
| Build | ||||||
| artifact | ||||||
| └──────────┘ |
git tag v1.2.3 && git push --tags
│
▼| Release Pipeline (on tag) | ||||
|---|---|---|---|---|
| ┌──────────┐ | ||||
| Test | ||||
| └────┬─────┘ | ||||
| PASS | ||||
| ▼ | ||||
| ┌──────────┐ ┌──────────────────────┐ | ||||
| GoReleaser | Docker Build | |||
| linux/ | linux/amd64 image | |||
| amd64 | linux/arm64 image | |||
| arm64 | ||||
| darwin/ | → ghcr.io push | |||
| amd64 | └──────────────────────┘ | |||
| arm64 | ||||
| windows/ | ||||
| amd64 | ||||
| → GitHub | ||||
| Release | ||||
| └──────────┘ |
6. GoReleaser 設定
コード例10: .goreleaser.yaml(フル構成)
# .goreleaser.yaml
version: 2
project_name: myapp
before:
hooks:
- go mod tidy
- go mod verify
- go test ./...
- go vet ./...
builds:
- id: server
main: ./cmd/server
binary: myapp-server
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm64
ignore:
- goos: windows
goarch: arm64
ldflags:
- -s -w
- -X main.version={{.Version}}
- -X main.buildTime={{.Date}}
- -X main.gitCommit={{.Commit}}
flags:
- -trimpath
- id: cli
main: ./cmd/cli
binary: myapp
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm64
archives:
- id: server-archive
builds:
- server
format: tar.gz
name_template: "{{ .ProjectName }}-server_{{ .Os }}_{{ .Arch }}"
format_overrides:
- goos: windows
format: zip
files:
- LICENSE
- README.md
- migrations/**/*
- id: cli-archive
builds:
- cli
format: tar.gz
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}"
format_overrides:
- goos: windows
format: zip
dockers:
- image_templates:
- "ghcr.io/myorg/myapp:{{ .Version }}-amd64"
use: buildx
ids:
- server
build_flag_templates:
- "--platform=linux/amd64"
- "--label=org.opencontainers.image.source=https://github.com/myorg/myapp"
goarch: amd64
- image_templates:
- "ghcr.io/myorg/myapp:{{ .Version }}-arm64"
use: buildx
ids:
- server
build_flag_templates:
- "--platform=linux/arm64"
goarch: arm64
docker_manifests:
- name_template: "ghcr.io/myorg/myapp:{{ .Version }}"
image_templates:
- "ghcr.io/myorg/myapp:{{ .Version }}-amd64"
- "ghcr.io/myorg/myapp:{{ .Version }}-arm64"
- name_template: "ghcr.io/myorg/myapp:latest"
image_templates:
- "ghcr.io/myorg/myapp:{{ .Version }}-amd64"
- "ghcr.io/myorg/myapp:{{ .Version }}-arm64"
brews:
- name: myapp
repository:
owner: myorg
name: homebrew-tap
directory: Formula
homepage: "https://github.com/myorg/myapp"
description: "My awesome app"
license: "MIT"
install: |
bin.install "myapp"
test: |
system "#{bin}/myapp", "--version"
checksum:
name_template: 'checksums.txt'
changelog:
sort: asc
groups:
- title: Features
regexp: '^.*?feat(\(.+\))?\!?:.+$'
order: 0
- title: Bug fixes
regexp: '^.*?fix(\(.+\))?\!?:.+$'
order: 1
- title: Others
order: 999
filters:
exclude:
- '^docs:'
- '^test:'
- '^chore:'7. Kubernetes デプロイ
コード例11: Kubernetes マニフェスト
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
labels:
app: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
template:
metadata:
labels:
app: myapp
spec:
serviceAccountName: myapp
securityContext:
runAsNonRoot: true
runAsUser: 65534
fsGroup: 65534
containers:
- name: myapp
image: ghcr.io/myorg/myapp:v1.0.0
ports:
- containerPort: 8080
name: http
protocol: TCP
env:
- name: LOG_LEVEL
value: "info"
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: myapp-secrets
key: database-url
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
# Liveness: プロセスが生存しているか
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
# Readiness: トラフィックを受け付けられるか
readinessProbe:
httpGet:
path: /readyz
port: http
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
# Startup: 起動完了したか(遅い起動用)
startupProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 0
periodSeconds: 3
failureThreshold: 10
terminationGracePeriodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: myapp
spec:
selector:
app: myapp
ports:
- port: 80
targetPort: http
protocol: TCP
type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: myapp
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: myapp
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: myapp
spec:
minAvailable: 1
selector:
matchLabels:
app: myapp8. サーバーレスデプロイ
コード例12: AWS Lambda
package main
import (
"context"
"encoding/json"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
)
// Handler はLambdaのハンドラ
func Handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
// リクエスト処理
body := map[string]interface{}{
"message": "Hello from Lambda!",
"path": request.Path,
"method": request.HTTPMethod,
}
jsonBody, _ := json.Marshal(body)
return events.APIGatewayProxyResponse{
StatusCode: 200,
Headers: map[string]string{
"Content-Type": "application/json",
},
Body: string(jsonBody),
}, nil
}
func main() {
lambda.Start(Handler)
}# Lambda用ビルド
lambda-build:
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 \
go build -trimpath -ldflags="-w -s" \
-o bootstrap ./cmd/lambda
zip function.zip bootstrap
lambda-deploy: lambda-build
aws lambda update-function-code \
--function-name myfunction \
--zip-file fileb://function.zip \
--architectures arm64コード例13: Google Cloud Run
# Cloud Run 用 Dockerfile
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 -trimpath -ldflags="-w -s" -o server ./cmd/server
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /server
# Cloud Run は PORT 環境変数でポートを指定する
ENV PORT=8080
EXPOSE 8080
ENTRYPOINT ["/server"]// Cloud Run用のサーバー
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
mux := http.NewServeMux()
mux.HandleFunc("/", handler)
log.Printf("Listening on :%s", port)
if err := http.ListenAndServe(":"+port, mux); err != nil {
log.Fatal(err)
}
}# Cloud Run デプロイ
gcloud run deploy myapp \
--source . \
--region asia-northeast1 \
--allow-unauthenticated \
--min-instances 0 \
--max-instances 10 \
--memory 256Mi \
--cpu 19. 設定管理
コード例14: 環境変数ベースの設定管理
package config
import (
"fmt"
"os"
"strconv"
"time"
)
// Config はアプリケーション設定
type Config struct {
Server ServerConfig
Database DatabaseConfig
Redis RedisConfig
Log LogConfig
}
type ServerConfig struct {
Port int
ReadTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
}
type DatabaseConfig struct {
URL string
MaxOpenConns int
MaxIdleConns int
ConnMaxLifetime time.Duration
}
type RedisConfig struct {
URL string
}
type LogConfig struct {
Level string
Format string
}
// Load は環境変数から設定を読み込む
func Load() (*Config, error) {
cfg := &Config{
Server: ServerConfig{
Port: getEnvInt("PORT", 8080),
ReadTimeout: getEnvDuration("SERVER_READ_TIMEOUT", 15*time.Second),
WriteTimeout: getEnvDuration("SERVER_WRITE_TIMEOUT", 15*time.Second),
IdleTimeout: getEnvDuration("SERVER_IDLE_TIMEOUT", 60*time.Second),
},
Database: DatabaseConfig{
URL: getEnvRequired("DATABASE_URL"),
MaxOpenConns: getEnvInt("DB_MAX_OPEN_CONNS", 25),
MaxIdleConns: getEnvInt("DB_MAX_IDLE_CONNS", 5),
ConnMaxLifetime: getEnvDuration("DB_CONN_MAX_LIFETIME", 5*time.Minute),
},
Redis: RedisConfig{
URL: getEnv("REDIS_URL", "redis://localhost:6379/0"),
},
Log: LogConfig{
Level: getEnv("LOG_LEVEL", "info"),
Format: getEnv("LOG_FORMAT", "json"),
},
}
return cfg, cfg.Validate()
}
func (c *Config) Validate() error {
if c.Database.URL == "" {
return fmt.Errorf("DATABASE_URL is required")
}
if c.Server.Port < 1 || c.Server.Port > 65535 {
return fmt.Errorf("invalid PORT: %d", c.Server.Port)
}
return nil
}
func getEnv(key, defaultVal string) string {
if v := os.Getenv(key); v != "" {
return v
}
return defaultVal
}
func getEnvRequired(key string) string {
v := os.Getenv(key)
if v == "" {
panic(fmt.Sprintf("required environment variable %s is not set", key))
}
return v
}
func getEnvInt(key string, defaultVal int) int {
if v := os.Getenv(key); v != "" {
i, err := strconv.Atoi(v)
if err != nil {
panic(fmt.Sprintf("invalid int value for %s: %s", key, v))
}
return i
}
return defaultVal
}
func getEnvDuration(key string, defaultVal time.Duration) time.Duration {
if v := os.Getenv(key); v != "" {
d, err := time.ParseDuration(v)
if err != nil {
panic(fmt.Sprintf("invalid duration for %s: %s", key, v))
}
return d
}
return defaultVal
}10. ldflags オプション比較表
| フラグ | 効果 | サイズ削減 | 用途 |
|---|---|---|---|
-w |
DWARFデバッグ情報を削除 | 約20-30% | 本番ビルド |
-s |
シンボルテーブルを削除 | 約10-20% | 本番ビルド |
-X pkg.var=val |
ビルド時に変数値を注入 | なし | バージョン情報埋め込み |
-extldflags "-static" |
外部リンカで静的リンク | なし | CGO使用時の静的ビルド |
-trimpath |
ビルドパスを削除 | わずか | セキュリティ向上 |
11. アンチパターン
アンチパターン1: ビルドステージをそのままデプロイ
# NG: ビルド環境ごとデプロイ(850MB+)
FROM golang:1.22
WORKDIR /app
COPY . .
RUN go build -o server .
CMD ["./server"]
# 問題: イメージサイズ巨大、ビルドツールが含まれる(攻撃対象面大)
# OK: マルチステージビルド(15-20MB)
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-w -s" -o server .
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]アンチパターン2: go mod download をキャッシュしない
# NG: ソース変更のたびに依存を再ダウンロード
FROM golang:1.22-alpine AS builder
COPY . .
RUN go build -o server .
# ソース1行変更 → go mod download からやり直し(数分のロス)
# OK: go.mod/go.sum を先にコピーしてキャッシュ活用
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./ # 依存定義のみ先にコピー
RUN go mod download # この層がキャッシュされる
COPY . . # ソース変更時もdownloadはスキップ
RUN go build -o server .アンチパターン3: rootユーザーでの実行
# NG: root で実行(セキュリティリスク)
FROM alpine:3.19
COPY --from=builder /app/server /server
CMD ["/server"]
# コンテナ内で root 権限 → 脆弱性があるとホストに影響
# OK: 非rootユーザーで実行
FROM alpine:3.19
RUN adduser -D -g '' appuser
COPY --from=builder /app/server /server
USER appuser
CMD ["/server"]アンチパターン4: Graceful Shutdown なし
// NG: シグナルを無視してすぐ終了
func main() {
http.ListenAndServe(":8080", handler)
}
// SIGTERM → 処理中のリクエストが切断される
// OK: Graceful Shutdown
func main() {
srv := &http.Server{Addr: ":8080", Handler: handler}
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.Shutdown(ctx)
}アンチパターン5: シークレットのハードコード
// NG: コード内にシークレットを記述
db, _ := sql.Open("postgres", "postgres://admin:P@ssw0rd@prod-db:5432/mydb")
// OK: 環境変数から読み取り
db, _ := sql.Open("postgres", os.Getenv("DATABASE_URL"))
// BETTER: シークレット管理サービスを使用
// AWS Secrets Manager, GCP Secret Manager, HashiCorp Vaultアンチパターン6: ヘルスチェックの未実装
// NG: ヘルスチェックエンドポイントがない
// → Kubernetes がPodの状態を判断できず、障害時に自動復旧しない
// OK: Liveness/Readiness/Startup の3つを実装
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK) // プロセスが動いていればOK
})
http.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) {
if err := db.PingContext(r.Context()); err != nil {
w.WriteHeader(http.StatusServiceUnavailable) // DBに繋がらなければNG
return
}
w.WriteHeader(http.StatusOK)
})実践演習
演習1: 基本的な実装
以下の要件を満たすコードを実装してください。
要件:
- 入力データの検証を行うこと
- エラーハンドリングを適切に実装すること
- テストコードも作成すること
# 演習1: 基本実装のテンプレート
class Exercise1:
"""基本的な実装パターンの演習"""
def __init__(self):
self.data = []
def validate_input(self, value):
"""入力値の検証"""
if value is None:
raise ValueError("入力値がNoneです")
return True
def process(self, value):
"""データ処理のメインロジック"""
self.validate_input(value)
self.data.append(value)
return self.data
def get_results(self):
"""処理結果の取得"""
return {
'count': len(self.data),
'data': self.data
}
# テスト
def test_exercise1():
ex = Exercise1()
assert ex.process(1) == [1]
assert ex.process(2) == [1, 2]
assert ex.get_results()['count'] == 2
try:
ex.process(None)
assert False, "例外が発生するべき"
except ValueError:
pass
print("全テスト合格!")
test_exercise1()演習2: 応用パターン
基本実装を拡張して、以下の機能を追加してください。
# 演習2: 応用パターン
from typing import List, Dict, Optional
from datetime import datetime
class AdvancedExercise:
"""応用パターンの演習"""
def __init__(self, max_size: int = 100):
self._items: List[Dict] = []
self._max_size = max_size
self._created_at = datetime.now()
def add(self, key: str, value: any) -> bool:
"""アイテムの追加(サイズ制限付き)"""
if len(self._items) >= self._max_size:
return False
self._items.append({
'key': key,
'value': value,
'timestamp': datetime.now().isoformat()
})
return True
def find(self, key: str) -> Optional[Dict]:
"""キーによる検索"""
for item in reversed(self._items):
if item['key'] == key:
return item
return None
def remove(self, key: str) -> bool:
"""キーによる削除"""
for i, item in enumerate(self._items):
if item['key'] == key:
self._items.pop(i)
return True
return False
def stats(self) -> Dict:
"""統計情報"""
return {
'total_items': len(self._items),
'max_size': self._max_size,
'usage_percent': len(self._items) / self._max_size * 100,
'uptime': str(datetime.now() - self._created_at)
}
# テスト
def test_advanced():
ex = AdvancedExercise(max_size=3)
assert ex.add("a", 1) == True
assert ex.add("b", 2) == True
assert ex.add("c", 3) == True
assert ex.add("d", 4) == False # サイズ制限
assert ex.find("b")['value'] == 2
assert ex.remove("b") == True
assert ex.find("b") is None
stats = ex.stats()
assert stats['total_items'] == 2
print("応用テスト全合格!")
test_advanced()演習3: パフォーマンス最適化
以下のコードのパフォーマンスを改善してください。
# 演習3: パフォーマンス最適化
import time
from functools import lru_cache
# 最適化前(O(n^2))
def slow_search(data: list, target: int) -> int:
"""非効率な検索"""
for i in range(len(data)):
for j in range(i + 1, len(data)):
if data[i] + data[j] == target:
return (i, j)
return (-1, -1)
# 最適化後(O(n))
def fast_search(data: list, target: int) -> tuple:
"""ハッシュマップを使った効率的な検索"""
seen = {}
for i, num in enumerate(data):
complement = target - num
if complement in seen:
return (seen[complement], i)
seen[num] = i
return (-1, -1)
# ベンチマーク
def benchmark():
import random
data = list(range(5000))
random.shuffle(data)
target = data[100] + data[4000]
start = time.time()
result1 = slow_search(data, target)
slow_time = time.time() - start
start = time.time()
result2 = fast_search(data, target)
fast_time = time.time() - start
print(f"非効率版: {slow_time:.4f}秒")
print(f"効率版: {fast_time:.6f}秒")
print(f"高速化率: {slow_time/fast_time:.0f}倍")
benchmark()ポイント:
- アルゴリズムの計算量を意識する
- 適切なデータ構造を選択する
- ベンチマークで効果を測定する
FAQ
Q1. CGO_ENABLED=0 にする必要がある場面は?
scratch や distroless など glibc を含まないイメージで実行する場合は CGO_ENABLED=0 が必須。標準ライブラリの net パッケージや os/user パッケージはデフォルトでCGOを使うが、CGO_ENABLED=0 で純Go実装にフォールバックする。
CGOが必要な場面:
- SQLite(go-sqlite3)を使う場合 → 代替:
modernc.org/sqlite(CGO不要) - 画像処理(libvips等)を使う場合
- OS固有のライブラリ(macOS Security Framework等)
Q2. Docker のマルチプラットフォームイメージの作り方は?
docker buildx build --platform linux/amd64,linux/arm64 でマニフェストリストを作成できる。GoReleaserを使う場合は、各アーキテクチャのイメージを個別にビルドし、docker manifest create で統合する方法もある。
# buildx でマルチプラットフォームビルド
docker buildx create --name mybuilder --use
docker buildx build --platform linux/amd64,linux/arm64 \
-t ghcr.io/myorg/myapp:v1.0.0 --push .Q3. バイナリサイズをさらに小さくする方法は?
| 手法 | サイズ削減 | トレードオフ |
|---|---|---|
-ldflags="-w -s" |
30-50% | デバッグ情報なし |
-trimpath |
わずか | ビルドパスなし |
upx 圧縮 |
50-70% | 起動時解凍コスト |
| 不要な依存の削除 | 可変 | なし |
| Go 1.22+ の改善 | 自動 | なし |
Q4. Kubernetes での推奨設定は?
- リソース制限: requests/limits を必ず設定
- ヘルスチェック: liveness/readiness/startup の3つ
- PDB: minAvailable で可用性を保証
- HPA: CPU/メモリベースの自動スケーリング
- terminationGracePeriodSeconds: Graceful Shutdown の猶予時間
- securityContext: runAsNonRoot, readOnlyRootFilesystem
Q5. デプロイ時のダウンタイムを最小化するには?
- Rolling Update: maxUnavailable=0 で常に全Podを維持
- Readiness Probe: 準備完了までトラフィック遮断
- Graceful Shutdown: 進行中のリクエスト完了を待つ
- PreStop Hook:
sleep 5でLB反映を待つ - PDB: minAvailable で同時停止数を制限
まとめ
| 概念 | 要点 |
|---|---|
| マルチステージビルド | ビルド環境と実行環境を分離して最小イメージ |
| distroless / scratch | 攻撃対象面を最小化する実行イメージ |
| CGO_ENABLED=0 | 完全静的リンクで外部依存排除 |
| -ldflags "-w -s" | デバッグ情報削除でバイナリサイズ削減 |
| -trimpath | ビルドパス削除でセキュリティ向上 |
| -X main.version=... | ビルド時のバージョン情報埋め込み |
| GOOS/GOARCH | クロスコンパイルの環境変数 |
| GoReleaser | マルチプラットフォームリリース自動化 |
| GitHub Actions | CI/CDでテスト・ビルド・デプロイ自動化 |
| Graceful Shutdown | SIGTERM受信→進行中リクエスト完了→リソース解放 |
| Kubernetes | ヘルスチェック・HPA・PDB・リソース制限 |
| サーバーレス | Lambda/Cloud Run で運用コスト最小化 |
| 設定管理 | 環境変数・シークレット管理サービス |
次に読むべきガイド
- 03-tools/04-best-practices.md -- ベストプラクティス:Effective Go
- 03-tools/02-profiling.md -- プロファイリング:pprof、trace
- 03-tools/00-cli-development.md -- CLI開発:cobra、flag、promptui
参考文献
- Docker公式 -- Multi-stage builds https://docs.docker.com/build/building/multi-stage/
- GoReleaser 公式ドキュメント https://goreleaser.com/
- Google -- distroless コンテナイメージ https://github.com/GoogleContainerTools/distroless
- Go公式 -- Build constraints https://pkg.go.dev/cmd/go#hdr-Build_constraints
- Kubernetes -- Configure Liveness, Readiness and Startup Probes https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
- AWS Lambda Go https://docs.aws.amazon.com/lambda/latest/dg/lambda-golang.html
- Google Cloud Run https://cloud.google.com/run/docs
- air -- Live reload for Go apps https://github.com/air-verse/air