Links up to five Managed Redis instances into one active geo-replication group β from a single declaration, because linking is reciprocal. Targets
hashicorp/azurerm ~> 4.0.
- βοΈ Creates one
azurerm_managed_redis_geo_replication, namedthis. Three inputs, and every one of them matters. - π΄ This resource's
idISmanaged_redis_idβ the provider says so outright. So it has no identity of its own, and two declarations against one instance produce a permanent alternating diff that neither Terraform nor Azure reports. - π΄ Linking is reciprocal, so declare this module ONCE per group. The instinct to declare it from every member for symmetry is precisely what breaks it.
- β
The self-referential rule is enforceable, and enforced:
linked_managed_redis_idsmust not containmanaged_redis_id. Both halves are inputs, which is rare in this family. β οΈ Every member must already share onegeo_replication_group_nameβ set on the other resource, so the join is a bare string and no validation on either side can compare the values.- π Takes no credential, unlike the parent module where plan access is key access.
π‘ Why it matters: three arguments, and the two hardest facts about them are that one is also the resource's own identity and one depends on a value this module cannot see.
If this module saves you time, please consider supporting its continued development:
- β Star the repository on GitHub
- π€ Connect on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
This DAG is shared with the parent terraform-azurerm-managed-redis module, validated once per focus. What differs is
which node is highlighted and what each node's text carries: this focus names the group-name rule and the SKU floor that
live on the other resource, while the parent focus names its grouped default_database.
flowchart TB
RG["terraform-azurerm-resource-group"]
MR["terraform-azurerm-managed-redis -- each member sets the SAME geo_replication_group_name and needs Balanced_B3 or higher"]
GEO["terraform-azurerm-managed-redis-geo-replication -- its id IS the anchor's id, so declare it ONCE per group"]
PEER["other terraform-azurerm-managed-redis instances in other regions -- up to 4 more, making a group of 5"]
APA["terraform-azurerm-managed-redis-access-policy-assignment -- grants one Entra ID principal data-plane access"]
PE["terraform-azurerm-private-endpoint"]
RC["terraform-azurerm-redis-cache -- a DIFFERENT ARM type, and its redis-linked-server geo-replication is a separate mechanism"]
RG -->|"name as resource_group_name"| MR
MR -->|"id as managed_redis_id -- the anchor"| GEO
PEER -->|"id as linked_managed_redis_ids -- at most 4"| GEO
MR -->|"group name set on default_database, invisible from the link resource"| PEER
MR -->|"id as the parent for a policy assignment"| APA
MR -->|"id as private_service_connection target"| PE
RC -->|"not interchangeable: Microsoft.Cache/redis versus redisEnterprise"| MR
style GEO fill:#0078D4,stroke:#004578,color:#ffffff
style MR fill:#004578,stroke:#00335c,color:#ffffff
style RG fill:#f2f4f7,stroke:#98a2b3,color:#1d2939
style PEER fill:#f2f4f7,stroke:#98a2b3,color:#1d2939
style APA fill:#f2f4f7,stroke:#98a2b3,color:#1d2939
style PE fill:#f2f4f7,stroke:#98a2b3,color:#1d2939
style RC fill:#f2f4f7,stroke:#98a2b3,color:#1d2939
flowchart TB
IN_ANCHOR["managed_redis_id -- the anchor. This resource's OWN id is this same value"]
IN_LINKED["linked_managed_redis_ids -- a SET, at most 4, must NOT contain the anchor, and no two entries may be one id in two casings"]
IN_T["timeouts -- all four, because the link set is genuinely updatable in place"]
THIS["azurerm_managed_redis_geo_replication.this"]
ARM["the SAME ARM object as the anchor instance -- no identity of its own, which is why two declarations collide"]
OUT_ID["id equals managed_redis_id, then anchor_name"]
OUT_GROUP["linked_names, group_size, remaining_group_capacity, spans_subscriptions, spans_resource_groups"]
OUT_FLAG["two_declarations_for_one_instance_collide_silently, group_names_must_match_and_nothing_here_can_check_it, every_member_needs_balanced_b3_or_higher, region_spread_is_not_knowable_here"]
IN_ANCHOR --> THIS
IN_LINKED --> THIS
IN_T --> THIS
THIS -->|"links reciprocally, so declare it ONCE per group"| ARM
THIS --> OUT_ID
THIS --> OUT_GROUP
THIS --> OUT_FLAG
style THIS fill:#0078D4,stroke:#004578,color:#ffffff
style ARM fill:#004578,stroke:#00335c,color:#ffffff
style IN_ANCHOR fill:#f2f4f7,stroke:#98a2b3,color:#1d2939
style IN_LINKED fill:#f2f4f7,stroke:#98a2b3,color:#1d2939
style IN_T fill:#f2f4f7,stroke:#98a2b3,color:#1d2939
style OUT_ID fill:#f2f4f7,stroke:#98a2b3,color:#1d2939
style OUT_GROUP fill:#f2f4f7,stroke:#98a2b3,color:#1d2939
style OUT_FLAG fill:#f2f4f7,stroke:#98a2b3,color:#1d2939
Resource inventory
| Resource | Count | Notes |
|---|---|---|
azurerm_managed_redis_geo_replication.this |
1 | The keystone. Its id is the anchor instance's id. |
timeouts block |
0β1 | All four operations β the link set is updatable in place. |
| The Managed Redis instances | 0 | None created here. Consumed by id, and left running on destroy. |
| Item | Value |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/azurerm |
~> 4.0 |
| Provider block | None. The caller configures the provider, its authentication, and its mandatory features {} block. |
| Module type | standalone |
| Keystone | azurerm_managed_redis_geo_replication.this |
Schema notes that bite
- π΄ The resource's
idis identical tomanaged_redis_id. The provider's attributes reference says so: "The ID of the Managed Redis Database Geo-Replication resource (same asmanaged_redis_id)", and its import path is just the cluster ID. Two declarations for one instance therefore claim one ARM object from two state addresses. - π΄ Linking is reciprocal. "If A is linked to B, both A and B will have the same linking state. There is no need to
have duplicate
azurerm_managed_redis_geo_replicationresources for each." - π΄ The bound is 4, not 5. "Can contain up to 4 Managed Redis IDs, making up a group of 5 in total" β the anchor is counted automatically.
β οΈ linked_managed_redis_idsis aset(string), so exact duplicates collapse β but ARM Resource IDs are case-insensitive, so two casings of one ID survive as two entries and count twice against the limit. This module rejects that.β οΈ managed_redis_idis force-new; the link set is not. Adding or removing a member is an in-place update, which is why all fourtimeoutskeys exist here.β οΈ The ARM type isredisEnterprise, notredis. Anazurerm_redis_cacheID is a different service and is rejected by name.- βΉοΈ A Resource ID does not carry the region, only the subscription and resource group β so this module cannot tell
whether the group actually spans regions. See
region_spread_is_not_knowable_here.
| Principal | Permission | Scope | Why |
|---|---|---|---|
| The Terraform identity | Microsoft.Cache/redisEnterprise/databases/write |
The anchor instance | Create and update the link set. |
| The Terraform identity | Microsoft.Cache/redisEnterprise/read |
Every member instance | Resolve each ID in the group. |
| The Terraform identity | Microsoft.Cache/redisEnterprise/databases/forceLinkToReplicationGroup/action |
Each linked instance | The operation that performs the linking. |
β οΈ Permissions are needed on every member, not just the anchor. And whenspans_subscriptionsorspans_resource_groupsistrue, those permissions live in more than one scope β which is also more than one place a lock or policy can block an update to this single resource.
βΉοΈ Plan access here is genuinely read-only. This resource holds no key, password or connection string. Worth stating because it does not hold for the parent module: there, the default database's access keys are computed, sensitive attributes Terraform must read, so planning the parent is credential access.
Microsoft.Cacheregistered on the subscription of every member.- Two to five existing Managed Redis instances, each created by
terraform-azurerm-managed-redisor equivalent. - The same
geo_replication_group_nameon every member, set on each instance's owndefault_databaseblock. Nothing here can verify it. Balanced_B3or higher on every member. The parent module rejects the two SKUs demonstrably below the floor when a group name is set; beyond those, an undersized member fails at apply.- No persistence on any member. The provider documents that persistence works only on non-geo-replicated databases.
- Only
RediSearchorRedisJSONmodules on any member, if modules are used at all.
terraform-azurerm-managed-redis-geo-replication/
βββ providers.tf # required_version >= 1.12.0; azurerm ~> 4.0. No provider block.
βββ variables.tf # 3 typed inputs, 7 validations.
βββ main.tf # One keystone `this`; a dynamic timeouts block; group facts derived in locals.
βββ outputs.tf # id first, then the group shape, then the constants for what cannot be checked here.
βββ README.md # This document.
βββ SCOPE.md # The cross-module contract.
βββ LICENSE # MIT.
βββ .gitignore # The canonical library ignore file.
provider "azurerm" {
features {}
}
module "cache_geo" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-managed-redis-geo-replication.git?ref=v1.0.0"
managed_redis_id = module.cache_eastus2.id
linked_managed_redis_ids = [module.cache_westus2.id]
}Two arguments, one declaration, and the whole group is linked. Do not add a second declaration from
cache_westus2 β see example 3.
Consumes
| Input | Type | Source module |
|---|---|---|
managed_redis_id |
Managed Redis Resource ID | terraform-azurerm-managed-redis β id |
linked_managed_redis_ids |
Managed Redis Resource IDs | terraform-azurerm-managed-redis β id, one per member |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
The anchor's Resource ID | Audit β not a new identifier |
anchor_name / linked_names |
Readable member names | Plan review, audit |
group_size / remaining_group_capacity |
Against the limit of 5 | Capacity planning |
spans_subscriptions / spans_resource_groups |
Permission and lock scoping | Composition assertions |
two_declarations_for_one_instance_collide_silently |
Constant true |
Read this one |
The examples below reference existing resources by ID or name rather than creating them; this module owns only its own resource. Those references are declared inputs:
variable "cache_australiaeast_id" {
description = "id of an existing cache australiaeast that these examples reference but do not create."
type = string
}
variable "cache_partner_region_id" {
description = "id of an existing cache partner region that these examples reference but do not create."
type = string
}
variable "cache_southeastasia_id" {
description = "id of an existing cache southeastasia that these examples reference but do not create."
type = string
}1 Β· A two-region group
module "cache_geo" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-managed-redis-geo-replication.git?ref=v1.0.0"
managed_redis_id = module.cache_eastus2.id
linked_managed_redis_ids = [module.cache_westus2.id]
}βΉοΈ
group_sizereports2β the linked set plus the anchor, which the provider counts automatically.
β οΈ Both instances must already carry the samegeo_replication_group_name. Setting it creates a one-member group on each; this resource is what joins them.
2 Β· A full five-instance group
module "cache_geo_global" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-managed-redis-geo-replication.git?ref=v1.0.0"
managed_redis_id = module.cache_eastus2.id
linked_managed_redis_ids = [
module.cache_westus2.id,
module.cache_northeurope.id,
var.cache_southeastasia_id,
var.cache_australiaeast_id,
]
}π΄ Four is the maximum for this set, and five for the group. The bound is on the input, not the group, because the anchor is counted for you.
remaining_group_capacityreports0here.
β οΈ Adding a sixth region means removing one. There is no larger tier.
3 Β· The declaration that must not be duplicated
# CORRECT: one declaration links the whole group, because linking is reciprocal.
module "cache_geo" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-managed-redis-geo-replication.git?ref=v1.0.0"
managed_redis_id = module.cache_eastus2.id
linked_managed_redis_ids = [module.cache_westus2.id]
}Declaring a second copy from cache_westus2 pointing back at cache_eastus2 looks symmetrical and is the one thing to
avoid. Because this resource's id is managed_redis_id, the two declarations would be two Terraform resources
claiming overlapping ARM state from different addresses β both plan cleanly, both apply, and every plan afterwards
proposes to revert the other's link set. Azure raises nothing, because each individual write is legitimate.
π΄
two_declarations_for_one_instance_collide_silentlycarries a failure-timing table with a Never row for both drift and Azure errors. This is the batch-31-class collision: no identity to conflict on, so no conflict is detected.
β The defence is a convention, not a check: exactly one geo-replication declaration per group, from whichever member you nominate as the anchor. "Anchor" is this module's word β Azure gives it no special standing.
4 Β· The self-referential rule, enforced
module "cache_geo_correct" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-managed-redis-geo-replication.git?ref=v1.0.0"
managed_redis_id = module.cache_eastus2.id
# The anchor is included automatically. Listing it here is rejected at plan time.
linked_managed_redis_ids = [module.cache_westus2.id]
}β This is one of the few cross-argument rules in this family that can be ENFORCED rather than reported, because both halves are inputs to the same module. The provider: "The ID of this Managed Redis is always included by default and does not need to be provided here."
β οΈ The comparison is case-insensitive, because ARM Resource IDs are β so a differently-cased copy of the anchor's ID is caught too.
βΉοΈ Its limit, stated: the check compares strings. It cannot tell that two different IDs refer to the same instance through, say, a differently-cased resource group in an otherwise identical path β which is why a second check rejects duplicate casings within the set itself.
5 Β· The group-name rule this module cannot check
locals {
# ONE source of truth. Nothing can verify that the members' names match, so do not type it twice.
redis_geo_group = "orders-global"
}
module "cache_eastus2" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-managed-redis.git?ref=v1.0.0"
name = "redis-orders-eastus2"
resource_group_name = module.cache_rg.name
location = "eastus2"
sku_name = "Balanced_B3"
default_database = {
geo_replication_group_name = local.redis_geo_group
}
}
module "cache_westus2" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-managed-redis.git?ref=v1.0.0"
name = "redis-orders-westus2"
resource_group_name = module.cache_rg.name
location = "westus2"
sku_name = "Balanced_B3"
default_database = {
geo_replication_group_name = local.redis_geo_group
}
}
module "cache_geo" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-managed-redis-geo-replication.git?ref=v1.0.0"
managed_redis_id = module.cache_eastus2.id
linked_managed_redis_ids = [module.cache_westus2.id]
}π΄ The join between these resources is a bare STRING. The group name lives on each instance's
default_databaseblock, so this module never sees it and no validation on either side can compare the values. A mismatch produces no error at all β the link simply never forms.
β Composition is the only defence, and that is what the
localabove is for.group_names_must_match_and_nothing_here_can_check_itis emitted so this is a documented limitation rather than a discovery.
6 Β· What every member needs, and where it is checked
module "cache_member" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-managed-redis.git?ref=v1.0.0"
name = "redis-orders-westus2"
resource_group_name = module.cache_rg.name
location = "westus2"
# Balanced_B3 or higher is required for geo-replication.
sku_name = "Balanced_B3"
default_database = {
geo_replication_group_name = local.redis_geo_group
# Only RediSearch and RedisJSON are permitted in a geo-replicated group.
modules = [{ name = "RedisJSON" }]
# Persistence is deliberately absent: it cannot coexist with geo-replication.
}
}
β οΈ Three member-side rules, all invisible from this module, and all emitted here as constants so they are not forgotten:every_member_needs_balanced_b3_or_higher,only_redisearch_and_redisjson_modules_are_allowed_in_a_group, andpersistence_cannot_be_combined_with_membership.
βΉοΈ The parent module enforces what it can, per instance: it rejects the two sub-floor SKUs and the disallowed modules when that instance also sets a group name. An instance that sets no group name of its own was never checked β another reason to wire the name deliberately.
7 Β· A group spanning subscriptions
module "cache_geo_cross_sub" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-managed-redis-geo-replication.git?ref=v1.0.0"
managed_redis_id = module.cache_eastus2.id
linked_managed_redis_ids = [var.cache_partner_region_id]
}
output "geo_spans_subscriptions" {
value = module.cache_geo_cross_sub.spans_subscriptions
}β Allowed and reported, not rejected. The provider publishes no cross-subscription prohibition for Managed Redis geo-replication β unlike the Machine Learning inference-cluster attachment elsewhere in this library, where Microsoft does, and where this suite therefore enforces it. "Enforce what the provider documents" is per resource.
β οΈ It changes who needs what. The Terraform identity needs read access on every member, so a cross-subscription group means permissions in more than one subscription.
8 Β· What this module cannot tell you about regions
module "cache_geo" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-managed-redis-geo-replication.git?ref=v1.0.0"
managed_redis_id = module.cache_eastus2.id
linked_managed_redis_ids = [module.cache_westus2.id]
}
# The region check has to come from the member modules, not from here.
check "geo_group_actually_spans_regions" {
assert {
condition = module.cache_eastus2.location != module.cache_westus2.location
error_message = "Both members of the geo-replication group are in the same region."
}
}π΄ A Managed Redis Resource ID does not carry the region β only the subscription and resource group. So this module cannot confirm that a "geo"-replication group is geographically distributed at all, and would happily link an instance to four others in the same datacentre.
region_spread_is_not_knowable_heresays so.
β The
checkblock above is where that assertion belongs, using each member module's ownlocationoutput.
9 Β· Adding a region later
module "cache_geo" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-managed-redis-geo-replication.git?ref=v1.0.0"
managed_redis_id = module.cache_eastus2.id
linked_managed_redis_ids = [
module.cache_westus2.id,
module.cache_northeurope.id, # added in a later change
]
}β Adding or removing a member is an in-place UPDATE, not a replacement. Only
managed_redis_idis force-new, which is why this resource has all fourtimeoutskeys.
β οΈ Changing the anchor is a replacement, and it dissolves and rebuilds the group. If you need a different anchor, prefer leaving it alone β the anchor has no operational significance.
10 Β· Custom timeouts
module "cache_geo_timeouts" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-managed-redis-geo-replication.git?ref=v1.0.0"
managed_redis_id = module.cache_eastus2.id
linked_managed_redis_ids = [module.cache_westus2.id]
timeouts = {
create = "45m"
read = "10m"
update = "45m"
delete = "45m"
}
}βΉοΈ All four keys exist here, because the link set is updatable in place. Linking several regions is a slow operation β the provider's default create is 30 minutes, and a five-member group can want more.
11 Β· Dissolving a group
# Removing this module's declaration -- or running `terraform destroy` against it -- unlinks every member.
module "cache_geo" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-managed-redis-geo-replication.git?ref=v1.0.0"
managed_redis_id = module.cache_eastus2.id
linked_managed_redis_ids = [module.cache_westus2.id]
}
output "unlinking_is_not_deleting" {
value = module.cache_geo.unlinking_does_not_delete_any_instance
}β Destroying this resource dissolves the group and leaves every instance running, billing, and holding whatever data it had. It is a link, not a lifecycle owner.
π΄ The reverse is the sharper direction. Deleting a linked instance outside Terraform leaves this resource pointing at an ID that no longer exists, and no drift is reported, because the stored ID is still the ID it was given.
12 Β· ποΈ End-to-end composition
provider "azurerm" {
features {}
}
locals {
redis_geo_group = "orders-global"
}
module "cache_rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-cache-prod"
location = "eastus2"
tags = {
environment = "prod"
workload = "cache"
}
}
module "cache_eastus2" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-managed-redis.git?ref=v1.0.0"
name = "redis-orders-eastus2"
resource_group_name = module.cache_rg.name
location = "eastus2"
sku_name = "Balanced_B5"
default_database = {
geo_replication_group_name = local.redis_geo_group
modules = [{ name = "RedisJSON" }]
}
tags = {
environment = "prod"
}
}
module "cache_westus2" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-managed-redis.git?ref=v1.0.0"
name = "redis-orders-westus2"
resource_group_name = module.cache_rg.name
location = "westus2"
sku_name = "Balanced_B5"
default_database = {
geo_replication_group_name = local.redis_geo_group
modules = [{ name = "RedisJSON" }]
}
tags = {
environment = "prod"
}
}
module "cache_northeurope" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-managed-redis.git?ref=v1.0.0"
name = "redis-orders-northeurope"
resource_group_name = module.cache_rg.name
location = "northeurope"
sku_name = "Balanced_B5"
default_database = {
geo_replication_group_name = local.redis_geo_group
modules = [{ name = "RedisJSON" }]
}
tags = {
environment = "prod"
}
}
# ONE declaration for the whole group. Linking is reciprocal.
module "cache_geo" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-managed-redis-geo-replication.git?ref=v1.0.0"
managed_redis_id = module.cache_eastus2.id
linked_managed_redis_ids = [
module.cache_westus2.id,
module.cache_northeurope.id,
]
}
# The composition's own output carries what no single member can enforce.
output "cache_geo_posture" {
value = {
# The group name matched only because it came from one local -- nothing checks it.
group_names = [
module.cache_eastus2.geo_replication_group_name,
module.cache_westus2.geo_replication_group_name,
module.cache_northeurope.geo_replication_group_name,
]
group_size = module.cache_geo.group_size
capacity_remaining = module.cache_geo.remaining_group_capacity
# Geo-replication IS the durability story here: persistence cannot coexist with it.
members_are_memory_only_individually = module.cache_eastus2.has_no_durability_beyond_the_instance
# And this module cannot confirm the group is geographically spread.
region_spread_unverifiable = module.cache_geo.region_spread_is_not_knowable_here
}
}
check "geo_group_is_actually_geographic" {
assert {
condition = length(distinct([
module.cache_eastus2.location,
module.cache_westus2.location,
module.cache_northeurope.location,
])) == 3
error_message = "The geo-replication group has members sharing a region, which defeats its purpose."
}
}π‘ Order comes from references alone β the instances from the resource group, the link from three instance
ids. Nodepends_onanywhere.
π΄ One geo-replication declaration, three members. A second declaration from any other member would collide with this one silently.
β The
checkblock asserts what this module structurally cannot: that the group spans regions, using each member's ownlocation.
β οΈ module.cache_rgis referenced fornameandlocationonly. The resource-group module takestagsas an input and emits none.
Required (2) β managed_redis_id (the anchor, and this resource's own id), linked_managed_redis_ids (a set, at
most 4, excluding the anchor).
Universal tail (1) β timeouts, all four keys. There is no tags input, because the resource exposes no tags
attribute β tag the member instances instead.
Full input schemas
| Name | Type | Default | Required |
|---|---|---|---|
managed_redis_id |
string |
β | β |
linked_managed_redis_ids |
set(string) |
β | β |
timeouts |
object({ create = optional(string), read = optional(string), update = optional(string), delete = optional(string) }) |
null |
β |
# Both ID arguments take the same shape:
# /subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.Cache/redisEnterprise/<name>
#
# linked_managed_redis_ids:
# - at least 1, at most 4 (the anchor makes 5)
# - must NOT contain managed_redis_id (case-insensitively)
# - must not contain one ID in two different casings7 validations, all proven to fire: the anchored ID shape on both arguments, the empty and over-four bounds, the
self-referential exclusion, the duplicate-casing check, and the timeouts duration format.
Nothing is grouped here β three arguments, and the two ID arguments are related by a rejection rather than a joint rule. See the SCOPE.
| Output | Description | Notes |
|---|---|---|
id |
The anchor's Resource ID | Emitted first; not a new identifier |
managed_redis_id |
The same value, echoed | |
anchor_name |
The anchor's name, parsed | "Anchor" is this module's word, not Azure's |
resource_group_name / subscription_id |
Parsed from the anchor's ID | |
linked_managed_redis_ids |
The other members, sorted | Excludes the anchor |
linked_names |
Their names, parsed and sorted | Readable in a plan |
group_size |
Linked set plus the anchor | The provider caps it at 5 |
remaining_group_capacity |
How many more can join | 0 means full |
spans_subscriptions |
Reported, not rejected | No prohibition is published |
spans_resource_groups |
Several lock and policy scopes | |
region_spread_is_not_knowable_here |
Constant true |
An ID carries no region |
two_declarations_for_one_instance_collide_silently |
Constant true |
Read this one |
group_names_must_match_and_nothing_here_can_check_it |
Constant true |
A bare-string join |
every_member_needs_balanced_b3_or_higher |
Constant true |
A fact about the other resource |
only_redisearch_and_redisjson_modules_are_allowed_in_a_group |
Constant true |
The provider enforces nothing |
persistence_cannot_be_combined_with_membership |
Constant true |
Alternative durability strategies |
unlinking_does_not_delete_any_instance |
Constant true |
Both directions |
accepts_no_credential |
Constant true |
Contrast with the parent module |
No output is a secret, and nothing this module accepts is one either.
The resource has no identity of its own, and that is the central fact. Its id is managed_redis_id β the provider's
attributes reference says so, and its import path is just the cluster ID. So two declarations against one instance are two
Terraform resources claiming one ARM object from two different state addresses, with nothing to conflict on: both plan,
both apply, and every plan afterwards proposes to revert the other's link set. Azure raises nothing, because each write is
individually legitimate. Combined with reciprocal linking β one declaration links the whole group β the natural instinct to
declare it from every member for symmetry is exactly the failure mode.
One cross-argument rule is enforceable and three are not, and the difference is visibility. The self-referential rule
has both halves as inputs, so linked_managed_redis_ids is checked against managed_redis_id at plan time,
case-insensitively. The group-name match, the Balanced_B3 floor and the module restriction all live on the other
resource's default_database block, invisible from here β so they are emitted as constants with the parent module named as
where enforcement happens. The group-name rule is the sharpest of the three because its join is a bare string: a
mismatch produces no error, and the link simply never forms.
The bound is on the input, not the group. Four linked IDs make a group of five, because the anchor is counted
automatically. And because set(string) deduplicates exact strings but ARM Resource IDs are case-insensitive, two casings
of one ID survive as two set members and consume two of the four slots β which a second validation rejects.
A Resource ID carries no region. Subscription and resource group, yes; region, no. So this module can report that a
group spans subscriptions or resource groups, and cannot report the one thing the feature exists for. That belongs in a
check block over the member modules' own location outputs, which the README demonstrates rather than pretending the
module could do it.
Update semantics are asymmetric. Adding or removing a member is an in-place update, so all four timeouts keys exist;
changing the anchor is a replacement that dissolves and rebuilds the group. Since the anchor has no operational
significance, leaving it alone is free.
Segment arithmetic. The anchor ID is nine segments, so the instance name is length - 1 and the resource group is
index 4 β written in a comment, because an off-by-one yields the literal redisEnterprise, which formats and validates
perfectly.
| Concern | Secure default (empty call) | Opt-out (caller must type it) |
|---|---|---|
| Both arguments are required | There is no empty call to harden, and no enum to flip. The module's value is in what it rejects | β |
| The anchor listed among the links | Rejected at plan time, case-insensitively | β |
| One ID in two casings | Rejected β a set would not catch it, and it consumes two of four slots | β |
| More than four links | Rejected, with the group-of-five arithmetic explained | β |
An azurerm_redis_cache ID |
Rejected by name β a different ARM type | β |
A child path such as /databases/default |
Rejected β the regex is anchored at both ends | β |
| Credentials | None accepted, none emitted | β |
| Cross-subscription groups | Allowed and reported β no prohibition is published, unlike the ML attachment elsewhere in this library | β |
| Region spread | Not checkable. Reported as a constant, with the check block that does work shown in the README |
β |
β οΈ What this table cannot cover. Three of the four rules governing group membership live on the member instances, not here. The module names them and names where they are enforced.
terraform init -backend=false
terraform validate
terraform fmt -checkPin the module by tag, never by branch:
source = "git::https://github.com/microsoftexpert/terraform-azurerm-managed-redis-geo-replication.git?ref=v1.0.0"Authored and verified plan-only. No cloud apply happens during authoring or review; a human applies from CI.
| Gate | What it proves | What it does not |
|---|---|---|
terraform init -backend=false |
The provider constraint resolves | Nothing about Azure |
terraform validate |
HCL parses; types line up; validations are declared | It does not run root-module variable validations from a calling configuration |
terraform fmt -check |
Canonical formatting | Nothing semantic |
terraform console with a .tfvars file |
The real offline harness. All 7 validations proven to fire, by line number | Nothing needing an Azure API call |
terraform plan |
Provider-level validation | Requires credentials; not run here |
| Apply | Whether the members' group names match, their SKUs qualify, and the group actually spans regions | β |
All 7 validations were proven individually by line number β Terraform hard-wraps messages, so grepping for a phrase silently matches nothing. The duplicate-casing fixture necessarily also trips the ID-shape check, because an uppercased ARM path fails the anchored regex too; that overlap is by construction and is stated rather than contrived around.
Every derived local was printed against four shapes β a two-member group, a full five-member group, a
cross-subscription group and a cross-resource-group group. That is what confirmed the nine-segment arithmetic yields
redis-eastus2 rather than the literal redisEnterprise, and that remaining_group_capacity reaches 0 exactly at the
provider's limit.
Outputs:
accepts_no_credential = true
anchor_name = "redis-orders-eastus2"
every_member_needs_balanced_b3_or_higher = true
group_names_must_match_and_nothing_here_can_check_it = true
group_size = 3
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-cache-prod/providers/Microsoft.Cache/redisEnterprise/redis-orders-eastus2"
linked_managed_redis_ids = [
"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-cache-prod/providers/Microsoft.Cache/redisEnterprise/redis-orders-northeurope",
"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-cache-prod/providers/Microsoft.Cache/redisEnterprise/redis-orders-westus2",
]
linked_names = [
"redis-orders-northeurope",
"redis-orders-westus2",
]
managed_redis_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-cache-prod/providers/Microsoft.Cache/redisEnterprise/redis-orders-eastus2"
only_redisearch_and_redisjson_modules_are_allowed_in_a_group = true
persistence_cannot_be_combined_with_membership = true
region_spread_is_not_knowable_here = true
remaining_group_capacity = 2
resource_group_name = "rg-cache-prod"
spans_resource_groups = false
spans_subscriptions = false
subscription_id = "00000000-0000-0000-0000-000000000000"
two_declarations_for_one_instance_collide_silently = true
unlinking_does_not_delete_any_instance = true
| Symptom | Cause | Fix |
|---|---|---|
| Every plan proposes to change the link set, forever | Two declarations of this module against one instance. Its id is managed_redis_id, so they claim one ARM object. |
Delete one. Linking is reciprocal β one declaration links the whole group. |
linked_managed_redis_ids contains managed_redis_id itself |
The anchor was listed among the links. | Remove it; the provider includes it automatically. |
linked_managed_redis_ids may contain at most 4 IDs |
Five or more links. | The group caps at 5 including the anchor. |
linked_managed_redis_ids contains the same Managed Redis twice under different casing |
Two spellings of one ARM path. A set does not deduplicate them. |
Use one spelling; each copy consumes a slot. |
must be a Managed Redis Resource ID ending .../redisEnterprise/<name> |
An azurerm_redis_cache ID (.../Microsoft.Cache/redis/<name>), or a child path. |
Pass module.<redis>.id. They are different services. |
| The link never forms and nothing errors | The members' geo_replication_group_name values differ. Nothing can check it. |
Derive the name from one local and pass it to every member. |
| Apply fails on a member's SKU | Geo-replication needs Balanced_B3 or higher on every member. |
Scale the undersized member. |
| Apply fails on a member's modules | Only RediSearch and RedisJSON are allowed in a group. |
Remove the others β which destroys that member's data. |
| A member has persistence enabled and refuses to link | Persistence works only on non-geo-replicated databases. | Choose one durability mechanism. |
| Destroying the module did not delete anything | It is a link, not a lifecycle owner. | Expected. Destroy the instances through their own modules. |
| Terraform reports no drift after a member was deleted | The stored ID is still the ID it was given. | Re-plan after recreating it, or taint this resource. |
- Provider resource:
azurerm_managed_redis_geo_replication - Platform: Active geo-replication for Azure Managed Redis
- Platform: Azure Managed Redis persistence β why it cannot coexist with membership
- Siblings:
terraform-azurerm-managed-redis(the members; where the group name, SKU floor and module rules are enforced),terraform-azurerm-redis-cache(a different ARM type with its own separate geo-replication mechanism),terraform-azurerm-resource-group - This module's
SCOPE.md
π "Infrastructure as Code should be standardized, consistent, and secure."