# Azure DevOps Best Practices: Breaking Down the Monolithic YAML

1. [Creating a multi-stage YAML pipeline in Azure DevOps for .NET projects](https://bogdanbujdea.dev/creating-a-multi-stage-yaml-pipeline-in-azure-devops-for-net-projects?source=more_series_bottom_blogs)
    
2. [Running tests with code coverage in Azure DevOps YAML pipelines](https://bogdanbujdea.dev/running-tests-with-code-coverage-in-azure-devops-yaml-pipelines?source=more_series_bottom_blogs)
    
3. [Static code analysis with NDepend in Azure Pipelines](https://bogdanbujdea.dev/static-code-analysis-with-ndepend-in-azure-pipelines?source=more_series_bottom_blogs)
    
4. [Running e2e tests with Playwright in Azure YAML Pipelines](https://bogdanbujdea.dev/running-e2e-tests-with-playwright-in-azure-yaml-pipelines?source=more_series_bottom_blogs)
    
5. [Publishing Playwright report as an artifact in Azure DevOps](https://bogdanbujdea.dev/publishing-playwright-report-as-an-artifact-in-azure-devops?source=more_series_bottom_blogs)
    
6. [Bicep Infrastructure Deployment from Azure DevOps YAML Pipelines](https://bogdanbujdea.dev/bicep-infrastructure-deployment-from-azure-devops-yaml-pipelines?source=more_series_bottom_blogs)
    
7. [Blue-green Deployments in Azure DevOps YAML Pipelines](https://bogdanbujdea.dev/blue-green-deployments-in-azure-devops-yaml-pipelines?source=more_series_bottom_blogs)
    
8. [Pre-Deployment Health Checks in Azure DevOps YAML Pipelines](https://bogdanbujdea.dev/pre-deployment-health-checks-in-azure-devops-yaml-pipelines?source=more_series_bottom_blogs)
    
9. [Azure DevOps Best Practices: Breaking Down the Monolithic YAML](https://bogdanbujdea.dev/azure-devops-best-practices-breaking-down-the-monolithic-yaml?source=more_series_bottom_blogs)
    

---

In the realm of Azure DevOps, infrastructure management via YAML files has become increasingly prominent. While this shift offers immense power, it can lead to lengthy and often complex `azure-pipelines.yaml` files. But what if there was a more structured, readable approach?

Today, we’ll unravel the art of breaking down a monolithic `azure-pipelines.yaml` into distinct files, each representing its own stage.

#### **Why Break Down Your YAML?**

* **Readability:** Smaller, function-specific files are easier to read, understand, and maintain, just like your code.
    
* **Collaboration:** Different teams can own different stages without stepping on each other's toes.
    
* **Error Management:** Isolating stages helps in pinpointing issues quickly.
    

# **Step-by-step Guide to Splitting Your YAML**

## **1\. Understand Your Existing Structure**

* First, take stock of your current `azure-pipelines.yaml`. How is it structured? What stages exist, and what does each stage entail? I'll use the yaml from our sample project as an example:
    

```yaml
trigger:
- main

pool:
  vmImage: 'ubuntu-latest'

variables:
  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'
  location: 'westeurope'
  configFileName: 'prod.json'
  resourceGroupName: 'azure-devops-yaml-pipeline'
  stagingAppUrl: 'https://bogdan-todo-app-staging.azurewebsites.net/health'


stages:
- stage: build
  jobs:
    - job: BuildSolution
      steps:
      - task: DotNetCoreCLI@2
        displayName: 'Build .NET solution'
        inputs:
          command: 'build'
      - task: DotNetCoreCLI@2
        displayName: 'Create publish artifact'
        inputs:
          command: 'publish'
          publishWebProjects: false
          arguments: '-o $(build.artifactStagingDirectory)'
          zipAfterPublish: false
      - task: PublishPipelineArtifact@1
        displayName: 'Publish artifact'
        inputs:
          targetPath: $(build.artifactStagingDirectory)
          artifact: 'drop'
          publishLocation: 'pipeline'
        
- stage: test
  jobs:
    - job: RunUnitTests
      steps:
      - task: DotNetCoreCLI@2
        displayName: 'Run unit tests'
        inputs:
          command: 'test'
          projects: '**/*[Tt]est*/*.csproj'

- stage: update_infrastructure
  displayName: 'Deploy Bicep Infrastructure'
  jobs:
    - job: UpdateAzureResources
      steps:
        - task: AzureCLI@2
          displayName: 'Deploy Bicep Infrastructure'
          inputs:
            azureSubscription: 'AzureConnection'
            scriptType: 'pscore'
            scriptLocation: 'scriptPath'
            scriptPath: './infrastructure/deploy.ps1'                
            arguments: '$(resourceGroupName) $(location) $(configFileName)'

- stage: deploy_app
  displayName: 'Deploy To App Service'
  jobs:
    - job: deploy
      steps:
        - task: DownloadPipelineArtifact@2
          displayName: 'Download pipeline artifact'
          inputs:
            buildType: 'current'
            artifactName: 'drop'
            targetPath: '$(Pipeline.Workspace)/drop'
        - task: AzureWebApp@1
          displayName: 'Deploy to staging slot'
          inputs:
            azureSubscription: 'AzureConnection'
            appType: 'webAppLinux'
            appName: 'bogdan-todo-app'
            package: '$(Pipeline.Workspace)/drop'
            deployToSlotOrASE: true
            slotName: 'staging'
            resourceGroupName: $(resourceGroupName)
            runtimeStack: 'DOTNETCORE|7.0'
            startUpCommand: 'dotnet ToDoApp.Server.dll'      
        - task: AzureCLI@2
          retryCountOnTaskFailure: 3
          displayName: 'Check API health before swapping slots'
          inputs:
            azureSubscription: 'AzureConnection'
            scriptType: 'pscore'
            scriptLocation: 'scriptPath'
            scriptPath: './infrastructure/healthcheck.ps1'                
            arguments: '$(stagingAppUrl)'   
        - task: AzureAppServiceManage@0
          displayName: 'Swap slot'
          inputs:
            azureSubscription: 'AzureConnection'
            Action: 'Swap Slots'
            WebAppName: 'bogdan-todo-app'
            ResourceGroupName: $(resourceGroupName)
            SourceSlot: 'staging'
```

## **2\. Create Distinct YAMLs for Each Stage**

In the yaml file above we have 4 stages, so we can create these yaml files:

* `build.yaml` - for building the .NET solution
    
* `test.yaml` - for running the tests
    
* `prod.yaml` - for deploying the production infrastructure using Bicep and then deploying the app
    

Let's create them one by one, and we'll start with the `build.yaml` file:

```yaml
stages:
- stage: Build
  jobs:
    - job: BuildSolution
      steps:
      - task: DotNetCoreCLI@2
        displayName: 'Build .NET solution'
        inputs:
          command: 'build'
      - task: DotNetCoreCLI@2
        displayName: 'Create publish artifact'
        inputs:
          command: 'publish'
          publishWebProjects: false
          arguments: '-o $(build.artifactStagingDirectory)'
          zipAfterPublish: false
      - task: PublishPipelineArtifact@1
        displayName: 'Publish artifact'
        inputs:
          targetPath: $(build.artifactStagingDirectory)
          artifact: 'drop'
          publishLocation: 'pipeline'
```

In the `build.yaml` file we build the project and then we publish it as an artifact that will be used in the later stages.

The `test.yaml` file looks like this:

```yaml
stages:        
- stage: Test
  dependsOn: Build
  jobs:
    - job: RunUnitTests
      steps:
      - task: DotNetCoreCLI@2
        displayName: 'Run unit tests'
        inputs:
          command: 'test'
          projects: '**/*[Tt]est*/*.csproj'
```

In this stage we are just running the unit tests. At the beginning of the file, you'll observe the use of the `dependsOn` keyword. This instructs the pipeline to wait for the completion of the `Build` stage before proceeding.

The last one is `production.yaml`:

```yaml
stages:
- stage: Production
  displayName: 'Deploy To Production Env'
  dependsOn: Test
  variables:
    location: 'westeurope'
    configFileName: 'prod.json'
    resourceGroupName: 'azure-devops-yaml-pipeline'
    stagingAppUrl: 'https://bogdan-todo-app-staging.azurewebsites.net/health'
  jobs:
    - job: UpdateAzureResources
      steps:
        - task: AzureCLI@2
          displayName: 'Deploy Bicep Infrastructure'
          inputs:
            azureSubscription: 'AzureConnection'
            scriptType: 'pscore'
            scriptLocation: 'scriptPath'
            scriptPath: './infrastructure/deploy.ps1'                
            arguments: '$(resourceGroupName) $(location) $(configFileName)'
    - job: Deploy
      dependsOn: UpdateAzureResources
      steps:
        - task: DownloadPipelineArtifact@2
          displayName: 'Download pipeline artifact'
          inputs:
            buildType: 'current'
            artifactName: 'drop'
            targetPath: '$(Pipeline.Workspace)/drop'
        - task: AzureWebApp@1
          displayName: 'Deploy to staging slot'
          inputs:
            azureSubscription: 'AzureConnection'
            appType: 'webAppLinux'
            appName: 'bogdan-todo-app'
            package: '$(Pipeline.Workspace)/drop'
            deployToSlotOrASE: true
            slotName: 'staging'
            resourceGroupName: $(resourceGroupName)
            runtimeStack: 'DOTNETCORE|7.0'
            startUpCommand: 'dotnet ToDoApp.Server.dll'      
        - task: AzureCLI@2
          retryCountOnTaskFailure: 3
          displayName: 'Check API health before swapping slots'
          inputs:
            azureSubscription: 'AzureConnection'
            scriptType: 'pscore'
            scriptLocation: 'scriptPath'
            scriptPath: './infrastructure/healthcheck.ps1'                
            arguments: '$(stagingAppUrl)'   
        - task: AzureAppServiceManage@0
          displayName: 'Swap slot'
          inputs:
            azureSubscription: 'AzureConnection'
            Action: 'Swap Slots'
            WebAppName: 'bogdan-todo-app'
            ResourceGroupName: $(resourceGroupName)
            SourceSlot: 'staging'
```

In this file, we have combined two previous stages: infrastructure deployment and deployment to the app service. The `Production` stage depends on the `Test` stage, so if your tests pass then we won't deploy our code to the production environment.

We have also relocated some variables from the azure-pipelines.yaml to this file, making them accessible to all the jobs within the `Production` stage.

One last thing I should mention is that jobs run in parallel, so I added the dependsOn clause to the `Deploy` job, this means that we will only deploy after the infrastructure deployment is finished successfully.

## **3\. Reference these files within the main YAML**

I will now move these files to a folder called `stages` and reference them like this in the `azure-pipelines.yaml`:

```yaml
trigger:
- main

pool:
  vmImage: 'ubuntu-latest'

variables:
  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'


stages:
- template: stages/build.yaml
- template: stages/test.yaml
- template: stages/production.yaml
```

That's it for now! We've successfully split our YAML into individual stages making it cleaner and easier to read and navigate.

The code is public and you can find it here: [https://dev.azure.com/bujdea/\_git/AzureDevopsYamlPipeline](https://dev.azure.com/bujdea/_git/AzureDevopsYamlPipeline)

# **Conclusion**

In conclusion, just as we emphasize writing clean, concise, and readable code, the same principles should be applied to our YAML files in Azure DevOps pipelines. Keeping our YAML configurations compact and straightforward not only ensures better maintainability but also reduces the potential for errors and facilitates collaboration. Let's treat our infrastructure definitions with the same care and attention to detail as our application code, striving for clarity and simplicity in every line.

## What's next

[In the next article](https://bogdanbujdea.dev/provisioning-new-environments-with-bicep-and-azure-devops-yaml-pipelines), I will add a new environment to our sample application in 5 simple steps. Feel free to subscribe to the newsletter below or follow me on Twitter if you'd like to be notified as soon as possible!
