It’s 3:14 AM. The production hotfix just failed because a self-hosted agent ran out of disk space on its C: drive, and the cleanup script I wrote three months ago was silently killed by a Group Policy update no one told me about. If you’re reading this, you’re my replacement. I’m gone. I’ve traded my laptop for a cabin in the woods where the only “pipeline” is the one carrying greywater away from my sink.
You probably think you’re here to “architect solutions.” You’re not. You’re here to be a glorified plumber for a leaky, pressurized system of YAML files and expired secrets. People here treat CI/CD like magic. They think code just “happens” to end up in production. They don’t see the duct tape. They don’t see the 401 Unauthorized errors that haunt my dreams.
Welcome to the “azure devops” environment. It is a house of cards built on a swamp.
Table of Contents
Warning to my Successor
The current state of our “azure devops” organization is “precarious.” That is the polite word. The real word involves more profanity. We have seventeen different projects, and not one of them follows the same branching strategy. I tried to enforce a standard. I failed. The developers complained that Branch Policies were “slowing them down,” so leadership let them bypass the “Minimum number of reviewers” check. Now, we have junior devs pushing directly to main because they “needed to test something.”
The service connections are a minefield. Half of them use Service Principals that are set to expire every 90 days because the Security Team thinks that makes us “safe.” It doesn’t. It just means that every three months, the entire deployment pipeline breaks, and I have to spend six hours rotating keys and updating Variable Groups.
Don’t trust the UI. The “azure devops” web portal is a liar. It will tell you a stage succeeded when it actually timed out and failed to report the status. Always check the raw logs. If the logs are truncated, God help you.
The YAML Hellscape We Call Pipelines
We moved to YAML pipelines because “Configuration as Code” was supposed to be the savior. It wasn’t. It just moved the bugs from a GUI we could see to a text file we can’t debug without committing twenty times. You will spend 40% of your life writing [skip ci] in commit messages just trying to get a single environment variable to pass from Stage A to Stage B.
Here is a snippet of the azure-pipelines.yml that currently handles our core infrastructure. It’s brittle. If you touch the indentation, it will break. If you look at it wrong, it will break.
trigger:
- main
pool:
vmImage: 'ubuntu-22.04'
variables:
- group: 'Global-Infra-Secrets'
- name: terraformVersion
value: '1.5.0'
stages:
- stage: Provision
jobs:
- job: ARM_Deploy
steps:
- checkout: self
- task: AzureResourceManagerTemplateDeployment@3
displayName: 'Deploy Core Infrastructure'
inputs:
deploymentScope: 'Resource Group'
azureResourceManagerConnection: 'SC-Azure-Production'
subscriptionId: '00000000-0000-0000-0000-000000000000'
action: 'Create Or Update Resource Group'
resourceGroupName: 'rg-prod-app-001'
location: 'East US'
templateLocation: 'Linked artifact'
csmFile: '$(Build.SourcesDirectory)/arm/main.json'
csmParametersFile: '$(Build.SourcesDirectory)/arm/main.parameters.json'
overrideParameters: '-environment prod -dbPassword $(DB_PASS_SECRET)'
deploymentMode: 'Incremental'
Notice the AzureResourceManagerTemplateDeployment@3 task. It’s using deploymentMode: 'Incremental'. Never, under any circumstances, change that to Complete. I did that once in the staging environment, and it deleted the entire Virtual Network because the ARM template didn’t explicitly define the subnets that were created manually by the networking team.
Also, pay attention to the vmImage: 'ubuntu-22.04'. We had to pin that because the ‘latest’ image updated to a version of Python that broke our legacy deployment scripts. If you try to upgrade this, the pip install commands will fail with a PEP 668 error about externally managed environments. You’ve been warned.
Why Our Service Connections Are a Security Nightmare
The “azure devops” service connections are managed by a group of people who don’t understand IAM roles. Most of these connections have “Contributor” access at the Subscription level. It’s a disaster waiting to happen.
The biggest headache you will face is the Project Collection Build Service account. This is the “ghost” user that actually runs your pipelines. When a pipeline fails with a cryptic error about “Permissions,” it’s usually because this service account doesn’t have Get and List permissions on the Key Vault, even though the Service Connection itself does.
You will see this error a lot:
Error: The user, group or application 'Project Collection Build Service (OrgName)' does not have secrets get permission on key vault 'kv-prod-secrets'.
When this happens, don’t bother using the “azure devops” UI to fix it. It won’t work. You have to go into the Azure Portal, find the Key Vault, and manually add an Access Policy for that specific GUID. And no, the GUID isn’t the same across projects.
The Ghost in the Machine: Self-hosted Agents
We have six Self-hosted Agents running on a set of aging Windows Server 2019 VMs in the basement (metaphorically, they’re in a restricted VNET). We use these because the Security Team won’t allow Microsoft-hosted agents to talk to our internal SQL Servers.
These agents are “pets,” not “cattle.” They have specific versions of the .NET SDK, Java, and a weird Oracle client installed that no one knows how to replicate. If one of these VMs dies, the “Legacy-Payment-Gateway” stops deploying.
The _work folder on these agents fills up with garbage every three days. I wrote a PowerShell script to clean it, but it frequently fails because some node_modules folder has a path longer than 260 characters, and Remove-Item just gives up.
If you need to debug them, don’t use the “azure devops” agent pool UI. It will show them as “Online” even if the service is hung. Log into the box and check the _diag folder. The logs there are the only source of truth.
The REST API and the 1MB JSON Limit
At some point, you’ll realize the “azure devops” CLI (az devops) is just a wrapper for a REST API that was designed by someone who hates developers. I tried to automate the creation of 50+ repositories using a script. It failed because the JSON payload for the “Create Project” call was slightly too large, and the API returned a 413 Request Entity Too Large error.
The API documentation is often wrong or outdated. For example, if you want to update a pipeline definition via the API, you have to send the entire JSON object back, including the parts you didn’t change. If you miss one field—like the queue object—the API will just null it out without telling you, and your pipeline will never run again.
Here is what happens when you try to use the CLI with an expired PAT:
$ az devops pipeline list --project "Core-Services"
Failed to authenticate using the provided Personal Access Token.
Check if the token is expired or if it has the correct scopes.
Error: 401 Unauthorized
$ # I just updated this token yesterday.
$ # Turns out, the 'Project Collection Service Accounts' group
$ # was removed from the 'Readers' role by a 'security audit' script.
Multi-stage Pipeline Artifacts: The “Where’s my zip file?” Mystery
This is the most common support ticket you will get. A developer will say, “I built the artifact in Stage 1, but Stage 2 says it can’t find it.”
In “azure devops”, artifacts don’t just “follow” you. You have to explicitly download them. But even then, it fails. Why? Because the DownloadPipelineArtifact@2 task defaults to downloading to $(Pipeline.Workspace). If your next task looks in $(Build.ArtifactStagingDirectory), it will find nothing.
Here is the exact sequence of failure you will see in the logs:
1. Stage 1: PublishPipelineArtifact@1 – “Artifact ‘drop’ published successfully.”
2. Stage 2: DownloadPipelineArtifact@2 – “Download successful.”
3. Stage 2: AzureWebApp@1 – “Error: File not found: D:\a\1\a\app.zip”
The fix is always the same: you have to explain to the developer, for the tenth time, that D:\a\1\a is not the same as D:\a\1.
And don’t get me started on Deployment Groups. They are deprecated in favor of “Environments,” but we still have 400 servers using the old agent.exe because moving them would require a reboot, and the Business doesn’t “do” reboots.
Variable Groups and the Key Vault Integration Lie
We use Variable Groups linked to Azure Key Vault. It sounds great on paper. In reality, it’s a synchronization nightmare. If you add a new secret to Key Vault, it doesn’t automatically show up in the Variable Group. You have to go into the “azure devops” UI, open the Variable Group, click “Add,” and then check the box for the new secret.
If you forget this step, your pipeline will fail with a “Variable not found” error, but it won’t tell you which variable. It will just output *** in the logs because “azure devops” is “protecting” you by masking the null value.
Also, if a secret in Key Vault has a trailing newline character (which happens if you upload it via some CLI tools), the pipeline will include that newline in the variable. This will break your connection strings, and you will spend three days wondering why the database password is “wrong” even though it looks correct in the portal.
Branch Policies and the Human Element
The “azure devops” Branch Policies are the only thing standing between us and total annihilation. We have them set to require a successful build before merging. This is good. What is bad is that the build takes 45 minutes because our test suite was written in 2014 and requires a full SQL Server instance to run.
Developers will ask you to “just disable the check for this one PR.” Don’t do it. If you do, they will merge a change that breaks the azure-pipelines.yml parser, and then no one can merge anything until you manually fix the YAML in the web editor like a caveman.
I once saw a developer try to “optimize” the pipeline by removing the checkout: self step. They wondered why the ls command failed. These are the people you are supporting.
The REST API and Large JSON Payloads
If you ever have to deal with the “azure devops” REST API for bulk updates, be prepared for the Project Collection Build Service permissions to bite you again. Specifically, when calling the _apis/build/definitions endpoint.
If you send a payload larger than 1MB—which is easy to do if you have a complex multi-stage pipeline with hundreds of variables—the API will return a generic 500 Internal Server Error. It won’t tell you the payload is too large. It will just die. I spent a week on a ticket with Microsoft only for them to tell me to “minify the JSON.” I’m an architect, not a JS compressor.
Final Checklist for the Damned
Before you take my desk, here is the list of things you need to check every Monday morning. If you don’t, the system will fail by Wednesday.
- Check the PATs: Go to the “Service Accounts” user and check the Personal Access Tokens. There are three that expire on a rolling basis. If “Build-Automation-Bot” expires, the nightly cleanup stops.
- Monitor the Agent Disk Space: Log into the self-hosted agents and delete the
C:\Windows\Tempfolder. For some reason, the “azure devops” agent service leaks temp files like a sieve. - Verify Key Vault Access: Ensure the
Project Collection Build Servicehasn’t been stripped of its “Get” permissions. This happens every time the Security Team runs their “Least Privilege” script. - Audit the Variable Groups: Make sure no one has hardcoded a production password into a “Dev” variable group. They do this because “it was easier for testing.”
- Check the Orphaned Releases: Go to the “Releases” tab (the Classic UI one, because we still have 50 pipelines there) and delete the “Abandoned” releases. If you don’t, the database grows until the UI becomes unresponsive.
- Pray to the YAML Parser: Before you commit any change to the
azure-pipelines.yml, run it through a linter. Then realize the linter is wrong because “azure devops” uses a custom implementation of YAML that doesn’t follow the spec. - Watch the Deployment Groups: If a server shows “Offline,” don’t try to restart the service. Just delete the agent folder and re-install it. It’s faster than trying to figure out why the
.agentfile got corrupted. - The “401 Unauthorized” Dance: If a pipeline fails with a 401 but the service connection tests “Successful,” delete the service connection and recreate it with the exact same name. I don’t know why this works. It just does.
I’m leaving the keys in the top drawer. The password for the “Emergency-Admin” account is written on a sticky note under the coffee machine in the breakroom. Don’t move it. It’s the only way we get back in when Azure AD (or “Entra ID,” or whatever they’re calling it this week) goes down.
Good luck. You’re going to need it. The “azure devops” environment is hungry, and it hasn’t fed in a while.
— The Architect (Retired)