NOT-RELEASED
Zero-downtime red-black deployments for Sequin CDC sinks backed by OpenSearch.
SRB is a pure YAML transformer. It takes a base Sequin config with unsuffixed index names and produces a suffixed config ready to apply. It does not call any external APIs in its core commands — that's the caller's job.
You write Sequin configs with base index names (jobs, clients). SRB adds red/black suffixes (jobs_red, clients_black) and manages the dual-write pattern for zero-downtime reindexing.
STEADY STATE DUAL-WRITE (deploy) AFTER PROMOTE
jobs_red (alias) → jobs_red (alias) → jobs_black (alias)
jobs_black (backfill) jobs_red dropped
State is tracked via annotations on Sequin sinks (red-black/role, red-black/base) and verified against OpenSearch aliases as ground truth.
Download a prebuilt binary from GitHub Releases:
# Linux x64
gh release download --repo meetsmore/sequin-red-black --pattern "srb-linux-x64"
chmod +x srb-linux-x64 && mv srb-linux-x64 /usr/local/bin/srb
# macOS arm64
gh release download --repo meetsmore/sequin-red-black --pattern "srb-darwin-arm64"
chmod +x srb-darwin-arm64 && mv srb-darwin-arm64 /usr/local/bin/srbOr build from source (requires Bun):
bun install
bun run build # produces dist/srbSRB separates offline commands (pure YAML transforms, no network) from online commands (connect to Sequin/OpenSearch).
Apply red-black suffixes to a base config.
# First deploy (defaults to red)
srb offline transform base.yaml > suffixed.yaml
# With known state
srb offline transform base.yaml --state state.json > suffixed.yaml
# With backfill (dual-write)
srb offline transform base.yaml --state state.json --backfill jobs > suffixed.yamlInput: base.yaml — a Sequin config with unsuffixed names:
sinks:
- name: jobs_sink
destination:
type: elasticsearch
index_name: jobs # no suffix
transform: jobs-transformOutput: suffixed YAML to stdout:
sinks:
- name: jobs_red_sink
destination:
type: elasticsearch
index_name: jobs_red # suffixed
transform: jobs-red-transform
annotations:
red-black/role: active
red-black/base: jobsOptions:
| Flag | Description |
|---|---|
--state <file> |
JSON file: {"jobs":"red","clients":"black"}. Defaults to all-red if omitted. |
--backfill idx1,idx2 |
Indexes that need backfill. Creates dual-write sinks with the opposite suffix. |
Generate a config with promoted sinks. Takes the live config (from sequin config export) and removes old active sinks, promoting pending sinks to active.
sequin config export > live.yaml
srb offline promote live.yaml --indexes jobs > promoted.yaml
# stderr: "Indexes to drop: jobs_red"Generate a config with pending sinks removed. Aborts a dual-write.
sequin config export > live.yaml
srb offline rollback live.yaml --indexes jobs > rolledback.yaml
# stderr: "Indexes to drop: jobs_black"These connect to Sequin and/or OpenSearch. Connection details come from flags or environment variables (OPENSEARCH_URL, SEQUIN_URL, SEQUIN_TOKEN).
Get the active suffix for each managed index from OpenSearch aliases (ground truth).
srb online opensearch-state --opensearch-url http://localhost:9200 \
--sequin-url http://localhost:7376 --sequin-token $TOKEN
# {"jobs":"red","clients":"black"}Get the active suffix from Sequin sink annotations.
srb online sequin-state --sequin-url http://localhost:7376 --sequin-token $TOKEN
# {"jobs":"red","clients":"black"}Combined status with doc counts, alias targets, and dual-write detection.
srb online status --jsonsrb offline transform base.yaml > suffixed.yaml
sequin config apply suffixed.yaml# 1. Get current state from OpenSearch aliases
srb online opensearch-state > state.json
# 2. Transform with dual-write sinks
srb offline transform base.yaml --state state.json --backfill jobs > suffixed.yaml
# 3. Create the new OpenSearch index
curl -XPUT "$OPENSEARCH_URL/jobs_black"
# 4. Apply — Sequin starts dual-writing to both indexes
sequin config apply suffixed.yaml# 1. Generate promoted config
sequin config export > live.yaml
srb offline promote live.yaml --indexes jobs > promoted.yaml
# 2. Swap the alias atomically
curl -XPOST "$OPENSEARCH_URL/_aliases" -H 'Content-Type: application/json' -d '{
"actions": [
{"remove": {"index": "*", "alias": "jobs"}},
{"add": {"index": "jobs_black", "alias": "jobs"}}
]
}'
# 3. Apply — Sequin stops dual-writing
sequin config apply promoted.yaml
# 4. Drop the old index
curl -XDELETE "$OPENSEARCH_URL/jobs_red"# 1. Generate rollback config
sequin config export > live.yaml
srb offline rollback live.yaml --indexes jobs > rolledback.yaml
# 2. Apply — Sequin stops dual-writing
sequin config apply rolledback.yaml
# 3. Drop the pending index
curl -XDELETE "$OPENSEARCH_URL/jobs_black"The state file is a simple JSON mapping of index base name to active suffix:
{
"jobs": "red",
"clients": "black"
}On first deploy, if no --state is provided, all indexes default to red.
The state comes from OpenSearch aliases (ground truth). Use srb online opensearch-state to generate it, or produce it yourself.
Composite actions are provided in actions/ for CI/CD integration:
| Action | Description |
|---|---|
actions/plan |
Transform config, run sequin config plan, comment on PR |
actions/deploy |
Fetch state, transform config, create indexes, apply |
actions/promote |
Generate promoted config, swap aliases, apply, drop old |
actions/rollback |
Generate rollback config, apply, drop pending |
actions/status |
Report current state |
Example usage in a consuming repo:
- uses: meetsmore/sequin-red-black/actions/deploy@v1
with:
config: /tmp/base.yaml
backfill-indexes: jobs
sequin-url: ${{ secrets.SEQUIN_URL }}
sequin-token: ${{ secrets.SEQUIN_TOKEN }}
opensearch-url: ${{ secrets.OPENSEARCH_URL }}bun install
bun run test # unit tests
bun run build # compile binary to dist/srbSRB's core is a set of pure functions with no I/O:
| Module | Purpose |
|---|---|
src/transform.ts |
transformConfig, promoteConfig, rollbackConfig |
src/suffixer.ts |
Suffix application and dual-sink injection |
src/state.ts |
State derivation from sinks/configs |
src/types.ts |
TypeScript interfaces and State type |
Online utilities for convenience:
| Module | Purpose |
|---|---|
src/opensearch.ts |
getAlias, getDocCount |
src/sequin.ts |
getSinks |