Home / Cosmos DB / Engineer Course
V15
🚢
Lesson · 5 min

Local Dev & CI/CD

Cosmos emulator, ARM templates, and ephemeral test containers.

TL;DR

Run a local Cosmos emulator in Docker for offline dev, ephemeral test containers in CI, and provision real Cosmos via Bicep / Terraform. The combination — fast local feedback, reproducible CI, and infra-as-code in production — is what makes Cosmos manageable at team scale.

Key takeaways
  • The Cosmos emulator runs in Docker — `mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator`. Start it once, point your SDK at the emulator endpoint with the well-known dev key.
  • Two emulator modes — full (heavy, supports all APIs) and the new linux-vNext (lightweight, faster startup, SQL API only). Use vNext for dev; full only when you need MongoDB/Cassandra APIs locally.
  • In CI — start the emulator as a service container, run integration tests against it, tear it down. ~30s startup, no Azure spend, deterministic.
  • In prod — Bicep is the canonical IaC for Azure resources; Terraform if you're multi-cloud. Either way, version-control your container, partition key, and index policy.
  • Don't share dev databases. Use ephemeral databases per developer or per CI run. Use the SDK to create and drop them; Cosmos can hold thousands of databases per account.

The last lesson, and arguably the most important. Everything in the previous 14 lessons is moot if you can’t iterate quickly, test reliably, and ship safely. Three pieces — local emulator, CI-friendly tests, infra-as-code — and you’re set.

Local development with the emulator

Start the emulator in Docker:

docker run --publish 8081:8081 \
  --publish 10250-10255:10250-10255 \
  mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator-vnext-preview

Point your SDK at it:

var client = new CosmosClient(
  "https://localhost:8081",
  // Well-known emulator key — same in everyone's repo, not a secret
  "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
  new CosmosClientOptions {
    HttpClientFactory = () => new HttpClient(new HttpClientHandler {
      ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
    })
  });

(The TLS bypass is only because the emulator uses a self-signed cert. Don’t carry that into prod code.)

The vNext emulator starts in ~5 seconds vs. the legacy one’s ~30. SQL API only, but that’s all most apps need. Run it as a service in your docker-compose.yml alongside your app and you have offline development.

CI integration tests

GitHub Actions example — runs the emulator as a service container:

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      cosmos:
        image: mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator-vnext-preview
        ports: ['8081:8081']
        options: --health-cmd "curl -kf https://localhost:8081/" --health-interval 5s --health-retries 30
    steps:
      - uses: actions/checkout@v4
      - run: dotnet test --filter Category=Integration
        env:
          COSMOS_ENDPOINT: https://localhost:8081
          COSMOS_KEY: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==

Each test creates and drops its own database. Parallel tests don’t interfere. No Azure spend, no flaky shared state.

Infrastructure as code

For the account, container, and policy — pick Bicep (Azure-native) or Terraform (multi-cloud). The Bicep equivalent of “one Cosmos account with one database and one container”:

resource account 'Microsoft.DocumentDB/databaseAccounts@2024-05-15' = {
  name: 'cosmos-${appName}-${env}'
  location: location
  kind: 'GlobalDocumentDB'
  properties: {
    consistencyPolicy: { defaultConsistencyLevel: 'Session' }
    locations: [{ locationName: location, failoverPriority: 0 }]
    capabilities: [{ name: 'EnableServerless' }]
    disableLocalAuth: true
  }
}

resource db 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2024-05-15' = {
  parent: account
  name: 'app'
  properties: { resource: { id: 'app' } }
}

resource orders 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2024-05-15' = {
  parent: db
  name: 'orders'
  properties: {
    resource: {
      id: 'orders'
      partitionKey: { paths: ['/userId'], kind: 'Hash' }
      indexingPolicy: {
        indexingMode: 'consistent'
        includedPaths: [{ path: '/*' }]
        excludedPaths: [{ path: '/payload/*' }, { path: '/_etag/?' }]
      }
    }
  }
}

This is a complete, deployable file. Check it in. Run it in CD. Production Cosmos is now reproducible.

Multi-environment topology

Common pattern — three environments, one repo:

EnvAccountThroughputBackup
devcosmos-app-devServerlessContinuous (24h)
stagingcosmos-app-stgAutoscale 1000 RU/sPeriodic (7d)
prodcosmos-app-prdAutoscale 4000 RU/s, multi-regionContinuous + zone-redundant

Same Bicep template, parameterized. Deploy via your CD pipeline on merge to main (dev), tag (staging), manual approval (prod).

Migrations

Cosmos is schema-free, but your app has implicit schema. When you change the shape — adding a required field, splitting a doc — you need a migration strategy:

  1. Dual-write — write the new shape to both old and new fields. Run for one release.
  2. Backfill — Change Feed processor or one-off Spark job to populate new fields on old docs.
  3. Read-side fallback — read code handles both shapes during transition.
  4. Cleanup — once all docs are migrated, drop the old fields.

This is the same dance as SQL “expand-contract” migrations — version control both old and new shapes in your tests.

Wrapping the course

You’ve now seen the whole machine — APIs, partitioning, modeling, consistency, CRUD, querying, indexing, SDKs, RUs, monitoring, Change Feed, global distribution, security, vector search, and operations. The next step isn’t more theory — it’s building.

Pick a small app — a URL shortener, a personal CRM, a notes app. Model it Cosmos-first (lesson V03). Provision via Bicep (this lesson). Run it for a few weeks. Watch the metrics. Tune the index. That’s the loop, and it’s how Cosmos becomes muscle memory.

🎯 Common questions
Q1. Does the emulator behave identically to the cloud?

Mostly — the SDK API surface, query language, and indexing are identical. Differences — slightly different RU cost calibration, no cross-region scenarios (it's single-node), no built-in TTL accuracy (timing varies slightly), and a few preview features lag. For 95% of integration tests it's faithful enough; for performance tests, always test against real Cosmos.

Q2. Can I run integration tests in parallel against one emulator?

Yes — give each test class its own database name (e.g. `test_${ci_run_id}_${class}`), create at setup, drop at teardown. The emulator handles ~hundreds of databases comfortably. Don't share containers — flaky test interference is a guaranteed waste of an afternoon.

Q3. Should infra-as-code live in the app repo or a separate repo?

Same repo as the app for the container schema (partition key, indexing policy, throughput) — those are tightly coupled to code changes. Separate "platform" repo for the account, network, and AAD wiring — those have a different change cadence and ownership.

📺 Video

The lesson video is on YouTube — coming once the upload goes public.

🧪 Simulator

A live simulator for this lesson's mechanic (e.g. RU calculator, partition-key picker). Coming in Phase 2.

🎨 Visualization

An interactive diagram of this lesson's core idea — coming as we build out the visualization library.

💻 Code

A copy-paste reference snippet plus a short build challenge.

Comments 0

Discuss this page. Markdown supported. Be kind.

Loading…
Loading comments…