Self-hosted vs Microsoft-hosted agents for data platform CI/CD workloads.
Datanest Digital — https://datanest.dev
Choosing the right agent pool strategy directly impacts pipeline execution time,
cost, and security. This guide provides sizing recommendations for the four pipeline
types included in this product, with concrete guidance on when to use Microsoft-hosted
agents vs self-hosted agents.
| Factor | Microsoft-Hosted | Self-Hosted |
|---|---|---|
| Setup effort | None | Medium to High |
| Maintenance | None (managed by Microsoft) | OS patching, tool updates |
| Cost model | Per-minute (free tier: 1,800 min/month) | Infrastructure cost only |
| Network access | Public internet only | Private VNET, on-premises |
| Startup latency | 30-90 seconds | Near-instant (if pre-warmed) |
| Disk space | ~10 GB free | Custom (100+ GB possible) |
| Pre-installed tools | Standard set | Fully customizable |
| Parallel jobs | 1 free, then paid | Limited by agent count |
| Stateful caching | No (ephemeral VMs) | Yes (persistent disk) |
| Private endpoint access | No | Yes |
Recommended: Self-hosted (for production), Microsoft-hosted (for dev)
| Reason |
|---|
| Terraform state storage may be behind a private endpoint |
| State files can be large; persistent disk caching avoids re-download |
| Provider plugins (~500 MB) benefit from disk caching between runs |
| Production infrastructure changes need network access to private VNETs |
Agent Sizing:
Recommended: Microsoft-hosted (all environments)
| Reason |
|---|
| Databricks CLI communicates over HTTPS to workspace public endpoints |
| No large local dependencies; CLI is a small binary |
| Python linting and unit tests run quickly on standard agents |
| No persistent state needed between runs |
If using private-link Databricks workspaces: Switch to self-hosted agents
with VNET connectivity to the workspace's private endpoint.
Agent Sizing (if self-hosted):
Recommended: Microsoft-hosted (all environments)
| Reason |
|---|
| ARM template deployments use Azure Resource Manager public APIs |
| No large binaries or build artifacts |
| Pipeline is lightweight and short-running (typically 2-5 minutes) |
Agent Sizing (if self-hosted):
Recommended: Self-hosted (for staging and production)
| Reason |
|---|
| SQL servers are typically behind private endpoints or firewall rules |
| DACPAC builds require .NET SDK which benefits from NuGet package caching |
| sqlpackage operations against large databases can be memory-intensive |
| Schema drift reports need direct database connectivity |
Agent Sizing:
| Option | Pros | Cons |
|---|---|---|
| Azure VM Scale Sets (VMSS) | Auto-scaling, managed lifecycle | Higher complexity |
| Azure Container Instances | Fast startup, cost-efficient | No persistent disk |
| Azure Kubernetes Service | Scalable, reusable clusters | Operational overhead |
| Single Azure VM | Simple, cheap | No auto-scaling |
Azure VM Scale Sets provide the best balance of auto-scaling, cost control, and
maintenance simplicity for self-hosted agents.
# Create a VMSS for pipeline agents
az vmss create \
--name "vmss-azdo-agents" \
--resource-group "rg-azdo-agents" \
--image "Ubuntu2204" \
--vm-sku "Standard_D2s_v5" \
--instance-count 0 \
--upgrade-policy-mode manual \
--vnet-name "vnet-data-platform" \
--subnet "snet-azdo-agents" \
--admin-username "azureagent" \
--generate-ssh-keysRegister as an Azure DevOps agent pool:
1. Go to Organization Settings > Agent pools > Add pool.
2. Select Azure virtual machine scale set.
3. Select the VMSS resource and configure:
| Setting | Recommended Value |
|---|---|
| Maximum number of VMs | 4 |
| Number of agents to keep on standby | 1 |
| Delay in minutes before deleting excess idle agents | 30 |
| Grace period (minutes) | 15 |
Install these tools on the self-hosted agent image:
| Tool | Version | Required By |
|---|---|---|
| Azure CLI | Latest | All pipelines |
| Terraform | 1.7.x | Terraform pipeline |
| Databricks CLI | 0.232.x | DABs pipeline |
| .NET SDK | 8.x | SQL migration pipeline |
| sqlpackage | Latest | SQL migration pipeline |
| Python | 3.11.x | DABs pipeline, linting |
| Git | Latest | All pipelines |
| jq | Latest | Various script steps |
| ruff | 0.4.x | Python linting |
| Docker (optional) | Latest | Container-based builds |
Not recommended for production deployments due to eviction risk.
| Scenario | Microsoft-Hosted | Self-Hosted (VMSS) |
|---|---|---|
| 50 pipeline runs/month, 10 min avg | Free tier | ~$15 (D2s_v5 spot) |
| 200 runs/month, 10 min avg | ~$40 (1 extra parallel) | ~$45 (D2s_v5) |
| 500 runs/month, 15 min avg | ~$120 (3 extra parallel) | ~$90 (D2s_v5) |
1. Network isolation: Place self-hosted agents in a dedicated subnet with NSG rules
that only allow outbound access to required endpoints.
2. Ephemeral agents: Configure VMSS agents to reimage after every job to prevent
credential leakage between pipeline runs.
3. Managed identity: Assign a system-assigned managed identity to the VMSS and use
it for Azure authentication instead of service principal secrets.
4. Agent updates: Enable automatic agent software updates in the pool configuration.
5. Firewall rules: Microsoft-hosted agents use dynamic IP ranges. If your resources
require IP allowlisting, self-hosted agents with a static outbound IP (via NAT Gateway)
are the only reliable option.
Copyright 2024-2026 Datanest Digital. All rights reserved.
End-to-end CI/CD architecture for deploying infrastructure, compute, orchestration, and storage layers as a single platform.
Datanest Digital — https://datanest.dev
A modern data platform on Azure consists of multiple layers — infrastructure (Terraform),
compute (Databricks), orchestration (Azure Data Factory), and storage (SQL databases,
Delta Lake). Each layer has distinct deployment concerns, but they share common
dependencies and must be deployed in the correct order.
This guide describes how to compose the four pipelines in this product into a unified
CI/CD architecture that deploys the entire platform end-to-end with proper dependency
ordering, environment isolation, and promotion gates.
┌──────────────────────────────────────────────────────────────────┐
│ Source Repository │
│ │
│ infrastructure/ databricks/ adf/ database/ │
│ └─ terraform/ └─ bundles/ └─ arm/ └─ dacpac/ │
│ ├─ modules/ └─ src/ └─ params/ └─ schemas/ │
│ └─ envs/ └─ tests/ └─ migrations/ │
└──────────┬──────────────┬─────────────┬──────────────┬──────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌──────────────┐ ┌────────────┐ ┌──────────┐ ┌──────────────┐
│ Terraform │ │ DABs │ │ ADF ARM │ │ SQL DACPAC │
│ Pipeline │ │ Pipeline │ │ Pipeline │ │ Pipeline │
└──────┬───────┘ └─────┬──────┘ └────┬─────┘ └──────┬───────┘
│ │ │ │
▼ ▼ ▼ ▼
┌──────────────────────────────────────────────────────────────────┐
│ Environment: dev │
│ VNET, Storage, Key Vault │ Databricks │ ADF │ SQL Database │
└──────────────────────────────┬───────────────────────────────────┘
│ (automated)
▼
┌──────────────────────────────────────────────────────────────────┐
│ Environment: staging │
│ VNET, Storage, Key Vault │ Databricks │ ADF │ SQL Database │
└──────────────────────────────┬───────────────────────────────────┘
│ (approval gate)
▼
┌──────────────────────────────────────────────────────────────────┐
│ Environment: prod │
│ VNET, Storage, Key Vault │ Databricks │ ADF │ SQL Database │
└──────────────────────────────────────────────────────────────────┘
Infrastructure must exist before application-layer resources can be deployed.
The correct dependency chain is:
1. Terraform Infrastructure (VNET, Storage, Key Vault, Databricks workspace, ADF, SQL Server)
│
├── 2a. Databricks DABs Deploy (notebooks, jobs, DLT pipelines)
│
├── 2b. ADF ARM Deploy (pipelines, datasets, linked services)
│
└── 2c. SQL Database Migration (schemas, stored procedures, views)
Steps 2a, 2b, and 2c can run in parallel after Terraform completes, because
they target independent resources.
Create a top-level orchestrator pipeline that invokes the four component pipelines
in the correct order:
# pipelines/orchestrator.yml
trigger:
branches:
include:
- main
paths:
include:
- infrastructure/**
- databricks/**
- adf/**
- database/**
parameters:
- name: environment
type: string
default: dev
values: [dev, staging, prod]
stages:
# --- Stage 1: Infrastructure ---
- stage: Infrastructure
displayName: "Deploy Infrastructure"
jobs:
- template: terraform-infrastructure.yml
parameters:
environment: ${{ parameters.environment }}
# --- Stage 2: Application Layer (parallel) ---
- stage: ApplicationLayer
displayName: "Deploy Application Layer"
dependsOn: Infrastructure
jobs:
- template: databricks-dabs-deploy.yml
parameters:
environment: ${{ parameters.environment }}
- template: adf-arm-deploy.yml
parameters:
environment: ${{ parameters.environment }}
- template: sql-database-migration.yml
parameters:
environment: ${{ parameters.environment }}
# --- Stage 3: Validation ---
- stage: Validation
displayName: "End-to-End Validation"
dependsOn: ApplicationLayer
jobs:
- job: IntegrationTests
steps:
- script: echo "Run integration test suite"Not every commit touches all four layers. Use path-based triggers to avoid
unnecessary deployments:
| Change Scope | Pipelines Triggered |
|---|---|
infrastructure/** only | Terraform only |
databricks/** only | DABs only |
adf/** only | ADF ARM only |
database/** only | SQL migration only |
| Multiple paths | All affected pipelines |
Each pipeline has its own trigger.paths.include filter. The orchestrator pipeline
triggers on any path change and can skip unchanged components using conditions.
stages:
- stage: Infrastructure
condition: |
or(
contains(variables['Build.SourceVersionMessage'], '[infra]'),
eq(variables['Build.Reason'], 'Manual'),
contains(join(',', split(variables['System.PullRequest.TargetBranch'], '/')), 'main')
)<resource-type>-<project>-<environment>
| Resource | dev | staging | prod |
|---|---|---|---|
| Resource Group | rg-data-platform-dev | rg-data-platform-staging | rg-data-platform-prod |
| Storage Account | stdataplatformdev | stdataplatformstaging | stdataplatformprod |
| Key Vault | kv-dataplatform-dev | kv-dataplatform-staging | kv-dataplatform-prod |
| Databricks Workspace | dbw-dataplatform-dev | dbw-dataplatform-staging | dbw-dataplatform-prod |
| Data Factory | adf-dataplatform-dev | adf-dataplatform-staging | adf-dataplatform-prod |
| SQL Server | sql-dataplatform-dev | sql-dataplatform-staging | sql-dataplatform-prod |
| Model | Description | Recommended For |
|---|---|---|
| Single subscription | All environments in one subscription with RGs | Small teams, low cost |
| Multi-subscription | Separate subscription per environment | Enterprise, compliance |
| Management group | Subscriptions grouped under a management group | Large organizations |
The multi-subscription model is recommended for production data platforms because
it provides the strongest isolation boundary for RBAC, cost tracking, and quota
management.
Each pipeline stage publishes artifacts that downstream stages can consume:
Terraform Plan → Plan Artifact (.tfplan)
│
Terraform Apply → Output Artifact (terraform-outputs.json)
│
├── DABs Deploy reads workspace URL from outputs
├── ADF Deploy reads linked service endpoints from outputs
└── SQL Migration reads server FQDN from outputs
# In the Terraform Apply stage:
- script: |
terraform output -json > $(Build.ArtifactStagingDirectory)/outputs.json
displayName: "Export Outputs"
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: "$(Build.ArtifactStagingDirectory)/outputs.json"
ArtifactName: "terraform-outputs"
# In a downstream stage:
- task: DownloadBuildArtifacts@1
inputs:
artifactName: "terraform-outputs"
- script: |
DATABRICKS_HOST=$(jq -r '.databricks_host.value' terraform-outputs/outputs.json)
echo "##vso[task.setvariable variable=DATABRICKS_HOST]$DATABRICKS_HOST"
displayName: "Parse Terraform Outputs"Terraform state tracks the desired state. Rollback options:
1. Revert the commit and re-run the pipeline (preferred).
2. Target-specific resource with terraform apply -target=.
3. Import previous state if state was corrupted.
Avoid terraform destroy as a rollback mechanism in production.
1. Redeploy previous bundle version from a tagged Git commit.
2. DABs tracks deployment state; redeploying an older commit restores previous
notebook and job definitions.
1. Incremental mode (default) only adds/updates resources; it does not delete.
2. Rollback by redeploying the previous ARM template version from Git history.
3. ADF also supports the live/publish model with adf_publish branch as a fallback.
1. DACPAC is declarative — it converges schema to the desired state.
2. Rollback by deploying the previous DACPAC artifact.
3. For data-destructive changes, take a database snapshot before deployment.
Track these four metrics to measure CI/CD effectiveness:
| Metric | Target (Elite) | Measured By |
|---|---|---|
| Deployment Frequency | Multiple per day | Pipeline run count per day |
| Lead Time for Changes | < 1 hour | Commit timestamp → prod deploy |
| Change Failure Rate | < 5% | Failed deploys / total deploys |
| Time to Restore Service | < 1 hour | Failure detection → successful fix |
Use the monitoring/pipeline-dashboard.yml definition to set up dashboards that
track these metrics automatically.
Configure alerts for:
Recommended repository layout for the unified data platform:
repo-root/
├── .azuredevops/
│ └── pipelines/
│ ├── orchestrator.yml
│ ├── terraform-infrastructure.yml
│ ├── databricks-dabs-deploy.yml
│ ├── adf-arm-deploy.yml
│ ├── sql-database-migration.yml
│ └── pr-validation.yml
├── templates/
│ ├── shared-steps.yml
│ └── notification-template.yml
├── infrastructure/
│ └── terraform/
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ ├── modules/
│ │ ├── networking/
│ │ ├── storage/
│ │ ├── databricks/
│ │ ├── data-factory/
│ │ └── sql-server/
│ └── environments/
│ ├── dev.tfvars
│ ├── staging.tfvars
│ └── prod.tfvars
├── databricks/
│ ├── databricks.yml # DABs bundle config
│ ├── resources/
│ │ ├── jobs.yml
│ │ └── dlt_pipelines.yml
│ └── src/
│ ├── notebooks/
│ └── libraries/
├── adf/
│ └── arm-templates/
│ ├── ARMTemplateForFactory.json
│ └── ARMTemplateParametersForFactory.json
├── database/
│ └── DataPlatformDB/
│ ├── DataPlatformDB.sqlproj
│ ├── Tables/
│ ├── Views/
│ ├── StoredProcedures/
│ └── PostDeployment/
├── tests/
│ ├── unit/
│ ├── integration/
│ └── smoke/
└── docs/
└── architecture/
Use this checklist when setting up the unified CI/CD architecture:
config/service-connections.md)config/variable-groups.md)config/branch-policies.md)config/release-gates.json)Copyright 2024-2026 Datanest Digital. All rights reserved.