Skip to content

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.

  1. Enable migrations on the connection

    Add migrations.enabled: true to your OpenSearch connection. This works both for connections declared directly in application.yml and for connections defined in a module config file such as opensearch-config.yml.

    opensearch-config.yml
    opensearch:
    - name: opensearch
    hosts:
    - https://localhost:9200
    username: admin
    password: secret
    ssl_verification: false
    migrations:
    enabled: true
  2. Create the migration directory

    Terminal window
    mkdir -p /migrations/opensearch

    Migrations live in one folder per connection name, so a connection named opensearch reads files from /migrations/opensearch/.

  3. 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" }
    }
    }
    }
  4. 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 directly
    php bin/migrate.php -c /path/to/config --sqlPath /path/to/migrations -m opensearch

    Omit -m (or pass -m sql) to run SQL migrations instead. The --sqlPath argument is the shared migrations root for both 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.

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_migrations index was lost succeeds instead of failing with resource_already_exists_exception.
  • hosts (required)
  • username / password (basic auth)
  • ssl_verification (default true)
  • timeout and connect_timeout in seconds
  • proxy (explicit proxy URL, or false to disable proxying)
  • The HTTP client is self-contained (no opensearch-php dependency). It uses the Swoole coroutine HTTP client inside a Swoole coroutine (the same approach as the winter-opensearch SwooleHttpHandler), 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.