apiops-cli generates ready-to-use Azure DevOps pipelines for extracting and publishing APIM configuration. This guide walks through setup, configuration, and customization.
- An Azure API Management instance (dev and optionally prod)
- An Azure DevOps project with a Git repository for your APIM configuration
- An Azure Resource Manager service connection configured per environment
- Variable groups for common and per-environment settings
- Node.js 22.x (used in pipelines)
The fastest way to get started is with apiops init:
apiops init --ci azure-devopsThis generates:
pipelines/
├── run-extractor.yaml # Manual extract pipeline
└── run-publisher.yaml # Publish on push to main
If you also want GitHub Actions workflows, omit the
--ciflag and select interactively, or use--ci github-actions. See GitHub Actions Integration.
File: pipelines/run-extractor.yaml
Trigger: Manual (no automatic trigger — trigger: none)
The extract pipeline pulls configuration from your APIM instance, publishes the result as a pipeline artifact, and creates a branch for PR review.
| Parameter | Type | Default | Description |
|---|---|---|---|
CONFIGURATION_YAML_PATH |
string | Extract All APIs |
Choose Extract All APIs for a full extract, or configuration.extractor.yaml to use a filter file |
resourceGroup |
string | $(APIM_RESOURCE_GROUP) |
Azure resource group containing your APIM instance |
serviceName |
string | $(APIM_SERVICE_NAME) |
Name of the APIM service instance |
flowchart TD
A[Manual trigger] --> B[Setup Node.js 22.x]
B --> C[npm ci]
C --> D{Configuration choice?}
D -->|Extract All APIs| E[apiops extract --resource-group ... --service-name ...]
D -->|configuration.extractor.yaml| F[apiops extract ... --filter configuration.extractor.yaml]
E --> G[Publish pipeline artifact]
F --> G
G --> H[Create branch apim-extract-BuildId]
H --> I{Changes detected?}
I -->|Yes| J[Commit + push branch]
I -->|No| K[Log 'No changes to commit']
J --> L[Log: create a pull request]
- Node.js setup — Installs Node.js 22.x via
UseNode@1 - Install dependencies — Runs
npm ci - Run extract — Executes
apiops extractviaAzureCLI@2task, authenticating through the service connection - Publish artifacts — Uploads the artifact directory as a pipeline artifact named
apim-artifacts - Create branch — Creates a branch named
apim-extract-$(Build.BuildId), commits changes, and pushes. If no changes are detected, it logs a message and skips the commit - Suggest PR — Logs a warning message prompting you to create a pull request
The key task is AzureCLI@2, which authenticates using your service connection:
- task: AzureCLI@2
displayName: 'Run APIM Extract (All APIs)'
condition: eq('${{ parameters.CONFIGURATION_YAML_PATH }}', 'Extract All APIs')
inputs:
azureSubscription: '$(AZURE_SERVICE_CONNECTION)'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
npx apiops extract \
--resource-group ${{ parameters.resourceGroup }} \
--service-name ${{ parameters.serviceName }} \
--output ./apim-artifacts \
--subscription-id $(AZURE_SUBSCRIPTION_ID)When the filter option is selected, --filter configuration.extractor.yaml is added to the command.
Why AzureCLI@2? This task injects Azure credentials into the shell environment, allowing
apiops extractto authenticate viaDefaultAzureCredential. See Authentication Guide.
File: pipelines/run-publisher.yaml
Trigger: Push to main branch (paths: artifact directory + configuration.*.yaml) and manual
The publish pipeline deploys APIM configuration to one or more environments using a multi-stage design with environment approval gates.
trigger:
branches:
include:
- main
paths:
include:
- './apim-artifacts/**'
- 'configuration.*.yaml'
pr: noneThe pipeline runs automatically when changes to artifact files or configuration files are merged to main. Pull requests do not trigger it.
| Parameter | Type | Default | Description |
|---|---|---|---|
COMMIT_ID_CHOICE |
string | publish-artifacts-in-last-commit |
Choose publish-artifacts-in-last-commit for incremental publish, or publish-all-artifacts-in-repo for a full publish |
ENVIRONMENT |
string | dev |
Which environment to publish to (for example dev or prod) |
The pipeline generates one stage per environment. The selected stage runs based on the ENVIRONMENT parameter.
flowchart LR
A[ENVIRONMENT=dev] --> B[Publish_dev]
C[ENVIRONMENT=prod] --> D[Publish_prod]
Each stage:
- Conditionally runs — Only executes when the
ENVIRONMENTparameter matches the stage name - Uses a deployment job — Wraps the publish step in a
deploymentjob targeting an Azure DevOps environment for approval gates - Loads per-environment variables — Each stage uses its own variable group (
apim-dev,apim-prod) - Authenticates per-environment — Uses environment-specific service connections (
AZURE_SERVICE_CONNECTION_DEV,AZURE_SERVICE_CONNECTION_PROD) - Substitutes tokens — Replaces
{#[TOKEN_NAME]#}placeholders inconfiguration.<env>.yamlwith secret variable values before publishing - Runs a dry-run validation — Executes
apiops publish --dry-runto verify the publish would succeed. If this step fails, the pipeline halts and the real publish is never attempted, preventing partial failures from leaving APIM in an inconsistent state. - Applies overrides — Passes
--overrides configuration.{env}.yamlto apply environment-specific overrides
For incremental publish (default), --commit-id $(Build.SourceVersion) is passed so only resources changed in the triggering commit are published:
- task: AzureCLI@2
displayName: 'Publish to dev (incremental - last commit only)'
condition: ne('${{ parameters.COMMIT_ID_CHOICE }}', 'publish-all-artifacts-in-repo')
inputs:
azureSubscription: 'AZURE_SERVICE_CONNECTION_DEV'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
npx apiops publish \
--resource-group $(APIM_RESOURCE_GROUP_DEV) \
--service-name $(APIM_SERVICE_NAME_DEV) \
--source ./apim-artifacts \
--overrides configuration.dev.yaml \
--commit-id $(Build.SourceVersion) \
--subscription-id $(AZURE_SUBSCRIPTION_ID)For a full publish (when COMMIT_ID_CHOICE = publish-all-artifacts-in-repo), --commit-id is omitted and all artifacts are published:
- task: AzureCLI@2
displayName: 'Publish to dev (all artifacts)'
condition: eq('${{ parameters.COMMIT_ID_CHOICE }}', 'publish-all-artifacts-in-repo')
inputs:
azureSubscription: '$(AZURE_SERVICE_CONNECTION_DEV)'
# ... same as above but without --commit-idThe generated pipelines reference variable groups that you must create in Azure DevOps:
| Variable | Description |
|---|---|
AZURE_SERVICE_CONNECTION |
Name of the Azure service connection |
AZURE_SUBSCRIPTION_ID |
Azure subscription ID |
APIM_RESOURCE_GROUP |
Resource group containing the APIM instance |
APIM_SERVICE_NAME |
APIM service name |
For each environment (e.g., apim-dev, apim-prod):
| Variable | Description |
|---|---|
AZURE_SERVICE_CONNECTION_{ENV} |
Service connection for this environment (e.g., AZURE_SERVICE_CONNECTION_DEV) |
AZURE_SUBSCRIPTION_ID |
Azure subscription ID for this environment |
APIM_RESOURCE_GROUP_{ENV} |
Resource group (e.g., APIM_RESOURCE_GROUP_DEV) |
APIM_SERVICE_NAME_{ENV} |
APIM service name (e.g., APIM_SERVICE_NAME_DEV) |
To create a variable group:
- Go to Pipelines → Library in Azure DevOps
- Click + Variable group
- Name it (e.g.,
apim-dev) and add the variables above - Link it to your pipeline in the pipeline settings
Each environment needs an Azure Resource Manager service connection:
- Go to Project Settings → Service connections
- Click New service connection → Azure Resource Manager
- Choose Workload Identity federation (automatic) or Service principal (manual)
- Scope to the subscription and resource group containing the target APIM instance
- Name it to match your variable (e.g., the value of
AZURE_SERVICE_CONNECTION_DEV)
The service principal backing the connection needs these RBAC roles on the APIM instance:
| Role | When |
|---|---|
| API Management Service Reader | Extract only |
| API Management Service Contributor | Publish (create/update resources) |
See Authentication Guide — RBAC roles for details.
Azure DevOps environments provide approval gates for deployments. The publish pipeline uses deployment jobs targeting named environments:
jobs:
- deployment: Deploy
environment: prod # ← gates defined hereTo add an approval gate:
- Go to Pipelines → Environments → prod
- Click the "⋮" menu → Approvals and checks
- Add Approvals and specify required approvers
- Optionally add Business hours, Exclusive lock, or Branch control checks
This means merging to main auto-deploys to dev, but prod waits for human approval.
- Re-run
apiops init --ci azure-devops --environments dev,staging,prod(or edit the pipeline YAML manually) - Create the
apim-stagingvariable group with the required variables - Create a service connection for staging
- Create the
stagingenvironment in Azure DevOps with desired approval gates - Add a
configuration.staging.yamloverride file
apiops init --ci azure-devops --artifact-dir ./my-artifactsThe generated pipelines will reference ./my-artifacts instead of ./apim-artifacts.
Insert a step before the AzureCLI@2 publish task:
- script: npx apiops publish --dry-run --source ./apim-artifacts ...
displayName: 'Dry run validation'In your package.json, pin to a specific version:
{
"dependencies": {
"@azure-tools/apiops-cli": "1.2.3"
}
}To replace {#[TOKEN_NAME]#} placeholders in configuration.<env>.yaml with secret variable values:
- Install the Replace Tokens extension in your Azure DevOps organization (if not already installed).
You can do this via CLI:
az devops extension install --publisher-id qetza --extension-id replacetokens-
Add secret variables to the
apim-<env>variable group. See the Azure DevOps documentation for adding variables to a variable group and marking variables as secret.For example, to substitute
{#[BACKEND_URL]#}in your configuration file:configuration.prod.yaml:backends: - name: my-backend properties: url: "{#[BACKEND_URL]#}"
Add
BACKEND_URLas a secret variable in theapim-prodvariable group with the actual backend URL as the value. -
The substitution step runs automatically before publish.
See the Token Substitution Guide for full details, including migration from APIOps Toolkit.
| Symptom | Cause | Fix |
|---|---|---|
AzureCLI@2 fails with "service connection not found" |
Variable group not linked or variable name mismatch | Verify the variable group is linked to the pipeline and AZURE_SERVICE_CONNECTION is defined |
| Extract shows "No changes to commit" | APIM config hasn't changed since last extract | Expected behavior — no branch is created |
| Publish stage is skipped | ENVIRONMENT parameter doesn't match the stage |
Set ENVIRONMENT to the specific stage name (for example dev or prod) |
npm ci fails |
package.json or package-lock.json missing |
Run apiops init to generate project files, then commit them |
| "publish-all-artifacts-in-repo" deploys everything | Expected — this mode publishes all artifacts, ignoring git diff | Use publish-artifacts-in-last-commit (default) for incremental |
| Approval gate blocks deployment | Environment checks configured | Approve in Pipelines → Environments → {env} |
| Run is stuck with "This pipeline needs permission to access a resource" | Environment resource isn't authorized for pipeline use | Authorize the environment in Azure DevOps or run the prompt step that PATCHes pipelinePermissions/environment/{id} with {"allPipelines":{"authorized":true}} |
--subscription-id error |
AZURE_SUBSCRIPTION_ID not set in variable group |
Add it to the relevant variable group |
- GitHub Actions Integration — alternative CI/CD platform
- apiops init — generates pipeline files
- apiops extract — extract command reference
- apiops publish — publish command reference
- Authentication Guide — auth methods and RBAC
- Environment Overrides — per-environment configuration
- Token Substitution — pipeline placeholder substitution with
{#[TOKEN_NAME]#} - Filtering Resources — extract specific APIs