Provisions an Azure Analysis Services server (managed enterprise BI / tabular-model engine) with a strongly-typed, secure-by-default contract. Targets
hashicorp/azurerm ~> 4.0.
This module manages an Analysis Services server and its inline settings:
- 📊 The Analysis Services server (
azurerm_analysis_services_server) — a managed tabular-model engine for enterprise BI. - 💳 A validated SKU across the development (
D1/B*) and production (S*) tiers. - 👥 Optional server administrators (
admin_users) and query-pool connection mode. - 🔒 Power BI service access off by default (
power_bi_service_enabled = false) — the bypass is opt-in. - 🧱 Optional IPv4 firewall rules, keyed by name, to restrict access.
- 🗄️ Optional backup blob container URI (sensitive — carries a SAS token).
- 🏷️ The universal
tagsandtimeoutstail.
💡 Why it matters: An Analysis Services server fronts business-critical BI models and often sensitive aggregated data. The provider also has a sharp edge —
power_bi_service_enabledmust be set whenever any firewall rule is defined — that is easy to trip over. This module makes the SKU and connection mode validated enums, keeps the Power BI service bypass off unless you ask for it, and always passes a concretepower_bi_service_enabled, so enabling the firewall never fails on that constraint.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- ⭐ Star this repository to help others discover this Terraform module.
- 🤝 Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!
flowchart LR
rg["terraform-azurerm-<br/>resource-group"]
aas["terraform-azurerm-<br/>analysis-services-server"]
diag["terraform-azurerm-<br/>monitor-diagnostic-setting"]
ra["terraform-azurerm-<br/>role-assignments"]
rg -->|"resource_group_name"| aas
aas -->|"id"| diag
aas -->|"id (scope)"| ra
classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
classDef target fill:#004578,stroke:#002d4d,color:#ffffff;
classDef sib fill:#eef3f8,stroke:#b8c4d0,color:#1b1b1b;
class aas me;
class rg target;
class diag,ra sib;
The server lives in a resource group; downstream, its id is the target for diagnostic settings and role assignments, and its server_full_name is the endpoint BI clients connect to.
flowchart LR
subgraph inputs["Inputs"]
core["name · sku · location"]
sec["power_bi_service_enabled (false)"]
fw["ipv4_firewall_rules"]
end
res["azurerm_analysis_services_server.this"]
subgraph outputs["Outputs"]
oid["id"]
onm["name"]
osf["server_full_name"]
end
core --> res
sec --> res
fw --> res
res --> oid
res --> onm
res --> osf
classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
class res me;
Resource inventory
| Resource | Count | Role |
|---|---|---|
azurerm_analysis_services_server.this |
1 | The Analysis Services server. |
ipv4_firewall_rule (dynamic block) |
0–N | Firewall rules from a keyed map. |
timeouts (dynamic block) |
0–1 | Optional per-operation timeouts. |
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
| Provider | hashicorp/azurerm ~> 4.0 |
| Provider block | None in this module — the caller configures provider "azurerm" { features {} }, auth, subscription, and region. |
Schema notes that bite (verified against the live provider schema):
- 🔒
name,location,resource_group_nameare force-new.namemust match the provider's^[a-z][0-9a-z]{2,62}$— lowercase alphanumeric, starting with a letter, 3 to 63 characters, and no hyphens, which is unusual for an Azure name. The length floor of three is easy to miss: a two-character name passes a character-set check and is refused by the provider. - 🔐
resource_group_namecarries a validator inside a shared helper, invisible in the binary schema and in a grep of this resource: 1–90 characters, alphanumerics/dash/underscore/period/parentheses, and it may not end with a period — a period is legal anywhere else in the name, so a character-set check alone will not catch it. - 💳
skuis updatable in place — scale between tiers without replacement, though the server restarts, dropping in-flight queries and clearing cached models. 🔴 The S-series is NOT contiguous: the accepted values areD1,B1,B2,S0,S1,S2,S4,S8,S9,S8v2,S9v2— there is noS3,S5,S6orS7, so a capacity calculation landing onS3must round toS2orS4, which differ by a factor of four. ⚠️ power_bi_service_enabledis required whenipv4_firewall_ruleis defined. This module always sends a concrete boolean (defaultfalse), so defining firewall rules never fails on that requirement.- 🗄️
backup_blob_container_uriis sensitive — it embeds a SAS token. Provision it out of band; never commit it. - 🌐 No dedicated public-network-access toggle — access is controlled by firewall rules (and/or private networking composed from a sibling module).
- Contributor on the target resource group (or a least-privilege custom role granting write on
Microsoft.AnalysisServices/servers) to create and manage the server. - User Access Administrator or Owner at the target scope where the caller also assigns roles on the created server.
- Least privilege: scope the role to the containing resource group rather than the subscription where possible.
- An existing resource group — this module does not create it.
- The
Microsoft.AnalysisServicesresource provider registered on the target subscription. - For backups: a Storage account + container with a SAS token, provisioned out of band.
- The caller configures the
provider "azurerm" { features {} }block, auth, and subscription.
terraform-azurerm-analysis-services-server/
├── providers.tf # terraform{} block: required_version + azurerm ~> 4.0 pin (no provider block)
├── variables.tf # deeply-typed inputs + validation; secure defaults; the tags/timeouts tail
├── main.tf # azurerm_analysis_services_server.this + dynamic ipv4_firewall_rule + timeouts
├── outputs.tf # id first, then name and server_full_name
├── README.md # this document
├── SCOPE.md # the cross-module contract
├── LICENSE # MIT
└── .gitignore # canonical library ignore set
# The caller configures the provider, auth, subscription, and the mandatory features {} block.
provider "azurerm" {
features {}
}
module "analysis_services" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-analysis-services-server.git?ref=v1.0.0"
name = "asprodcontoso"
resource_group_name = "rg-analytics-eastus"
location = "eastus"
sku = "S0"
tags = { environment = "prod", owner = "bi-platform" }
}ℹ️ Pin the module by immutable tag (
?ref=v1.0.0), never a branch.
Consumes
| Input | Type | Source module |
|---|---|---|
resource_group_name |
string |
terraform-azurerm-resource-group (its name) |
location |
string |
caller / resource group location |
backup_blob_container_uri |
string (sensitive) |
out of band (Storage container + SAS) |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
The server Azure Resource ID (emitted first). | diagnostic-setting, role-assignments |
name |
The server name. | tagging / diagnostics |
server_full_name |
The connection endpoint. | BI clients / downstream config |
1 · Minimal call
module "analysis_services" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-analysis-services-server.git?ref=v1.0.0"
name = "asprodcontoso"
resource_group_name = "rg-analytics-eastus"
location = "eastus"
sku = "S0"
}🔒
power_bi_service_enableddefaults tofalse— the Power BI service bypass is off with no extra input.
2 · Development SKU (D1)
module "analysis_services" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-analysis-services-server.git?ref=v1.0.0"
name = "asdevcontoso"
resource_group_name = "rg-analytics-dev"
location = "eastus"
sku = "D1"
}💡
skuis updatable in place, so aD1dev server can be scaled to anS-tier later without replacement.
3 · Production SKU (S1)
module "analysis_services" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-analysis-services-server.git?ref=v1.0.0"
name = "asprodcontoso"
resource_group_name = "rg-analytics-eastus"
location = "eastus"
sku = "S1"
}4 · Server administrators
module "analysis_services" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-analysis-services-server.git?ref=v1.0.0"
name = "asprodcontoso"
resource_group_name = "rg-analytics-eastus"
location = "eastus"
sku = "S0"
admin_users = ["bi-admin@contoso.com", "analytics-lead@contoso.com"]
}5 · Read-only query pool mode
module "analysis_services" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-analysis-services-server.git?ref=v1.0.0"
name = "asprodcontoso"
resource_group_name = "rg-analytics-eastus"
location = "eastus"
sku = "S1"
querypool_connection_mode = "ReadOnly"
}ℹ️
ReadOnlykeeps the read-write server out of query operations; leave unset for the default (All).
6 · Restrict access with firewall rules
module "analysis_services" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-analysis-services-server.git?ref=v1.0.0"
name = "asprodcontoso"
resource_group_name = "rg-analytics-eastus"
location = "eastus"
sku = "S0"
ipv4_firewall_rules = {
office = {
range_start = "203.0.113.0"
range_end = "203.0.113.255"
}
}
}🔒 Defining any firewall rule enables IP filtering.
power_bi_service_enabledis stillfalsehere — the module passes it automatically, satisfying the provider's "required with firewall" rule.
7 · Allow the Power BI service (explicit opt-in)
module "analysis_services" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-analysis-services-server.git?ref=v1.0.0"
name = "asprodcontoso"
resource_group_name = "rg-analytics-eastus"
location = "eastus"
sku = "S1"
power_bi_service_enabled = true # ⚠️ allows the Power BI service to reach the server
ipv4_firewall_rules = {
office = { range_start = "203.0.113.0", range_end = "203.0.113.255" }
}
}
⚠️ Turn this on only when Power BI (cloud) must reach the server through the firewall.
8 · Firewall rules at scale (for_each over a map)
locals {
allowed_ranges = {
hq = { range_start = "203.0.113.0", range_end = "203.0.113.255" }
branch = { range_start = "198.51.100.0", range_end = "198.51.100.127" }
vpn = { range_start = "192.0.2.10", range_end = "192.0.2.20" }
}
}
module "analysis_services" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-analysis-services-server.git?ref=v1.0.0"
name = "asprodcontoso"
resource_group_name = "rg-analytics-eastus"
location = "eastus"
sku = "S1"
ipv4_firewall_rules = local.allowed_ranges
}💡 Stable keys (
hq/branch/vpn) mean adding or removing one range never re-indexes the others.
9 · Backup blob container (sensitive)
module "analysis_services" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-analysis-services-server.git?ref=v1.0.0"
name = "asprodcontoso"
resource_group_name = "rg-analytics-eastus"
location = "eastus"
sku = "S1"
backup_blob_container_uri = var.backup_container_sas_uri # from Key Vault / CI secret
}🔒 Provide the SAS URI out of band (a variable fed from Key Vault or a CI secret); it's marked sensitive and never committed or emitted.
10 · Governance tags
module "analysis_services" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-analysis-services-server.git?ref=v1.0.0"
name = "asprodcontoso"
resource_group_name = "rg-analytics-eastus"
location = "eastus"
sku = "S0"
tags = {
environment = "prod"
cost_center = "CC-4470"
owner = "bi-platform"
data_class = "analytics"
}
}11 · Custom timeouts
module "analysis_services" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-analysis-services-server.git?ref=v1.0.0"
name = "asprodcontoso"
resource_group_name = "rg-analytics-eastus"
location = "eastus"
sku = "S0"
timeouts = {
create = "30m"
update = "30m"
}
}12 · Diagnostics on the server (consume by id)
module "analysis_services" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-analysis-services-server.git?ref=v1.0.0"
name = "asprodcontoso"
resource_group_name = "rg-analytics-eastus"
location = "eastus"
sku = "S0"
}
module "aas_diagnostics" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-monitor-diagnostic-setting.git?ref=v1.0.0"
name = "aas-to-law"
target_resource_id = module.analysis_services.id
log_analytics_workspace_id = module.analytics_law.id
}13 · Least-privilege role assignment (consume by id)
module "analysis_services" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-analysis-services-server.git?ref=v1.0.0"
name = "asprodcontoso"
resource_group_name = "rg-analytics-eastus"
location = "eastus"
sku = "S0"
}
module "aas_rbac" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-role-assignments.git?ref=v1.0.0"
scope = module.analysis_services.id
role_assignments = {
reader = {
role_definition_name = "Reader"
principal_id = var.bi_readers_group_object_id
}
}
}🔒 Assign the least-privilege role at the server's
idscope, not the whole subscription.
14 · 🏗️ End-to-end composition
Resource group → Analysis Services server (firewalled) → diagnostics + RBAC, wired by output references.
provider "azurerm" {
features {}
}
module "analytics_rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-analytics-eastus"
location = "eastus"
tags = { environment = "prod", owner = "bi-platform" }
}
module "analytics_law" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-log-analytics-workspace.git?ref=v1.0.0"
name = "law-analytics-eastus"
resource_group_name = module.analytics_rg.name
location = "eastus"
}
module "analysis_services" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-analysis-services-server.git?ref=v1.0.0"
name = "asprodcontoso"
resource_group_name = module.analytics_rg.name
location = "eastus"
sku = "S1"
admin_users = ["bi-admin@contoso.com"]
ipv4_firewall_rules = {
hq = { range_start = "203.0.113.0", range_end = "203.0.113.255" }
}
tags = { environment = "prod", data_class = "analytics" }
}
module "aas_diagnostics" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-monitor-diagnostic-setting.git?ref=v1.0.0"
name = "aas-to-law"
target_resource_id = module.analysis_services.id
log_analytics_workspace_id = module.analytics_law.id
}
output "analysis_services_endpoint" {
value = module.analysis_services.server_full_name
}💡 Every dependency is an output reference, so Terraform orders RG → workspace → server → diagnostics automatically, no
depends_on.
Required
| Name | Type | Description |
|---|---|---|
name |
string |
Server name (lowercase alphanumeric, starts with a letter). Force-new. |
resource_group_name |
string |
Existing resource group. Force-new. |
location |
string |
Azure region. Force-new. |
sku |
string |
One of D1, B1, B2, S0, S1, S2, S4, S8, S9, S8v2, S9v2. |
Optional
| Name | Type | Default | Description |
|---|---|---|---|
admin_users |
list(string) |
[] |
Server administrators. Three forms: a user UPN alice@contoso.com, a service principal app:<appid>@<tenantid>, or a security group obj:<groupid>@<tenantid>. |
querypool_connection_mode |
string |
null |
All / ReadOnly (provider default All). |
power_bi_service_enabled |
bool |
false |
Allow the Power BI service to access the server. |
backup_blob_container_uri |
string (sensitive) |
null |
Blob container URI + SAS token for backups. |
ipv4_firewall_rules |
map(object({...})) |
{} |
Firewall rules keyed by name. |
tags |
map(string) |
{} |
Tags. |
timeouts |
object({...}) |
null |
Create/read/update/delete timeouts. |
Full input schemas
variable "sku" {
type = string
# one of: D1, B1, B2, S0, S1, S2, S4, S8, S9, S8v2, S9v2
# NOTE the gaps: there is no S3, S5, S6 or S7.
}
variable "admin_users" {
type = list(string)
default = []
# A user UPN, a service principal "app:<appid>@<tenantid>", or a
# security group "obj:<groupid>@<tenantid>". The last two carry no dot
# after the @, because a tenant ID is a GUID.
}
variable "querypool_connection_mode" {
type = string
default = null # null | "All" | "ReadOnly"
}
variable "power_bi_service_enabled" {
type = bool
default = false
}
variable "backup_blob_container_uri" {
type = string
default = null
sensitive = true
}
variable "ipv4_firewall_rules" {
type = map(object({
range_start = string
range_end = string
}))
default = {}
}| Output | Description | Kind |
|---|---|---|
id |
The Azure Resource ID of the Analysis Services server | Passthrough |
name |
The name of the Analysis Services server | Passthrough |
location |
Azure region the resource is deployed in, in the canonical form Azure uses | Passthrough |
server_full_name |
The full name of the server, used as the connection endpoint (for example "asazure://.asazure.windows.net/") | Passthrough |
resource_group_name |
Name of the resource group holding the server | Passthrough |
sku |
The SKU in force | Passthrough |
querypool_connection_mode |
Whether read-write servers also serve queries (All) or are excluded from the query pool (ReadOnly) | Passthrough |
power_bi_service_enabled |
Whether the Power BI service is permitted to reach this server | Passthrough |
admin_user_count |
How many principals hold server-administrator rights | Passthrough |
has_no_administrators |
True when admin_users is empty | Passthrough |
is_developer_sku |
True on the D1 developer SKU, which is not intended for production workloads and carries no availability commitment | Derived |
billed_whether_used |
Constant true | Constant |
firewall_requires_power_bi_flag |
Constant true, and a pairing that reads backwards | Constant |
firewall_is_open_when_no_rules_are_set |
Constant true, and the reason this module's empty call is not the locked-down one | Passthrough |
backup_container_drift_is_invisible |
Constant true | Constant |
backup_uri_embeds_a_sas_token |
Constant true | Constant |
backup_uri_is_plaintext_http |
🔴 Derived — the SAS URI was given over plaintext http://. Not refused, deliberately; reported so it can be caught in review. |
Derived |
has_non_user_administrators |
Derived — an admin entry is a service principal or security group rather than a user | Derived |
sku_series_is_not_contiguous |
Constant true — there is no S3, S5, S6 or S7 |
Constant |
sku_change_restarts_the_server |
Constant true — the tier is not force-new, but changing it is a brief outage | Constant |
admin_membership_is_never_verified |
Constant true — the provider checks neither the format nor the existence of an admin principal | Constant |
- Identity is force-new.
name,location, andresource_group_nameforce replacement; the name has a strict format (lowercase alphanumeric, leading letter) enforced by avalidation {}block at plan. - The Power BI / firewall coupling. The provider requires
power_bi_service_enabledto be set whenever firewall rules exist. This module always passes a concrete boolean (defaultfalse), so you can add firewall rules without separately remembering to set it — and turning the Power BI bypass on is a deliberate, visible change. for_eachkey stability. Firewall rules key on a caller-chosen name; renaming a key replaces just that rule, never the others.- Secret handling.
backup_blob_container_uricarries a SAS token and is marked sensitive; it is an input only and never emitted. The module accepts no other secret. features {}dependence. The provider will not initialize without a caller-sideprovider "azurerm" { features {} }block; this module declares none.
| Concern | Secure default (empty call) | Opt-out (caller must type it) |
|---|---|---|
| Power BI service access | power_bi_service_enabled = false |
set to true |
| Network access | firewall rules recommended (+ private networking via a sibling) | leave ipv4_firewall_rules empty |
| Backup SAS token | sensitive input, never emitted | — |
| Input safety | sku and querypool_connection_mode constrained by validation {}; name format-checked |
— |
The empty call keeps the Power BI service bypass off. Enabling it — or opening access — is an explicit, reviewable change.
terraform init -backend=false
terraform validate
terraform fmt -check- Pin the module by immutable tag (
?ref=v1.0.0) — never a branch. - Plan-only artifact.
terraform plan/applyare a separate, human-reviewed step against a sub-production environment, never in this authoring flow and never in CI.
The offline proof gate this module ships against:
terraform init -backend=false— resolves theazurerm ~> 4.0provider without a backend.terraform validate— confirms the configuration, theobject()schemas, and everyvalidation {}block type-check.terraform fmt -check— confirms canonical formatting.
What only plan/apply against a live subscription can exercise: SKU availability in the target region, the firewall/Power BI interaction on the live API, and the validity of the backup SAS token. Those are validated in the separate human-reviewed deployment step.
$ terraform output
id = "/subscriptions/0000.../resourceGroups/rg-analytics-eastus/providers/Microsoft.AnalysisServices/servers/asprodcontoso"
name = "asprodcontoso"
server_full_name = "asazure://eastus.asazure.windows.net/asprodcontoso"| Symptom | Cause | Fix |
|---|---|---|
sku must be one of ... at plan |
An unsupported SKU string — most often S3, S5, S6 or S7, none of which exist. |
The set is not a range: D1, B1, B2, S0, S1, S2, S4, S8, S9, S8v2, S9v2. Round S3 to S2 or S4 — they differ by a factor of four in capacity, so pick deliberately. |
name must be lowercase alphanumeric ... |
Name has hyphens, underscores, or a leading digit. | Use lowercase letters/digits starting with a letter. |
| Apply fails: Power BI service must be set with firewall | (Only if replicating outside this module) power_bi_service_enabled unset while firewall rules exist. |
This module always sends it; if hand-writing HCL, set power_bi_service_enabled explicitly. |
| Terraform wants to replace the server | A force-new field (name, location, resource_group_name) changed. |
Revert, or treat it as a deliberate migration. |
| Clients cannot reach the server | Firewall rules exclude the client IP. | Add the client range to ipv4_firewall_rules (or use private networking). |
provider not initialized / features error |
Caller root module missing provider "azurerm" { features {} }. |
Add the features {} block to the caller. |
azurerm_analysis_services_serverresource- Azure Analysis Services documentation
- Sibling modules:
terraform-azurerm-resource-group,terraform-azurerm-monitor-diagnostic-setting,terraform-azurerm-role-assignments,terraform-azurerm-storage-account - This module's
SCOPE.md
💙 "Infrastructure as Code should be standardized, consistent, and secure."