Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 114 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ Publish the OpenSearch client configuration:
php artisan vendor:publish --provider="DirectoryTree\OpenSearchClient\OpenSearchClientServiceProvider"
```

Publish the migration configuration:
Publish the migration and deployment configuration:

```bash
php artisan vendor:publish --provider="DirectoryTree\OpenSearchMigrations\OpenSearchMigrationsServiceProvider"
```

## Configuration

The migration configuration is published to `config/opensearch-migrations.php`:
Migration history and index naming are configured in `config/opensearch-migrations.php`:

```php
'table' => env('OPENSEARCH_MIGRATIONS_TABLE', 'opensearch_migrations'),
Expand All @@ -47,6 +47,17 @@ The migration configuration is published to `config/opensearch-migrations.php`:
'alias_name_prefix' => env('OPENSEARCH_MIGRATIONS_ALIAS_NAME_PREFIX', env('SCOUT_PREFIX', '')),
```

Deployment storage is configured separately in `config/opensearch-deployments.php`:

```php
'table' => env('OPENSEARCH_DEPLOYMENTS_TABLE', 'opensearch_deployments'),

'connection' => env(
'OPENSEARCH_DEPLOYMENTS_CONNECTION',
env('OPENSEARCH_MIGRATIONS_CONNECTION')
),
```

## Creating Migrations

Create a migration:
Expand Down Expand Up @@ -124,6 +135,107 @@ Show migration status:
php artisan opensearch:migrate:status
```

## Zero-Downtime Index Deployments

The deployer creates versioned physical indexes and tracks their lifecycle in the `opensearch_deployments` table. Applications remain responsible for backfilling and validating candidate documents.

First, create a stable alias for the index in a migration:

```php
Index::putAlias('posts', 'posts_search');
```

Provision a candidate with the latest mapping and settings:

```php
use DirectoryTree\OpenSearchAdapter\Indices\Mapping;
use DirectoryTree\OpenSearchAdapter\Indices\Settings;
use DirectoryTree\OpenSearchMigrations\Deployer;

$deployer = app(Deployer::class);

$deployment = $deployer->provision(
name: 'posts',
alias: 'posts_search',
configure: function (Mapping $mapping, Settings $settings) {
$mapping->text('title');
$mapping->text('body');
},
);
```

The returned deployment exposes the physical candidate index for inspection while writes continue targeting only the active alias:

```php
$deployment->candidateIndex;
$deployment->writeIndexes(); // ['posts_search']
```

Once the candidate has been inspected, begin backfilling before importing historical documents. This enables concurrent writes and deletions to the candidate:

```php
$deployment = $deployer->backfill('posts');

$deployment->writeIndexes();
```

The same transition is available from the command line:

```bash
php artisan opensearch:deploy:backfill posts
```

After application-specific validation succeeds, mark the candidate ready and atomically move the alias:

```php
$deployer->markReady('posts');
$deployer->cutover('posts');
```

```bash
php artisan opensearch:deploy:ready posts
php artisan opensearch:deploy:cutover posts
```

Cancel and delete a candidate that should not be promoted:

```php
$deployer->cancel('posts');
```

```bash
php artisan opensearch:deploy:cancel posts
```

The previous index remains in the deployment's write indexes during the rollback window:

```php
$deployer->rollback('posts');
```

```bash
php artisan opensearch:deploy:rollback posts
```

Once the new index is verified in production, delete the previous physical index and complete the deployment:

```php
$deployer->retire('posts');
```

```bash
php artisan opensearch:deploy:retire posts
```

Inspect one or every deployment at any point in the lifecycle:

```bash
php artisan opensearch:deploy:status
php artisan opensearch:deploy:status posts
```

`opensearch:migrate:fresh` deletes all deployment records because it drops all physical indexes. Migration reset and refresh commands refuse to run while managed deployments exist.

## Credits

This package builds on a lot of the foundation and prior work from [Ivan Babenko](https://github.com/babenkoivan) and his Elasticsearch Laravel ecosystem packages.
Expand Down
29 changes: 29 additions & 0 deletions config/opensearch-deployments.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

return [
/*
|--------------------------------------------------------------------------
| Deployment Table
|--------------------------------------------------------------------------
|
| This table stores the state of versioned OpenSearch index deployments.
|
*/

'table' => env('OPENSEARCH_DEPLOYMENTS_TABLE', 'opensearch_deployments'),

/*
|--------------------------------------------------------------------------
| Database Connection
|--------------------------------------------------------------------------
|
| This connection stores OpenSearch deployment state. When null, the
| application's default database connection will be used.
|
*/

'connection' => env(
'OPENSEARCH_DEPLOYMENTS_CONNECTION',
env('OPENSEARCH_MIGRATIONS_CONNECTION')
),
];
47 changes: 47 additions & 0 deletions src/Console/DeploymentBackfillCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace DirectoryTree\OpenSearchMigrations\Console;

use DirectoryTree\OpenSearchMigrations\Deployer;
use Illuminate\Console\Command;
use Illuminate\Console\ConfirmableTrait;

/**
* Begin backfilling an OpenSearch deployment.
*/
class DeploymentBackfillCommand extends Command
{
use ConfirmableTrait;

/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'opensearch:deploy:backfill
{name : The logical index name}
{--force : Force the operation to run when in production}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Enable candidate writes before backfilling an index';

/**
* Execute the console command.
*/
public function handle(Deployer $deployer): int
{
if (! $this->confirmToProceed()) {
return static::FAILURE;
}

$deployer->backfill($this->argument('name'));

$this->components->info("Deployment [{$this->argument('name')}] is ready to be backfilled.");

return static::SUCCESS;
}
}
47 changes: 47 additions & 0 deletions src/Console/DeploymentCancelCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace DirectoryTree\OpenSearchMigrations\Console;

use DirectoryTree\OpenSearchMigrations\Deployer;
use Illuminate\Console\Command;
use Illuminate\Console\ConfirmableTrait;

/**
* Cancel an OpenSearch deployment.
*/
class DeploymentCancelCommand extends Command
{
use ConfirmableTrait;

/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'opensearch:deploy:cancel
{name : The logical index name}
{--force : Force the operation to run when in production}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Cancel an index deployment and delete its candidate';

/**
* Execute the console command.
*/
public function handle(Deployer $deployer): int
{
if (! $this->confirmToProceed()) {
return static::FAILURE;
}

$deployer->cancel($this->argument('name'));

$this->components->info("Deployment [{$this->argument('name')}] was cancelled.");

return static::SUCCESS;
}
}
47 changes: 47 additions & 0 deletions src/Console/DeploymentCutoverCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace DirectoryTree\OpenSearchMigrations\Console;

use DirectoryTree\OpenSearchMigrations\Deployer;
use Illuminate\Console\Command;
use Illuminate\Console\ConfirmableTrait;

/**
* Cut over an OpenSearch deployment.
*/
class DeploymentCutoverCommand extends Command
{
use ConfirmableTrait;

/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'opensearch:deploy:cutover
{name : The logical index name}
{--force : Force the operation to run when in production}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Switch an index alias to its ready candidate';

/**
* Execute the console command.
*/
public function handle(Deployer $deployer): int
{
if (! $this->confirmToProceed()) {
return static::FAILURE;
}

$deployer->cutover($this->argument('name'));

$this->components->info("Deployment [{$this->argument('name')}] was cut over.");

return static::SUCCESS;
}
}
47 changes: 47 additions & 0 deletions src/Console/DeploymentReadyCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace DirectoryTree\OpenSearchMigrations\Console;

use DirectoryTree\OpenSearchMigrations\Deployer;
use Illuminate\Console\Command;
use Illuminate\Console\ConfirmableTrait;

/**
* Mark an OpenSearch deployment as ready.
*/
class DeploymentReadyCommand extends Command
{
use ConfirmableTrait;

/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'opensearch:deploy:ready
{name : The logical index name}
{--force : Force the operation to run when in production}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Mark an index deployment as ready for cutover';

/**
* Execute the console command.
*/
public function handle(Deployer $deployer): int
{
if (! $this->confirmToProceed()) {
return static::FAILURE;
}

$deployer->markReady($this->argument('name'));

$this->components->info("Deployment [{$this->argument('name')}] is ready for cutover.");

return static::SUCCESS;
}
}
Loading
Loading