Closed
Labels
EnhancementImprovement to existing functionalityImprovement to existing functionalityP2Next sprintNext sprintclose:fixedFixed by a previous PR or releaseFixed by a previous PR or release
Description
Summary
Support deploying multiple environments (e.g., dev, staging, prod) to the same Azure API Management instance, with clear separation between environments via naming conventions or prefixes.
Problem
Currently, apiops-cli assumes a 1:1 mapping between environments and APIM instances. Many organizations want to consolidate multiple environments into a single APIM instance for cost or operational reasons, but need clear separation between environment-specific APIs.
Proposed Solution
Extract
- Support extracting APIs from a single APIM instance while tagging/grouping them by environment (e.g., via API name prefix, path prefix, or tags).
- Allow filter configuration to scope extraction to a specific environment subset.
Publish
- When publishing to a target environment (e.g., "Dev"), automatically apply an environment-specific prefix or suffix to API names/paths (e.g.,
dev-my-apior/dev/my-api). - Support configuration to define the naming/prefixing strategy per environment.
- Ensure environment-specific named values, backends, and other resources are correctly scoped.
Configuration
- Extend the configuration system to support an
environmentconcept with per-environment prefixing rules. - Allow users to define environment mappings in their configuration files (e.g.,
configuration.dev.yamltargets the same APIM instance but with adev-prefix).
Acceptance Criteria
- Extract supports filtering by environment within a single APIM instance
- Publish supports applying environment-specific prefixes/suffixes to APIs
- Configuration schema supports environment-to-prefix mappings
- Documentation covers multi-environment setup on a single APIM instance
- Tests cover extract and publish with environment prefixing
Metadata
Metadata
Assignees
Labels
EnhancementImprovement to existing functionalityImprovement to existing functionalityP2Next sprintNext sprintclose:fixedFixed by a previous PR or releaseFixed by a previous PR or release
Activity
[-]feat: Support multi-environment deployment to a single APIM instance[/-][+]Support multi-environment deployment to a single APIM instance[/+]jxyiliu1 commentedon Jul 3, 2026
Hope we could have this feature implemented soon, our organization is waiting for this feature for long....
petehauge commentedon Jul 3, 2026
Thanks jxyiliu1 for the heads-up! I also saw htwashere was interested in this feature too.
I'm working on a design now, but there are some unexpected complexities. Could you review the rough design & plans below and let me know if you have any concerns so we can refine the design?
Thanks!
htwashere commentedon Jul 3, 2026
Hi Peter Hauge (@petehauge), thanks for your attention to this feature. Here are my thoughts to your design notes above:
Point 1: I think it is reasonable. Your ApiOps Extract currently asks for an APIM instance as a source of truth. APIM enforces the API-name to be unique anyway. If we use one APIM instance to serve multiple environments for the same API, we need to differentiate them via the API-name anyway so technically, APIM will consider them as two separate APIs (as you are pointing it out in point 3)
Point 2: In our use case after examining your ApiOps Extract, I think we will be using your filter feature to limit the extracted resources. So I think when we publish to the next APIM, those global resources will be excluded?
Point 3: Could the NamePrefix be a token where we can place anywhere in the API-Name string? I am just thinking for organizations like ours who already have certain naming convention. Or, perhaps the environment be a "suffix" better?
Point 4: I think you are correct regarding deep-rewrite. I am not too familiar with the overall structure of the files yet.
BTW, is there an explanation on all the files that are extracted for the resources? At this moment, I am trying to understand the purpose of file apiInformation.json in the context of the extract and publish. Where does APIM use it?
I will be away next week so will be slow in responding to you in subsequent dialogues.
Thank you!
Henry
petehauge commentedon Jul 3, 2026
Hi htwashere -
For 2 - you're correct, that if you filter on extract, anything filtered out don't have associated files on disk and aren't published
For 3 - could you share your naming scheme? We could do either a prefix or suffix - just wanted to confirm that we wouldn't need to replace tokens in the middle of the names?
We don't have any docs on explanation of the specific files but we could probably write something. If you need it - could you log a new github issue requesting the docs? In terms of apiInformation.json, these are typically the properties associated with an individual API. I included a screenshot of an example I have - things like the revision, path, display name, protocols, service URL, etc. It's the key properties we get back when we do a 'get' against the API in API Management. You'll see a lot of these things on the "Settings" tab for an API (2nd screenshot below)
htwashere commentedon Jul 3, 2026
Hi Peter Hauge (@petehauge) , our naming convention is still being standardized but I see many of our teams are settling to: "dept env apiname" or "dept apiname env". I think we can settle for a suffix but if you could do replace tokens for the name, I can see that being the best? While we are on this name topic, I suppose with the displayName, we will continue to use the "override" feature to change the Display Name so no issue there?
Thanks for the apiInformation.json explanation. Are you using it for any logic processing in apiops publish? I am thinking if that file needs to be considered (overrides) for the target APIM?
petehauge commentedon Jul 4, 2026
Hi htwashere - on the replacing tokens, this is hard because either:
Via the cli, we could do the replacement, but I'm just concerned about the user experience for it - that it might be error prone and difficult for users. If we support both prefix & suffix, it sounds like that would be enough (and is straightforward from a user experience perspective)?
On the apiInformation.json file - there are some things we currently do during publish:
(and then if this issue is fixed, it will add another bullet to apply name prefix/suffix)
petehauge commentedon Jul 4, 2026
Multi-Environment Publish — Design Proposal
Scoping assumptions:
namePrefixand/ornameSuffixdeclared in the override file.1. Config surface
Extend the override YAML with a new top-level
environmentblock:Key rules:
environmentis optional. If absent, behaviour is identical to today (100% back-compat).v1 → v2per the repo'supdate-config-schema-versionskill.v1files continue to load (noenvironmentblock = no-op).Why in the override file, not CLI flags: Env metadata belongs with env-specific values, is version-controlled, reviewable, and testable. CLI flags could be added later as an escape hatch but shouldn't be the primary UX.
2. Which resource types get affixed
Not everything can be renamed safely. Proposed default set:
{{ref}}— see §4Users override the default via
appliesTowhen needed.3. Where the transform lives — a single
env-mappermoduleNew file:
src/services/env-mapper.ts. Exports:Design choice — map at the publish edge, not throughout the code. Everywhere that reads artifacts, applies overrides, does dependency graph work: keep using canonical names. Rewrite to deployed names in exactly two places:
client.putResource(...)inresource-publisher.tsclient.deleteResource(...)in the delete pathAdvantages: filter, override-merger, artifact-store, dependency-graph, transitive-resolver all stay untouched. Testability stays high.
applyOverrides()still matches by canonical name, which is what the user wrote in their YAML.4. Cross-reference rewrites (the hard part)
At publish time the following JSON/XML payloads carry embedded references that must be rewritten from canonical → deployed:
ProductApi,ProductGroup,GatewayApiassociation entriesentry.nametoDeployedNamefor the child type (API/Group). Already loops per-entry inresource-publisher.ts:307-322— inject the mapping there.Subscription.properties.scope/apis/<apiName>or/products/<productName>normalizeSubscriptionScopepath to also affix the trailing segment.ApiRelease.properties.apiId/apis/<apiName>[;rev=N]normalizeApiReleaseApiIdto affix.sourceApiIdcredentials{{ref}}valuesrewriteNamedValueReferencesalready looks up the canonical named value. If NamedValue is in the affix set, itsdisplayNameoverride (if any) is already applied there — but if the user didn't set displayName, we must also affix it. Extend that function to affix displayName when NamedValue ∈appliesToand no explicit override was provided.{{namedValueRef}},<include-fragment fragment-id="..."/>,<send-request>back-refs, etc.properties.pathapiPathPrefixwhen present. Small, isolated change in the Api branch ofresource-publisher.ts.descriptor.workspaceand childnameParts[0](parent id)mapDescriptorhandles both segments.5. Policy XML rewriting
New util
src/services/policy-ref-rewriter.ts:Approach:
{{name}}tokens and known element/attribute patterns (<include-fragment fragment-id="...">,<set-backend-service backend-id="...">). This matches the existing "opaque payload" philosophy (FR-009) — we only touch tokens we can confidently identify.appliesTo. Unknown tokens pass through unchanged (they may be runtime context refs like{{context.request.headers}}).resource-publisher.tsbefore PUT.Risk: false-positives if a customer's XML text incidentally matches. Mitigated by (a) restricting to tokens whose base name is in the known artifact set and (b) opt-out via
appliesTonot listing that type.6.
delete-unmatched— the safety-critical pieceToday
computeDeleteActionswill happily delete a Prod resource when publishing Dev. Change:client.listResources(context, type)returns ARM (deployed) names.EnvMappingis present and this type ∈appliesTo: skip anything not in this env's namespace (isInEnvNamespace(deployedName, type, m) === false).toCanonicalName, then do the existing "is this in local artifacts?" check.mapDescriptor).This makes shared-APIM publish safe: Dev can never delete Prod because Prod resources are outside the
dev-*namespace.Edge case: if a user drops the prefix mid-life (
namePrefix: ""after previously publishing withdev-), delete-unmatched can't distinguish the two envs anymore. Document as a known constraint.7. Collision & validation checks (fail fast at publish start)
Add a pre-flight validator in
publish-service.ts:environmentpresent, warn if prefix/suffix contain characters APIM disallows in resource names (must match^[A-Za-z0-9-]+$for most types).appliesToincludes a type that can't be renamed (ServicePolicy, association children).apiPathPrefixis set but no API in the artifact set — likely a typo.8. Extract stays canonical — but add one convenience
Per the scoping assumption, extract needs no code change. Worth adding one docs section: users should extract from a dedicated non-shared APIM (or an APIM whose resources happen to have no prefix). If someone points extract at a shared APIM, they'll get all envs' resources mixed together — call this out explicitly.
Optional (Phase 3, not blocking):
apiops extract --strip-prefix dev-for teams that only have a shared APIM. Purely a name-strip pass at write time. Not required for MVP.9.
initchangesMinimal:
configuration.<env>.yamlgets a commented-outenvironment:block with prefix/suffix examples.configure-overrides-prompt.md) gains a section explaining when to enable it.apiops publish --overrides configuration.<env>.yamlcommand handles both single-APIM-per-env and shared-APIM setups.10. Testing
env-mapper.ts(round-trip: canonical → deployed → canonical for every type; unaffected types unchanged).resource-publisher.tscovering: association entries, subscription scope, apiRelease apiId, api path prefix, logger credentials.dev-fooandprod-foo— publish withnamePrefix: dev-must skipprod-foo.environmentblock.github-actions commentedon Aug 7, 2026
It will be automatically closed after 60 days of inactivity.
To keep this issue open:
stalelabel.feat: multi-environment publish to shared APIM instance