🪟 Windows TippsHandy zu langsam? Diese Einstellungen kosten unnötig Leistung(16.09.2026 um 15:30 Uhr)
🪟 Windows TippsUmrüstung der Beleuchtung der Bundespressekonferenz auf LED(16.09.2026 um 15:36 Uhr)
🤖 Android TippsUmrüstung der Beleuchtung der Bundespressekonferenz auf LED(16.09.2026 um 15:36 Uhr)
🔧 ProgrammierungRobot Fleet Management Software: A Complete Guide(16.09.2026 um 15:24 Uhr)
🕵️ SicherheitslückenKnown MCP Vulnerabilities and How an MCP Gateway Blocks Them(16.09.2026 um 15:21 Uhr)
🪟 Windows TippsHandy zu langsam? Diese Einstellungen kosten unnötig Leistung(16.09.2026 um 15:30 Uhr)
🪟 Windows TippsUmrüstung der Beleuchtung der Bundespressekonferenz auf LED(16.09.2026 um 15:36 Uhr)
🤖 Android TippsUmrüstung der Beleuchtung der Bundespressekonferenz auf LED(16.09.2026 um 15:36 Uhr)
🔧 ProgrammierungRobot Fleet Management Software: A Complete Guide(16.09.2026 um 15:24 Uhr)
🕵️ SicherheitslückenKnown MCP Vulnerabilities and How an MCP Gateway Blocks Them(16.09.2026 um 15:21 Uhr)

🔧 Programmierung 🕛 vor 2 Monaten 11 Min Lesezeit
0

From 15+ Manual Deployments to 2 YAML Templates: Scaling Azure DevOps Pipelines for IIS-Hosted Apps

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht




The Problem



I manage 8 web applications hosted on a single Windows VM running IIS. Each application has 2–3 deployable components:




  • An Angular frontend

  • A .NET backend API

  • Often a separate integration API



That's roughly 20 deployable units on one server. For years, every deployment was manual — build locally or on the server, copy files over, restart IIS, pray.



When I finally decided to automate with Azure DevOps pipelines, I hit the obvious trap immediately: the naive approach means one pipeline per component. That's 20 pipelines today — and every time a new application is built (which happens regularly), I'd be creating 2–3 more. Each pipeline would duplicate the same build/copy/restart logic. Fixing a deployment bug would mean fixing it 20 times.



On top of the pipeline-count problem, the manual process had three landmines baked into it:





  1. iisreset kills everything. The old scripts restarted the entire IIS server to deploy one app — taking down all 8 applications for every single deployment.


  2. Config file overwrites. Each app has server-side config (appsettings.json for .NET, assets/config.json for Angular) that must never be replaced by whatever is in source control. One careless copy and production points at the wrong database.


  3. No consistency. Every app had slightly different variable names, folder conventions, and deploy steps. Nothing was reusable; everything was tribal knowledge.



So the real requirement wasn't "create pipelines." It was:




Build a deployment system where adding a new application requires zero new pipeline logic.










The Solution: Template-Based Pipeline Architecture



Azure DevOps supports YAML templates — reusable pipeline definitions that live in one repository and are referenced by other repositories. That's the entire foundation of this design.






The architecture






CODE
flowchart TD
subgraph TPL["📦 pipeline-templates repo (logic lives here, ONCE)"]
T1["build-deploy-dotnet.yml"]
T2["build-deploy-angular.yml"]
end

subgraph APPS["Application repos (thin callers, ~20 lines each)"]
A1["Portal<br/>azure-pipelines.yml"]
A2["Orders<br/>azure-pipelines.yml"]
A3["Ops<br/>azure-pipelines.yml"]
A4["Billing / Reports / Notify / ...<br/>azure-pipelines.yml"]
end

subgraph LIB["🔑 Variable groups (one per app)"]
V1["PORTAL-PROD"]
V2["ORDERS-PROD"]
V3["OPS-PROD"]
V4["..."]
end

A1 -->|references| TPL
A2 -->|references| TPL
A3 -->|references| TPL
A4 -->|references| TPL
A1 -.->|reads paths from| V1
A2 -.->|reads paths from| V2
A3 -.->|reads paths from| V3
A4 -.->|reads paths from| V4

TPL ==>|deploys via self-hosted agent| VM["🖥️ Windows VM — IIS<br/>(all 8 apps)"]






Three building blocks:



1. One shared pipeline-templates repository. It contains exactly two templates:




















Template Handles
build-deploy-dotnet.yml Restore → build → publish → protect configs → stop app pool → robocopy deploy → start app pool
build-deploy-angular.yml Node setup → npm cing build → protect configs → robocopy deploy → recycle app pool


2. A thin azure-pipelines.yml in each app repo. ~20 lines. It declares the templates repo as a resource, points at the app's variable group, and passes a handful of parameters (app pool name, artifact name). It contains zero deployment logic.




CODE
trigger: none   # manual, deliberate deployments only

resources:
repositories:
- repository: templates
type: git
name: pipeline-templates

parameters:
- name: deployBackend
type: boolean
default: false
- name: deployFrontend
type: boolean
default: false

variables:
- group: PORTAL-PROD

stages:
- ${{ if eq(parameters.deployBackend, true) }}:
- template: templates/build-deploy-dotnet.yml@templates
parameters:
artifactName: Portal_API_Drop
appPoolName: PortalAPIService
- ${{ if eq(parameters.deployFrontend, true) }}:
- template: templates/build-deploy-angular.yml@templates
parameters:
artifactName: Portal_Web_Drop
appPoolName: PortalFrontEnd






The checkbox parameters (deployBackend, deployFrontend, deployIntegration) mean one pipeline per app covers all its components — the person running it just ticks what they want deployed.



3. Standardized variable groups. Every app gets its own variable group (PORTAL-PROD, ORDERS-PROD, …) — but the variable names inside are identical across all apps. Only the values differ:




CODE
BACKEND_SOURCE_PATH        (where the .sln lives in the repo)
BACKEND_DEPLOYMENT_PATH (C:\inetpub\wwwroot\<App>\...)
FRONTEND_SOURCE_PATH
FRONTEND_DIST_PATH
FRONTEND_TARGET_PATH
NODE_VERSION
AGENT_POOL_NAME / AGENT_NAME
INTEGRATION_* (only if the app has one)






This is the trick that makes templates truly generic: the app's identity lives in the group name, never in the variable names. The template just reads $(BACKEND_DEPLOYMENT_PATH) and works for any app.









The Deployment Flow



Here's what one deployment actually does, end to end:




CODE
flowchart TD
A["▶️ Run pipeline manually<br/>(tick Backend / Frontend / Integration)"] --> B["Checkout app repo +<br/>templates repo"]
B --> C["Build<br/>(dotnet publish / ng build)"]
C --> D["🛡️ Layer 1: delete protected configs<br/>from BUILD OUTPUT<br/>(appsettings.json, assets/config.json)"]
D --> E["Publish pipeline artifact"]
E --> F["⏸️ Stop ONLY this app's<br/>IIS app pool"]
F --> G["🛡️ Layer 2: robocopy /XF<br/>excludes protected files<br/>during copy"]
G --> H{"robocopy<br/>exit code < 8?"}
H -->|yes| I["▶️ Start app pool"]
H -->|no| J["❌ Fail the pipeline<br/>(pool restarted, investigate)"]
I --> K["✅ App live —<br/>other 7 apps never blinked"]






Two design decisions here are worth calling out because they came directly from the failure modes of the manual era:






App-pool-level recycle, not iisreset



The template stops and starts only the app pool of the component being deployed. The other applications on the VM stay up throughout. Deploying Portal no longer causes an outage for Orders, Billing, Reports, and everyone else. This alone changed deployments from "schedule it after hours" to "just deploy it."






Config protection at two independent layers



Server-side config files are protected with defense in depth:





  1. Layer 1 — build output cleanup: protected files are deleted from the build output before anything is copied. If it's not in the artifact, it can't overwrite anything.


  2. Layer 2 — copy-time exclusion: the deploy uses robocopy with the /XF flag listing protected filenames, so even if layer 1 misses something, the copy itself refuses to touch appsettings.json or config.json at the destination.



Either layer failing alone still leaves production config intact. Both would have to fail simultaneously — and the post-deploy verification step is simply checking the config file's Date Modified is old.



Why robocopy over xcopy? Reliable retry behavior (/R:2 /W:5), sane exit codes you can gate on, and the /XF exclusion list lives visibly in the pipeline YAML instead of in a mystery exclude-file sitting somewhere on the server.









The Rollout: Pilot → Validate → Replicate



Rolling out an unproven template to 8 production apps simultaneously is how you take prod down 8 times in one afternoon. Instead:




CODE
flowchart LR
P["🧪 Pilot: Portal<br/>(prove the templates)"] --> V["Validate<br/>configs untouched?<br/>only its pool recycled?<br/>app responds?"]
V --> R["🏭 Assembly line:<br/>Ops → Reports →<br/>Notify → Orders → Billing →<br/>Gateway"]
R --> S["All pipelines registered<br/>with SAVE only —<br/>team runs them deliberately"]








  • Portal was the pilot — including its separate integration-API repo (which gets its own thin caller file, same templates).

  • Any template bug found during the pilot gets fixed once, and every future app inherits the fix before it even onboards.

  • Every subsequent app was pure assembly line: clone the variable group, update values, drop in the thin YAML, register the pipeline with Save (not Run). Nothing touches production until someone deliberately runs it, one component at a time, simplest apps first.



Onboarding time per app after the pilot: ~10 minutes.









Real-World Gotchas (the stuff no tutorial mentions)



Angular 20's application builder changes the dist path. Modern Angular builds output to dist/<project>/browser — that extra /browser folder broke the copy path until FRONTEND_DIST_PATH accounted for it. Older Angular apps output straight to dist/<project> or plain dist. This is exactly why dist path is a per-app variable, not template logic.



Variable group names must match the YAML exactly. The #1 source of "variable group could not be found" errors was a mismatch between - group: PORTAL-PROD in YAML and the actual group name in the Library (including accidental suffixes). Character-for-character.



Variable group pipeline permissions. New pipelines fail with authorization errors until the group grants access. During rollout, setting the group's pipeline permissions to Open access (fine when the group holds no secrets — ours hold only paths) removes the friction; you can tighten to per-pipeline authorization later.



Nested and non-standard repo layouts. One app had its Angular project buried at Frontend/Client/<AppName>; another had a frontend-only repo; one had suspicious folder names on the server (...Working) left over from manual-deploy days. Templates survive all of this because paths are data, not logic — you verify what IIS actually points at, put the truth in the variable group, done.



robocopy /XF matches filenames anywhere in the tree. Excluding config.json protects assets/config.json but would also skip any other config.json in the build. For config-protection purposes that's exactly the behavior you want — just know it.









How Generic Is This, Really?



Very. Nothing in this design is specific to my apps. The abstraction boundary is clean:
































Lives in templates (shared, fixed) Lives in variable group / parameters (per app)
Build steps for a .NET API Source path, .NET version
Build steps for an Angular app Node version, dist path
Config protection (both layers) Which files are protected (parameter)
App pool stop/copy/start sequence App pool name
robocopy flags, error gating Source and target paths


So this pattern transfers directly if you have:





  • Any number of apps on shared IIS servers — the pool-level recycle is what makes multi-tenant VMs safe to deploy to.


  • Different stacks — a build-deploy-node.yml or build-deploy-python.yml template slots in beside the existing two; app repos just reference a different template.


  • Multiple environments — the same templates work for staging/UAT: create PORTAL-UAT variable groups pointing at different paths/servers and pass a different agent. The template never changes.


  • Multiple servers — the agent name/pool is a variable too. New VM = new agent registration + new values in the groups.



The invariant that makes it all work: templates contain behavior, variable groups contain facts. As long as you never let an app-specific fact leak into a template, the system scales linearly with zero added complexity.









What's Next



The system is live but young. The roadmap:





  1. Finish onboarding the last apps — a couple were deferred (unclear IIS pool mappings needed verification first). They're a 10-minute job each once verified.


  2. Run-and-validate pass — pipelines were registered Save-only; each needs its first deliberate run with the post-deploy checklist (config timestamps unchanged, neighbors still up, endpoint responds).


  3. CI triggers — everything is manual today (trigger: none) by design. Once trust is earned, branch-based triggers for lower environments are a one-line change in the thin YAML.


  4. Approval gates — ADO Environments support manual approval checks before prod deploys, with no extra pipeline logic.


  5. Smarter rollback — a backup/restore step existed in early versions and was removed for speed; a versioned-artifact rollback (redeploy the previous pipeline artifact) is the cleaner long-term answer.


  6. Secrets to Key Vault — variable groups currently hold only paths; when secrets enter the picture, they'll link to Azure Key Vault rather than living in the Library.


  7. Deployment visibility — an executive dashboard over the ADO REST APIs (build runs, PR activity, failure reasons) is already in progress, closing the loop from "we deploy reliably" to "leadership can see it."









Takeaways





  • Don't count pipelines — count places logic lives. 8 apps, 20 deployables, and the deployment logic exists in exactly 2 files.


  • Standardize variable names, vary the values. That's the entire trick behind generic templates.


  • Protect production config with two independent layers. One layer is a hope; two is a design.


  • Recycle app pools, never the server. Shared IIS VMs and iisreset don't mix.


  • Pilot on one app before replicating. Fix template bugs once, inherit fixes everywhere.


  • Register with Save, run deliberately. Automation should reduce risk, not add surprise.



Onboarding the next application my team builds will take 10 minutes and zero new pipeline logic. That was the whole point.






Built with Azure DevOps YAML templates, variable groups, a self-hosted agent, robocopy, and a healthy fear of iisreset.

Vollständiger Original-Artikel
Den kompletten Beitrag mit allen Details direkt auf dev.to lesen.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
Shipping a Self-Contained macOS App: How 24 Homebrew dylibs Broke My DeepSeek Harness Installer
1 Quelle
SK Hynix reportedly in talks with Intel to build memory chips in US
1 Quelle
What AI Can Teach Us About Being Human
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten From 15+ Manual Deployments to 2 YAML Templates: Scaling Azure DevOps Pipelines for IIS-Hosted Apps

Thematisch verwandte Begriffe: From, Manual, Deployments, YAML · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...