Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

☁️ Azure Analysis Services Server Terraform Module

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.

Terraform azurerm Module Version Type Resources


🧩 Overview

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 tags and timeouts tail.

💡 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_enabled must 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 concrete power_bi_service_enabled, so enabling the firewall never fails on that constraint.

❤️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

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!

🗺️ Where this fits in the family

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;
Loading

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.

🧬 What this module builds

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;
Loading

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.

✅ Provider / Versions

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_name are force-new. name must 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_name carries 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.
  • 💳 sku is 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 are D1, B1, B2, S0, S1, S2, S4, S8, S9, S8v2, S9v2there is no S3, S5, S6 or S7, so a capacity calculation landing on S3 must round to S2 or S4, which differ by a factor of four.
  • ⚠️ power_bi_service_enabled is required when ipv4_firewall_rule is defined. This module always sends a concrete boolean (default false), so defining firewall rules never fails on that requirement.
  • 🗄️ backup_blob_container_uri is 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).

🔑 Required Azure RBAC Roles / Permissions

  • 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.

Azure Prerequisites

  • An existing resource group — this module does not create it.
  • The Microsoft.AnalysisServices resource 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.

📁 Module Structure

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

⚙️ Quick Start

# 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.

🔌 Cross-Module Contract

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

📚 Example Library

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_enabled defaults to false — 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"
}

💡 sku is updatable in place, so a D1 dev server can be scaled to an S-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"
}

ℹ️ ReadOnly keeps 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_enabled is still false here — 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 id scope, 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.

📥 Inputs

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 = {}
}

🧾 Outputs

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

🧠 Architecture Notes

  • Identity is force-new. name, location, and resource_group_name force replacement; the name has a strict format (lowercase alphanumeric, leading letter) enforced by a validation {} block at plan.
  • The Power BI / firewall coupling. The provider requires power_bi_service_enabled to be set whenever firewall rules exist. This module always passes a concrete boolean (default false), 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_each key stability. Firewall rules key on a caller-chosen name; renaming a key replaces just that rule, never the others.
  • Secret handling. backup_blob_container_uri carries 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-side provider "azurerm" { features {} } block; this module declares none.

🧱 Design Principles

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.

🚀 Runbook

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 / apply are a separate, human-reviewed step against a sub-production environment, never in this authoring flow and never in CI.

🧪 Testing

The offline proof gate this module ships against:

  • terraform init -backend=false — resolves the azurerm ~> 4.0 provider without a backend.
  • terraform validate — confirms the configuration, the object() schemas, and every validation {} 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.

💬 Example Output

$ 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"

🔍 Troubleshooting

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.

🔗 Related Docs


💙 "Infrastructure as Code should be standardized, consistent, and secure."