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
Use the same runner script as for SQL migrations, adding
-m opensearch:Terminal window php bin/migrate.php --configDir /path/to/config --sqlPath /path/to/migrations -m opensearchOmit
-m(or pass-m sql) to run SQL migrations instead.--sqlPathis 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 natural order of their relative path (including sub-folders such as release-1.0/; 1.9.0/ runs before 1.10.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)
Values support the same property expressions as everywhere else in the application: $env.NAME, $ini.key (from the propertySources declared in application.yml) and fallback chains such as $env.NAME || default. This applies to connections in application.yml and to those read from module config files such as opensearch-config.yml, so the migrator connects with exactly the settings the running application uses. Keep credentials out of version control this way:
opensearch: - name: opensearch hosts: - "$env.OPENSEARCH_HOST || https://localhost:9200" username: "$ini.opensearchUsername" password: "$ini.opensearchPassword" ssl_verification: "$env.OPENSEARCH_SSL_VERIFY || false" migrations: enabled: trueA reference that cannot be resolved (for example a key missing from the ini file) stops the migration with an error instead of skipping the connection.
- The HTTP client is self-contained (no
opensearch-phpdependency). It uses the Swoole coroutine HTTP client inside a Swoole coroutine 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 behavior.