diff --git a/azure/examples/migration/.terraform.lock.hcl b/azure/examples/migration/.terraform.lock.hcl new file mode 100644 index 00000000..fdd92071 --- /dev/null +++ b/azure/examples/migration/.terraform.lock.hcl @@ -0,0 +1,161 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/alekc/kubectl" { + version = "2.1.3" + constraints = "~> 2.0" + hashes = [ + "h1:poWSAAtK4FI1x79C2OyLaNrvWUGTQdr1ZT58edDz+Rs=", + "zh:0e601ae36ebc32eb8c10aff4c48c1125e471fa09f5668465af7581c9057fa22c", + "zh:1773f08a412d1a5f89bac174fe1efdfd255ecdda92d31a2e31937e4abf843a2f", + "zh:1da2db1f940c5d34e31c2384c7bd7acba68725cc1d3ba6db0fec42efe80dbfb7", + "zh:20dc810fb09031bcfea4f276e1311e8286d8d55705f55433598418b7bcc76357", + "zh:326a01c86ba90f6c6eb121bacaabb85cfa9059d6587aea935a9bbb6d3d8e3f3f", + "zh:5a3737ea1e08421fe3e700dc833c6fd2c7b8c3f32f5444e844b3fe0c2352757b", + "zh:5f490acbd0348faefea273cb358db24e684cbdcac07c71002ee26b6cfd2c54a0", + "zh:777688cda955213ba637e2ac6b1994e438a5af4d127a34ecb9bb010a8254f8a8", + "zh:7acc32371053592f55ee0bcbbc2f696a8466415dea7f4bc5a6573f03953fc926", + "zh:81f0108e2efe5ae71e651a8826b61d0ce6918811ccfdc0e5b81b2cfb0f7f57fe", + "zh:88b785ea7185720cf40679cb8fa17e57b8b07fd6322cf2d4000b835282033d81", + "zh:89d833336b5cd027e671b46f9c5bc7d10c5109e95297639bbec8001da89aa2f7", + "zh:df108339a89d4372e5b13f77bd9d53c02a04362fb5d85e1d9b6b47292e30821c", + "zh:e8a2e3a5c50ca124e6014c361d72a9940d8e815f37ae2d1e9487ac77c3043013", + ] +} + +provider "registry.terraform.io/hashicorp/azuread" { + version = "3.8.0" + constraints = ">= 2.45.0" + hashes = [ + "h1:E2YWNE3Qry4bQMlmmZ33X4hLY5hOGrEZrlRg4anI2uw=", + "zh:0d26cfbf9417acd1c2295ccd5b0052abeac85ad1c3f6422ff09bf6a1ce16f00d", + "zh:144d4ea92fed541a6376bc76ad65ba4738dfd7bcab4c9d6cc20d35001338d06d", + "zh:1c3e89cf19118fc07d7b04257251fc9897e722c16e0a0df7b07fcd261f8c12e7", + "zh:2061d2cb64d8167d0af37e6610d0dc051977bd1ccc0e5cdd5ab02525ee239f96", + "zh:75562fdc3b313b7e538907199bfa588a1fcbc40113b0f3b7bfb496fbc358a32f", + "zh:78bef022ae9b1b0c636b7dd32bcda13fb273023f3a888cc005f3aa20e365b417", + "zh:ad8dbee59843154f8e93b24db9939a4257d13c7c86331eb93f1691294bc4e31f", + "zh:b3d83d7ac57073631704336a188cb746c473f728fc7ccb76abecb520e83fdf65", + "zh:c0bf9e0be73843de9089597be2720e4093b3ba320fbad99ab86da47681e77949", + "zh:c9a4c27d2b0800d3f4ece19d66c1fa574f7cd4ff66277af8f120d65e8f03f48e", + "zh:cd9ad8c848e17d9824045c33132cc0e87aa4d58cdb7bee6c0f6c3f9bc27892d5", + "zh:fbcafc21cdd19451274b905f9ee8a5b758ca63cc231e7544c815642e4a399c6d", + ] +} + +provider "registry.terraform.io/hashicorp/azurerm" { + version = "4.54.0" + constraints = ">= 3.75.0, ~> 4.0, 4.54.0" + hashes = [ + "h1:AeE+jsY9HfzMrTLjQZZ8IWtI/XxqBxbd3BRDSbGU2oM=", + "zh:0adda2cfb2ae9ec394943164cbd5ab1f1fac89a0125ad3966a97363b06b1bd11", + "zh:23dcc71a1586c2b8644476ccd3b4d4d22aa651d6ceb03d32f801bb7ecb09c84f", + "zh:4573833c692a87df167e3adf71c4291879e1a5d2e430ba5255509d3510c7a2f5", + "zh:49132e138bb28b02aa36a00fdcfcf818c4a6d150e3b5148e4d910efac5aaf1bf", + "zh:5dda12ad7f69f91847b99365f66b8dfb1d6ea913d2d06fadbabcea236cc1b346", + "zh:6e45c59dbc54c56c1255f4bb45db15a2ec75dcb2a9125adfa812a667132b332a", + "zh:76802f69f1fa8e894e9c96d6f7098698d1f9c036f30b46a40207fce5ed373ef0", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:846e7222bdeee0150830d82cd2f09619e2239347eba1d05f0409c78a684502d8", + "zh:8822918829f89354ab65b1d588d3185191bbd81e3479510dcbec801d3e3617b0", + "zh:901074c726047a141e256e3229f3e55a5dd4033fec57f889c0118b71e818331b", + "zh:a240979f94f50d2f6ceda2651e5146652468f312f03691f0949876524d160a9d", + ] +} + +provider "registry.terraform.io/hashicorp/external" { + version = "2.3.5" + constraints = ">= 2.3.4" + hashes = [ + "h1:FnUk98MI5nOh3VJ16cHf8mchQLewLfN1qZG/MqNgPrI=", + "zh:6e89509d056091266532fa64de8c06950010498adf9070bf6ff85bc485a82562", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:86868aec05b58dc0aa1904646a2c26b9367d69b890c9ad70c33c0d3aa7b1485a", + "zh:a2ce38fda83a62fa5fb5a70e6ca8453b168575feb3459fa39803f6f40bd42154", + "zh:a6c72798f4a9a36d1d1433c0372006cc9b904e8cfd60a2ae03ac5b7d2abd2398", + "zh:a8a3141d2fc71c86bf7f3c13b0b3be8a1b0f0144a47572a15af4dfafc051e28a", + "zh:aa20a1242eb97445ad26ebcfb9babf2cd675bdb81cac5f989268ebefa4ef278c", + "zh:b58a22445fb8804e933dcf835ab06c29a0f33148dce61316814783ee7f4e4332", + "zh:cb5626a661ee761e0576defb2a2d75230a3244799d380864f3089c66e99d0dcc", + "zh:d1acb00d20445f682c4e705c965e5220530209c95609194c2dc39324f3d4fcce", + "zh:d91a254ba77b69a29d8eae8ed0e9367cbf0ea6ac1a85b58e190f8cb096a40871", + "zh:f6592327673c9f85cdb6f20336faef240abae7621b834f189c4a62276ea5db41", + ] +} + +provider "registry.terraform.io/hashicorp/helm" { + version = "2.17.0" + constraints = "~> 2.0, >= 2.5.0" + hashes = [ + "h1:kQMkcPVvHOguOqnxoEU2sm1ND9vCHiT8TvZ2x6v/Rsw=", + "zh:06fb4e9932f0afc1904d2279e6e99353c2ddac0d765305ce90519af410706bd4", + "zh:104eccfc781fc868da3c7fec4385ad14ed183eb985c96331a1a937ac79c2d1a7", + "zh:129345c82359837bb3f0070ce4891ec232697052f7d5ccf61d43d818912cf5f3", + "zh:3956187ec239f4045975b35e8c30741f701aa494c386aaa04ebabffe7749f81c", + "zh:66a9686d92a6b3ec43de3ca3fde60ef3d89fb76259ed3313ca4eb9bb8c13b7dd", + "zh:88644260090aa621e7e8083585c468c8dd5e09a3c01a432fb05da5c4623af940", + "zh:a248f650d174a883b32c5b94f9e725f4057e623b00f171936dcdcc840fad0b3e", + "zh:aa498c1f1ab93be5c8fbf6d48af51dc6ef0f10b2ea88d67bcb9f02d1d80d3930", + "zh:bf01e0f2ec2468c53596e027d376532a2d30feb72b0b5b810334d043109ae32f", + "zh:c46fa84cc8388e5ca87eb575a534ebcf68819c5a5724142998b487cb11246654", + "zh:d0c0f15ffc115c0965cbfe5c81f18c2e114113e7a1e6829f6bfd879ce5744fbb", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "2.38.0" + constraints = "~> 2.0, >= 2.10.0" + hashes = [ + "h1:soK8Lt0SZ6dB+HsypFRDzuX/npqlMU6M0fvyaR1yW0k=", + "zh:0af928d776eb269b192dc0ea0f8a3f0f5ec117224cd644bdacdc682300f84ba0", + "zh:1be998e67206f7cfc4ffe77c01a09ac91ce725de0abaec9030b22c0a832af44f", + "zh:326803fe5946023687d603f6f1bab24de7af3d426b01d20e51d4e6fbe4e7ec1b", + "zh:4a99ec8d91193af961de1abb1f824be73df07489301d62e6141a656b3ebfff12", + "zh:5136e51765d6a0b9e4dbcc3b38821e9736bd2136cf15e9aac11668f22db117d2", + "zh:63fab47349852d7802fb032e4f2b6a101ee1ce34b62557a9ad0f0f0f5b6ecfdc", + "zh:924fb0257e2d03e03e2bfe9c7b99aa73c195b1f19412ca09960001bee3c50d15", + "zh:b63a0be5e233f8f6727c56bed3b61eb9456ca7a8bb29539fba0837f1badf1396", + "zh:d39861aa21077f1bc899bc53e7233262e530ba8a3a2d737449b100daeb303e4d", + "zh:de0805e10ebe4c83ce3b728a67f6b0f9d18be32b25146aa89116634df5145ad4", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + "zh:faf23e45f0090eef8ba28a8aac7ec5d4fdf11a36c40a8d286304567d71c1e7db", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.8.1" + constraints = "~> 3.1, >= 3.5.0, ~> 3.5" + hashes = [ + "h1:u8AKlWVDTH5r9YLSeswoVEjiY72Rt4/ch7U+61ZDkiQ=", + "zh:08dd03b918c7b55713026037c5400c48af5b9f468f483463321bd18e17b907b4", + "zh:0eee654a5542dc1d41920bbf2419032d6f0d5625b03bd81339e5b33394a3e0ae", + "zh:229665ddf060aa0ed315597908483eee5b818a17d09b6417a0f52fd9405c4f57", + "zh:2469d2e48f28076254a2a3fc327f184914566d9e40c5780b8d96ebf7205f8bc0", + "zh:37d7eb334d9561f335e748280f5535a384a88675af9a9eac439d4cfd663bcb66", + "zh:741101426a2f2c52dee37122f0f4a2f2d6af6d852cb1db634480a86398fa3511", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:a902473f08ef8df62cfe6116bd6c157070a93f66622384300de235a533e9d4a9", + "zh:b85c511a23e57a2147355932b3b6dce2a11e856b941165793a0c3d7578d94d05", + "zh:c5172226d18eaac95b1daac80172287b69d4ce32750c82ad77fa0768be4ea4b8", + "zh:dab4434dba34aad569b0bc243c2d3f3ff86dd7740def373f2a49816bd2ff819b", + "zh:f49fd62aa8c5525a5c17abd51e27ca5e213881d58882fd42fec4a545b53c9699", + ] +} + +provider "registry.terraform.io/isometry/deepmerge" { + version = "1.2.1" + constraints = "~> 1.0" + hashes = [ + "h1:+GBRWyzNYKj47qmSBcV28lrIIgk3Gusj48maI+jrL0Q=", + "zh:13be4c31971addc10e26a003e22b8867dba41737ffbc9de86ed84555c4a539b7", + "zh:320a939a594c8a2563f4c11108e02428e7fda3bd51a4fc2298089299cf23f516", + "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", + "zh:9407fe1f9d332ecbd5252faa5b04f62b7fa47f19efe35a9e92d30ef2c603db45", + "zh:a0af6ad4d4a52a1355df5b73fe38e406f50d9fd5c8af165929a602f906de0ff7", + "zh:a44628f6e9bc612dbdcb013a8cf33aca3893f1697c4492847859f7db5b59c4dd", + "zh:abaaa7d71f7975505824adcebed320aae43fc7e13c901ffc51448f6eb59585fe", + "zh:ad2b1b3f348ad478ee64d294a85835625e560324e58a826530b13f49fafa9bac", + "zh:e8bec252634868283e47ef1208cb89b53c1e0cdd2ad804acf91d0ff368048416", + ] +} diff --git a/azure/examples/migration/README.md b/azure/examples/migration/README.md new file mode 100644 index 00000000..c1b53586 --- /dev/null +++ b/azure/examples/migration/README.md @@ -0,0 +1,478 @@ +# Migration Guide: Azure Terraform Module + +Migrate from the old `azure-old/` monolithic module to the new `materialize-terraform-self-managed` modular approach (`azure/modules/*` + `kubernetes/modules/*`). + +> **Important:** This migration example is a **starting point**, not a turnkey solution. Every deployment is different — your VNet layout, node pool sizing, VM sizes, database configuration, and custom modifications all affect the migration. **You are expected to review and adapt both `main.tf` and `auto-migrate.py` to match your specific infrastructure before running anything.** Always use `--dry-run` first, carefully inspect `terraform plan` output, and never apply changes you don't understand. The migration script modifies Terraform state, which is difficult to undo if done incorrectly. +> +> **Limitations:** This migration supports **single Materialize instance** deployments. If your old configuration defines multiple `materialize_instances`, you'll need to adapt `main.tf` and `auto-migrate.py` manually. This migration also upgrades the azurerm provider from v3 to v4, which may surface additional attribute-level diffs in `terraform plan`. + +## Quick Start + +**Prerequisites:** Terraform CLI (>= 1.8), Python 3.7+, Azure CLI, kubectl, access to your old Terraform state + +```bash +# CRITICAL: Verify old state access first +cd /path/to/old/terraform +terraform state pull | jq '.resources | length' +# Must show 30+ resources + +# Setup +cp -r azure/examples/migration /path/to/new/terraform +cd /path/to/new/terraform +chmod +x auto-migrate.py + +# Configure: copy and edit terraform.tfvars +cp terraform.tfvars.example terraform.tfvars +# Edit terraform.tfvars to match your existing infrastructure: +# - REQUIRED: subscription_id, name_prefix, license_key, materialize_instance_name +# - REQUIRED: old_db_password, external_login_password_mz_system +# - INFRA: location, vnet_address_space, subnet CIDRs, service_cidr +# - SIZING: node pool VM sizes and min/max node counts +# - DATABASE: postgres_version, database_sku_name, database_storage_mb + +# Test migration (dry-run) +./auto-migrate.py /path/to/old/terraform . --dry-run + +# Run migration +./auto-migrate.py /path/to/old/terraform . + +# Verify and apply +terraform init +terraform plan +terraform apply + +# Health check +kubectl get materialize -A +terraform output load_balancer_details +``` + +**Expected Results:** +- **Dry-run**: ~30 resources total, ~20+ moved, ~5-8 skipped (data sources, cert-manager manifests due to provider type change, key vault), 0 failed +- **Terraform plan**: ~4-6 additions (self-signed cert `kubectl_manifest` resources, materialize instance `kubectl_manifest`, federated identity credential), some minor updates +- **Preserved**: AKS cluster, PostgreSQL server, VNet, storage account, all Materialize instances + +## Migration Checklist + +Track your progress: + +- [ ] **Prerequisites** - Verify Terraform version, state access, backup current state +- [ ] **Prepare** - Copy migration example, configure terraform.tfvars +- [ ] **Customize terraform.tfvars** - Set all required variables (see `terraform.tfvars.example`) +- [ ] **Review main.tf and auto-migrate.py** - Verify they match your infrastructure; adapt if you have custom modifications +- [ ] **Dry Run** - Test with `--dry-run`, review output carefully, verify 0 failures +- [ ] **Migrate State** - Run auto-migrate.py (handles state moves between old and new) +- [ ] **Verify Plan** - Check terraform plan shows only safe changes +- [ ] **Apply** - Run terraform apply +- [ ] **Health Check** - Verify Materialize is running and accessible +- [ ] **Cleanup** - Remove `.migration-work/` after verification, delete old Key Vault manually + +## Critical: State Access Requirement + +**The migration script requires access to your old Terraform state.** It cannot migrate infrastructure that isn't in state. + +**Verify before migration:** +```bash +cd /path/to/old/terraform +terraform state pull | jq '.resources | length' +# Should show 30+ resources +# If <10, your state isn't accessible +``` + +**Common issues:** +- **Remote backend not configured**: State is in Azure Storage or Terraform Cloud but no backend config in directory + - Fix: Ensure backend configuration in `backend.tf` or `versions.tf` +- **Not initialized**: Old directory needs `terraform init` +- **Wrong directory**: Pointing to module source instead of deployment directory +- **State migrated elsewhere**: Reconfigure old directory to point to actual backend + +**What happens with incomplete state:** +- Migration fails with validation error (safety check) +- If bypassed, terraform apply attempts to create all resources +- Results in "already exists" errors everywhere + +## Configuring terraform.tfvars + +All migration configuration is done via `terraform.tfvars`. Copy `terraform.tfvars.example` and update the values to match your existing infrastructure. You should **not** need to edit `main.tf`. + +See `terraform.tfvars.example` for the full list with inline documentation and Azure CLI commands to discover each value. + +**Required variables** (no defaults - must be set): + +| Variable | Description | How to Find | +|----------|-------------|-------------| +| `subscription_id` | Azure subscription ID | `az account show --query id -o tsv` | +| `resource_group_name` | Existing resource group name | `az group list --query "[?tags.module=='materialize'].name" -o tsv` | +| `name_prefix` | Prefix used for all resource names | If your AKS cluster is `mycompany-aks`, prefix is `mycompany` | +| `license_key` | Materialize license key | From your old config or https://materialize.com/register | +| `materialize_instance_name` | Your Materialize instance name | `kubectl get materialize -A -o jsonpath='{.items[0].metadata.name}'` | +| `old_db_password` | Existing PostgreSQL database password | From your old `terraform.tfvars` (`database_config.password`) | +| `external_login_password_mz_system` | Existing mz_system user password | From old config (`external_login_password_mz_system`) | + +**Infrastructure variables** (have defaults matching old module - verify they match yours): + +| Variable | Default | How to Verify | +|----------|---------|---------------| +| `location` | `eastus2` | `az aks show --name -aks --resource-group --query location -o tsv` | +| `vnet_address_space` | `10.0.0.0/16` | `az network vnet show --name -vnet --resource-group --query addressSpace.addressPrefixes[0] -o tsv` | +| `kubernetes_version` | `null` (see note) | `az aks show --name -aks --resource-group --query kubernetesVersion -o tsv` | +| `service_cidr` | `10.1.0.0/16` | `az aks show --name -aks --resource-group --query networkProfile.serviceCidr -o tsv` | +| `database_sku_name` | `GP_Standard_D2s_v3` | `az postgres flexible-server show --name --resource-group --query sku.name -o tsv` | +| `environmentd_version` | `null` (see note) | `kubectl get materialize -A -o jsonpath='{.items[0].spec.environmentdImageRef}'` (extract version tag) | + +**Important:** `kubernetes_version` and `environmentd_version` default to `null`. If not set: +- `kubernetes_version = null` means "latest recommended" — terraform plan may show a Kubernetes version upgrade. **Set this to match your current version.** +- `environmentd_version = null` means the module uses its built-in default (which may not match your running version). **Set this to your current version tag** (e.g., `v0.130.0`). + +**Data-path critical variables** - getting these wrong means Materialize won't find its existing data: +- `old_db_password` - used in `metadata_backend_url` to connect to the existing PostgreSQL server +- `materialize_instance_name` - used as the Materialize instance name and in backend URL construction +- `database_name` - used in `metadata_backend_url` (default: `materialize`) + +## What Changed: Old vs New Module + +| Aspect | Old Module | New Module | Migration Impact | +|--------|-----------|------------|------------------| +| **Structure** | Monolithic (`azure-old/`) | Modular (`azure/modules/*` + `kubernetes/modules/*`) | State paths change | +| **Provider** | azurerm >= 3.75 | azurerm 4.54.0 | Provider upgrade | +| **AKS Network** | `network_plugin=azure`, `network_policy=azure` | New module defaults to `cilium` | Migration keeps old config inline | +| **AKS Outbound** | Default `loadBalancer` | New module uses `userAssignedNATGateway` | Migration keeps old config inline | +| **Networking** | Direct VNet/Subnet resources | New uses Azure Verified Module (AVM) + NAT gateway | Migration keeps old resources inline | +| **Database Naming** | `{prefix}-{random}-pg` | New module uses `{prefix}-pg` | Migration keeps old naming inline | +| **Storage Auth** | SAS tokens via Key Vault | Workload identity via federated credential | SAS tokens dropped, workload identity added | +| **Certificates** | `module.certificates` with `kubernetes_manifest` | `cert_manager` + `self_signed_cluster_issuer` with `kubectl_manifest` | Module rename, cert manifests recreated | +| **Operator** | External GitHub source with `count` (`module.operator[0]`) | Local module without count (`module.operator`) | `[0]` index removed | +| **Instances** | Managed by operator module | Separate `materialize-instance` module | Namespace + secret moved | +| **Load Balancers** | `for_each` on instances | Direct call (single instance) | `for_each` key removed | +| **CoreDNS** | Not managed | Optional module | Commented out in migration | +| **DB Password** | `random_password` resource | Explicit variable (`old_db_password`) | Must provide existing password | +| **TLS Config** | In operator Helm values | Passed via `helm_values` variable | Configured in migration `main.tf` | + +Most changes are state path updates (no infrastructure changes). Resources with provider type changes (`kubernetes_manifest` to `kubectl_manifest`) are skipped during state migration and created fresh — `kubectl apply` adopts the existing Kubernetes resources with zero disruption. + +**Why inline resources?** The new AKS module hardcodes `outbound_type = "userAssignedNATGateway"` and `network_policy = "cilium"`, both of which force AKS cluster recreation. The new networking module creates a NAT gateway that doesn't exist in old setups. The new database module changes server naming. To avoid destructive changes, the migration config defines these resources inline with the exact old configuration. + +## How Auto-Migration Works + +The `auto-migrate.py` script: + +1. **Validates state access** - Ensures old state has actual infrastructure (30+ resources) +2. **Analyzes structure** - Auto-detects if you wrapped the module (e.g., `module "mz" { ... }`) +3. **Applies transformation rules**: + - Moves networking from module to root: `module.networking.*` → `*` + - Moves AKS from module to root: `module.aks.*` → `*` + - Renames AKS system nodepool: `module.aks.*.materialize` → `*.system` + - Moves database from module to root: `module.database.*` → `*` + - Keeps materialize nodepool paths unchanged + - Keeps storage module (skips Key Vault resources) + - Renames certificates: `module.certificates.*` → `module.cert_manager.*` + - Removes `[0]` from operator: `module.operator[0].*` → `module.operator.*` + - Moves instance resources to `module.materialize_instance.*` + - Removes `for_each` from load balancers: `module.load_balancers["name"].*` → `module.load_balancers.*` + - Skips all data sources (automatically recreated) + - Skips `kubernetes_manifest` resources (recreated as `kubectl_manifest`) +4. **Validates migrated state** (read-only check for potential issues) +5. **Migrates resources** - Uses `terraform state mv` between local state files +6. **Pushes updated states** - Updates both old and new remote backends + +**Work files** (in `.migration-work/`): +- `old-state-backup-TIMESTAMP.tfstate` - Original backup for emergency restore +- `old.tfstate`, `new.tfstate` - Working copies +- Never modifies original state files directly + +## Expected Changes After Migration + +**Additions** (new resources that adopt existing Kubernetes objects): +1. **3 self-signed cert resources** (`kubectl_manifest`) - Adopts existing cert-manager K8s resources (self-signed issuer, root CA certificate, root CA cluster issuer) +2. **1 materialize instance** (`kubectl_manifest`) - Adopts existing Materialize CRD instance +3. **1 federated identity credential** (`azurerm_federated_identity_credential`) - Enables workload identity for storage access (replaces SAS token auth) + +**Safe updates** (no infrastructure replacement): +1. **Storage account network rules** - `default_action` changes from `Allow` to `Deny` (more secure, subnets still allowed) +2. **Storage account TLS** - `min_tls_version` set to `TLS1_2` (was unset) +3. **Backend secret** - Updated `persist_backend_url` (SAS token removed, workload identity used instead) +4. **Operator helm release** - Helm values updated with new structure (node selectors, license key checks, etc.) + +**Preserved** (no replacement): +- AKS cluster (network_plugin, network_policy, outbound_type all preserved) +- PostgreSQL Flexible Server (server name with random suffix preserved) +- VNet, subnets, DNS zone +- Storage account and container +- Node pools (system + materialize) +- Operator and cert-manager helm releases +- All Materialize instances and data + +**STOP if you see these being destroyed:** +- AKS Cluster (`azurerm_kubernetes_cluster.aks`) +- PostgreSQL Server (`azurerm_postgresql_flexible_server.postgres`) +- Storage Account (`module.storage.azurerm_storage_account.materialize`) +- Materialize instances +- VNet or subnets + +If critical infrastructure is being destroyed, verify your `terraform.tfvars` values match your existing infrastructure. + +## Post-Migration: Optional Improvements + +After migration is verified and stable, you can gradually adopt new module features: + +- **Switch to new AKS module** — Adopts NAT gateway outbound and cilium networking (requires cluster recreation — plan maintenance window) +- **Switch to new networking module** — Uses Azure Verified Module (AVM) with NAT gateway +- **Switch to new database module** — Drops random suffix naming (requires DB migration) +- **Uncomment CoreDNS module** — Manage CoreDNS via Terraform (AKS manages it by default) +- **Add node taints** on the Materialize node pool for workload isolation +- **Add node selectors** to operator and cert-manager for scheduling control +- **Delete old Key Vault** — The old SAS token Key Vault is no longer needed (see Cleanup section) + +## Troubleshooting + +| Problem | Solution | +|---------|----------| +| State pull fails | Check Azure credentials (`az login`), backend config, run `terraform init` in old directory | +| "Only X resources" error (X < 10) | Old state not accessible — verify backend configuration | +| Provider version mismatch | Old module uses azurerm v3, new uses v4. Run `terraform init -upgrade` in new directory | +| AKS cluster being recreated | Verify `main.tf` has `network_plugin = "azure"`, `network_policy = "azure"`, no `outbound_type` | +| Database server being recreated | Verify `random_string.postgres_name_suffix` is in state (preserves `{prefix}-{random}-pg` naming) | +| Helm releases timing out | Remove from state: `terraform state rm 'module.operator.helm_release.materialize_operator'` etc. See [Helm Releases](#helm-releases-timing-out) | +| Storage "already exists" error | Import: `terraform import 'module.storage.azurerm_storage_account.materialize' '/subscriptions/.../storageAccounts/'` | +| Materialize can't find data | Verify `old_db_password`, `database_name`, and `materialize_instance_name` in `terraform.tfvars` | +| TLS errors after migration | Verify `use_self_signed_cluster_issuer` matches old module setting | +| Plan shows Kubernetes version change | Set `kubernetes_version` in terraform.tfvars to match your cluster (null means "latest") | +| Plan shows environmentd version change | Set `environmentd_version` in terraform.tfvars to your current version tag | +| Node pool rotation in plan | Verify `tags` in terraform.tfvars includes `managed_by = "terraform"` and `module = "materialize"` (used as node labels) | +| Plan shows too many destroys | Review `terraform.tfvars` values, ensure they match existing infrastructure | + +**If script fails with "No transformation rule":** +- Your setup has custom resources not in the standard module +- Add custom transformation rules to `auto-migrate.py` (see [Manual Migration](#manual-migration-for-custom-setups)) + +### Data Path Matching + +The most critical part of migration is ensuring Materialize connects to the same data. These values must match exactly: + +**metadata_backend_url** (PostgreSQL connection): +``` +postgres://user:password@host/DATABASE_NAME?sslmode=require +``` +- `user` = `database_username` variable (default: `materialize`) +- `password` = `old_db_password` variable (your existing database password) +- `DATABASE_NAME` = `database_name` variable (default: `materialize`) + +**persist_backend_url** (Azure Blob Storage path): +``` +https://.blob.core.windows.net/materialize +``` +- The new format uses workload identity instead of SAS tokens +- The storage container name must be `materialize` (default) +- Materialize uses the same blob paths internally regardless of auth method + +**external_login_password_mz_system** (login password): +- Must be your existing password, not a new random one +- Get from old config: look for `external_login_password_mz_system` in your old `terraform.tfvars` + +### Helm Releases Timing Out + +If helm releases (materialize_operator, cert_manager) timeout during `terraform apply`: +- They're already installed and working in your cluster +- Terraform is trying to update them (which is unnecessary) + +**Quick fix - Remove from Terraform state:** +```bash +# These helm releases will continue running, but Terraform stops managing them +terraform state rm 'module.operator.helm_release.materialize_operator' +terraform state rm 'module.cert_manager.helm_release.cert_manager' + +# Now apply will skip these resources +terraform apply +``` + +**Why this works:** +- Helm releases are already running and functional +- Removing from state doesn't delete them from Kubernetes +- They'll continue running independently +- You can manage them manually via `helm upgrade` if needed + +## Rollback Procedure + +**Before terraform apply** (if you need to abort): +```bash +cd /path/to/old/terraform +terraform state push /path/to/new/terraform/.migration-work/old-state-backup-*.tfstate +terraform plan # Should show no changes +``` + +**After terraform apply** (if something went wrong): +```bash +cd /path/to/old/terraform +terraform state push /path/to/new/terraform/.migration-work/old-state-backup-*.tfstate +terraform apply # Restore original configuration +``` + +**Emergency restore** (if backends are corrupted): +1. Locate backup file: `.migration-work/old-state-backup-TIMESTAMP.tfstate` +2. Push to backend: `terraform state push ` +3. Verify: `terraform plan` should match pre-migration state + +## Post-Migration Verification + +```bash +# 1. Check Terraform state +terraform state list | wc -l +# Should show ~25-30 resources + +# 2. Verify Materialize instances +kubectl get materialize -A +# Should show STATUS: running + +# 3. Check pods +kubectl get pods -n materialize-environment +# All should be Running or Completed + +# 4. Test connectivity +terraform output load_balancer_details +# Use the balancerd_ip to connect: +psql -h -p 6875 -U mz_system -d materialize +# Use: terraform output -raw external_login_password_mz_system + +# 5. Verify storage +az storage container list --account-name $(terraform output -json storage | jq -r '.name') --auth-mode login +# Should show the "materialize" container + +# 6. Check PostgreSQL +terraform output database +# Verify server name matches your existing server +``` + +**Success criteria:** +- Auto-migrate.py shows 0 failures +- Terraform plan shows only expected changes (additions for `kubectl_manifest`, federated identity credential) +- Terraform apply succeeds +- `kubectl get materialize -A` shows STATUS: running +- Can connect via psql to load balancer endpoint +- Test queries return expected data (verify your tables/views are present) + +## Manual Migration (For Custom Setups) + +If you've heavily customized the old module or the automated script doesn't work, you can: + +### Option 1: Customize the Automated Script (Recommended) + +Add custom transformation rules to `auto-migrate.py` for your modifications: + +```python +# In _build_rules() method, add your rules FIRST (before default rules) + +# Example: Custom module wrapper +MigrationRule( + pattern=r'^module\.my_wrapper\.module\.materialize\.(.+)$', + transform=lambda m: m.group(1), # Remove wrapper + description="Remove custom wrapper module" +), + +# Example: Custom resource to skip +MigrationRule( + pattern=r'^module\.custom_monitoring\..*$', + transform=lambda m: None, # None means skip + description="Skip custom monitoring resources" +), + +# Example: Rename custom module +MigrationRule( + pattern=r'^module\.my_custom_name\.(.+)$', + transform=lambda m: f'module.networking.{m.group(1)}', + description="Rename custom module to networking" +), +``` + +**Test your rules:** +```bash +./auto-migrate.py /path/to/old/terraform . --dry-run +``` + +Check output for: +- `No transformation rule` - Add rules for these +- Incorrect transformations - Adjust regex patterns +- Resources that should skip but aren't - Add skip rules + +### Option 2: Full Manual Migration + +For one-off heavily customized setups where automation isn't practical. + +**Manual process:** + +```bash +# 1. Inventory resources +cd /path/to/old/terraform +terraform state list > resources.txt + +# 2. Create work directory +mkdir migration-work +cd migration-work + +# 3. Pull both states +terraform -chdir=../old state pull > old.tfstate +terraform -chdir=../new state pull > new.tfstate + +# 4. Backup +cp old.tfstate old-backup.tfstate +cp new.tfstate new-backup.tfstate + +# 5. Move resources one by one +terraform state mv -state=old.tfstate -state-out=new.tfstate \ + 'module.networking.azurerm_virtual_network.vnet' \ + 'azurerm_virtual_network.vnet' + +# Repeat for all resources in resources.txt +# Adjust paths based on transformation rules in auto-migrate.py + +# 6. Push updated states +terraform -chdir=../old state push old.tfstate +terraform -chdir=../new state push new.tfstate + +# 7. Verify +cd ../new +terraform plan +``` + +**Reference transformation rules from auto-migrate.py:** +- `module.networking.*` → root level (remove module prefix) +- `module.aks.*` → root level (remove module prefix) +- `module.aks.azurerm_kubernetes_cluster_node_pool.materialize` → `azurerm_kubernetes_cluster_node_pool.system` (rename) +- `module.database.*` → root level (remove module prefix) +- `module.materialize_nodepool.*` → keep unchanged +- `module.storage.*` → keep unchanged (skip `azurerm_key_vault`) +- `module.certificates.kubernetes_namespace.cert_manager[0]` → `module.cert_manager.kubernetes_namespace.cert_manager` +- `module.certificates.helm_release.cert_manager[0]` → `module.cert_manager.helm_release.cert_manager` +- `module.operator[0].*` → `module.operator.*` (remove `[0]`) +- `module.operator[0].kubernetes_namespace.instance_namespaces[*]` → `module.materialize_instance.kubernetes_namespace.instance[0]` +- `module.operator[0].kubernetes_secret.materialize_backends[*]` → `module.materialize_instance.kubernetes_secret.materialize_backend` +- `module.load_balancers["name"].*` → `module.load_balancers.*` (remove for_each key) +- Skip all data sources (recreated automatically) +- Skip `kubernetes_manifest` resources (recreated as `kubectl_manifest`) + +## Cleanup + +**After verifying everything works:** + +```bash +# Remove migration work directory +rm -rf .migration-work/ + +# (Optional) Clean up old Terraform directory +cd /path/to/old/terraform +rm -rf .terraform terraform.tfstate* +``` + +**Manual cleanup - Old Key Vault:** + +The old module created an Azure Key Vault for SAS token storage. This is no longer needed (workload identity replaces SAS tokens). The Key Vault is skipped during migration (not moved to new state), so it remains in Azure but is not managed by Terraform. + +To delete it manually: +```bash +# Find the old key vault +az keyvault list --resource-group --query "[?contains(name, 'sas')].name" -o tsv + +# Delete it (soft-delete) +az keyvault delete --name --resource-group + +# Purge it (permanent delete) +az keyvault purge --name +``` diff --git a/azure/examples/migration/main.tf b/azure/examples/migration/main.tf new file mode 100644 index 00000000..366fdd08 --- /dev/null +++ b/azure/examples/migration/main.tf @@ -0,0 +1,615 @@ +# ============================================================================= +# Migration Reference Configuration +# ============================================================================= +# +# This file migrates from the old monolithic Azure module (azure-old/) to the +# new modular architecture (azure/modules/* + kubernetes/modules/*). +# +# MIGRATION STRATEGY FOR ZERO-DOWNTIME +# ============================================================================= +# +# Infrastructure resources (networking, AKS, database) are defined INLINE +# to preserve exact configuration and avoid breaking changes: +# +# 1. AKS cluster: The new AKS module changes outbound_type to NAT gateway +# and network_policy to cilium - both force cluster recreation. Inline +# preserves the existing cluster configuration exactly. +# +# 2. Networking: The new module uses Azure Verified Module (AVM) for VNet +# with NAT gateway. Inline avoids AVM nested state and NAT gateway. +# +# 3. Database: The old module names servers {prefix}-{random}-pg. The new +# module uses {prefix}-pg. Inline preserves the random naming. +# +# After migration, you can gradually adopt new modules: +# - Switch to new AKS module (with NAT gateway + cilium) +# - Switch to new networking module (with AVM) +# - Migrate database to new module +# +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Providers +# ----------------------------------------------------------------------------- + +provider "azurerm" { + subscription_id = var.subscription_id + + features { + resource_group { + prevent_deletion_if_contains_resources = false + } + key_vault { + purge_soft_delete_on_destroy = true + recover_soft_deleted_key_vaults = false + } + } +} + +provider "kubernetes" { + host = azurerm_kubernetes_cluster.aks.kube_config[0].host + client_certificate = base64decode(azurerm_kubernetes_cluster.aks.kube_config[0].client_certificate) + client_key = base64decode(azurerm_kubernetes_cluster.aks.kube_config[0].client_key) + cluster_ca_certificate = base64decode(azurerm_kubernetes_cluster.aks.kube_config[0].cluster_ca_certificate) +} + +provider "helm" { + kubernetes { + host = azurerm_kubernetes_cluster.aks.kube_config[0].host + client_certificate = base64decode(azurerm_kubernetes_cluster.aks.kube_config[0].client_certificate) + client_key = base64decode(azurerm_kubernetes_cluster.aks.kube_config[0].client_key) + cluster_ca_certificate = base64decode(azurerm_kubernetes_cluster.aks.kube_config[0].cluster_ca_certificate) + } +} + +provider "kubectl" { + host = azurerm_kubernetes_cluster.aks.kube_config[0].host + client_certificate = base64decode(azurerm_kubernetes_cluster.aks.kube_config[0].client_certificate) + client_key = base64decode(azurerm_kubernetes_cluster.aks.kube_config[0].client_key) + cluster_ca_certificate = base64decode(azurerm_kubernetes_cluster.aks.kube_config[0].cluster_ca_certificate) + + load_config_file = false +} + +# ----------------------------------------------------------------------------- +# Resource Group (existing — data source, not created) +# ----------------------------------------------------------------------------- + +data "azurerm_resource_group" "materialize" { + name = var.resource_group_name +} + +# ----------------------------------------------------------------------------- +# Networking (INLINE — preserves old module resources exactly) +# ----------------------------------------------------------------------------- +# State paths after migration: +# azurerm_virtual_network.vnet +# azurerm_subnet.aks +# azurerm_subnet.postgres +# random_id.dns_zone_suffix +# azurerm_private_dns_zone.postgres +# azurerm_private_dns_zone_virtual_network_link.postgres + +resource "azurerm_virtual_network" "vnet" { + name = "${var.name_prefix}-vnet" + resource_group_name = data.azurerm_resource_group.materialize.name + location = var.location + address_space = [var.vnet_address_space] + tags = local.common_labels +} + +resource "azurerm_subnet" "aks" { + name = "${var.name_prefix}-aks-subnet" + resource_group_name = data.azurerm_resource_group.materialize.name + virtual_network_name = azurerm_virtual_network.vnet.name + address_prefixes = [var.aks_subnet_cidr] + + service_endpoints = ["Microsoft.Storage", "Microsoft.Sql"] +} + +resource "azurerm_subnet" "postgres" { + name = "${var.name_prefix}-pg-subnet" + resource_group_name = data.azurerm_resource_group.materialize.name + virtual_network_name = azurerm_virtual_network.vnet.name + address_prefixes = [var.postgres_subnet_cidr] + + service_endpoints = ["Microsoft.Storage"] + + delegation { + name = "postgres-delegation" + service_delegation { + name = "Microsoft.DBforPostgreSQL/flexibleServers" + actions = [ + "Microsoft.Network/virtualNetworks/subnets/join/action", + ] + } + } +} + +resource "random_id" "dns_zone_suffix" { + byte_length = 4 +} + +resource "azurerm_private_dns_zone" "postgres" { + name = "materialize${random_id.dns_zone_suffix.hex}.postgres.database.azure.com" + resource_group_name = data.azurerm_resource_group.materialize.name + tags = local.common_labels +} + +resource "azurerm_private_dns_zone_virtual_network_link" "postgres" { + name = "${var.name_prefix}-pg-dns-link" + private_dns_zone_name = azurerm_private_dns_zone.postgres.name + resource_group_name = data.azurerm_resource_group.materialize.name + virtual_network_id = azurerm_virtual_network.vnet.id + registration_enabled = true + tags = local.common_labels +} + +# ----------------------------------------------------------------------------- +# AKS Cluster (INLINE — preserves old cluster config exactly) +# ----------------------------------------------------------------------------- +# State paths after migration: +# azurerm_user_assigned_identity.aks_identity +# azurerm_role_assignment.aks_network_contributer +# azurerm_user_assigned_identity.workload_identity +# azurerm_kubernetes_cluster.aks +# azurerm_kubernetes_cluster_node_pool.system +# +# MIGRATION: The old AKS module used: +# - network_plugin = "azure", network_policy = "azure" (no cilium) +# - No outbound_type (defaults to loadBalancer, no NAT gateway) +# - A separate "materialize" node pool for system workloads +# These are preserved exactly to avoid cluster recreation. + +resource "azurerm_user_assigned_identity" "aks_identity" { + name = "${var.name_prefix}-aks-identity" + resource_group_name = data.azurerm_resource_group.materialize.name + location = var.location + tags = local.common_labels +} + +data "azurerm_subscription" "current" {} + +resource "azurerm_role_assignment" "aks_network_contributer" { + scope = "/subscriptions/${data.azurerm_subscription.current.subscription_id}/resourceGroups/${data.azurerm_resource_group.materialize.name}/providers/Microsoft.Network/virtualNetworks/${azurerm_virtual_network.vnet.name}/subnets/${azurerm_subnet.aks.name}" + role_definition_name = "Network Contributor" + principal_id = azurerm_user_assigned_identity.aks_identity.principal_id +} + +resource "azurerm_user_assigned_identity" "workload_identity" { + name = "${var.name_prefix}-workload-identity" + resource_group_name = data.azurerm_resource_group.materialize.name + location = var.location + tags = local.common_labels +} + +resource "azurerm_kubernetes_cluster" "aks" { + name = "${var.name_prefix}-aks" + resource_group_name = data.azurerm_resource_group.materialize.name + location = var.location + dns_prefix = "${var.name_prefix}-aks" + kubernetes_version = var.kubernetes_version + + default_node_pool { + temporary_name_for_rotation = "default2" + name = "default" + vm_size = var.default_node_pool_vm_size + node_count = 1 + vnet_subnet_id = azurerm_subnet.aks.id + + upgrade_settings { + max_surge = "10%" + drain_timeout_in_minutes = 0 + node_soak_duration_in_minutes = 0 + } + } + + identity { + type = "UserAssigned" + identity_ids = [azurerm_user_assigned_identity.aks_identity.id] + } + + oidc_issuer_enabled = true + workload_identity_enabled = true + + # MIGRATION: Matches old module's network configuration exactly. + # Do NOT change network_plugin, network_policy, or add outbound_type — + # these changes force AKS cluster recreation. + network_profile { + network_plugin = "azure" + network_policy = "azure" + service_cidr = var.service_cidr + dns_service_ip = cidrhost(var.service_cidr, 10) + } + + tags = local.common_labels + + depends_on = [ + azurerm_role_assignment.aks_network_contributer, + ] +} + +# MIGRATION: The old AKS module had a separate "materialize" node pool +# for system workloads. This preserves it as "system" to avoid losing +# the nodes that run system pods. +resource "azurerm_kubernetes_cluster_node_pool" "system" { + name = substr(replace(var.name_prefix, "-", ""), 0, 12) + temporary_name_for_rotation = "${substr(replace(var.name_prefix, "-", ""), 0, 12)}2" + kubernetes_cluster_id = azurerm_kubernetes_cluster.aks.id + vm_size = var.system_node_pool_vm_size + auto_scaling_enabled = true + min_count = var.system_node_pool_min_nodes + max_count = var.system_node_pool_max_nodes + vnet_subnet_id = azurerm_subnet.aks.id + os_disk_size_gb = var.system_node_pool_disk_size_gb + + node_labels = { + "workload" = "system" + } + + upgrade_settings { + max_surge = "10%" + drain_timeout_in_minutes = 0 + node_soak_duration_in_minutes = 0 + } + + tags = local.common_labels +} + +# ----------------------------------------------------------------------------- +# Materialize Node Pool +# ----------------------------------------------------------------------------- +# State path: module.materialize_nodepool.* + +module "materialize_nodepool" { + source = "../../modules/nodepool" + + prefix = "${var.name_prefix}-mz-swap" + cluster_id = azurerm_kubernetes_cluster.aks.id + subnet_id = azurerm_subnet.aks.id + + autoscaling_config = { + enabled = true + min_nodes = var.materialize_node_pool_min_nodes + max_nodes = var.materialize_node_pool_max_nodes + node_count = null + } + + vm_size = var.materialize_node_pool_vm_size + disk_size_gb = var.materialize_node_pool_disk_size_gb + swap_enabled = true + + # MIGRATION: Pin disk setup image to match old module version. + # The new module defaults to v0.4.1 but old used v0.4.0. + disk_setup_image = var.disk_setup_image + + labels = local.common_labels + tags = local.common_labels + + depends_on = [azurerm_kubernetes_cluster.aks] +} + +# ----------------------------------------------------------------------------- +# Database (INLINE — preserves old {prefix}-{random}-pg naming) +# ----------------------------------------------------------------------------- +# State paths after migration: +# random_string.postgres_name_suffix +# azurerm_postgresql_flexible_server.postgres +# azurerm_postgresql_flexible_server_database.materialize + +resource "random_string" "postgres_name_suffix" { + length = 4 + special = false + upper = false +} + +resource "azurerm_postgresql_flexible_server" "postgres" { + name = "${var.name_prefix}-${random_string.postgres_name_suffix.result}-pg" + resource_group_name = data.azurerm_resource_group.materialize.name + location = var.location + version = var.postgres_version + delegated_subnet_id = azurerm_subnet.postgres.id + private_dns_zone_id = azurerm_private_dns_zone.postgres.id + + public_network_access_enabled = false + + administrator_login = var.database_username + administrator_password = var.old_db_password + + storage_mb = var.database_storage_mb + sku_name = var.database_sku_name + + backup_retention_days = 7 + + lifecycle { + ignore_changes = [ + zone + ] + } + + tags = local.common_labels + + depends_on = [ + azurerm_private_dns_zone_virtual_network_link.postgres, + ] +} + +resource "azurerm_postgresql_flexible_server_database" "materialize" { + name = var.database_name + server_id = azurerm_postgresql_flexible_server.postgres.id +} + +# ----------------------------------------------------------------------------- +# Storage +# ----------------------------------------------------------------------------- +# State path: module.storage.* +# +# MIGRATION: Same resources as old module (storage account, container, +# random_string, role_assignment) PLUS new federated identity credential +# for workload identity. Old key vault and SAS tokens are dropped. + +module "storage" { + source = "../../modules/storage" + + resource_group_name = data.azurerm_resource_group.materialize.name + location = var.location + prefix = var.name_prefix + workload_identity_principal_id = azurerm_user_assigned_identity.workload_identity.principal_id + subnets = [azurerm_subnet.aks.id] + container_name = "materialize" + + # Workload identity federation configuration + workload_identity_id = azurerm_user_assigned_identity.workload_identity.id + oidc_issuer_url = azurerm_kubernetes_cluster.aks.oidc_issuer_url + service_account_namespace = local.materialize_instance_namespace + service_account_name = local.materialize_instance_name + + # MIGRATION: Old module used "Allow" as default_action for network rules. + # The new module defaults to "Deny" (more secure). We preserve "Allow" to + # avoid changes during migration. You can switch to "Deny" post-migration. + network_rules_default_action = "Allow" + + storage_account_tags = local.common_labels + + depends_on = [azurerm_kubernetes_cluster.aks] +} + +# ----------------------------------------------------------------------------- +# Certificate Manager +# ----------------------------------------------------------------------------- +# State path: module.cert_manager.* +# +# MIGRATION: Renamed from module.certificates → module.cert_manager. +# The cert-manager namespace and helm release are state-moved. +# Self-signed issuer resources are skipped (type change to kubectl_manifest). + +module "cert_manager" { + source = "../../../kubernetes/modules/cert-manager" + + # MIGRATION: Match old module's chart version to avoid unintended upgrades. + chart_version = var.cert_manager_chart_version + + # MIGRATION: Old module didn't set node_selector for cert-manager. + # Leave empty to match old behavior. + node_selector = {} + + depends_on = [ + azurerm_kubernetes_cluster.aks, + ] +} + +module "self_signed_cluster_issuer" { + count = var.use_self_signed_cluster_issuer ? 1 : 0 + + source = "../../../kubernetes/modules/self-signed-cluster-issuer" + + name_prefix = var.name_prefix + + depends_on = [ + module.cert_manager, + ] +} + +# ----------------------------------------------------------------------------- +# Materialize Operator +# ----------------------------------------------------------------------------- +# State path: module.operator.* +# +# MIGRATION: The old module used an external GitHub source with count. +# The new module is local. State migration removes the [0] index. + +module "operator" { + source = "../../modules/operator" + + # MIGRATION: The old module named the helm release "${namespace}-${environment}" + # which defaults to "materialize-${prefix}". The new module uses name_prefix + # directly as the helm release name. We pass the old combined name here to + # avoid a helm release replacement (destroy + recreate), but note that an + # operator replacement is generally fine — it's just a controller and any + # brief downtime does not affect running Materialize instances. + name_prefix = "materialize-${var.name_prefix}" + operator_version = var.operator_version + location = var.location + + # MIGRATION: Old module didn't set node selectors or tolerations + # for the operator pod or instance workloads via the operator. + instance_pod_tolerations = [] + instance_node_selector = {} + operator_node_selector = {} + + # AKS has built-in metrics server + install_metrics_server = false + + # MIGRATION: Pass TLS and environmentd configuration via helm_values to match + # old module behavior. The old module configured: + # - TLS via defaultCertificateSpecs + # - environmentd nodeSelector to schedule on swap-enabled nodes + # The new operator module's instance_node_selector applies to ALL workloads + # (environmentd, clusterd, balancerd, console), but the old module only set it + # for environmentd. We pass it via helm_values to match exactly. + helm_values = merge( + { + environmentd = { + nodeSelector = { + "materialize.cloud/swap" = "true" + } + } + }, + var.use_self_signed_cluster_issuer ? { + tls = { + defaultCertificateSpecs = { + balancerdExternal = { + dnsNames = ["balancerd"] + issuerRef = { + name = "${var.name_prefix}-root-ca" + kind = "ClusterIssuer" + } + } + consoleExternal = { + dnsNames = ["console"] + issuerRef = { + name = "${var.name_prefix}-root-ca" + kind = "ClusterIssuer" + } + } + internal = { + issuerRef = { + name = "${var.name_prefix}-root-ca" + kind = "ClusterIssuer" + } + } + } + } + } : {} + ) + + depends_on = [ + azurerm_kubernetes_cluster.aks, + azurerm_postgresql_flexible_server.postgres, + module.storage, + ] +} + +# ----------------------------------------------------------------------------- +# Materialize Instance +# ----------------------------------------------------------------------------- +# State path: module.materialize_instance.* +# +# MIGRATION: Instance resources moved from old operator module to this +# dedicated module. Uses kubectl_manifest (not kubernetes_manifest), +# so the CRD resource is created fresh but adopts the existing K8s resource. + +module "materialize_instance" { + source = "../../../kubernetes/modules/materialize-instance" + instance_name = local.materialize_instance_name + instance_namespace = local.materialize_instance_namespace + + metadata_backend_url = local.metadata_backend_url + persist_backend_url = local.persist_backend_url + + # The password for the external login to the Materialize instance + authenticator_kind = "Password" + external_login_password_mz_system = var.external_login_password_mz_system + + # Azure workload identity annotations for service account + service_account_annotations = { + "azure.workload.identity/client-id" = azurerm_user_assigned_identity.workload_identity.client_id + } + pod_labels = { + "azure.workload.identity/use" = "true" + } + + license_key = var.license_key + + environmentd_version = var.environmentd_version + + force_rollout = var.force_rollout + request_rollout = var.request_rollout + + issuer_ref = var.use_self_signed_cluster_issuer ? { + name = "${var.name_prefix}-root-ca" + kind = "ClusterIssuer" + } : null + + depends_on = [ + azurerm_kubernetes_cluster.aks, + azurerm_postgresql_flexible_server.postgres, + module.storage, + module.self_signed_cluster_issuer, + module.operator, + module.materialize_nodepool, + ] +} + +# ----------------------------------------------------------------------------- +# Load Balancers +# ----------------------------------------------------------------------------- +# State path: module.load_balancers.* +# +# MIGRATION: Old module used for_each on instances. New uses direct call. + +module "load_balancers" { + source = "../../modules/load_balancers" + + instance_name = local.materialize_instance_name + namespace = local.materialize_instance_namespace + resource_id = module.materialize_instance.instance_resource_id + internal = var.internal_load_balancer + ingress_cidr_blocks = var.internal_load_balancer ? null : var.ingress_cidr_blocks + + depends_on = [ + module.materialize_instance, + ] +} + +# ----------------------------------------------------------------------------- +# CoreDNS (COMMENTED OUT — new feature, not in old setup) +# ----------------------------------------------------------------------------- +# MIGRATION: CoreDNS module is new. AKS manages CoreDNS by default. +# After migration is verified, uncomment to manage CoreDNS via Terraform. +# +# module "coredns" { +# source = "../../../kubernetes/modules/coredns" +# node_selector = {} +# kubeconfig_data = azurerm_kubernetes_cluster.aks.kube_config_raw +# depends_on = [ +# azurerm_kubernetes_cluster.aks, +# ] +# } + +# ----------------------------------------------------------------------------- +# Locals +# ----------------------------------------------------------------------------- + +locals { + materialize_instance_namespace = var.materialize_instance_namespace + materialize_instance_name = var.materialize_instance_name + + # MIGRATION: Replicates old module's local.common_labels which always + # included managed_by and module keys. This is critical because these + # labels are used as node_labels on the materialize nodepool, and + # changing node_labels triggers a node pool rotation. + common_labels = merge(var.tags, { + managed_by = "terraform" + module = "materialize" + }) + + # MIGRATION: metadata_backend_url matches old module format exactly. + # Old module used: postgres://user:pass@host/db?sslmode=require + metadata_backend_url = format( + "postgres://%s:%s@%s/%s?sslmode=require", + var.database_username, + var.old_db_password, + azurerm_postgresql_flexible_server.postgres.fqdn, + var.database_name + ) + + # MIGRATION: persist_backend_url changes from SAS token to workload identity. + # Old format: {blob_endpoint}{container}?{sas_token} + # New format: {blob_endpoint}{container} (auth via workload identity) + persist_backend_url = format( + "%s%s", + module.storage.primary_blob_endpoint, + module.storage.container_name, + ) +} diff --git a/azure/examples/migration/outputs.tf b/azure/examples/migration/outputs.tf new file mode 100644 index 00000000..6c95a833 --- /dev/null +++ b/azure/examples/migration/outputs.tf @@ -0,0 +1,102 @@ +# ============================================================================= +# Migration Reference Outputs +# ============================================================================= + +# Networking +output "networking" { + description = "Networking details" + value = { + vnet_id = azurerm_virtual_network.vnet.id + vnet_name = azurerm_virtual_network.vnet.name + aks_subnet_id = azurerm_subnet.aks.id + pg_subnet_id = azurerm_subnet.postgres.id + dns_zone_id = azurerm_private_dns_zone.postgres.id + } +} + +# AKS Cluster +output "aks_cluster" { + description = "AKS cluster details" + value = { + name = azurerm_kubernetes_cluster.aks.name + endpoint = azurerm_kubernetes_cluster.aks.kube_config[0].host + location = azurerm_kubernetes_cluster.aks.location + } + sensitive = true +} + +output "kube_config_raw" { + description = "The raw kube_config for the AKS cluster" + value = azurerm_kubernetes_cluster.aks.kube_config_raw + sensitive = true +} + +# Database +output "database" { + description = "Azure Database for PostgreSQL details" + value = { + server_name = azurerm_postgresql_flexible_server.postgres.name + fqdn = azurerm_postgresql_flexible_server.postgres.fqdn + } +} + +# Storage +output "storage" { + description = "Azure Storage Account details" + value = { + name = module.storage.storage_account_name + blob_endpoint = module.storage.primary_blob_endpoint + container_name = module.storage.container_name + } +} + +# Operator +output "operator" { + description = "Materialize operator details" + value = { + namespace = module.operator.operator_namespace + } +} + +# Materialize Instance +output "materialize_instance_name" { + description = "Materialize instance name" + value = local.materialize_instance_name +} + +output "materialize_instance_namespace" { + description = "Materialize instance namespace" + value = local.materialize_instance_namespace +} + +output "materialize_instance_resource_id" { + description = "Materialize instance resource ID" + value = module.materialize_instance.instance_resource_id +} + +output "materialize_instance_metadata_backend_url" { + description = "Materialize instance metadata backend URL" + value = local.metadata_backend_url + sensitive = true +} + +output "materialize_instance_persist_backend_url" { + description = "Materialize instance persist backend URL" + value = local.persist_backend_url + sensitive = true +} + +# Load Balancer +output "load_balancer_details" { + description = "Details of the Materialize instance load balancers" + value = { + console_ip = module.load_balancers.console_load_balancer_ip + balancerd_ip = module.load_balancers.balancerd_load_balancer_ip + } +} + +output "external_login_password_mz_system" { + description = "Password for external login to Materialize" + value = var.external_login_password_mz_system + sensitive = true +} diff --git a/azure/examples/migration/terraform.tfvars.example b/azure/examples/migration/terraform.tfvars.example new file mode 100644 index 00000000..fa490473 --- /dev/null +++ b/azure/examples/migration/terraform.tfvars.example @@ -0,0 +1,142 @@ +# ============================================================================= +# Migration Configuration Example +# ============================================================================= +# +# IMPORTANT: Before running terraform commands: +# 1. Copy this file: cp terraform.tfvars.example terraform.tfvars +# 2. Update the values below to match your existing infrastructure +# 3. The values MUST match your current setup to ensure successful migration +# +# ============================================================================= + +# ------------------------------------------------------------------------------ +# REQUIRED: These must be set +# ------------------------------------------------------------------------------ + +# Azure subscription ID +# Run: az account show --query id -o tsv +subscription_id = "your-subscription-id-here" + +# Existing resource group name - MUST MATCH your current setup +# Run: az group list --query "[?tags.module=='materialize'].name" -o tsv +resource_group_name = "your-resource-group-name" + +# Prefix used for resource names - MUST MATCH your existing resources +# Example: if your AKS cluster is named "mycompany-aks", use "mycompany" +name_prefix = "your-prefix-here" + +# Materialize license key +# Get from: https://materialize.com/register +license_key = "your-license-key-here" + +# Your existing Materialize instance name +# Run: kubectl get materialize -A -o jsonpath='{.items[0].metadata.name}' +materialize_instance_name = "your-instance-name" + +# Your existing database password +# From your old terraform.tfvars (database_config.password) +old_db_password = "your-existing-db-password-here" + +# Your existing mz_system user password +# Get from old config: look for external_login_password_mz_system in your old terraform.tfvars +external_login_password_mz_system = "your-existing-mz-system-password-here" + +# ------------------------------------------------------------------------------ +# INFRASTRUCTURE: Match your existing setup +# ------------------------------------------------------------------------------ + +# Azure region - MUST MATCH where your current resources are deployed +# Run: az aks show --name -aks --resource-group --query location -o tsv +location = "eastus2" + +# VNet configuration - MUST MATCH existing VNet +# Run: az network vnet show --name -vnet --resource-group --query addressSpace.addressPrefixes[0] -o tsv +vnet_address_space = "10.0.0.0/16" + +# Subnet CIDRs - MUST MATCH existing subnets +# Run: az network vnet subnet show --name -aks-subnet --vnet-name -vnet --resource-group --query addressPrefix -o tsv +aks_subnet_cidr = "10.0.0.0/22" + +# Run: az network vnet subnet show --name -pg-subnet --vnet-name -vnet --resource-group --query addressPrefix -o tsv +postgres_subnet_cidr = "10.0.4.0/24" + +# Kubernetes service CIDR - MUST MATCH existing AKS cluster +# Run: az aks show --name -aks --resource-group --query networkProfile.serviceCidr -o tsv +service_cidr = "10.1.0.0/16" + +# AKS cluster version - MUST MATCH existing cluster +# Run: az aks show --name -aks --resource-group --query kubernetesVersion -o tsv +# kubernetes_version = "1.32" + +# Default node pool VM size +# Run: az aks show --name -aks --resource-group --query agentPoolProfiles[0].vmSize -o tsv +default_node_pool_vm_size = "Standard_D2s_v3" + +# System node pool sizing (the "materialize" pool in the old AKS module) +# Run: az aks nodepool show --cluster-name -aks --resource-group --name --query '{vmSize:vmSize,minCount:minCount,maxCount:maxCount}' +system_node_pool_vm_size = "Standard_D2ps_v6" +system_node_pool_disk_size_gb = 100 +system_node_pool_min_nodes = 2 +system_node_pool_max_nodes = 4 + +# Materialize node pool sizing +# Run: az aks nodepool show --cluster-name -aks --resource-group --name --query '{vmSize:vmSize,minCount:minCount,maxCount:maxCount}' +materialize_node_pool_vm_size = "Standard_E4pds_v6" +materialize_node_pool_disk_size_gb = 100 +materialize_node_pool_min_nodes = 1 +materialize_node_pool_max_nodes = 4 + +# Database configuration - MUST MATCH existing PostgreSQL Flexible Server +# Run: az postgres flexible-server show --name --resource-group --query '{version:version,sku:sku.name,storageMb:storage.storageSizeGb}' +database_username = "materialize" +database_name = "materialize" +postgres_version = "15" +database_sku_name = "GP_Standard_D2s_v3" +database_storage_mb = 32768 + +# ------------------------------------------------------------------------------ +# OPTIONAL: These defaults should match your existing setup, update if needed +# ------------------------------------------------------------------------------ + +# Tags applied to all resources - should match your existing tags +tags = { + managed_by = "terraform" + module = "materialize" +} + +# TLS configuration - must match old module setting (default: true) +use_self_signed_cluster_issuer = true + +# Materialize instance namespace (default: "materialize-environment") +# materialize_instance_namespace = "materialize-environment" + +# Materialize environmentd version tag - MUST MATCH existing +# Run: kubectl get materialize -A -o jsonpath='{.items[0].spec.environmentdImageRef}' +# Then extract just the version tag (e.g., from "materialize/environmentd:v0.130.0" use "v0.130.0") +# If not set, defaults to the module's built-in version which may not match yours! +# environmentd_version = "v0.130.0" + +# Materialize operator Helm chart version - MUST MATCH existing +# Run: helm list -n materialize -o json | jq -r '.[0].chart' +# Extract version from chart name (e.g., "materialize-operator-v26.13.0" → "v26.13.0") +# If not set, defaults to the module's built-in version which may downgrade your operator! +# operator_version = "v26.13.0" + +# Disk setup image version - should match existing daemonset to avoid unnecessary updates +# Run: kubectl get daemonset -n disk-setup -o jsonpath='{.items[0].spec.template.spec.initContainers[0].image}' +# Default: "materialize/ephemeral-storage-setup-image:v0.4.0" (matches old module) +# disk_setup_image = "materialize/ephemeral-storage-setup-image:v0.4.0" + +# Whether the load balancers should be internal (true) or internet-facing (false) +internal_load_balancer = true + +# CIDR blocks allowed to access Materialize load balancers +# Default allows all - restrict for production! +# ingress_cidr_blocks = ["0.0.0.0/0"] + +# ------------------------------------------------------------------------------ +# ADVANCED: Rollout configuration (usually leave as defaults) +# ------------------------------------------------------------------------------ + +# force_rollout = "00000000-0000-0000-0000-000000000003" +# request_rollout = "00000000-0000-0000-0000-000000000003" diff --git a/azure/examples/migration/variables.tf b/azure/examples/migration/variables.tf new file mode 100644 index 00000000..419c1ac6 --- /dev/null +++ b/azure/examples/migration/variables.tf @@ -0,0 +1,289 @@ +# ============================================================================= +# Migration Reference Variables +# ============================================================================= +# +# Update these variables to match your existing infrastructure. +# Set values in terraform.tfvars (see terraform.tfvars.example). +# +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Azure Configuration +# ----------------------------------------------------------------------------- + +variable "subscription_id" { + description = "Azure subscription ID where resources exist" + type = string +} + +variable "resource_group_name" { + description = "Name of the existing resource group (must match existing)" + type = string +} + +variable "location" { + description = "Azure region where resources exist (must match existing)" + type = string + default = "eastus2" +} + +variable "tags" { + description = "Tags to apply to resources (should match existing tags)" + type = map(string) + default = {} +} + +# ----------------------------------------------------------------------------- +# Core Configuration +# ----------------------------------------------------------------------------- + +variable "name_prefix" { + description = "Prefix used for all resource names (must match existing)" + type = string + validation { + condition = length(var.name_prefix) >= 3 && length(var.name_prefix) <= 16 && can(regex("^[a-z0-9-]+$", var.name_prefix)) + error_message = "Prefix must be between 3-16 characters, lowercase alphanumeric and hyphens only." + } +} + +variable "license_key" { + description = "Materialize license key" + type = string + sensitive = true +} + +# ----------------------------------------------------------------------------- +# Materialize Instance Configuration +# ----------------------------------------------------------------------------- + +variable "materialize_instance_name" { + description = "Name of your existing Materialize instance. Run: kubectl get materialize -A" + type = string +} + +variable "materialize_instance_namespace" { + description = "Kubernetes namespace for the Materialize instance" + type = string + default = "materialize-environment" +} + +variable "environmentd_version" { + description = "Materialize environmentd version tag. Must match your existing version. Run: kubectl get materialize -A -o jsonpath='{.items[0].spec.environmentdImageRef}' and extract the tag (e.g., v0.130.0)." + type = string + default = null +} + +# ----------------------------------------------------------------------------- +# Migration Secrets +# ----------------------------------------------------------------------------- + +variable "old_db_password" { + description = "Existing database password from your old configuration" + type = string + sensitive = true +} + +variable "external_login_password_mz_system" { + description = "Password for mz_system user from your old configuration" + type = string + sensitive = true +} + +# ----------------------------------------------------------------------------- +# Networking +# ----------------------------------------------------------------------------- + +variable "vnet_address_space" { + description = "VNet address space (must match existing)" + type = string + default = "10.0.0.0/16" +} + +variable "aks_subnet_cidr" { + description = "CIDR for the AKS subnet (must match existing)" + type = string + default = "10.0.0.0/20" +} + +variable "postgres_subnet_cidr" { + description = "CIDR for the PostgreSQL subnet (must match existing)" + type = string + default = "10.0.16.0/24" +} + +variable "service_cidr" { + description = "Kubernetes service CIDR (must match existing AKS cluster)" + type = string + default = "10.1.0.0/16" +} + +# ----------------------------------------------------------------------------- +# AKS Cluster +# ----------------------------------------------------------------------------- + +variable "kubernetes_version" { + description = "Kubernetes version for AKS cluster (must match existing). Run: az aks show --name -aks --resource-group --query kubernetesVersion" + type = string + default = null +} + +variable "default_node_pool_vm_size" { + description = "VM size for the AKS default node pool (must match existing)" + type = string + default = "Standard_D2s_v3" +} + +# ----------------------------------------------------------------------------- +# System Node Pool (from old AKS module) +# ----------------------------------------------------------------------------- + +variable "system_node_pool_vm_size" { + description = "VM size for the system node pool (old default: Standard_D2ps_v6)" + type = string + default = "Standard_D2ps_v6" +} + +variable "system_node_pool_disk_size_gb" { + description = "Disk size in GB for system node pool" + type = number + default = 100 +} + +variable "system_node_pool_min_nodes" { + description = "Minimum nodes in system node pool (old default: 2)" + type = number + default = 2 +} + +variable "system_node_pool_max_nodes" { + description = "Maximum nodes in system node pool (old default: 4)" + type = number + default = 4 +} + +# ----------------------------------------------------------------------------- +# Materialize Node Pool +# ----------------------------------------------------------------------------- + +variable "materialize_node_pool_vm_size" { + description = "VM size for Materialize node pool (old default: Standard_E4pds_v6)" + type = string + default = "Standard_E4pds_v6" +} + +variable "materialize_node_pool_disk_size_gb" { + description = "Disk size in GB for Materialize node pool" + type = number + default = 100 +} + +variable "materialize_node_pool_min_nodes" { + description = "Minimum nodes in Materialize node pool (old default: 1)" + type = number + default = 1 +} + +variable "materialize_node_pool_max_nodes" { + description = "Maximum nodes in Materialize node pool (old default: 4)" + type = number + default = 4 +} + +# ----------------------------------------------------------------------------- +# Database (PostgreSQL Flexible Server) +# ----------------------------------------------------------------------------- + +variable "database_username" { + description = "PostgreSQL administrator login (must match existing)" + type = string + default = "materialize" +} + +variable "database_name" { + description = "PostgreSQL database name (must match existing)" + type = string + default = "materialize" +} + +variable "postgres_version" { + description = "PostgreSQL version (must match existing)" + type = string + default = "15" +} + +variable "database_sku_name" { + description = "SKU name for PostgreSQL Flexible Server (must match existing)" + type = string + default = "GP_Standard_D2s_v3" +} + +variable "database_storage_mb" { + description = "Storage in MB for PostgreSQL (must match existing)" + type = number + default = 32768 +} + +# ----------------------------------------------------------------------------- +# TLS Configuration +# ----------------------------------------------------------------------------- + +variable "use_self_signed_cluster_issuer" { + description = "Whether to enable TLS using the self-signed cluster issuer. Must match your old module's setting." + type = bool + default = true +} + +variable "cert_manager_chart_version" { + description = "cert-manager Helm chart version. Default matches old module's version to avoid unintended upgrades." + type = string + default = "v1.17.1" +} + +# ----------------------------------------------------------------------------- +# Access Control +# ----------------------------------------------------------------------------- + +variable "ingress_cidr_blocks" { + description = "List of CIDR blocks to allow access to Materialize load balancers" + type = list(string) + default = null + nullable = true +} + +variable "internal_load_balancer" { + description = "Whether to use an internal load balancer" + type = bool + default = true +} + +# ----------------------------------------------------------------------------- +# Operator Configuration +# ----------------------------------------------------------------------------- + +variable "operator_version" { + description = "Materialize operator Helm chart version. Must match your existing operator version to avoid unintended upgrades/downgrades. Run: helm list -n materialize -o json | jq -r '.[0].chart' to find current version." + type = string + default = null +} + +variable "disk_setup_image" { + description = "Docker image for the disk setup daemonset. Old module default: v0.4.0. Set to match your existing daemonset to avoid unnecessary updates. Run: kubectl get daemonset disk-setup -n disk-setup -o jsonpath='{.spec.template.spec.initContainers[0].image}'" + type = string + default = "materialize/ephemeral-storage-setup-image:v0.4.0" +} + +# ----------------------------------------------------------------------------- +# Rollout Configuration (usually leave as defaults) +# ----------------------------------------------------------------------------- + +variable "force_rollout" { + description = "UUID to force a rollout" + type = string + default = "00000000-0000-0000-0000-000000000003" +} + +variable "request_rollout" { + description = "UUID to request a rollout" + type = string + default = "00000000-0000-0000-0000-000000000003" +} diff --git a/azure/examples/migration/versions.tf b/azure/examples/migration/versions.tf new file mode 100644 index 00000000..07aaf9d6 --- /dev/null +++ b/azure/examples/migration/versions.tf @@ -0,0 +1,30 @@ +terraform { + required_version = ">= 1.8" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "4.54.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.0" + } + helm = { + source = "hashicorp/helm" + version = "~> 2.0" + } + random = { + source = "hashicorp/random" + version = "~> 3.5" + } + deepmerge = { + source = "isometry/deepmerge" + version = "~> 1.0" + } + kubectl = { + source = "alekc/kubectl" + version = "~> 2.0" + } + } +} diff --git a/azure/modules/storage/main.tf b/azure/modules/storage/main.tf index fa0bf021..de7312b8 100644 --- a/azure/modules/storage/main.tf +++ b/azure/modules/storage/main.tf @@ -12,7 +12,7 @@ resource "azurerm_storage_account" "materialize" { dynamic "network_rules" { for_each = length(var.subnets) == 0 ? [] : ["has_subnets"] content { - default_action = "Deny" + default_action = var.network_rules_default_action bypass = ["AzureServices"] virtual_network_subnet_ids = var.subnets } diff --git a/azure/modules/storage/variables.tf b/azure/modules/storage/variables.tf index 3e8119d8..13f068ec 100644 --- a/azure/modules/storage/variables.tf +++ b/azure/modules/storage/variables.tf @@ -75,3 +75,15 @@ variable "service_account_name" { type = string nullable = false } + +variable "network_rules_default_action" { + description = "The default action for storage account network rules when subnets are configured. Use 'Allow' to permit access from all networks (with subnet rules as additions), or 'Deny' to restrict access to only the configured subnets." + type = string + default = "Deny" + nullable = false + + validation { + condition = contains(["Allow", "Deny"], var.network_rules_default_action) + error_message = "Valid values for network_rules_default_action are: Allow, Deny." + } +}