This guide covers how to publish multiple environments (dev, qa, prod) to a single Azure API Management instance using the environment: block in your override files.
For standard single-APIM-per-env deployments, you do not need this guide — see Environment Overrides.
Use the shared-APIM pattern when:
- You want to reduce cost by running dev, qa, and prod APIs on one APIM instance.
- You need to demo or test multiple environments side-by-side on the same gateway endpoint.
- You are operating a Developer or Consumption tier APIM where creating per-environment instances is impractical.
Do not use this pattern when:
- Each environment has its own dedicated APIM instance (the default, simpler approach).
- Your security or compliance policy requires full network isolation between environments.
- You need independent SLA guarantees per environment — a shared APIM is a shared failure domain.
The trade-off is straightforward: shared APIM saves cost and consolidates gateway management, but all environments compete for the same capacity and any APIM-level outage affects all of them simultaneously.
All artifact files on disk use canonical names — the bare names extracted from your source-of-truth APIM (e.g., petstore-api, my-backend). At publish time, apiops publish reads the environment: block from your override file and applies a prefix and/or suffix to every resource name before sending it to APIM.
For example, with namePrefix: "dev-", the canonical artifact petstore-api is deployed as dev-petstore-api. The prod publish with namePrefix: "prod-" deploys the same artifact as prod-petstore-api. Both coexist on the shared APIM without colliding.
Cross-references inside resources — policy {{token}} references, fragment IDs, backend IDs, subscription scopes, API release API IDs, and association links — are rewritten automatically to use the affixed names. You write your artifacts and overrides using canonical names throughout; the tool handles renaming at the boundary.
You need a canonical source-of-truth APIM — a dedicated APIM instance (or clean committed artifacts) that contains your API configuration with bare, unprefixed names. Extract from this instance to produce your artifact files.
Warning: Do not extract from the shared APIM. Extracting from a shared APIM produces mixed-environment artifacts — you will get
dev-petstore-api,prod-petstore-api, and similar prefixed names in your artifact directory. Those prefixed names become the canonical names in your artifacts, and the affix logic will then double-prefix them on the next publish (e.g.,dev-dev-petstore-api). Always extract from a dedicated source-of-truth APIM with clean, unprefixed names.
# Extract from your source-of-truth APIM (dedicated, unprefixed)
apiops extract \
--resource-group rg-source \
--service-name apim-source \
--output ./apim-artifactsAdd an environment: block to each environment's override file. The block is a top-level sibling of the resource sections (apis, backends, etc.).
# configuration.dev.yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/v1/override-config.schema.json
environment:
namePrefix: "dev-"
apiPathPrefix: "dev/"
apis:
- name: petstore-api # canonical name — no prefix here
properties:
serviceUrl: https://petstore-dev.contoso.com
backends:
- name: petstore-backend # canonical name
properties:
url: https://petstore-dev.contoso.com# configuration.prod.yaml
environment:
namePrefix: "prod-"
apiPathPrefix: "prod/"
apis:
- name: petstore-api # same canonical name
properties:
serviceUrl: https://petstore.contoso.com
backends:
- name: petstore-backend
properties:
url: https://petstore.contoso.comAll override name values must be the canonical (unprefixed) name, matching the artifact on disk. The prefix is applied by the tool at publish time — not by you in the override file.
| Field | Type | Description |
|---|---|---|
namePrefix |
string |
Prepended to resource names for types in appliesTo. |
nameSuffix |
string |
Appended to resource names for types in appliesTo. |
appliesTo |
string[] |
Resource type names to affix. Omit to use the default set. |
apiPathPrefix |
string |
Prepended to each API's properties.path. Not applied when a per-API path override is present. |
You can use namePrefix, nameSuffix, or both simultaneously. For example, namePrefix: "dev-" and nameSuffix: "-v2" produces dev-petstore-api-v2.
⚠️ At least one name affix is required. If you specify anenvironment:block, you must set at least one ofnamePrefixornameSuffix. A block with onlyapiPathPrefix(or with no fields set) is rejected at publish time with an error.Why: Without a name affix, resource names collide across environments on the shared APIM instance. Worse,
apiops publish --delete-unmatchedwould treat every other environment's resources as stale and delete them — silently destroying the shared instance. Path-only isolation is intentionally not supported for this reason.Use a separator character in your affix (e.g.,
"dev-"not"dev", or"-dev"not"dev"). This prevents partial-name matches during--delete-unmatched— anamePrefixof"dev"(no separator) would matchdeveloper-*,development-*, and any other resources whose canonical names happen to start with"dev".If you don't need a name affix, remove the
environment:block entirely and publish under canonical names (single-environment mode).
When appliesTo is omitted, the following resource types receive the prefix/suffix:
| Resource type | appliesTo value |
|---|---|
| Api | Api |
| Product | Product |
| NamedValue | NamedValue |
| Backend | Backend |
| Logger | Logger |
| PolicyFragment | PolicyFragment |
| VersionSet | VersionSet |
| Tag | Tag |
| Group | Group |
| Subscription | Subscription |
| Workspace | Workspace |
The following types are not affixed by default but can be added to appliesTo:
| Resource type | appliesTo value |
Notes |
|---|---|---|
| Gateway | Gateway |
Self-hosted gateways. |
| Diagnostic | Diagnostic |
Service-level diagnostics (e.g., applicationinsights). Usually a singleton per APIM — affix carefully. |
| GlobalSchema | GlobalSchema |
Service-wide shared schemas. |
| PolicyRestriction | PolicyRestriction |
Policy restriction rules. |
| Documentation | Documentation |
API documentation entries. |
Some resource types cannot be affixed because they are singletons, derived from their parent, or association links whose identity is fully determined by their parent names:
- Singletons:
ServicePolicy,ApiPolicy,ProductPolicy,ApiOperationPolicy,GraphQLResolverPolicy,ApiWiki,ProductWiki,McpServer - Association children (affixed via parent):
ProductApi,ProductGroup,ProductTag,GatewayApi,ApiTag,ApiDiagnostic - Sub-resource children (affixed via parent API):
ApiOperation,ApiSchema,ApiRelease,ApiTagDescription,GraphQLResolver
Including any of these in appliesTo is a validation error.
environment:
namePrefix: "dev-"
appliesTo:
- Api
- Product
- NamedValue
- Backend
- Logger
- PolicyFragment
- VersionSet
- Tag
- Group
- Subscription
- Workspace
- Gateway # opt-in: affix self-hosted gateway names tooTo affix only APIs and backends (and nothing else):
environment:
namePrefix: "dev-"
appliesTo:
- Api
- BackendAny type not listed is left with its canonical name on the shared APIM, which means all environments share the same named value, product, etc. Only do this intentionally — shared resources must be compatible across environments.
An explicit name in an override entry always refers to the canonical (unprefixed) name. The environment: block affix is applied after override properties are merged. You never write prefixed names in override files.
apiPathPrefixis applied toproperties.pathfor every API inappliesTo.- If a specific API entry in the override file includes a
properties.pathvalue, that explicit override wins andapiPathPrefixis not applied to that API. - The two do not stack — an explicit
pathoverride completely replaces the prefix behavior for that API.
environment:
namePrefix: "dev-"
apiPathPrefix: "dev/" # applied to all APIs by default
apis:
- name: petstore-api
properties:
serviceUrl: https://petstore-dev.contoso.com
# No path override → apiPathPrefix applies → deployed path is "dev/petstore"
- name: legacy-api
properties:
serviceUrl: https://legacy-dev.contoso.com
path: "internal/legacy" # explicit override → apiPathPrefix NOT appliedNamedValues have both a name (resource key) and a displayName (the {{token}} identifier used in policy XML). When NamedValue is in appliesTo:
- Both the resource name and the displayName receive the affix.
- This ensures
{{my-token}}in a policy becomes{{dev-my-token}}for dev — cross-env policy isolation is maintained automatically. - If you set an explicit
displayNamein a NamedValue override entry, that value is used as-is (no affix applied to it).
When appliesTo includes the relevant types, apiops publish rewrites the following references in artifacts before sending them to APIM. You do not need to update artifact files or override files manually.
| Reference | Location | How it is rewritten |
|---|---|---|
{{namedValueToken}} |
Policy XML | Token rewritten to affixed displayName when NamedValue ∈ appliesTo and the named value artifact exists. Unknown tokens pass through unchanged. |
fragment-id="..." |
Policy XML <include-fragment> |
Fragment ID rewritten when PolicyFragment ∈ appliesTo. |
backend-id="..." |
Policy XML <set-backend-service> |
Backend ID rewritten when Backend ∈ appliesTo. |
Subscription scope |
ARM subscription properties | Trailing segment rewritten when the referenced type (Api or Product) is in appliesTo. |
ApiRelease apiId |
ARM release properties | Api ID segment rewritten when Api ∈ appliesTo. |
| ProductApi association | ARM resource path | Both product and API name segments rewritten per appliesTo. |
| ProductGroup association | ARM resource path | Both product and group name segments rewritten per appliesTo. |
| GatewayApi association | ARM resource path | Gateway and API name segments rewritten per appliesTo. |
API properties.path |
ARM API properties | Prefix applied unless explicit path override present (see Precedence rules). |
When you run apiops publish --delete-unmatched against a shared APIM, the delete pass is namespace-scoped: only resources whose name matches the current environment's prefix/suffix pattern are candidates for deletion.
A publish with namePrefix: "dev-" will only delete resources that start with dev-. Resources named prod-*, qa-*, or any other prefix are invisible to the delete pass and are never touched.
Warning: Do not drop the prefix mid-life.
If you initially deploy an environment with
namePrefix: "dev-"and later remove or change the prefix in your override file,--delete-unmatchedcan no longer identify which resources belong to that environment. It will see all resources without the old prefix as candidates for deletion, potentially removing resources from other environments.If you need to rename or remove the prefix, do so in a controlled migration: first remove
--delete-unmatchedfrom your pipeline, rename/remove the prefix, republish all resources, then manually delete the old prefixed resources.
This example shows a three-environment pipeline (dev, qa, prod) all publishing to a single APIM instance named apim-shared.
project/
├── apim-artifacts/
│ ├── apis/
│ │ └── petstore-api/
│ │ └── apiInformation.json
│ ├── backends/
│ │ └── petstore-backend/
│ │ └── backendInformation.json
│ └── namedValues/
│ └── api-key/
│ └── namedValueInformation.json
├── configuration.dev.yaml
├── configuration.qa.yaml
└── configuration.prod.yaml
# configuration.dev.yaml
environment:
namePrefix: "dev-"
apiPathPrefix: "dev/"
apis:
- name: petstore-api
properties:
serviceUrl: https://petstore-dev.contoso.com
backends:
- name: petstore-backend
properties:
url: https://petstore-dev.contoso.com
namedValues:
- name: api-key
properties:
value: "{#[DEV_API_KEY]#}"# configuration.qa.yaml
environment:
namePrefix: "qa-"
apiPathPrefix: "qa/"
apis:
- name: petstore-api
properties:
serviceUrl: https://petstore-qa.contoso.com
backends:
- name: petstore-backend
properties:
url: https://petstore-qa.contoso.com
namedValues:
- name: api-key
properties:
value: "{#[QA_API_KEY]#}"# configuration.prod.yaml
environment:
namePrefix: "prod-"
apiPathPrefix: "prod/"
apis:
- name: petstore-api
properties:
serviceUrl: https://petstore.contoso.com
backends:
- name: petstore-backend
properties:
url: https://petstore.contoso.com
namedValues:
- name: api-key
properties:
value: "{#[PROD_API_KEY]#}"apiops extract \
--resource-group rg-source \
--service-name apim-source \
--output ./apim-artifacts# Dev
apiops publish \
--resource-group rg-shared \
--service-name apim-shared \
--source ./apim-artifacts \
--overrides configuration.dev.yaml \
--delete-unmatched
# QA
apiops publish \
--resource-group rg-shared \
--service-name apim-shared \
--source ./apim-artifacts \
--overrides configuration.qa.yaml \
--delete-unmatched
# Prod
apiops publish \
--resource-group rg-shared \
--service-name apim-shared \
--source ./apim-artifacts \
--overrides configuration.prod.yaml \
--delete-unmatchedAfter all three publishes, the shared APIM contains:
| Resource type | Dev | QA | Prod |
|---|---|---|---|
| API | dev-petstore-api (path: dev/petstore) |
qa-petstore-api (path: qa/petstore) |
prod-petstore-api (path: prod/petstore) |
| Backend | dev-petstore-backend |
qa-petstore-backend |
prod-petstore-backend |
| NamedValue | dev-api-key |
qa-api-key |
prod-api-key |
Each environment's resources are fully isolated by namespace. A request to https://apim-shared.azure-api.net/dev/petstore/pets routes to the dev backend; prod/petstore/pets routes to prod.
Cause: Either NamedValue is not in appliesTo, or the artifact for myThing does not exist in the artifact directory.
Fix: Verify that:
NamedValueis included inappliesTo(or you are using the default set, which includes it).- A
namedValues/myThing/artifact directory exists in your--sourcepath. - The token name in the policy exactly matches the NamedValue's
displayNamein the artifact (case-sensitive).
Unknown tokens (no matching artifact) pass through unchanged and are left as-is in the deployed policy.
Cause: Two environments are publishing to the same API path. This happens when apiPathPrefix is omitted or set to the same value for multiple environments.
Fix: Ensure each environment has a unique apiPathPrefix. If you intentionally share a path across environments (only one env active at a time), omit apiPathPrefix for that resource and manage path assignment manually via a per-API path override.
Cause: The environment whose resources were deleted did not have namePrefix or nameSuffix set, so its resources had no namespace prefix and were indistinguishable from unmanaged resources.
Fix: Every environment on a shared APIM must have a unique namePrefix or nameSuffix. Add the missing environment: block to the override file for the affected environment, republish it to restore the deleted resources, and ensure all subsequent publishes include the environment: block.
No special configuration is needed — the two patterns are independent and can be mixed across override files.
# configuration.dev.yaml — shared APIM, env prefix required
environment:
namePrefix: "dev-"
apiPathPrefix: "dev/"# configuration.prod.yaml — dedicated APIM, no environment block needed
# No environment: block
apis:
- name: petstore-api
properties:
serviceUrl: https://petstore.contoso.comPublish each with its respective --service-name (shared APIM for dev/qa, dedicated APIM for prod). The environment: block is only interpreted during the publish where it appears.
- Policy XML rewriting is regex-based. The rewriter uses targeted regular expressions for
{{token}},fragment-id="...", andbackend-id="..."patterns. Tokens or attributes inside CDATA sections or XML comments may not be rewritten correctly. Avoid placing canonical resource name references inside CDATA blocks or comments in policy XML. - Workspace container is not affixed. The Workspace resource name itself is never prefixed/suffixed, even if
Workspaceis inappliesTo. Resources inside a workspace are affixed normally via their parent workspace's name. ServicePolicyand singleton types are never affixed. The service-level policy, API policies, product policies, and operation policies derive their identity from their parent resource and are handled transitively — see Types that are never affixed.- No
--strip-prefixfor extraction. There is no built-in flag to strip environment prefixes when extracting from a shared APIM. Always extract from a dedicated source-of-truth APIM with canonical names.