Neonのブランチ機能でPostgreSQLデータベースを管理
Neonとは
Neon(https://neon.tech)は、サーバーレスPostgreSQLデータベースサービスです。従来のPostgreSQLホスティングサービスと異なり、以下の特徴があります:
- サーバーレス: 使用量に応じた自動スケーリング
- 従量課金: アイドル時はコストゼロ
- 高速プロビジョニング: 数秒でデータベース作成
- ブランチ機能: Gitのようにデータベースをブランチ化
- ポイントインタイムリカバリ: 過去の任意の時点に復元可能
特に注目すべきはブランチ機能です。開発、ステージング、本番環境を簡単に分離でき、データベースマイグレーションのテストが安全に行えます。
Neonの基本セットアップ
アカウント作成とプロジェクト初期化
- Neon(https://neon.tech)にアクセス
- GitHubアカウントでサインアップ
- 新しいプロジェクトを作成
# Neon CLIのインストール
npm install -g neonctl
# 認証
neonctl auth
# プロジェクト一覧
neonctl projects list
最初のデータベース接続
Neonのダッシュボードから接続文字列を取得:
postgresql://user:password@ep-xxx.region.aws.neon.tech/dbname
Node.jsでの接続例:
import { Pool } from 'pg';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: { rejectUnauthorized: false },
});
async function testConnection() {
const client = await pool.connect();
try {
const result = await client.query('SELECT NOW()');
console.log('Connected:', result.rows[0]);
} finally {
client.release();
}
}
testConnection();
ブランチ機能の基本
ブランチの作成
Gitのブランチのように、データベースのスナップショットを作成できます:
# メインブランチから新しいブランチを作成
neonctl branches create \
--name dev \
--project-id your-project-id
# 特定のブランチから派生させる
neonctl branches create \
--name feature-user-auth \
--parent dev \
--project-id your-project-id
Web UIからも作成可能:
- Neonダッシュボードを開く
- 「Branches」タブをクリック
- 「Create Branch」ボタンをクリック
- ブランチ名と親ブランチを指定
ブランチ一覧の確認
# すべてのブランチを表示
neonctl branches list --project-id your-project-id
# 詳細情報を表示
neonctl branches get dev --project-id your-project-id
ブランチの削除
neonctl branches delete feature-user-auth --project-id your-project-id
実践的なブランチ戦略
開発ワークフローの例
main (本番)
├── staging (ステージング)
└── dev (開発)
├── feature-1 (機能ブランチ1)
└── feature-2 (機能ブランチ2)
このような構造を作成するスクリプト:
// setup-branches.js
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
async function createBranch(name, parent = 'main') {
try {
const cmd = `neonctl branches create --name ${name} --parent ${parent} --project-id ${process.env.NEON_PROJECT_ID}`;
const { stdout } = await execAsync(cmd);
console.log(`✓ Created branch: ${name}`);
return stdout;
} catch (error) {
console.error(`✗ Failed to create ${name}:`, error.message);
}
}
async function setupBranches() {
console.log('Setting up branch structure...\n');
await createBranch('staging', 'main');
await createBranch('dev', 'main');
await createBranch('feature-user-profiles', 'dev');
await createBranch('feature-notifications', 'dev');
console.log('\n✓ Branch structure created successfully!');
}
setupBranches();
データベースマイグレーションとブランチ
Drizzle ORMとの統合
Drizzle ORMを使ったマイグレーション管理:
// drizzle.config.ts
import type { Config } from 'drizzle-kit';
export default {
schema: './src/db/schema.ts',
out: './drizzle',
driver: 'pg',
dbCredentials: {
connectionString: process.env.DATABASE_URL!,
},
} satisfies Config;
スキーマ定義:
// src/db/schema.ts
import { pgTable, serial, text, timestamp, boolean } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: text('email').notNull().unique(),
name: text('name').notNull(),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
content: text('content').notNull(),
published: boolean('published').default(false),
authorId: serial('author_id').references(() => users.id),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
マイグレーションスクリプト:
// scripts/migrate.ts
import { drizzle } from 'drizzle-orm/postgres-js';
import { migrate } from 'drizzle-orm/postgres-js/migrator';
import postgres from 'postgres';
async function runMigration() {
const connectionString = process.env.DATABASE_URL!;
console.log(`Running migrations on: ${connectionString.split('@')[1]}`);
const sql = postgres(connectionString, { max: 1 });
const db = drizzle(sql);
try {
await migrate(db, { migrationsFolder: './drizzle' });
console.log('✓ Migrations completed successfully!');
} catch (error) {
console.error('✗ Migration failed:', error);
process.exit(1);
} finally {
await sql.end();
}
}
runMigration();
ブランチごとのマイグレーション
# 開発ブランチでマイグレーションをテスト
DATABASE_URL=$(neonctl connection-string dev) npm run migrate
# 問題なければステージングに適用
DATABASE_URL=$(neonctl connection-string staging) npm run migrate
# 最終的に本番に適用
DATABASE_URL=$(neonctl connection-string main) npm run migrate
自動化スクリプト:
// scripts/safe-migrate.js
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
async function getConnectionString(branch) {
const { stdout } = await execAsync(
`neonctl connection-string ${branch} --project-id ${process.env.NEON_PROJECT_ID}`
);
return stdout.trim();
}
async function safeMigrate(branch) {
try {
console.log(`\n📦 Migrating branch: ${branch}`);
// ブランチの接続文字列を取得
const dbUrl = await getConnectionString(branch);
// マイグレーション実行
await execAsync(`DATABASE_URL="${dbUrl}" npm run migrate`);
console.log(`✓ Successfully migrated ${branch}`);
return true;
} catch (error) {
console.error(`✗ Migration failed on ${branch}:`, error.message);
return false;
}
}
async function main() {
const branches = ['dev', 'staging', 'main'];
for (const branch of branches) {
const success = await safeMigrate(branch);
if (!success) {
console.error('\n❌ Migration pipeline stopped due to error');
process.exit(1);
}
// 次のブランチに進む前に確認を求める
if (branch !== 'main') {
console.log(`\nReady to migrate ${branches[branches.indexOf(branch) + 1]}?`);
console.log('Press Ctrl+C to cancel, or wait 5 seconds to continue...');
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
console.log('\n✓ All migrations completed successfully!');
}
main();
テストデータの管理
ブランチにシードデータを投入
// scripts/seed.ts
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import { users, posts } from '../src/db/schema';
async function seed() {
const sql = postgres(process.env.DATABASE_URL!);
const db = drizzle(sql);
try {
console.log('🌱 Seeding database...');
// ユーザーを作成
const [user1, user2] = await db.insert(users).values([
{
email: 'alice@example.com',
name: 'Alice',
},
{
email: 'bob@example.com',
name: 'Bob',
},
]).returning();
console.log('✓ Created users');
// 投稿を作成
await db.insert(posts).values([
{
title: 'First Post',
content: 'Hello, World!',
published: true,
authorId: user1.id,
},
{
title: 'Draft Post',
content: 'Work in progress...',
published: false,
authorId: user2.id,
},
{
title: 'Another Post',
content: 'More content here',
published: true,
authorId: user1.id,
},
]);
console.log('✓ Created posts');
console.log('✓ Seeding completed successfully!');
} catch (error) {
console.error('✗ Seeding failed:', error);
process.exit(1);
} finally {
await sql.end();
}
}
seed();
ブランチごとに異なるシードデータ:
// scripts/seed-by-branch.ts
const seedData = {
dev: {
users: [
{ email: 'dev1@test.com', name: 'Dev User 1' },
{ email: 'dev2@test.com', name: 'Dev User 2' },
],
// 大量のテストデータ
},
staging: {
users: [
{ email: 'staging@test.com', name: 'Staging User' },
],
// 本番に近いデータ
},
main: {
// 本番データ(通常はシードしない)
},
};
async function seedByBranch(branch: string) {
const data = seedData[branch];
if (!data) {
console.log(`No seed data for branch: ${branch}`);
return;
}
// データ投入処理
// ...
}
環境変数の管理
.envファイルの分離
# .env.dev
DATABASE_URL=postgresql://user:pass@ep-dev.neon.tech/db
# .env.staging
DATABASE_URL=postgresql://user:pass@ep-staging.neon.tech/db
# .env.production
DATABASE_URL=postgresql://user:pass@ep-main.neon.tech/db
環境切り替えスクリプト:
// scripts/switch-env.js
import fs from 'fs';
const env = process.argv[2] || 'dev';
const envFile = `.env.${env}`;
if (!fs.existsSync(envFile)) {
console.error(`Environment file not found: ${envFile}`);
process.exit(1);
}
fs.copyFileSync(envFile, '.env');
console.log(`✓ Switched to ${env} environment`);
使用方法:
# 開発環境に切り替え
node scripts/switch-env.js dev
# アプリケーション起動
npm run dev
ブランチのリセットとロールバック
特定の時点に復元
Neonはポイントインタイムリカバリ(PITR)をサポートしています:
# 1時間前の状態に復元
neonctl branches restore dev \
--timestamp "2025-02-05 10:00:00" \
--project-id your-project-id
# 別の名前で復元
neonctl branches create \
--name dev-backup \
--parent dev \
--timestamp "2025-02-05 10:00:00" \
--project-id your-project-id
データのコピー
あるブランチから別のブランチにデータをコピー:
# pg_dumpとpg_restoreを使用
pg_dump $SOURCE_DATABASE_URL > dump.sql
psql $TARGET_DATABASE_URL < dump.sql
自動化スクリプト:
// scripts/copy-branch-data.js
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
async function copyBranchData(from, to) {
try {
console.log(`Copying data from ${from} to ${to}...`);
const sourceUrl = await getConnectionString(from);
const targetUrl = await getConnectionString(to);
// ダンプ
await execAsync(`pg_dump "${sourceUrl}" > /tmp/branch-dump.sql`);
console.log('✓ Dump created');
// リストア
await execAsync(`psql "${targetUrl}" < /tmp/branch-dump.sql`);
console.log('✓ Data restored');
// クリーンアップ
await execAsync('rm /tmp/branch-dump.sql');
console.log('✓ Copy completed successfully!');
} catch (error) {
console.error('✗ Copy failed:', error.message);
process.exit(1);
}
}
async function getConnectionString(branch) {
const { stdout } = await execAsync(
`neonctl connection-string ${branch} --project-id ${process.env.NEON_PROJECT_ID}`
);
return stdout.trim();
}
// 使用例: node copy-branch-data.js main dev
const [from, to] = process.argv.slice(2);
copyBranchData(from, to);
CI/CDとの統合
GitHub Actionsでのマイグレーション
# .github/workflows/migrate.yml
name: Database Migration
on:
push:
branches:
- main
- staging
- dev
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Determine branch
id: branch
run: |
BRANCH_NAME=${GITHUB_REF#refs/heads/}
echo "name=$BRANCH_NAME" >> $GITHUB_OUTPUT
- name: Get Neon database URL
env:
NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
NEON_PROJECT_ID: ${{ secrets.NEON_PROJECT_ID }}
run: |
npm install -g neonctl
DB_URL=$(neonctl connection-string ${{ steps.branch.outputs.name }} --project-id $NEON_PROJECT_ID)
echo "DATABASE_URL=$DB_URL" >> $GITHUB_ENV
- name: Run migrations
run: npm run migrate
- name: Run seeds (dev/staging only)
if: steps.branch.outputs.name != 'main'
run: npm run seed
プレビュー環境の自動作成
Pull Requestごとに一時的なブランチを作成:
# .github/workflows/preview.yml
name: Preview Environment
on:
pull_request:
types: [opened, synchronize]
jobs:
create-preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Create preview branch
env:
NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
NEON_PROJECT_ID: ${{ secrets.NEON_PROJECT_ID }}
run: |
npm install -g neonctl
BRANCH_NAME="pr-${{ github.event.pull_request.number }}"
neonctl branches create \
--name $BRANCH_NAME \
--parent dev \
--project-id $NEON_PROJECT_ID
DB_URL=$(neonctl connection-string $BRANCH_NAME --project-id $NEON_PROJECT_ID)
echo "Preview DB: $DB_URL"
- name: Comment PR
uses: actions/github-script@v6
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '✓ Preview database created: `pr-${{ github.event.pull_request.number }}`'
})
パフォーマンスとコスト最適化
自動スケーリング設定
# 最小・最大コンピュートユニットを設定
neonctl branches update dev \
--compute-min 0.25 \
--compute-max 2 \
--project-id your-project-id
不要なブランチの自動削除
// scripts/cleanup-old-branches.js
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
async function cleanupOldBranches() {
const { stdout } = await execAsync(
`neonctl branches list --project-id ${process.env.NEON_PROJECT_ID} --output json`
);
const branches = JSON.parse(stdout);
const now = Date.now();
const maxAge = 7 * 24 * 60 * 60 * 1000; // 7日
for (const branch of branches) {
// メインブランチは除外
if (['main', 'staging', 'dev'].includes(branch.name)) {
continue;
}
const age = now - new Date(branch.created_at).getTime();
if (age > maxAge) {
console.log(`Deleting old branch: ${branch.name}`);
await execAsync(
`neonctl branches delete ${branch.name} --project-id ${process.env.NEON_PROJECT_ID}`
);
}
}
}
cleanupOldBranches();
まとめ
Neonのブランチ機能を活用することで、以下のメリットが得られます:
- 安全なマイグレーション: 本番に影響を与えずにスキーマ変更をテスト
- 環境分離: 開発、ステージング、本番を明確に分離
- 高速プロビジョニング: 数秒で新しい環境を作成
- 柔軟なロールバック: 任意の時点に復元可能
- コスト効率: 使用した分だけの課金
Gitのようなワークフローでデータベースを管理できるため、開発チームの生産性が大幅に向上します。ぜひNeonを試してみてください。