Last reviewed: May 2026
Build the AWS services on the DP-900 exam with plain Terraform — one block at a time, each tied back to an exam domain. The same code works on OpenTofu.
By the end of this lab you'll have provisioned, with plain Terraform, the three data-platform shapes DP-900 tests — an Azure Storage account for blob/analytical data with a lifecycle policy, a relational Azure SQL Database on a Basic-tier server, and a serverless Cosmos DB account for non-relational JSON. One stack, three substrates, every one tied to a DP-900 domain.
Every resource is plain Terraform. Drop the snippets into a single main.tf, run terraform init, then terraform apply step-by-step.
>= 1.5 or OpenTofu >= 1.6.az login).Mixed bag — two cheap, one with a real bill:
Whole stack is ~$5–8/month running. Destroy promptly when done — the SQL DB is the one line item that bills 24/7 regardless of whether you're using it.
Standard Azure opener consolidated into one block: pin azurerm ~> 4.0, register the random and random_password providers (we'll use both — random_id for unique resource naming, random_password for the SQL admin password), and create the Resource Group.
terraform {
required_version = ">= 1.5"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
}
}
provider "azurerm" {
features {}
}
resource "random_id" "suffix" {
byte_length = 2
}
locals {
tags = {
Project = "certlabpro-dp-900"
ManagedBy = "terraform"
}
}
resource "azurerm_resource_group" "main" {
name = "certlabpro-dp-900-rg"
location = "eastus"
tags = local.tags
}Azure Storage is the analytical-data substrate every DP-900 question assumes (Hot / Cool / Archive tiering, the lifecycle policy that transitions cold data automatically). We turn on hierarchical namespace (is_hns_enabled = true) which converts the account into Azure Data Lake Storage Gen2 — the version Azure Synapse, Databricks, and Fabric all expect for analytical workloads.
The lifecycle policy is the most-tested DP-900 cost-optimization mechanism: Hot tier for active data, Cool after 30 days, Archive after 90 days. Three tiers, three retention windows — the exam will name these numbers in scenarios.
resource "azurerm_storage_account" "analytics" {
name = "dp900data${random_id.suffix.hex}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
account_tier = "Standard"
account_replication_type = "LRS"
account_kind = "StorageV2"
is_hns_enabled = true # Data Lake Storage Gen2
https_traffic_only_enabled = true
min_tls_version = "TLS1_2"
allow_nested_items_to_be_public = false
tags = local.tags
}
resource "azurerm_storage_management_policy" "analytics" {
storage_account_id = azurerm_storage_account.analytics.id
rule {
name = "tier-cold-data"
enabled = true
filters {
blob_types = ["blockBlob"]
prefix_match = ["raw/"]
}
actions {
base_blob {
tier_to_cool_after_days_since_modification_greater_than = 30
tier_to_archive_after_days_since_modification_greater_than = 90
}
}
}
}Relational data on Azure means Azure SQL Database (PaaS, fully-managed). DP-900 tests the distinction between SQL Database (single database, serverless or DTU-based, PaaS) vs SQL Managed Instance (near-100% SQL Server compatibility, large-instance PaaS) vs SQL Server on VM (IaaS, full control).
We provision a SQL Server logical container (the connection-string endpoint) plus one database under it on the Basic tier — the cheapest paid SKU. Always-encrypted-in-transit (TLS 1.2) is enforced. The firewall rule allows Azure services to connect (the typical DP-900 "how does my App Service talk to my SQL?" question — this rule is the answer).
resource "random_password" "sql_admin" {
length = 24
special = true
min_upper = 2
min_lower = 2
min_numeric = 2
}
resource "azurerm_mssql_server" "main" {
name = "certlabpro-dp-900-${random_id.suffix.hex}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
version = "12.0"
administrator_login = "sqladmin"
administrator_login_password = random_password.sql_admin.result
minimum_tls_version = "1.2"
public_network_access_enabled = true
tags = local.tags
}
resource "azurerm_mssql_database" "main" {
name = "certlabpro-dp-900-db"
server_id = azurerm_mssql_server.main.id
sku_name = "Basic"
max_size_gb = 2
zone_redundant = false
tags = local.tags
}
resource "azurerm_mssql_firewall_rule" "allow_azure_services" {
name = "AllowAzureServices"
server_id = azurerm_mssql_server.main.id
start_ip_address = "0.0.0.0"
end_ip_address = "0.0.0.0" # 0.0.0.0/0.0.0.0 = "allow Azure services" sentinel
}Non-relational data on Azure means Azure Cosmos DB, a globally-distributed multi-API database. DP-900 specifically tests the API distinction — NoSQL (Core / SQL API, the most common), MongoDB, Cassandra, Gremlin, and Table. We use Core SQL API + serverless capacity mode (pay-per-request, no idle billing — the DP-900 cost-anti-pattern question for serverless-vs-provisioned).
The consistency_level = "Session" is the Cosmos DB default and the most-tested consistency level on DP-900: per-session linearizability with great latency. The other four — Strong, Bounded staleness, Eventual, Consistent prefix — are conceptual exam topics.
With the three data substrates in place (blob in Step 2, SQL in Step 3, Cosmos in Step 4), the DP-900 Core Data Concepts domain has tactile shape: "which Azure service for which data shape" is answered by these three resources.
resource "azurerm_cosmosdb_account" "main" {
name = "certlabpro-dp-900-${random_id.suffix.hex}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
offer_type = "Standard"
kind = "GlobalDocumentDB" # Core (SQL) API
capabilities {
name = "EnableServerless"
}
consistency_policy {
consistency_level = "Session"
}
geo_location {
location = azurerm_resource_group.main.location
failover_priority = 0
}
tags = local.tags
}A standard terraform destroy tears down everything in this lab. Two notes:
random_password — it's stored in Terraform state. If you commit state to git, you've committed the password. Use a remote state backend (Azure Storage with versioning + soft delete) in any non-trivial setup.DP-900 covers many data services this lab can't fit — Azure Synapse Analytics (covered in DP-600/DP-700), Azure Databricks, Azure Data Factory, Microsoft Fabric, Azure Stream Analytics, Event Hubs, Azure SQL Managed Instance, Azure Database for PostgreSQL / MySQL / MariaDB, Power BI, and the Azure analytics services that bill expensively while idle (Synapse dedicated SQL pools, dedicated Spark pools).
We stick to the three core data shapes (blob/analytical, relational, non-relational) because DP-900 is fundamentally a which-service-for-which-shape exam. Once you internalize the three substrates this lab provisions, every other DP-900 question becomes a layering question on top of them.
For service-by-service coverage, see the Browse, Playbook, and Editorial sections of this cert page.