【Day 31】データベース初期データ(シードデータ)の自動投入テクニック 〜新人の手動DBリストアを完全撤廃! entrypoint-initdb の仕組み、C# EF Core 起動時マイグレーション、ゼロセットアップDB環境の構築〜
「新メンバーがチームに入った日、DB環境構築で丸一日潰していませんか?」
前回のDay 30では、「コード・構成(Git)」と「実データ(Volume)」の3層分離原則を徹底解説し、巨大なDBファイルをGitにコミットしてリポジトリを破壊する事故の防止策を学びました。
しかし、ここで現場のエンジニアから最も多く寄せられる切実な疑問があります:
『mdfファイルをGitに含めないのは理解できた。では、新人が開発を始めるための初期データ(マスターデータやテスト用データ)はどうやって渡せばいいのか?』
かつてのオンプレミス現場では、先輩エンジニアがUSBメモリや共有フォルダで数GBのダンプファイルを手渡し、手作業でSSMSからリストアさせていました。
しかし現代のコンテナ開発では、『リポジトリをクローンして docker compose up を1回実行するだけで、必要な初期データがすべて自動投入されたDBが立ち上がる』という「ゼロセットアップDB環境」 が標準です。
本講義では、Docker標準の entrypoint-initdb の自動実行メカニズムから、C# EF Core による Database.Migrate() とテストデータ注入テクニックまで、現場で即座に役立つ自動化ノウハウを完全網羅します!
INDEX 本日の講義カリキュラム
- 第1章: USB手動配布の悲劇と「ゼロセットアップDB」の必然性
- 第2章: コンテナ初回起動時の自動実行メカニズム(entrypoint-initdb.d)
- 第3章: C# EF Core Migration と Database.Migrate() による自動適用
- 第4章: チーム開発におけるゼロセットアップDBの Docker Compose 実践構成
- 第5章: 現場トラブルシューティング&ベストプラクティス
- 第6章: 第6部総まとめと理解度チェッククイズ(次回予告:第7部 Dockerネットワーク入門)
第1章: USB手動配布の悲劇と「ゼロセットアップDB」の必然性
多くの開発現場で、新メンバー配属時やPCリプレイス時に次のような光景が繰り返されてきました:
- 先輩社員が「これ、開発用のDBバックアップだから」とUSBメモリを新人に手渡す
- 新人がSQL Server Management Studio(SSMS)を開き、手動で「データベースの復元」を実行
- 「SQL Serverのバージョンが合わない(上位バージョンで作られたbakファイルは下位バージョンに復元不可)」「論理ファイル名のマッピングエラー」「ログインユーザーのSID不整合」などでエラー連発
- 気づけば環境構築だけで新人の入社初日〜2日目が終了してしまう…
上図(左側)の通り、手動によるDBダンプ配布には致命的な問題があります。手順の属人化、バージョンの不一致、そして「誰のPCのDBが一番最新の状態か分からない」という深刻な環境ドリフトです。
これに対して現代のコンテナ開発が目指すのが、上図(右側)の 「ゼロセットアップDB(Zero-Setup Database)」 です。
新人が行う操作はたった2つ:
git clone <リポジトリURL>でコードを取得するdocker compose up -dを実行する
これだけで、最新のスキーマ定義、商品マスタ、初期テストユーザーが完璧に整ったデータベースコンテナが自動的に構築されます。人間が手作業でSQLを実行したり、USBメモリを受け渡す必要は1ミリもありません。
第2章: コンテナ初回起動時の自動実行メカニズム(entrypoint-initdb.d)
「コンテナを起動した瞬間に、どうやって初期SQLスクリプトを実行させるのか?」
多くの公式データベースイメージ(PostgreSQL、MySQL、MariaDBなど)には、この初期データ自動投入のための標準仕様である /docker-entrypoint-initdb.d/ という仕組みが最初から組み込まれています。
entrypoint-initdb.d の動作原理
上図に示した通り、データベースコンテナの起動スクリプト(docker-entrypoint.sh)は、内部で次のような厳格な判定を行っています:
🔍 初回起動判定の3ステップロジック
- データディレクトリの確認: マウントされたVolume(例:
/var/lib/postgresql/data)を検査し、既存のデータベースファイルが存在するかチェックする。 - 未初期化(空)の場合のみ実行: データが空の場合(初回起動時)、
/docker-entrypoint-initdb.d/配下に存在する.sqlや.shスクリプトを ファイル名のアルファベット昇順(例: 01-schema.sql ➡ 02-seed.sql) で順番に自動実行する。 - データが存在する場合はスキップ: 既にデータベースが作成されている場合(2回目以降の
docker compose up)、スクリプトの実行を自動的にスキップし、既存データを完全に保護する。
この「Volumeが空の初回起動時のみ実行される」という特性が極めて重要です。日々の開発でコンテナを停止・再起動しても、データが勝手に初期化されたり上書きされて消える心配は一切ありません!
SQL Server(MSSQL)における初期スクリプト実行テクニック
ここでMicrosoft SQL Server(mcr.microsoft.com/mssql/server)を扱うC#開発者が必ず突き当たる壁があります:
「SQL Serverの公式イメージには /docker-entrypoint-initdb.d が用意されていない!」 という事実です。
SQL Serverでコンテナ起動時に自動で初期SQLを流し込むには、現場では以下の 「ワンショット初期化コンテナ(initコンテナパターン)」 を採用するのが最も堅牢で推奨されるベストプラクティスです。
# docker-compose.yml での init コンテナパターン services: db: image: mcr.microsoft.com/mssql/server:2022-latest environment: - ACCEPT_EULA=Y - MSSQL_SA_PASSWORD=${SA_PASSWORD} volumes: - sqlserver_data:/var/opt/mssql/data healthcheck: test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "$$MSSQL_SA_PASSWORD" -C -Q 'SELECT 1' || exit 1"] interval: 5s timeout: 5s retries: 10 # ★DB本体が健全起動した後に1度だけ実行されて終了する初期化専用サービス db-init: image: mcr.microsoft.com/mssql-tools depends_on: db: condition: service_healthy volumes: - ./init-scripts:/scripts:ro entrypoint: > /bin/bash -c " echo 'SQL Server初期化スクリプトを実行中...'; /opt/mssql-tools/bin/sqlcmd -S db -U sa -P "$$SA_PASSWORD" -d master -i /scripts/01_create_tables_and_seed.sql; echo '初期データの投入が完了しました!'; " environment: - SA_PASSWORD=${SA_PASSWORD} restart: "no"
この構成では、db-init サービスが depends_on と service_healthy により 「SQL Serverがクエリを受け付けられる状態になった瞬間」 を検知して自動実行されます。SQLの実行が終われば自動的に停止するため、余計なリソースを消費しません。
第3章: C# EF Core Migration と Database.Migrate() による自動適用
初期データを投入するもう一つの強力なアプローチが、.NET / C# の標準ORMである Entity Framework Core(EF Core) を活用したコードベースのマイグレーションとシーディングです。
Program.cs での起動時マイグレーション
C# Web API アプリケーションの起動時(Program.cs)に以下のコードを記述しておくことで、コンテナ起動時に自動で最新マイグレーションをSQL Serverに適用させることができます:
// Program.cs (.NET 8 / 9 形式) var builder = WebApplication.CreateBuilder(args); // DbContextの登録 builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); var app = builder.Build(); // ★コンテナ起動時にデータベースを自動マイグレーション&シード using (var scope = app.Services.CreateScope()) { var services = scope.ServiceProvider; try { var context = services.GetRequiredService<AppDbContext>(); // 未適用のマイグレーションがあれば自動適用(テーブル自動生成) await context.Database.MigrateAsync(); // 必要に応じて開発用テストデータをシード await DbInitializer.SeedDataAsync(context); } catch (Exception ex) { var logger = services.GetRequiredService<ILogger<Program>>(); logger.LogError(ex, "データベースのマイグレーション中にエラーが発生しました。"); throw; } } app.MapGet("/api/products", async (AppDbContext db) => await db.Products.ToListAsync()); app.Run();
HasData によるマスターデータのコード化
EF Coreの modelBuilder.Entity<T>().HasData(...) を使用すると、初期マスタデータ(例: 都道府県マスタ、権限ロール、ステータス定義)をC#コードとして完全にGit管理できます:
// AppDbContext.cs public class AppDbContext : DbContext { public DbSet<Role> Roles { get; set; } public DbSet<Product> Products { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // 必須マスタデータをMigrationファイル内に生成 modelBuilder.Entity<Role>().HasData( new Role { Id = 1, Name = "Administrator" }, new Role { Id = 2, Name = "Member" }, new Role { Id = 3, Name = "Guest" } ); } }
💡 現場の使い分け黄金ルール
① システム必須の小規模マスタデータ(権限・区分値): EF Core の HasData() でMigrationコードとしてGit管理。
② 開発・ローカルテスト用の大量ダミーデータ(1万件の注文履歴など): init.sql や独立したSeederスクリプトで管理し、本番マイグレーションを汚さない!
第4章: チーム開発におけるゼロセットアップDBの Docker Compose 実践構成
それでは、実際に現場でそのまま導入できる 「ゼロセットアップDB環境」の完全なリポジトリ構造と設定ファイル を見ていきましょう。
推奨ディレクトリ構造
my-csharp-project/ ├── .env.example # パスワード設定のテンプレート(Git共有) ├── .gitignore # .env や *.mdf を完全ブロック ├── docker-compose.yml # サービス定義(WebAPI, DB, 初期化設定) ├── db-init/ # 初期データ用SQLスクリプト配置フォルダ │ ├── 01_create_schema.sql # テーブル生成DDL(EF Core不使用時) │ └── 02_seed_test_data.sql # テスト用顧客・商品レコードINSERT └── src/ └── MyWebApi/ ├── Dockerfile ├── Program.cs └── appsettings.json
PostgreSQLを採用した場合の超シンプル Compose 例
PostgreSQLを採用している場合、設定は驚くほどシンプルです。./db-init フォルダを /docker-entrypoint-initdb.d にマウントするだけで、初回起動時に自動実行されます:
services: webapi: build: ./src/MyWebApi ports: - "5000:5000" environment: - ConnectionStrings__DefaultConnection=Host=db;Port=5432;Database=shop_dev;Username=postgres;Password=${POSTGRES_PASSWORD} depends_on: db: condition: service_healthy db: image: postgres:16-alpine environment: - POSTGRES_DB=shop_dev - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} volumes: # 実データはNamed Volumeで永続保持 - postgres_data:/var/lib/postgresql/data # ★初回起動時のみ実行される初期化SQLフォルダを読み取り専用マウント - ./db-init:/docker-entrypoint-initdb.d:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres -d shop_dev"] interval: 3s timeout: 3s retries: 5 volumes: postgres_data:
新人がプロジェクトに着任した初日、リポジトリを git clone して .env.example を .env にコピーし、docker compose up -d と打つだけで、数分後には完全なテスト環境がブラウザやSwaggerで動作します。
第5章: 現場トラブルシューティング&ベストプラクティス
初期データ自動投入を運用する中で、チームが必ず遭遇する 「3大ハマりポイント」とその即効解決策 を解説します。
🚨 トラブル①: 「init.sql を書き換えたのに、コンテナを再起動しても反映されない!」
【原因】: entrypoint-initdb.d は 「Volumeが空の初回起動時のみ」 実行されます。既にNamed VolumeにDBデータが作成されている場合、2回目以降の起動では完全にスキップされます。
【解決策】: Volumeを破棄して再作成します:
docker compose down -v
docker compose up -d
-v オプションを付けることで古いVolumeが削除され、初期化スクリプトが再び実行されます。
🚨 トラブル②: 「複数のSQLファイルの実行順序がバラバラで外部キー制約エラーになる!」
【原因】: entrypoint-initdb.d はファイルを辞書順(アルファベット順)に実行します。schema.sql より先に seed.sql が呼ばれるとテーブル未作成エラーになります。
【解決策】: ファイル名の先頭に必ず 01_, 02_ のような連番プレフィックスを付与します:
01_create_tables.sql ➡ 02_seed_master.sql ➡ 03_seed_test_orders.sql
🚨 トラブル③: 「本番環境でも Database.Migrate() を起動時に実行していいの?」
【現場プラクティス】: 単一インスタンスのローカル開発や検証環境では非常に便利ですが、本番環境で複数インスタンス(KubernetesやECS等)が同時にスケール起動する場合、マイグレーション競合やデッドロックのリスク があります。
本番環境では、CI/CDパイプライン(GitHub Actionsなど)のデプロイステップで dotnet ef database update を単独実行するか、専用のマイグレーションJobを実行するのがエンタープライズの標準手法です。
第6章: 第6部総まとめと理解度チェッククイズ
これで第6部「データの永続化とボリューム(Volume & Bind Mount)」の全カリキュラム(Day 27〜31)がすべて完了しました!
コンテナの基本原則であるステートレス設計から、Named Volumeによる堅牢なデータ保持、Bind Mountによる爆速開発、GitとVolumeの境界線ルール、そして今回のゼロセットアップDB環境の構築まで、「データ永続化と開発スピードを完璧に両立させるプロフェッショナルの技術」 を完全にマスターしました!
🎯 理解度チェッククイズ!
問: 開発チームのメンバーが「初期投入用スクリプト(init.sql)にテストユーザーを追加したのですが、docker compose restart を実行してもデータベースに反映されません」と困っています。理由と正しい反映手順を説明してください。
正解と解説を確認する
【正解と解説】
理由: データベースコンテナの entrypoint-initdb.d 機構は、データディレクトリ(Volume)が空である「初回起動時のみ」動作する安全設計になっているためです。再起動(restart)では既にデータが存在するため、スクリプトの実行はスキップされます。
正しい反映手順:
① docker compose down -v を実行して既存のNamed Volumeを削除する。
② docker compose up -d を実行して空のVolumeで再起動し、更新された init.sql を自動実行させる。
🚀 次回予告:第7部「Dockerネットワークの基礎とサービス間通信」開幕!
次回Day 32からは、いよいよ待望の 第7部「Dockerネットワーク」 に突入します!
「なぜ docker-compose.yml の中では localhost ではなくサービス名(db)で通信できるのか?」
Docker内部に自動生成される仮想ブリッジネットワーク(Bridge Network)や組み込みDNSサーバーの驚くべき仕組みを徹底的に解き明かします。ご期待ください!
コメント
コメントを投稿