TutorialsTerraform › database.tf

Terraform Tutorial · File 5 of 9

database.tf

Creates the PostgreSQL database server this project's API reads and writes to, the empty database inside it, and the firewall rules that decide which network addresses are allowed to connect.

Codey the Sr Developer, standing with a pointer, ready to walk through the guide
Resources created → 4 Engine → PostgreSQL 16 Tier → Burstable (B1ms)

Purpose of this file

Provision the PostgreSQL server and database the API depends on, and control which network addresses may reach it.

Codey pointing to the left
where the data actually lives

This is where the project's persistent data lives. The API (created in app_service.tf) is stateless — it can restart or redeploy at any time without losing anything, because none of the actual data lives inside it. Everything that needs to survive a restart lives here instead.

The full file code sample

terraform/database.tf# B_ is Terraform's prefix for the Burstable tier.
resource "azurerm_postgresql_flexible_server" "metals" {
  name                          = "${local.prefix}-pg"
  resource_group_name           = azurerm_resource_group.metals.name
  location                      = azurerm_resource_group.metals.location
  version                       = "16"
  administrator_login           = local.db_user
  administrator_password        = var.db_password
  sku_name                      = "B_Standard_B1ms"
  storage_mb                    = 32768
  auto_grow_enabled             = false
  backup_retention_days         = 7
  geo_redundant_backup_enabled  = false
  public_network_access_enabled = true
  # Azure assigned this at creation; it can only be changed by swapping with
  # high_availability.standby_availability_zone, not cleared.
  zone = "3"

  authentication {
    password_auth_enabled         = true
    active_directory_auth_enabled = false
  }

  # Omitting high_availability keeps HA disabled, as in the PowerShell script.
  tags = { Environment = "Development" }
}

resource "azurerm_postgresql_flexible_server_database" "metals" {
  name      = local.db_name
  server_id = azurerm_postgresql_flexible_server.metals.id
  charset   = "UTF8"
  collation = "en_US.utf8"
}

# Matches --public-access 0.0.0.0: Azure services, not all internet addresses.
resource "azurerm_postgresql_flexible_server_firewall_rule" "azure_services" {
  name             = "AllowAzureServices"
  server_id        = azurerm_postgresql_flexible_server.metals.id
  start_ip_address = "0.0.0.0"
  end_ip_address   = "0.0.0.0"
}

resource "azurerm_postgresql_flexible_server_firewall_rule" "local_client" {
  name             = "LocalClient-${replace(var.client_ip, ".", "-")}"
  server_id        = azurerm_postgresql_flexible_server.metals.id
  start_ip_address = var.client_ip
  end_ip_address   = var.client_ip
}

The database server azurerm_postgresql_flexible_server

This is the actual PostgreSQL server — the running database engine that the database itself and every connection depend on.

Identity and locationname · resource_group_name · location

The name is built from the same shared local.prefix covered in main.tf. The resource group and location are read straight off the resource group created there (azurerm_resource_group.metals.name / .location) rather than repeated as separate values, so this server is guaranteed to land in the exact same resource group and region as everything else.

version = "16"engine version

Which major version of PostgreSQL to run. Version 16 is a recent, well-supported release at the time this project was written.

administrator_login / administrator_passwordcredentials

The admin username comes from local.db_user ("metalsadmin"), and the password comes from var.db_password — the sensitive variable covered on the variables.tf page, always supplied through TF_VAR_db_password rather than written into any file.

sku_name = "B_Standard_B1ms"pricing tier

The B_ prefix means the Burstable tier — a lower-cost tier suited to development and light workloads, which can use extra CPU briefly when needed rather than being provisioned for constant peak load. Standard_B1ms is the specific virtual machine size within that tier.

storage_mb, auto_grow_enabled, backupscapacity & durability
  • storage_mb = 32768 — 32 GiB of allocated storage.
  • auto_grow_enabled = false — storage stays fixed at that size instead of expanding automatically (and billing) as data grows.
  • backup_retention_days = 7 — automatic backups are kept for a week.
  • geo_redundant_backup_enabled = false — backups stay in one Azure region rather than being copied to a second region, which keeps costs down for a non-production environment.
authentication { }how you log in

password_auth_enabled = true allows the standard username/password login used by administrator_login above. active_directory_auth_enabled = false turns off the alternative of logging in with a Microsoft Entra ID identity, since this project doesn't use it for database access (Entra ID is used elsewhere — see the backend and github_oidc.tf — just not here).

zone = "3"pinned, not chosen

Azure picks an availability zone when the server is created. Terraform then sees that assigned value as configuration it doesn't manage, and proposes clearing it on every plan — which Azure rejects outright, since a zone can only be swapped with a high-availability standby, never removed. Writing the actual value into the configuration makes the plan match reality and the noise disappears.

If you rebuild this environment from scratch, Azure may assign a different zone. Set this to whatever the new server actually got — it's a record of a decision Azure made, not one this project is making.

public_network_access_enabled = truereachable from the internet

Allows connections from outside Azure's private network, gated by the firewall rules described below. Without this, nothing outside Azure — including your own laptop — could reach the database at all.

The database itself azurerm_postgresql_flexible_server_database

A server vs. a databaseimportant distinction

The resource above creates the server — think of it as the whole running PostgreSQL installation. A server can host multiple separate databases inside it. This second resource creates one specific database, named local.db_name ("metals"), attached to that server via server_id = azurerm_postgresql_flexible_server.metals.id. charset and collation control text encoding and sort order, set here to standard UTF-8/US-English defaults.

The firewall rules who's allowed to connect

By default, a PostgreSQL flexible server with public access enabled still rejects every connection — each allowed source has to be listed explicitly as a firewall rule.

azure_servicesspecial-case rule
start_ip_address = "0.0.0.0"
end_ip_address   = "0.0.0.0"

This looks like it should mean “every address on the internet,” but Azure treats a firewall rule where both the start and end IP are exactly 0.0.0.0 as a special case: allow other Azure services to connect, not the public internet. This is what lets the API's web app (which also runs inside Azure, but isn't a fixed IP address itself) reach the database.

local_clientyour own address
name             = "LocalClient-${replace(var.client_ip, ".", "-")}"
start_ip_address = var.client_ip
end_ip_address   = var.client_ip

Opens the firewall for exactly one address: var.client_ip, the value supplied in variables.tf. Using the same single address for both start_ip_address and end_ip_address allows just that one IP, not a range. The rule's name can't contain dots, so replace() swaps each . in the IP address for a - — a naming workaround, not a change to which address is actually allowed.

Key terms for beginners

Flexible Server
Azure's name for its managed PostgreSQL hosting product — Azure handles patching, backups, and infrastructure, while you manage the schema and data.
SKU / tier
Which size and performance class of server to run — roughly analogous to choosing a size of virtual machine. Bigger tiers cost more and handle more load.
Firewall rule (database)
An explicit allow-list entry naming a single IP address or range permitted to connect to the database server. Anything not listed is rejected.
Collation
The rules a database uses to sort and compare text — for example, whether uppercase and lowercase letters sort together.
Codey giving a thumbs up

The database is up and reachable — now let's build the web app that talks to it.