Twelve-Factor App
Twelve-Factor は、クラウドネイティブな SaaS アプリを構築するための簡潔で言語に依存しないマニフェストです。単一のコードベースをバージョン管理システムで管理し、設定とコードを明確に分離し、ステートレスなプロセス、バックエンドサービスをアタッチされたリソースとして扱い、ログをイベントストリームとして扱うなど、様々な原則をまとめています。
本稿では、Twelve-Factor の項目に触れ、 Go と Python でこれらを実装する方法を説明します。 具体的なライブラリと実装パターンおよびコード例、クラウドプロバイダーの公開資料へのポインタを含みます。
Twelve-Factorの短いガイド
公式 Twelve-Factor サイト: 12factor.net
Codebase(コードベース) — アプリごとのリポジトリで複数のデプロイを実現。バージョン管理システム(git)を使用。ベストプラクティス:インフラ/運用スクリプトを同じリポジトリ内、または隣接するインフラリポジトリに保持し、クリアなビルド/リリースドキュメントを作成します。
Dependencies(依存関係) — 明示的に宣言し隔離(ロックファイル、ベンダリング)。言語ネイティブなパッケージ管理と CI を使用してビルドを再現します。
Config(設定) — 設定を環境変数に格納(秘密/設定をコードに埋め込まない)。ベストプラクティス:
DATABASE_URL、API キー、フィーチャーフラグを環境変数またはシークレットとして扱い、.envはリポジトリにコミットしません。Backing services(バックエンドサービス) — DB、キャッシュ、メッセージブローカーをアタッチされたリソースとして扱う(設定がそれらを指す)。接続 URL を使用し、コード変更なしにサービスをスワップできるようにします。
Build, release, run(ビルド、リリース、実行) — ビルド(アーティファクト)、リリース(アーティファクト + 設定の結合)、実行(プロセスの実行)のステージを分離し、CI/CD で自動化します。
Processes(プロセス) — 1 つ以上のステートレスプロセスとして実行し、状態はバックエンドサービスに永続化します。
Port binding(ポートバインディング) — ポートバインディングを通じてサービスをエクスポート(HTTP サーバーは
$PORTにバインド)。アプリを自己完結型にします。Concurrency(並行性) — 複数のプロセスタイプを実行してスケール。プロセスは水平スケーリングのためのファーストクラスです。
Disposability(廃棄容易性) — 高速起動とグレースフルシャットダウン(SIGTERM ハンドリング、K8s での準備完了/存続確認プローブ)。
Dev/prod parity(開発/本番一致) — 開発、ステージング、本番環境を類似させる:データ、時間、メンバー、依存関係。
Logs(ログ) — ログをイベントストリームとして扱う。キャプチャ/集約を実行環境に委任(例:構造化ログ → stdout/stderr → アグリゲータ)。
Admin processes(管理プロセス) — 一度きりの管理/メンテナンスタスク(マイグレーション、REPL)をアプリと同じ環境で実行。
Twelve-Factor は概念レベルで意図的にシンプルかつ規範的です。 多くのチームはベースラインとして採用し、サービスメッシュ、シークレットマネージャー、テレメトリー/オブザーバビリティ、プラットフォーム固有パターン(Kubernetes、サーバーレスなど)を伴い拡張します。
Twelve-Factor は Heroku の経験から生まれ、今も正規の指針です。 2024/2025年に Heroku からオープンソース化され、コミュニティによって進化できるようになりました。正規サイトでリストと根拠を確認してください。(Heroku ブログ - 2024年11月12日)
Go での考え方
Go アプリはコンパイル済みバイナリをアーティファクトとして配布して動かします。これは Twelve-Factor の ビルド → リリース → 実行 モデルにうまく適合します。検討すべきギャップは、環境変数を型付き Go 設定にきちんとマップする方法 および起動/シャットダウンを安全に保つ方法です。
一般的なライブラリ
Go コミュニティの多くの例では、Twelve-Factor に沿って envconfig または viper
でアプリを構築する手順を説明しています。設定の注入パターンとローカル開発用の godotenv
を使用する方法を示すチュートリアルとブログ投稿もあります。(blog.gopheracademy.com)
os/os.Getenv— 標準的な低レベルの読み取り。小さなアプリには適しています。kelseyhightower/envconfig— 環境変数を型付き Go 構造体にマップするための構造体タグ駆動型マッピング。小さくて慣用的です。(GitHub)spf13/viper— 柔軟な設定ライブラリ:環境変数、ファイル(JSON/TOML/YAML)、リモートソース(Consul)をサポートし、Cobra CLI と統合。複数のソースが必要なアプリに最適。(GitHub)joho/godotenv— ローカル開発用の.envファイルローダー(本番環境では .env を使用しないでください。プラットフォームのシークレット/設定マップを使用)。開発者のオンボーディングに便利。(GitHub)spf13/cobra— CLI を手早く実装できるため管理プロセスや使い捨てコマンドに役立ちます。(GitHub)
実装パターン(Go)
- 設定の単一ソース — 型付き
Config構造体を構築します。起動時に環境から設定し、コンポーネントに渡します(グローバル可変状態を回避)。 - 単一の接続 URL を使用 — 可能な場合、複数の DB フィールドではなく、例えば
DATABASE_URLを使用。 - グレースフルシャットダウン —
context.Contextを使用し、SIGTERM/SIGINTシグナルを受け取ったらリクエストを完了させる(タイムアウト付き)。 - Twelve-Factor開発の便利さ — ローカル開発でのみ
godotenvを使用します。CI と本番は CI シークレットマネージャーとプラットフォーム設定マップから環境を取得します。 - ログ - 標準出力 (stdout) に構造化ログを書き出します(例:
log/slog、zerolog、logrus など)。プラットフォームが収集できるようにします。
例えば、Viper は config.yaml を読み込み、AutomaticEnv() で環境変数の上書きを許可し、Cobra
を使用してフラグを結合できます。設定情報を多層的に管理したりフィーチャートグルが必要な場合は、重厚的にはなりますが強力です。
例:envconfig を使用した Go 設定
package config
import "github.com/kelseyhightower/envconfig"
type Config struct {
Port string `envconfig:"PORT" default:"8080"`
DatabaseURL string `envconfig:"DATABASE_URL" required:"true"`
LogLevel string `envconfig:"LOG_LEVEL" default:"info"`
}
func Load() (*Config, error) {
var c Config
if err := envconfig.Process("", &c); err != nil {
return nil, err
}
return &c, nil
}main.go は config.Load() を一度読み込み、*Config
をサーバーに渡します。このアプローチはコンパクトかつ明示的で、Twelve-Factor の設定原則に適合します。
Python での考え方
Python チームはフレームワーク(Django、Flask、FastAPI)を使用するケースが多々あります。Twelve-Factor
の原則「設定を環境変数に格納する」は人気のあるライブラリとも相性が良く、環境変数の読み取りと解析、型の検証、 .env
ファイルのサポートをシンプルにしてくれます。
主要な Python ライブラリ
python-dotenv— ローカル開発では.envを環境変数に読み込みます(.envはコミットしません)。直感的なヘルパーです。(PyPI)pydantic/pydantic-settings(BaseSettings) — 型安全な設定クラスは環境変数および.envファイルから検証とデフォルト付きで情報を読み込みます。FastAPI などモダンなコードベースに最適です。Pydantic は チームはフレームワーク(Django、Flask、FastAPI)を使用するケースが多々あります。Twelve-Factor パターンとの互換性を明示的に言及しています。(Pydantic)dynaconf— Twelve-Factor ガイドに触発されたレイヤー化された設定システム。複数の形式と環境のレイヤリング、シークレット/リモートバックエンドをサポート。シンプルな環境マッピング以上の柔軟性が必要な場合に最適。(dynaconf.com)environs,python-decouple,django-environ— 環境変数の解析と Django 統合のための軽量ヘルパーです。(PyPI, django-environ.readthedocs.io)
フレームワーク固有の統合:Django 用の django-environ、FastAPI 用の
pydantic、マルチフォーマットサポートが必要なプロジェクト用の
dynaconf。これらはコミュニティ全体で広く使用されています。使用例とパターンについてはライブラリドキュメントを参照してください。
実装パターン(Python)
- 設定クラス —
Settings(Pydantic BaseSettings)オブジェクトを定義し、起動時にインスタンス化。これにより、デフォルト値、ドキュメント、検証を一元化します。 - 単一値の接続文字列を使用 — 例:
DATABASE_URLはsqlalchemy/dj-dburlで解析され、複数の環境変数のキーが散在することを避けます。 - ローカル開発 —
python-dotenvまたはpydanticの組み込み.envサポートを使用してローカル開発環境で環境変数を読み込みます。CIや本番環境は機密情報管理システムからシークレットを取得します。コミットされたファイルではありません。 - ログ — stdout/stderr への構造化ログを使用します。一元的なアグリゲータ(ELK、SaaS)を設定します。
Dynaconf は [default]、[development]、[production]
レイヤーをサポートし、値をオーバーライドするために環境変数を読み込みます。ファイルと環境の両方のレイヤリングを提供する単一のライブラリが必要なアプリに最適です。
例:Pydantic BaseSettings
from pydantic import BaseSettings, AnyUrl
class Settings(BaseSettings):
database_url: AnyUrl
port: int = 8000
log_level: str = "info"
class Config:
env_file = ".env" # ローカル開発の便利さのみ
settings = Settings()settings.database_url は起動時に検証されます。本番では、.env
は無視されます。プラットフォームからの環境変数がオーバーライドするためです。Pydantic
はこのパターンを明示的にサポートしています。
モダンな適応
- シークレット管理 — Twelve-Factor は「環境変数」と言いますが、本番環境でのベストプラクティスは機密情報をシークレットマネージャー(AWS Secrets Manager、HashiCorp Vault、プラットフォームシークレット)に配置し、実行時に環境に注入するか、ファイルとしてマウントします。原則は同じです:コード外の設定。
- Kubernetes — Twelve-Factor モデルを K8s プリミティブにマップします:設定用の ConfigMaps/Secrets、プロセス用の Deployments、処理可能性用の準備完了/存続確認プローブ。
.envファイル — コミュニティの合意:ローカル開発の便利さに.envを使用しますが、本番シークレットとしては.envを 扱わない でください。python-dotenvとgodotenvなどのライブラリはこの開発ユースケースを明示的にサポートしています。- 構造化ログとテレメトリー — Twelve-Factor の「ログをイベントストリームとして」は構造化 JSON を stdout に発行し、プラットフォームに集約/処理を依存することと一致します。
- 検証 + スキーマ — 型付き設定(Go 構造体、Pydantic)を使用します。設定の問題は本番実行時ではなく起動時に表示されます。
📚 主要クラウドプロバイダーが公開するガイド
主要クラウドプロバイダー(アマゾン ウェブ サービス、Microsoft Azure、Google Cloud)は ガイドドキュメント / リファレンスガイド を公開しています。特に Twelve-Factor / クラウドネイティブアプリの実装を支援するもの(または類似)を強調しています。各ガイドの提供内容と重要な理由を説明します。
アマゾン ウェブ サービス (AWS)
“AWS アーキテクチャで学ぶ The Twelve Factors App 本格入門” (AWS builders.flash)
- 何について: Twelve-Factor の各項目を説明し、なぜ重要か、AWSで実装する方法 を示す日本語の記事です。
- なぜ有用: AWS コンテキストで説明し(ビルド → デプロイ → ランタイム)、AWS サービス(ECS/Fargate, ECR, CodeBuild/CodePipeline, CloudWatch)にマップします。AWS での Twelve-Factorの実装に最適です。
“Amazon ECS と AWS Fargate を利用した Twelve-Factor Apps の開発” (AWS ブログ)
- 何について: ECS/Fargate 上のコンテナ、バックエンドサービス、CI/CD、ログ管理を使用したサンプルソリューションを説明するチュートリアルスタイルの AWS ブログ投稿です。Twelve-Factorスタイルのアーキテクチャが AWS コンテナワークロードにどのように対応するかを示します。
- なぜ有用: アプリケーションをコンテナ化し、AWS 上で Twelve-Factor の原則に従う「リファレンスアーキテクチャ」が必要な場合に有用です。
Microsoft Azure
“Cloud-native architecture & 12-Factor App guidance” (Microsoft Learn)
- 何について: Azure ドキュメントページでは、クラウドネイティブアーキテクチャは Twelve-Factor をクラウドネイティブアプリの「確実な基盤」として明示的に参照しています。 クラウドネイティブデザイン(ステートレスプロセス、弾性性、設定分離など)が Azure インフラにどのように対応するかを説明しています。
- なぜ有用: 特に Azure Kubernetes Service(AKS)、コンテナサービス、またはサーバーレスを使用する場合に、Twelve-Factor の考え方を Azure アーキテクチャパターンと一致させるのに役立ちます。
Azure App Configuration ドキュメント / ガイド (Microsoft Learn)
- 何について: Azure App Configuration サービスは Twelve-Factor の「Config」原則を実装するツールとして提示されます。 設定をコードから外部化し、マイクロサービス/コンテナベースのデプロイメントで特に動的設定管理を有効にします。
- なぜ有用: Azure 上でコンテナ化または分散アプリを実行するチーム、外部設定管理が必要、設定をコード外に保つ必要 — マイクロサービスまたはマルチ環境デプロイメント特に有用です。
Google Cloud
スケーラブルで復元性の高いアプリのためのパターン / “Application development” セクション (Google Cloud Documentation)
- 何について: Google Cloud ドキュメント内の一般的なランディングエリア。アプリケーション開発(コンピュート、ホスティング、コンテナ、データ、オブザーバビリティなど)についてのリソースを集約します — Google Cloud 上でクラウドネイティブアプリを設計するための良い出発点です。
- なぜ有用: クラウドネイティブの構成要素(コンピュート、ストレージ、マネージドサービス)を探索するのに役立ちます。これらは Twelve-Factorのアイデア(バックエンドサービス、ステートレスコンテナ、設定外部化など)と一致します。
“From the Twelve to Sixteen-Factor App” (Google Cloud)
- 何について: AI 時代では元の Twelve-Factor モデルを拡張すべきだと主張する最近(2025)の Google Cloud ブログ投稿です。 AI アプリを含む最新のユースケースのための Twelve-Factor の思考の現代的な進化を提供します。
- なぜ有用: アプリケーションが AI/ML ワークロードに関わる場合に有用です。従来の CRUD またはサービスバックアップアプリを超えた最新のユースケースのための Twelve-Factor を適応/強化する方法を示します。
互いに補完する関係
- AWS ガイドは非常に具体的です。Twelve-Factor の各原則を特定の AWS サービス(ECS/Fargate、ECR、CloudWatch、CodeBuild など)にマップします。AWS 上で Twelve-Factorを採用する際に車輪を再発明せずに実装しやすくします。
- Azure のクラウドネイティブアーキテクチャガイダンス + App Configuration サービスは、より管理された/設定駆動型のアプローチを提供します。特にコンテナ化またはマイクロサービスベースのアプリに最適です。Azure エコシステム(AKS、Functions など)を既に使用している場合に最適です。
- Google Cloud のドキュメントはより一般的ですが(アプリケーション開発センター)、コミュニティと公式の Twelve-Factor の思考の進化があります。「Sixteen-Factor」投稿は AI/ML ワークロードに特に興味深く、 Twelve-Factor は廃止されていないが、最新のアプリパターンのために適応が必要であることを示唆しています。
- 3 つのプロバイダー全体で繰り返されるパターンがあります:設定外部化、コンテナ化(ステートレスプロセス)、マネージドクラウドサービスとしてのバックエンドサービスは Twelve-Factor が提唱するものものです。これらのドキュメントは Twelve-Factor の原則をプロバイダー固有のベストプラクティスに根付かせるのに役立ちます。
アプリケーションの例
ここでは、Twelve-Factor のパターンに従ったサンプルアプリを示します。以下の内容が含まれます。
- Go 製 Web アプリ($PORT にバインドし、環境変数を読み取り、PostgreSQL や Redis といったバックエンドサービスに接続)
- PostgreSQL(メインDB)
- Redis(キャッシュ)
- pgAdmin(データベース管理 UI)
- Prometheus + Grafana(オブザーバビリティ)
- Alloy + Loki(ログ集約 ― 任意だが一般的)
- クリーンな分離のためのネットワークとボリューム
これらは Docker Compose を使って構築できます。 Compose の secrets セクションで参照される POSTGRES_PASSWORD と
PGADMIN_DEFAULT_PASSWORD を含む .env ファイルが必要です。
- docker-compose.yml
- .env
- Dockerfile
- main.go
- go.mod
- go.sum
- grafana-datasources.yaml
- prometheus.yml
- loki-local-config.yaml
- alloy-local-config.alloy
docker-compose.yml を例示します。 最上段の要素で configs と secrets
を使い、開発環境から導入しやすくします。
docker-compose.yml:
---
services:
webapp:
build:
context: ./webapp
dockerfile: Dockerfile
depends_on:
- pgdb
- redis
networks:
- appnet
ports:
- '8080:8080'
environment:
PORT: '8080'
DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD}@pgdb:5432/appdb?sslmode=disable
REDIS_URL: redis://redis:6379/0
pgdb:
image: postgres:18
volumes:
- pgdata:/var/lib/postgresql
networks:
- appnet
secrets:
- postgres-passwd
environment:
POSTGRES_USER: postgres
POSTGRES_DB: appdb
POSTGRES_PASSWORD_FILE: /run/secrets/postgres-passwd
pgadmin:
image: dpage/pgadmin4:9.10
depends_on:
- pgdb
networks:
- appnet
ports:
- '5050:80'
secrets:
- pdadmin-passwd
environment:
PGADMIN_DEFAULT_EMAIL: admin@example.com
PGADMIN_DEFAULT_PASSWORD_FILE: /run/secrets/pdadmin-passwd
PGADMIN_DISABLE_POSTFIX: 'true'
configs:
- source: pgadmin-servers
target: /pgadmin4/servers.json
redis:
image: redis:8.4
command: ['redis-server', '--appendonly', 'yes']
volumes:
- redisdata:/data
networks:
- appnet
# --------------------------
# Observability stack
# --------------------------
prometheus:
image: prom/prometheus
depends_on:
- webapp
networks:
- appnet
configs:
- source: prometheus-config
target: /etc/prometheus/prometheus.yml
grafana:
image: grafana/grafana
depends_on:
- prometheus
volumes:
- grafana:/var/lib/grafana
networks:
- appnet
ports:
- '3000:3000'
configs:
- source: grafana-datasources-config
target: /etc/grafana/provisioning/datasources/ds.yaml
loki:
image: grafana/loki:3.6
command: -config.file=/etc/loki/loki-config.yaml
volumes:
- lokidata:/loki
networks:
- appnet
configs:
- source: loki-config
target: /etc/loki/loki-config.yaml
alloy:
image: grafana/alloy:v1.12.0
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: run --server.http.listen-addr=0.0.0.0:12345 --storage.path=/var/lib/alloy/data /etc/alloy/config.alloy
depends_on:
- loki
ports:
- 12345:12345
networks:
- appnet
configs:
- source: alloy-config
target: /etc/alloy/config.alloy
networks:
appnet:
volumes:
pgdata:
redisdata:
grafana:
lokidata:
configs:
prometheus-config:
file: ./observability/prometheus.yml
loki-config:
file: ./observability/loki-local-config.yaml
alloy-config:
file: ./observability/alloy-local-config.alloy
grafana-datasources-config:
file: ./observability/grafana-datasources.yaml
pgadmin-servers:
content: |-
{
"Servers": {
"1": {
"Name": "Backend Database",
"Group": "Twelve-Factor Example",
"Port": 5432,
"Username": "postgres",
"Host": "pgdb",
"MaintenanceDB": "postgres",
"ConnectionParameters": {
"sslmode": "prefer",
"connect_timeout": 10
}
}
}
}
secrets:
postgres-passwd:
environment: 'POSTGRES_PASSWORD'
pdadmin-passwd:
environment: 'PGADMIN_DEFAULT_PASSWORD'サンプルの Go アプリでは、オブザーバビリティのための “/healthz”、"/readyz"、"/metrics" といった基本的なハンドラーを定義しています。
webapp/main.go:
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/go-redis/redis/v8"
"github.com/jackc/pgx/v5"
"github.com/kelseyhightower/envconfig"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
type App struct {
DB *pgx.Conn
Redis *redis.Client
}
type Config struct {
Port string `envconfig:"PORT" default:"8080"`
DatabaseURL string `envconfig:"DATABASE_URL" required:"true"`
RedisURL string `envconfig:"REDIS_URL" required:"true"`
}
func main() {
// --- Load config from env (12-factor friendly)
var cfg Config
if err := envconfig.Process("", &cfg); err != nil {
log.Fatalf("failed to load config: %v", err)
}
port := cfg.Port
dbURL := cfg.DatabaseURL
redisURL := cfg.RedisURL
// --- Connect to PostgreSQL with pgx
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
db, err := pgx.Connect(ctx, dbURL)
cancel()
if err != nil {
log.Fatalf("failed to connect to DB: %v", err)
}
defer db.Close(context.Background())
// --- Connect to Redis
opt, err := redis.ParseURL(redisURL)
if err != nil {
log.Fatalf("invalid Redis URL: %v", err)
}
rdb := redis.NewClient(opt)
app := &App{DB: db, Redis: rdb}
// --- Routes
mux := http.NewServeMux()
mux.HandleFunc("/healthz", app.handleHealthz)
mux.HandleFunc("/readyz", app.handleReadyz)
mux.Handle("/metrics", promhttp.Handler())
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello from Go webapp!")
})
srv := &http.Server{
Addr: ":" + port,
Handler: mux,
}
// --- Start server
go func() {
log.Printf("Starting server on %s", srv.Addr)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen error: %v", err)
}
}()
// --- Graceful shutdown
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT)
<-stop
log.Println("Shutting down gracefully...")
ctx, cancel = context.WithTimeout(context.Background(), 8*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("Server Shutdown: %v", err)
}
log.Println("Goodbye!")
}
func (a *App) handleHealthz(w http.ResponseWriter, _ *http.Request) {
// Simple: if server is running, it's alive
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}
func (a *App) handleReadyz(w http.ResponseWriter, _ *http.Request) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
// Check DB
if err := a.DB.Ping(ctx); err != nil {
http.Error(w, "DB not ready", http.StatusServiceUnavailable)
return
}
// Check Redis
if _, err := a.Redis.Ping(ctx).Result(); err != nil {
http.Error(w, "Redis not ready", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("ready"))
}GET /healthzはプロセスが生存している限り常に 200 を返します。GET /readyzは DB 接続(DB.PingContext)と Redis 接続(Redis.Ping)を確認します。依存サービスが準備できていない場合は 503 を返します。- Prometheus が “/metrics” エンドポイントをスクレイプします。
Liveness と Readiness については Kubernetes のドキュメント を参照してください。 Prometheus では、ターゲットからメトリクスを取得する HTTP パスを示す metrics_path のデフォルト値は “/metrics” です。 (Prometheus)