|
| 1 | +# Air-Gapped Setup: Azure DevOps — Local npm Registry |
| 2 | + |
| 3 | +Deploy APIM configuration using apiops-cli on [self-hosted Azure Pipelines agents](https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/agents?view=azure-devops#self-hosted-agents) with **no internet access** at runtime. This walkthrough uses an Azure Artifacts npm feed as the package source so agents never reach the public npm registry. |
| 4 | + |
| 5 | +> Looking for the alternative that doesn't require a registry? See [Offline Tarball walkthrough](air-gapped-azure-devops-offline-tarball.md). |
| 6 | +
|
| 7 | +--- |
| 8 | + |
| 9 | +## When to Use This Guide |
| 10 | + |
| 11 | +- Self-hosted agents in a private network with no outbound internet |
| 12 | +- You can host (or already have) an Azure Artifacts npm feed reachable from the agent |
| 13 | +- Corporate networks that block access to the public npm registry |
| 14 | + |
| 15 | +--- |
| 16 | + |
| 17 | +## Architecture Overview |
| 18 | + |
| 19 | +```mermaid |
| 20 | +flowchart LR |
| 21 | + subgraph Connected Zone |
| 22 | + A[npm install from public registry] --> B[Azure Artifacts feed] |
| 23 | + end |
| 24 | + subgraph Air-Gapped Network |
| 25 | + B -->|Controlled sync window| C[Local feed replica] |
| 26 | + C --> D[Self-hosted Agent] |
| 27 | + D --> E[npm ci] |
| 28 | + E --> F[apiops extract / publish] |
| 29 | + end |
| 30 | +``` |
| 31 | + |
| 32 | +--- |
| 33 | + |
| 34 | +## Prerequisites |
| 35 | + |
| 36 | +| Requirement | Details | |
| 37 | +|-------------|---------| |
| 38 | +| **Connected workstation** | A machine with internet access to seed the feed | |
| 39 | +| **Node.js 22.x** | Installed on both the workstation and the agent (includes npm) | |
| 40 | +| **[Self-hosted Azure Pipelines agent](https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/agents?view=azure-devops#self-hosted-agents)** | Registered in your agent pool, running in the air-gapped network | |
| 41 | +| **Azure connectivity from agent** | The agent must reach your APIM instance's ARM endpoint (network-level, not npm) | |
| 42 | +| **Local npm registry** | An [Azure Artifacts npm feed](https://learn.microsoft.com/en-us/azure/devops/artifacts/npm/npmrc?view=azure-devops) accessible from the air-gapped network | |
| 43 | + |
| 44 | +> **On-premises Azure DevOps:** If you run [Azure DevOps Server](https://learn.microsoft.com/en-us/azure/devops/server/install/get-started?view=azure-devops-2022) (formerly TFS), the same approach applies — Azure Artifacts is included in the server installation. |
| 45 | +
|
| 46 | +--- |
| 47 | + |
| 48 | +## Step 1 — Configure the Azure Artifacts npm Feed |
| 49 | + |
| 50 | +Set up an Azure Artifacts npm feed that serves packages to your air-gapped agents without requiring internet access at install time. |
| 51 | + |
| 52 | +1. **[Create a new feed](https://learn.microsoft.com/en-us/azure/devops/artifacts/get-started-npm?view=azure-devops#create-a-feed)** in your Azure DevOps organization. |
| 53 | +2. **[Configure an upstream source](https://learn.microsoft.com/en-us/azure/devops/artifacts/how-to/set-up-upstream-sources?view=azure-devops)** pointing to `https://registry.npmjs.org`. The upstream is only used during controlled sync windows; once `@peterhauge/apiops-cli` and its dependencies are cached, the feed serves them locally. |
| 54 | +3. **[Populate the feed](https://learn.microsoft.com/en-us/azure/devops/artifacts/npm/npmrc?view=azure-devops)** from a connected workstation by running `npm install @peterhauge/apiops-cli` against the feed registry URL. This pulls the package and its transitive dependencies into the feed cache. |
| 55 | +4. **[Add a project `.npmrc`](https://learn.microsoft.com/en-us/azure/devops/artifacts/npm/npmrc?view=azure-devops)** that points `registry=` at your feed URL and sets `always-auth=true`. Commit this file so pipelines and developers resolve against the local feed. |
| 56 | + |
| 57 | +> **Tip:** Use the **Connect to feed** button in the Azure Artifacts UI to get the exact registry URL and a ready-to-copy `.npmrc` snippet for your feed. |
| 58 | +
|
| 59 | +--- |
| 60 | + |
| 61 | +## Step 2 — Initialize the Repository |
| 62 | + |
| 63 | +```bash |
| 64 | +apiops init \ |
| 65 | + --ci azure-devops \ |
| 66 | + --environments dev,prod \ |
| 67 | + --non-interactive |
| 68 | +``` |
| 69 | + |
| 70 | +This generates: |
| 71 | + |
| 72 | +| File | Purpose | |
| 73 | +|------|---------| |
| 74 | +| `package.json` | Declares the CLI as a dependency | |
| 75 | +| `pipelines/run-extractor.yaml` | Extract pipeline | |
| 76 | +| `pipelines/run-publisher.yaml` | Publish pipeline | |
| 77 | +| `configuration.*.yaml` | Override templates | |
| 78 | + |
| 79 | +--- |
| 80 | + |
| 81 | +## Step 3 — Generate the Lock File |
| 82 | + |
| 83 | +```bash |
| 84 | +npm install |
| 85 | +``` |
| 86 | + |
| 87 | +This creates `package-lock.json`. Commit it — the lock file is **required** for `npm ci` to work. |
| 88 | + |
| 89 | +--- |
| 90 | + |
| 91 | +## Step 4 — Configure the Self-Hosted Agent |
| 92 | + |
| 93 | +Install and register the agent in the air-gapped network per the [self-hosted agent documentation](https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/linux-agent?view=azure-devops): |
| 94 | + |
| 95 | +Ensure: |
| 96 | + |
| 97 | +1. **Node.js 22.x** is installed and on `PATH` |
| 98 | +2. **Network access to the Azure Artifacts feed** — the agent can resolve packages from the local feed |
| 99 | +3. **Network access to Azure ARM** — the agent must reach `management.azure.com` (or [sovereign cloud equivalent](https://learn.microsoft.com/en-us/azure/developer/identity/national-cloud)) |
| 100 | +4. **Network access to Azure DevOps** — the agent must reach your Azure DevOps org for job dispatch |
| 101 | +5. **Git** is installed (required by the `checkout` step) |
| 102 | + |
| 103 | +> **Agent pool:** Add your air-gapped agents to a [dedicated agent pool](https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/pools-queues?view=azure-devops) (e.g., `air-gapped-pool`) so pipelines target them explicitly. |
| 104 | +
|
| 105 | +--- |
| 106 | + |
| 107 | +## Step 5 — Modify Pipelines for Air-Gapped Operation |
| 108 | + |
| 109 | +The generated pipelines (`pipelines/run-extractor.yaml` and `pipelines/run-publisher.yaml`) need minimal edits: |
| 110 | + |
| 111 | +| Edit | What to Change | |
| 112 | +|------|----------------| |
| 113 | +| **Agent pool** | Replace `pool: vmImage: ubuntu-latest` with `pool: name: air-gapped-pool` ([pool YAML schema](https://learn.microsoft.com/en-us/azure/devops/pipelines/yaml-schema/pool?view=azure-pipelines)) | |
| 114 | +| **Remove NodeTool task** | Delete the `NodeTool@0` step (Node.js is pre-installed on the agent) | |
| 115 | +| **Add feed auth** | Insert [`npmAuthenticate@0`](https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/npm-authenticate-v0?view=azure-pipelines) before the `npm ci` step (see below) | |
| 116 | + |
| 117 | +### Feed Authentication |
| 118 | + |
| 119 | +Add this task before any `npm ci` step in both pipelines: |
| 120 | + |
| 121 | +```yaml |
| 122 | +- task: npmAuthenticate@0 |
| 123 | + inputs: |
| 124 | + workingFile: .npmrc |
| 125 | +``` |
| 126 | +
|
| 127 | +`npmAuthenticate@0` reads the committed `.npmrc` and injects credentials for the Azure Artifacts feed using the pipeline's built-in build service identity — no service connection needed for feeds in the same organization. The `npm ci` step then works as-is. |
| 128 | + |
| 129 | +> **Azure authentication:** The `AzureCLI@2` task handles Azure authentication separately, via the service connection. The service connection injects tokens for `DefaultAzureCredential`. |
| 130 | + |
| 131 | +--- |
| 132 | + |
| 133 | +## Step 6 — Configure Variable Groups and Service Connections |
| 134 | + |
| 135 | +Follow the standard [Azure DevOps integration guide](../ci-cd/azure-devops.md#variable-groups-configuration) to set up: |
| 136 | + |
| 137 | +1. **Variable group `apim-common`** — variables shared across all environments (e.g., subscription ID, tenant ID, common tags) |
| 138 | +2. **Variable groups `apim-dev`, `apim-prod`** — environment-specific variables (e.g., APIM instance name, resource group) that override values from `apim-common` |
| 139 | +3. **Service connections** — Azure Resource Manager connections scoped to your APIM instances; they can be referenced from either variable group |
| 140 | + |
| 141 | +These are configured in Azure DevOps and are injected at pipeline runtime. |
| 142 | + |
| 143 | +--- |
| 144 | + |
| 145 | +## Step 7 — Commit and Validate |
| 146 | + |
| 147 | +```bash |
| 148 | +git add . |
| 149 | +git commit -m "feat: air-gapped apiops setup with local registry" |
| 150 | +git push |
| 151 | +``` |
| 152 | + |
| 153 | +Trigger the extract pipeline manually from **Pipelines → Run pipeline** and verify: |
| 154 | + |
| 155 | +1. `npm ci` resolves all packages from the local Azure Artifacts feed (no calls to npmjs.org) |
| 156 | +2. `apiops extract` authenticates via the service connection and runs successfully |
| 157 | + |
| 158 | +> **✅ Setup complete.** Your air-gapped apiops pipelines are now operational. The remaining sections cover ongoing maintenance and reference material — read them as needed. |
| 159 | + |
| 160 | +--- |
| 161 | + |
| 162 | +## Upgrading the CLI Version |
| 163 | + |
| 164 | +Sync the feed during a connectivity window to pull the new version, then update `package.json` and regenerate `package-lock.json`. Commit both. |
| 165 | + |
| 166 | +--- |
| 167 | + |
| 168 | +## Troubleshooting |
| 169 | + |
| 170 | +| Problem | Cause | Fix | |
| 171 | +|---------|-------|-----| |
| 172 | +| `npm ci` fails with `E404` | Package not in local feed | Sync the feed during a connectivity window | |
| 173 | +| `npm ci` fails with "lockfile mismatch" | `package-lock.json` out of sync with `package.json` | Re-run `npm install` on connected workstation, commit updated lock file | |
| 174 | +| `npx apiops` not found | `npm ci` didn't complete or `.bin` not in PATH | Verify `node_modules/.bin/apiops` exists after install | |
| 175 | +| Azure auth fails | Agent can't reach Entra ID or ARM endpoint | Verify network allows traffic to `login.microsoftonline.com` and `management.azure.com` (or sovereign equivalents) | |
| 176 | +| `AzureCLI@2` service connection error | Service connection not linked or misconfigured | Verify variable group is linked to pipeline and connection name matches | |
| 177 | +| Agent not picking up jobs | Pool name mismatch or agent offline | Confirm pool name in YAML matches the registered agent pool | |
| 178 | +| `npmAuthenticate@0` fails | Feed permissions or `.npmrc` path wrong | Ensure the build service identity has Reader access to the feed | |
| 179 | + |
| 180 | +--- |
| 181 | + |
| 182 | +## Further Reading |
| 183 | + |
| 184 | +- [Offline Tarball walkthrough](air-gapped-azure-devops-offline-tarball.md) — alternative for environments without a registry |
| 185 | +- [apiops init reference](../commands/init.md) |
| 186 | +- [Azure DevOps integration](../ci-cd/azure-devops.md) — standard (connected) setup |
| 187 | +- [Authentication guide](../guides/authentication.md) — service principal and managed identity options |
| 188 | +- [Azure Artifacts npm feeds](https://learn.microsoft.com/en-us/azure/devops/artifacts/npm/npmrc?view=azure-devops) — official feed setup docs |
| 189 | +- [Self-hosted agents](https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/agents?view=azure-devops#self-hosted-agents) — agent installation and configuration |
| 190 | +- [Azure DevOps Server](https://learn.microsoft.com/en-us/azure/devops/server/install/get-started?view=azure-devops-2022) — on-premises installation |
| 191 | +- [National cloud endpoints](https://learn.microsoft.com/en-us/azure/developer/identity/national-cloud) — sovereign cloud identity configuration |
| 192 | +- [Entra ID authentication endpoints](https://learn.microsoft.com/en-us/azure/developer/identity/national-cloud#azure-ad-authentication-endpoints) — per-cloud token acquisition endpoints |
0 commit comments