OpenSearch Migrations: Version Index Templates and Policies
Winter Boot’s OpenSearch migration runner applies JSON-based index templates, ISM policies, and index schemas against configured OpenSearch connections. It tracks execution the same way SQL migrations do, so you can version-control your search cluster schema alongside your database schema.
Quick start
Section titled “Quick start”-
Enable migrations on the connection
Add
migrations.enabled: trueto your OpenSearch connection. This works both for connections declared directly inapplication.ymland for connections defined in a module config file such asopensearch-config.yml.opensearch-config.yml opensearch:- name: opensearchhosts:- https://localhost:9200username: adminpassword: secretssl_verification: falsemigrations:enabled: true -
Create the migration directory
Terminal window mkdir -p /migrations/opensearchMigrations live in one folder per connection name, so a connection named
opensearchreads files from/migrations/opensearch/. -
Add JSON files
Create
/migrations/opensearch/sf-entities-template.json:{"settings": {"index": {"number_of_shards": 1,"number_of_replicas": 0}},"mappings": {"properties": {"entity_name": { "type": "keyword" }}}} -
Run migrations
Terminal window # Using the PHAR built with build/sqlmigrator/build.sh./winter-migrations-app.phar -c /path/to/config --sqlPath /path/to/migrations -m opensearch# Or using PHP CLI directlyphp bin/migrate.php -c /path/to/config --sqlPath /path/to/migrations -m opensearchOmit
-m(or pass-m sql) to run SQL migrations instead. The--sqlPathargument is the shared migrations root for both types.
Migration file types
Section titled “Migration file types”An explicit envelope ({method, path, body?, name?}) is always sent as a raw request. Otherwise the filename decides first:
| Filename | Action |
|---|---|
*-template.json |
PUT /_index_template/{name} |
*-policy.json |
PUT /_plugins/_ism/policies/{name} |
| anything else | derived from content (see below) |
For *-template.json, a full composable template body (with index_patterns) is sent as-is. A raw settings / mappings / aliases definition is wrapped into a valid template, defaulting index_patterns to ["{name}*"] (which covers the bare index plus dated variants like sf-events-2026.09.04). Put an explicit index_patterns list in the file to override that default.
For other filenames the content decides:
| Content | Action |
|---|---|
document with index_patterns |
PUT /_index_template/{name} |
document with top-level policy object |
PUT /_plugins/_ism/policies/{name} |
document with settings / mappings / aliases |
PUT /{name} (create index) |
The resource {name} defaults to the file name without .json and without a trailing -template, -policy, or -index suffix, so sf-entities-template.json manages the sf-entities index (or template, or policy, depending on content). Index names are lowercased because OpenSearch requires lowercase index names.
Files are executed in alphabetical order (including sub-folders such as release-1.0/), and the relative path is the migration identity.
Idempotency
Section titled “Idempotency”Executed migrations are recorded as documents in the winter_migrations index (one document per connection plus relative path, the OpenSearch equivalent of the winter_migrations SQL table), together with a SHA-256 hash of the file content. Re-runs skip files whose hash is unchanged and re-apply files whose content changed, so editing a template or policy file and re-running updates it in place.
Every operation is also safe to replay:
- Index template and ISM policy PUTs are natural upserts.
- Index creation first checks whether the index already exists, so re-running against a cluster whose
winter_migrationsindex was lost succeeds instead of failing withresource_already_exists_exception.
Connection configuration keys
Section titled “Connection configuration keys”hosts(required)username/password(basic auth)ssl_verification(defaulttrue)timeoutandconnect_timeoutin secondsproxy(explicit proxy URL, orfalseto disable proxying)
- The HTTP client is self-contained (no
opensearch-phpdependency). It uses the Swoole coroutine HTTP client inside a Swoole coroutine (the same approach as the winter-opensearchSwooleHttpHandler), cURL when available, and the PHP stream wrapper as a fallback. Hosts are tried in order on transport failure. - First failure stops all migrations. There is no automatic rollback, matching the SQL migration behaviour.