Files
michaelschiemer/docs/deployment/database-migration-strategy.md
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

776 lines
20 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Production Database Migration Strategy
Sichere und zuverlässige Database Migration Strategies für Production Deployment des Custom PHP Frameworks.
## Migration System Overview
Das Framework nutzt ein **Safe Rollback Architecture** System:
```
Migration Interface (Forward-Only)
└─→ SafelyReversible Interface (Optional - nur bei safe rollback)
└─→ MigrationRunner
├─→ Apply (up)
└─→ Rollback (down) - nur wenn SafelyReversible
```
**Core Principle**: Migrations sind **forward-only by default**. Rollback nur wenn SICHER (no data loss).
## Safe vs Unsafe Migrations
### ✅ Safe for Rollback (implement SafelyReversible)
Diese Migrations können sicher rückgängig gemacht werden:
- **Creating new tables** (can be dropped without data loss)
- **Adding nullable columns** (can be removed)
- **Creating/dropping indexes** (no data affected)
- **Renaming columns** (data preserved)
- **Adding/removing foreign keys** (constraints only)
- **Adding CHECK constraints** (can be removed)
- **Creating empty tables** (no data to lose)
### ❌ Unsafe for Rollback (only Migration interface)
Diese Migrations können NICHT sicher zurückgerollt werden:
- **Dropping columns with data** (data is LOST)
- **Transforming data formats** (original format lost)
- **Changing column types** (data loss risk)
- **Merging/splitting tables** (data restructured)
- **Deleting data** (information cannot be restored)
- **Complex data migrations** (multiple steps, state changes)
## Migration Implementation
### 1. Safe Migration Example
```php
<?php
declare(strict_types=1);
namespace App\Domain\User\Migrations;
use App\Framework\Database\Migration\{Migration, SafelyReversible};
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\Schema\{Blueprint, Schema};
use App\Framework\Database\ValueObjects\{TableName, ColumnName, IndexName};
use App\Framework\Database\Migration\ValueObjects\MigrationVersion;
/**
* Create user_profiles table
*
* SAFE: New table can be dropped without data loss
*/
final readonly class CreateUserProfilesTable implements Migration, SafelyReversible
{
public function up(ConnectionInterface $connection): void
{
$schema = new Schema($connection);
$schema->create(TableName::fromString('user_profiles'), function (Blueprint $table) {
// Primary Key
$table->string(ColumnName::fromString('ulid'), 26)->primary();
// Foreign Key to users
$table->string(ColumnName::fromString('user_id'), 26);
// Profile Data (nullable - safe to drop)
$table->string(ColumnName::fromString('bio'))->nullable();
$table->string(ColumnName::fromString('avatar_url'))->nullable();
$table->string(ColumnName::fromString('website'))->nullable();
// Timestamps
$table->timestamps();
// Foreign Key Constraint
$table->foreign(ColumnName::fromString('user_id'))
->references(ColumnName::fromString('ulid'))
->on(TableName::fromString('users'))
->onDelete(ForeignKeyAction::CASCADE);
// Index
$table->index(
ColumnName::fromString('user_id'),
IndexName::fromString('idx_user_profiles_user_id')
);
});
$schema->execute();
}
/**
* Rollback is SAFE because:
* - Table is new (no existing data to lose)
* - If table has data, it's from testing/staging only
* - Production: Use fix-forward migration instead
*/
public function down(ConnectionInterface $connection): void
{
$schema = new Schema($connection);
$schema->dropIfExists(TableName::fromString('user_profiles'));
$schema->execute();
}
public function getVersion(): MigrationVersion
{
return MigrationVersion::fromString('2024_01_15_143000');
}
public function getDescription(): string
{
return 'Create user_profiles table';
}
}
```
### 2. Unsafe Migration Example
```php
<?php
declare(strict_types=1);
namespace App\Domain\User\Migrations;
use App\Framework\Database\Migration\Migration;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\Schema\{Blueprint, Schema};
use App\Framework\Database\Migration\ValueObjects\MigrationVersion;
/**
* Remove deprecated legacy_id column
*
* UNSAFE: Dropping column with data - NOT reversible
*/
final readonly class RemoveLegacyIdColumn implements Migration
{
public function up(ConnectionInterface $connection): void
{
$schema = new Schema($connection);
$schema->table('users', function (Blueprint $table) {
// Data is LOST after this operation!
$table->dropColumn('legacy_id');
});
$schema->execute();
}
// NO down() method - data cannot be recovered
// Use fix-forward migration if needed
public function getVersion(): MigrationVersion
{
return MigrationVersion::fromString('2024_01_15_150000');
}
public function getDescription(): string
{
return 'Remove deprecated legacy_id column from users table';
}
}
```
### 3. Fix-Forward Migration Example
Statt unsicheren Rollback: Neue Forward-Migration erstellen.
```php
<?php
declare(strict_types=1);
/**
* Restore accidentally removed deprecated_field
*
* Fix-Forward Strategy: Create new migration to undo changes
*/
final readonly class RestoreDeprecatedField implements Migration, SafelyReversible
{
public function up(ConnectionInterface $connection): void
{
$schema = new Schema($connection);
$schema->table('users', function (Blueprint $table) {
// Restore column (data cannot be recovered, but column structure can)
$table->string('deprecated_field')->nullable();
});
$schema->execute();
}
/**
* SAFE: Column is empty after restoration, can be dropped
*/
public function down(ConnectionInterface $connection): void
{
$schema = new Schema($connection);
$schema->table('users', function (Blueprint $table) {
$table->dropColumn('deprecated_field');
});
$schema->execute();
}
public function getVersion(): MigrationVersion
{
return MigrationVersion::fromString('2024_01_15_160000');
}
public function getDescription(): string
{
return 'Restore deprecated_field column to users table';
}
}
```
## Migration Commands
### Console Commands
```bash
# Create new migration
php console.php make:migration CreateUsersTable [Domain]
# Run all pending migrations
php console.php db:migrate
# Check migration status
php console.php db:status
# Rollback last migration (only if SafelyReversible)
php console.php db:rollback [steps]
# Test migration (dry-run)
php console.php db:migrate --dry-run
# Force migration (skip confirmation)
php console.php db:migrate --force
```
### Production Migration Workflow
```bash
# 1. Backup database before migration
./scripts/backup-database.sh
# 2. Check migration status
docker exec php php console.php db:status
# 3. Test migration (dry-run)
docker exec php php console.php db:migrate --dry-run
# 4. Apply migrations
docker exec php php console.php db:migrate
# 5. Verify migration success
docker exec php php console.php db:status
# 6. Run application health check
curl -f https://your-domain.com/health || exit 1
```
## Migration Rollback Safety Check
Der `MigrationRunner` prüft automatisch, ob eine Migration sicher rollbar ist:
```bash
$ php console.php db:rollback 1
🔄 Rolling back migrations...
⚠️ Safety Check: Only migrations implementing SafelyReversible will be rolled back.
❌ Rollback failed: Migration 2024_01_15_150000 does not support safe rollback
This migration cannot be safely rolled back.
Reason: Data loss would occur during rollback.
💡 Recommendation:
Create a new forward migration to undo the changes instead:
php console.php make:migration FixYourChanges
📖 See docs/deployment/database-migration-strategy.md for guidelines.
```
## Production Migration Best Practices
### 1. Pre-Migration Checklist
- [ ] **Test in Staging**: Run migration in staging environment first
- [ ] **Backup Database**: Create full database backup before migration
- [ ] **Review SQL**: Inspect generated SQL for correctness
- [ ] **Check Dependencies**: Verify migration dependencies are applied
- [ ] **Monitor Resources**: Ensure sufficient disk space and memory
- [ ] **Schedule Maintenance**: Plan migration during low-traffic window
- [ ] **Prepare Rollback**: Have rollback plan ready (fix-forward migration)
- [ ] **Team Notification**: Inform team of deployment window
- [ ] **Health Checks**: Verify health check endpoints before migration
### 2. Migration Execution Strategy
**Option A: Zero-Downtime Migration** (Recommended)
```bash
# 1. Deploy new code (migration not yet applied)
./scripts/deploy-production.sh --skip-migrations
# 2. Verify application works with old schema
curl -f https://your-domain.com/health
# 3. Apply backward-compatible migration
docker exec php php console.php db:migrate
# 4. Verify application works with new schema
curl -f https://your-domain.com/health
# 5. Complete deployment
```
**Option B: Maintenance Window Migration**
```bash
# 1. Enable maintenance mode
./scripts/maintenance-mode.sh enable
# 2. Backup database
./scripts/backup-database.sh
# 3. Apply migrations
docker exec php php console.php db:migrate
# 4. Verify health
curl -f https://your-domain.com/health
# 5. Disable maintenance mode
./scripts/maintenance-mode.sh disable
```
### 3. Backward-Compatible Migrations
**Guidelines for Zero-Downtime**:
**Safe Operations**:
- Adding nullable columns
- Creating new tables
- Adding indexes (CONCURRENTLY in PostgreSQL)
- Renaming columns (in two-step process)
**Unsafe Operations**:
- Dropping columns (old code will fail)
- Renaming columns (one-step process)
- Changing column types (old code may fail)
- Adding NOT NULL columns (without default)
**Two-Step Column Rename Example**:
```php
// Step 1: Add new column (nullable)
final readonly class AddNewEmailColumn implements Migration, SafelyReversible
{
public function up(ConnectionInterface $connection): void
{
$schema = new Schema($connection);
$schema->table('users', function (Blueprint $table) {
$table->string('email_address')->nullable();
});
$schema->execute();
// Copy data from old column
$connection->execute("UPDATE users SET email_address = email WHERE email_address IS NULL");
}
}
// Deploy new code (uses email_address, falls back to email)
// Step 2: Drop old column (after code deployed)
final readonly class DropOldEmailColumn implements Migration
{
public function up(ConnectionInterface $connection): void
{
$schema = new Schema($connection);
$schema->table('users', function (Blueprint $table) {
$table->dropColumn('email');
});
$schema->execute();
}
}
```
### 4. Performance Considerations
**Long-Running Migrations**:
```php
// ❌ Blocking operation (can timeout)
$table->addIndex(['email']); // Locks table during index creation
// ✅ Non-blocking operation (PostgreSQL)
$connection->execute("CREATE INDEX CONCURRENTLY idx_users_email ON users (email)");
```
**Large Table Migrations**:
```php
// ❌ Single UPDATE for millions of rows
$connection->execute("UPDATE users SET status = 'active' WHERE status IS NULL");
// ✅ Batch processing
$batchSize = 10000;
$offset = 0;
do {
$affected = $connection->execute(
"UPDATE users SET status = 'active'
WHERE status IS NULL
AND ulid IN (
SELECT ulid FROM users WHERE status IS NULL LIMIT ? OFFSET ?
)",
[$batchSize, $offset]
);
$offset += $batchSize;
// Small delay to reduce load
usleep(100000); // 100ms
} while ($affected > 0);
```
### 5. Data Migration Patterns
**Option 1: In-Migration Data Transform**:
```php
final readonly class MigrateUserRoles implements Migration
{
public function up(ConnectionInterface $connection): void
{
// Schema change
$schema = new Schema($connection);
$schema->table('users', function (Blueprint $table) {
$table->string('role')->default('user');
});
$schema->execute();
// Data migration
$connection->execute("
UPDATE users
SET role = CASE
WHEN is_admin THEN 'admin'
WHEN is_moderator THEN 'moderator'
ELSE 'user'
END
");
// Drop old columns
$schema->table('users', function (Blueprint $table) {
$table->dropColumn('is_admin', 'is_moderator');
});
$schema->execute();
}
}
```
**Option 2: Separate Data Migration Script**:
```php
// Migration: Schema only
final readonly class AddRoleColumn implements Migration, SafelyReversible
{
public function up(ConnectionInterface $connection): void
{
$schema = new Schema($connection);
$schema->table('users', function (Blueprint $table) {
$table->string('role')->nullable();
});
$schema->execute();
}
}
// Separate script: Data migration
// scripts/data-migrations/migrate-user-roles.php
$repository = $container->get(UserRepository::class);
$users = $repository->findAll();
foreach ($users as $user) {
$role = $user->isAdmin() ? 'admin'
: ($user->isModerator() ? 'moderator' : 'user');
$user->setRole($role);
$repository->save($user);
}
```
## Migration Testing
### 1. Local Testing
```bash
# Reset database and re-run all migrations
php console.php db:fresh
# Run specific migration
php console.php db:migrate --step=1
# Test rollback (if SafelyReversible)
php console.php db:rollback --step=1
# Re-apply migration
php console.php db:migrate --step=1
```
### 2. Staging Environment Testing
```bash
# Copy production data to staging (anonymized)
./scripts/anonymize-production-data.sh
# Apply migrations in staging
ssh staging "cd /app && php console.php db:migrate"
# Run integration tests
ssh staging "cd /app && ./vendor/bin/pest --testsuite=Integration"
# Verify application health
curl -f https://staging.your-domain.com/health
```
### 3. Migration Test Checklist
- [ ] **Migration applies successfully** (no errors)
- [ ] **Schema matches expectations** (inspect database)
- [ ] **Data integrity preserved** (no data loss)
- [ ] **Application health checks pass** (all endpoints work)
- [ ] **Performance acceptable** (migration completes in reasonable time)
- [ ] **Rollback works** (if SafelyReversible)
- [ ] **Idempotency verified** (can run migration multiple times)
- [ ] **Foreign key constraints intact** (referential integrity)
- [ ] **Indexes created properly** (query performance maintained)
## Monitoring & Validation
### Post-Migration Health Checks
```bash
# 1. Database connection test
docker exec php php console.php db:test-connection
# 2. Table integrity check
docker exec php php console.php db:check-integrity
# 3. Application health check
curl -f https://your-domain.com/health
# 4. Check for errors in logs
docker-compose logs php | grep -i error
# 5. Verify data consistency
docker exec php php console.php db:verify-data
```
### Migration Metrics
**Track These Metrics**:
- Migration execution time
- Database size before/after
- Number of affected rows
- Query performance (slow query log)
- Error rate (application logs)
- Health check failures
**Example Monitoring**:
```php
// Log migration metrics
$startTime = microtime(true);
$sizeBeforeMB = $this->getDatabaseSize();
$migration->up($connection);
$executionTimeMs = (microtime(true) - $startTime) * 1000;
$sizeAfterMB = $this->getDatabaseSize();
$this->logger->info('Migration completed', [
'migration' => $migration->getVersion()->value,
'execution_time_ms' => $executionTimeMs,
'size_before_mb' => $sizeBeforeMB,
'size_after_mb' => $sizeAfterMB,
'size_delta_mb' => $sizeAfterMB - $sizeBeforeMB,
]);
```
## Rollback Strategy
### When to Rollback vs Fix-Forward
**Rollback (if SafelyReversible)**:
- Migration applied in last 5 minutes
- No production traffic affected yet
- Schema change only (no data migration)
- Quick fix available
**Fix-Forward (Recommended for Production)**:
- Migration applied > 5 minutes ago
- Production traffic affected
- Data migrations applied
- Complex schema changes
- Uncertain about data loss
### Rollback Execution
```bash
# Check if migration supports rollback
docker exec php php console.php db:status
# Rollback last migration (if safe)
docker exec php php console.php db:rollback --step=1
# Verify rollback success
docker exec php php console.php db:status
# Check application health
curl -f https://your-domain.com/health
```
### Fix-Forward Execution
```bash
# 1. Create fix-forward migration
docker exec php php console.php make:migration FixBrokenMigration
# 2. Implement fix in new migration
# Edit: src/.../Migrations/2024_XX_XX_XXXXXX_FixBrokenMigration.php
# 3. Test in staging
ssh staging "cd /app && php console.php db:migrate"
# 4. Apply in production
docker exec php php console.php db:migrate
# 5. Verify fix
curl -f https://your-domain.com/health
```
## Disaster Recovery
### Database Backup Before Migration
```bash
#!/bin/bash
# scripts/backup-database.sh
BACKUP_DIR="/backups/database"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
DB_NAME="michaelschiemer_prod"
# Create backup directory
mkdir -p "$BACKUP_DIR"
# Backup database
docker exec db pg_dump -U postgres "$DB_NAME" | \
gzip > "$BACKUP_DIR/backup_${TIMESTAMP}.sql.gz"
# Verify backup
if [ $? -eq 0 ]; then
echo "✅ Backup successful: $BACKUP_DIR/backup_${TIMESTAMP}.sql.gz"
else
echo "❌ Backup failed"
exit 1
fi
# Keep only last 30 days of backups
find "$BACKUP_DIR" -name "backup_*.sql.gz" -mtime +30 -delete
```
### Database Restore
```bash
#!/bin/bash
# scripts/restore-database.sh
BACKUP_FILE="$1"
if [ -z "$BACKUP_FILE" ]; then
echo "Usage: $0 /path/to/backup.sql.gz"
exit 1
fi
# Confirm restore
echo "⚠️ This will REPLACE the current database with backup: $BACKUP_FILE"
read -p "Are you sure? (yes/no): " confirm
if [ "$confirm" != "yes" ]; then
echo "Restore cancelled"
exit 0
fi
# Drop and recreate database
docker exec db psql -U postgres -c "DROP DATABASE IF EXISTS michaelschiemer_prod"
docker exec db psql -U postgres -c "CREATE DATABASE michaelschiemer_prod"
# Restore backup
gunzip -c "$BACKUP_FILE" | docker exec -i db psql -U postgres michaelschiemer_prod
echo "✅ Database restored from backup"
```
## Troubleshooting
### Problem: Migration Times Out
**Cause**: Long-running operation on large table
**Solution**:
```php
// Increase timeout for migration
$connection->execute("SET statement_timeout = '300s'");
// Or split into smaller batches
$batchSize = 10000;
// ... batch processing code
```
### Problem: Migration Fails Midway
**Cause**: Error during schema change or data migration
**Solution**:
```bash
# Check migration status
docker exec php php console.php db:status
# If migration is partially applied:
# 1. Manual cleanup (if needed)
docker exec db psql -U postgres michaelschiemer_prod
# 2. Mark migration as failed
docker exec php php console.php db:reset [version]
# 3. Fix migration code
# 4. Re-run migration
docker exec php php console.php db:migrate
```
### Problem: Foreign Key Constraint Violation
**Cause**: Data inconsistency or missing referenced rows
**Solution**:
```sql
-- Find orphaned rows
SELECT * FROM child_table
WHERE parent_id NOT IN (SELECT id FROM parent_table);
-- Fix data before migration
DELETE FROM child_table
WHERE parent_id NOT IN (SELECT id FROM parent_table);
-- Then run migration
```
## See Also
- **Production Prerequisites**: `docs/deployment/production-prerequisites.md`
- **Database Patterns**: `docs/claude/database-patterns.md`
- **Deployment Guide**: `docs/deployment/deployment-guide.md` (TODO)
- **Rollback Guide**: `docs/deployment/rollback-guide.md` (TODO)