A single IP-range firewall rule on an Azure Cache for Redis, targeting
hashicorp/azurerm ~> 4.0. It allows client connections from a contiguous address range whenever the cache permits public network access.
This module manages one Redis firewall rule and nothing else:
- π₯ A single
azurerm_redis_firewall_rulekeyed to an existing Redis cache. - π A contiguous allowed IP range expressed as
start_ip/end_ip(set both to the same value for a single address). - π References the cache by
redis_cache_nameandresource_group_name; it never creates the cache itself. - π§Ύ Emits the firewall rule's resource
idfor downstream references.
π‘ Why it matters: A Redis cache with public network access on rejects every client not covered by a firewall rule. This module lets a composition declare each allowed range as a small, independently keyed record β so adding, removing, or re-scoping one range never disturbs the others.
If this module saves you time, a little support goes a long way:
- β Star the repository on GitHub.
- πΌ Connect on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
flowchart LR
rc["terraform-azurerm-redis-cache"]
me["terraform-azurerm-redis-firewall-rule"]
v["azurerm_redis_firewall_rule"]
client["client IP range"]
rc -->|"redis_cache_name"| me
me -->|"creates"| v
client -.->|"allowed by (public access only)"| v
classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
classDef target fill:#004578,stroke:#002d4d,color:#ffffff;
classDef ext fill:#f2f2f2,stroke:#c8c8c8,color:#111111;
class me me;
class v target;
class rc,client ext;
The Redis cache is owned by terraform-azurerm-redis-cache; this module consumes its name and adds allowed ranges. The dotted edge is a reminder that firewall rules are consulted only when the cache exposes a public endpoint.
flowchart LR
in_id["name / redis_cache_name / resource_group_name"]
in_ip["start_ip / end_ip"]
res["azurerm_redis_firewall_rule.this"]
out_id["id"]
in_id -->|"input"| res
in_ip -->|"input"| res
res -->|"output"| out_id
classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
class res me;
Resource inventory
| Resource | Cardinality | Role |
|---|---|---|
azurerm_redis_firewall_rule.this |
single (keystone) | The allowed IP range on the target cache. |
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
| azurerm provider | ~> 4.0 |
| Provider block | None in the module β the caller configures provider "azurerm", including the mandatory features {} block, plus authentication. |
Schema notes that bite
name,redis_cache_name, andresource_group_nameare immutable β changing any of them forces a new resource (the rule is destroyed and recreated).start_ipandend_ipupdate in place β widening or narrowing a range does not replace the rule.- Firewall rules apply only when the cache allows public network access. With the secure default of
terraform-azurerm-redis-cache(public access off), a rule is created but never consulted β reach the cache over a private endpoint instead. - For a single address, set
start_ipandend_ipto the same value.
Least-privilege at the Redis cache scope (or its resource group):
Microsoft.Cache/redis/firewallRules/writeMicrosoft.Cache/redis/firewallRules/readMicrosoft.Cache/redis/firewallRules/delete
The built-in Contributor role scoped to the cache (or its resource group) covers these.
- An existing Redis cache.
- The
Microsoft.Cacheresource provider registered on the subscription.
terraform-azurerm-redis-firewall-rule/
βββ providers.tf # required_version + azurerm ~> 4.0 pin (no provider block)
βββ variables.tf # name, redis_cache_name, resource_group_name, start_ip, end_ip, timeouts
βββ main.tf # azurerm_redis_firewall_rule.this + dynamic timeouts
βββ outputs.tf # id
βββ README.md # this document
βββ SCOPE.md # cross-module contract
βββ LICENSE # MIT
βββ .gitignore # canonical library ignore set
provider "azurerm" {
features {}
}
module "office_range" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-redis-firewall-rule.git?ref=v1.0.0"
name = "office"
redis_cache_name = "cache-prod-eastus"
resource_group_name = "rg-data-prod"
start_ip = "203.0.113.0"
end_ip = "203.0.113.255"
}βΉοΈ The caller configures the provider, authentication, and the
features {}block. This module declares neither a provider block nor any auth input.
Consumes
| Input | Type | Source module |
|---|---|---|
resource_group_name |
string |
terraform-azurerm-resource-group |
redis_cache_name |
string |
terraform-azurerm-redis-cache |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
Resource ID of the firewall rule | references / audit |
1 Β· Single IP address
Set start_ip and end_ip to the same value to allow exactly one client address.
module "single_host" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-redis-firewall-rule.git?ref=v1.0.0"
name = "bastion"
redis_cache_name = "cache-prod-eastus"
resource_group_name = "rg-data-prod"
start_ip = "203.0.113.10"
end_ip = "203.0.113.10"
}2 Β· A contiguous range
module "app_tier" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-redis-firewall-rule.git?ref=v1.0.0"
name = "apptier"
redis_cache_name = "cache-prod-eastus"
resource_group_name = "rg-data-prod"
start_ip = "203.0.113.32"
end_ip = "203.0.113.63"
}3 Β· An office CIDR expressed as start/end
A /24 (203.0.113.0/24) becomes the first and last usable host of the block.
module "office" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-redis-firewall-rule.git?ref=v1.0.0"
name = "hq_office"
redis_cache_name = "cache-prod-eastus"
resource_group_name = "rg-data-prod"
start_ip = "203.0.113.0"
end_ip = "203.0.113.255"
}βΉοΈ Rule names accept letters, numbers, and underscores; hyphens are not permitted in a Redis firewall rule name.
4 Β· Naming rules per environment
Keep the rule name stable across environments and vary only the cache it targets.
module "office_dev" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-redis-firewall-rule.git?ref=v1.0.0"
name = "office"
redis_cache_name = "cache-dev-eastus"
resource_group_name = "rg-data-dev"
start_ip = "203.0.113.0"
end_ip = "203.0.113.255"
}
β οΈ Changingname,redis_cache_name, orresource_group_nameforces replacement. Renaming for cosmetic reasons briefly removes the allowed range.
5 Β· for_each over multiple ranges
Key each range by a stable label so adding or removing one never re-indexes the rest.
locals {
redis_ranges = {
office = { start = "203.0.113.0", end = "203.0.113.255" }
datastage = { start = "198.51.100.16", end = "198.51.100.31" }
monitor = { start = "198.51.100.200", end = "198.51.100.200" }
}
}
module "redis_ranges" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-redis-firewall-rule.git?ref=v1.0.0"
for_each = local.redis_ranges
name = each.key
redis_cache_name = "cache-prod-eastus"
resource_group_name = "rg-data-prod"
start_ip = each.value.start
end_ip = each.value.end
}6 Β· Explicit per-operation timeouts
module "slow_range" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-redis-firewall-rule.git?ref=v1.0.0"
name = "batch"
redis_cache_name = "cache-prod-eastus"
resource_group_name = "rg-data-prod"
start_ip = "198.51.100.0"
end_ip = "198.51.100.63"
timeouts = {
create = "30m"
delete = "30m"
}
}βΉοΈ Leave
timeoutsunset (null) to use provider defaults; supply only the operations you need.
7 Β· A CI runner egress IP
module "ci_runner" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-redis-firewall-rule.git?ref=v1.0.0"
name = "ci_runner"
redis_cache_name = "cache-build-eastus2"
resource_group_name = "rg-build"
start_ip = "20.42.0.10"
end_ip = "20.42.0.10"
}8 Β· A partner data feed range
module "partner_feed" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-redis-firewall-rule.git?ref=v1.0.0"
name = "partner_feed"
redis_cache_name = "cache-prod-centralus"
resource_group_name = "rg-data-prod"
start_ip = "192.0.2.128"
end_ip = "192.0.2.159"
}
β οΈ Firewall rules broaden reachability. Grant the narrowest range the partner actually egresses from.
9 Β· Reading the emitted id
module "office" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-redis-firewall-rule.git?ref=v1.0.0"
name = "office"
redis_cache_name = "cache-prod-eastus"
resource_group_name = "rg-data-prod"
start_ip = "203.0.113.0"
end_ip = "203.0.113.255"
}
output "office_rule_id" {
value = module.office.id
}10 Β· Non-overlapping ranges on one cache
Split allowed space into disjoint, self-documenting rules rather than one broad block.
module "corp_west" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-redis-firewall-rule.git?ref=v1.0.0"
name = "corp_west"
redis_cache_name = "cache-prod-westus2"
resource_group_name = "rg-data-prod"
start_ip = "203.0.113.0"
end_ip = "203.0.113.127"
}
module "corp_east" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-redis-firewall-rule.git?ref=v1.0.0"
name = "corp_east"
redis_cache_name = "cache-prod-westus2"
resource_group_name = "rg-data-prod"
start_ip = "203.0.113.128"
end_ip = "203.0.113.255"
}
β οΈ Keep ranges disjoint. Overlapping rules are accepted but obscure which record grants a given address and complicate later removal.
11 Β· Ranges sourced from a variable
variable "allowed_ranges" {
type = map(object({
start_ip = string
end_ip = string
}))
default = {}
}
module "redis_ranges" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-redis-firewall-rule.git?ref=v1.0.0"
for_each = var.allowed_ranges
name = each.key
redis_cache_name = "cache-prod-eastus"
resource_group_name = "rg-data-prod"
start_ip = each.value.start_ip
end_ip = each.value.end_ip
}π‘ An empty default means the empty call grants no public reachability at all β ranges are opt-in.
12 Β· Consuming a cache module output directly
module "cache" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-redis-cache.git?ref=v1.0.0"
location = "eastus2"
name = "cache-example"
resource_group_name = "rg-example"
}
module "office" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-redis-firewall-rule.git?ref=v1.0.0"
name = "office"
redis_cache_name = module.cache.name
resource_group_name = "rg-data-prod"
start_ip = "203.0.113.0"
end_ip = "203.0.113.255"
}13 Β· ποΈ End-to-end composition
Wire a resource group and a Redis cache into a keyed set of firewall rules.
provider "azurerm" {
features {}
}
module "rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-data-prod"
location = "eastus"
}
module "cache" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-redis-cache.git?ref=v1.0.0"
name = "cache-prod-eastus"
resource_group_name = module.rg.name
location = module.rg.location
# The cache module keeps public network access off by default; enable it here
# only if these firewall rules are meant to take effect over the public endpoint.
public_network_access_enabled = true
}
locals {
redis_ranges = {
office = { start = "203.0.113.0", end = "203.0.113.255" }
ci = { start = "20.42.0.10", end = "20.42.0.10" }
partner = { start = "192.0.2.128", end = "192.0.2.159" }
}
}
module "redis_ranges" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-redis-firewall-rule.git?ref=v1.0.0"
for_each = local.redis_ranges
name = each.key
redis_cache_name = module.cache.name
resource_group_name = module.rg.name
start_ip = each.value.start
end_ip = each.value.end
}
output "redis_rule_ids" {
value = { for k, m in module.redis_ranges : k => m.id }
}π The firewall rules above only take effect because the cache enables public network access. For a private-endpoint topology, keep public access off and manage reachability through the network layer instead β the rules become inert.
Identity (required, force-new)
nameβ name of the firewall rule.redis_cache_nameβ name of the target Redis cache.resource_group_nameβ resource group holding the cache.
Allowed range (required, updatable)
start_ipβ lowest IP in the allowed range.end_ipβ highest IP in the allowed range (equal tostart_ipfor a single address).
Universal tail
timeoutsβ optional per-operation timeouts. This resource does not supporttags, so the tail is timeouts only.
Full input schemas
variable "name" {
description = "Name of the firewall rule. Immutable β changing it forces a new resource."
type = string
}
variable "redis_cache_name" {
description = "Name of the Redis cache. Immutable β changing it forces a new resource."
type = string
}
variable "resource_group_name" {
description = "Name of the resource group holding the Redis cache. Immutable β changing it forces a new resource."
type = string
}
variable "start_ip" {
description = "Lowest IP address included in the allowed range."
type = string
}
variable "end_ip" {
description = "Highest IP address included in the allowed range. Use the same value as start_ip for a single address."
type = string
}
variable "timeouts" {
type = object({
create = optional(string)
read = optional(string)
update = optional(string)
delete = optional(string)
})
default = null
description = "Optional per-operation timeouts (Go duration strings such as \"30m\"). Leave null for provider defaults."
}| Output | Description | Kind |
|---|---|---|
id |
Resource ID of the Redis firewall rule | Passthrough |
name |
Name of the firewall rule, as created | Passthrough |
redis_cache_name |
Name of the parent Redis cache, re-read from the created resource's ID by the provider rather than echoed from the input | Passthrough |
resource_group_name |
Name of the resource group holding the parent Redis cache, re-read from the created resource's ID | Passthrough |
start_ip |
Lowest address permitted by this rule, inclusive, as stored by Azure | Passthrough |
end_ip |
Highest address permitted by this rule, inclusive, as stored by Azure | Passthrough |
permits_the_entire_internet |
True when this rule spans 0.0.0.0 through 255.255.255.255, which permits every host on the internet to reach the cache's public endpoint | Passthrough |
permits_a_single_address |
True when both endpoints are the same address, which is the narrowest rule this resource can express and the form to prefer | Passthrough |
permitted_address_count |
Number of addresses this rule permits, inclusive of both endpoints | Passthrough |
range_is_entirely_private_rfc1918 |
True when the whole permitted range sits inside one of the RFC 1918 private blocks -- 10.0.0.0/8, 172.16.0.0/12 or 192.168.0.0/16 | Passthrough |
secure_default_cannot_apply_to_this_resource |
Constant true, and the reason this module reports rather than defaults | Constant |
parent_cache_public_endpoint_is_open_by_default |
Constant true | Constant |
rule_has_no_effect_while_public_access_is_disabled |
Constant true | Constant |
widening_the_range_updates_in_place |
Constant true, and it is a review hazard | Constant |
renaming_the_rule_replaces_it |
Constant true | Constant |
resource_supports_no_tags |
Constant true | Constant |
range_overlap_with_sibling_rules_is_unchecked |
Constant true | Constant |
- Force-new identity fields.
name,redis_cache_name, andresource_group_nameare immutable. Terraform destroys and recreates the rule when any of them changes, which briefly removes the allowed range during the replace. - In-place range edits.
start_ipandend_ipupdate without replacement, so tuning a range is a clean, non-disruptive change. - Public-access-only applicability. A Redis firewall rule governs the cache's public endpoint. When the cache disables public network access, the rule is created and stored but never consulted β do not rely on it as the sole reachability control in a hardened topology.
- Private-endpoint alternative. For caches without public access, connect over a private endpoint and manage exposure through subnet, NSG, and private-DNS wiring rather than firewall rules.
for_eachkey stability. When managing many ranges, key the map by a meaningful, stable label. Re-keying causes churn; changing only the range values does not.features {}dependence. Theazurermprovider will not initialize without a caller-sideprovider "azurerm" { features {} }block. This module intentionally ships no provider block.
| Principle | How this module applies it |
|---|---|
| Least exposure | Firewall rules only matter under public network access; the module adds no reachability on its own and grants nothing unless a caller supplies a range. Prefer private endpoints for hardened caches. |
| Opt-in breadth | Each allowed range is an explicit, independently keyed record β the caller types every address that becomes reachable. |
| No tags | azurerm_redis_firewall_rule does not support tags, so the universal tail is timeouts only. |
| Single primary resource | The module owns only the firewall rule and references the cache by name, keeping ownership boundaries clean. |
terraform init -backend=false
terraform validate
terraform fmt -checkPin the module with ?ref=v1.0.0 β never a branch. This library is plan-only; a human applies from CI after review.
terraform init -backend=falseresolves the provider without configuring a backend.terraform validateconfirms the type contract: all five required inputs are strings andtimeoutsmatches its object shape.terraform fmt -checkenforces canonical formatting.- What only
plan/applyexercises: whether the target cache exists, whether public access is enabled, and whether the IP range is well-formed against the live API. Validation cannot see live tenant state.
$ terraform output
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data-prod/providers/Microsoft.Cache/Redis/cache-prod-eastus/firewallRules/office"For a for_each deployment:
$ terraform output redis_rule_ids
{
"ci" = ".../Redis/cache-prod-eastus/firewallRules/ci"
"office" = ".../Redis/cache-prod-eastus/firewallRules/office"
"partner" = ".../Redis/cache-prod-eastus/firewallRules/partner"
}| Symptom | Cause | Fix |
|---|---|---|
| Rule applies cleanly but clients still cannot connect | Cache has public network access disabled, so the rule is never consulted. | Enable public access on the cache if you intend to use firewall rules, or connect over a private endpoint and drop the rule. |
| Two rules seem to conflict | Overlapping IP ranges across separate rules. | Redefine the ranges to be disjoint so each address is granted by exactly one rule. |
| Every plan shows the rule being replaced | name, redis_cache_name, or resource_group_name is changing (these are force-new). |
Keep the identity fields stable; edit only start_ip / end_ip, which update in place. |
provider "azurerm": features block must be specified |
The caller's root module is missing features {}. |
Add provider "azurerm" { features {} } to the root β the module never declares one. |
| Apply rejects the rule name | Name contains hyphens or other unsupported characters. | Use letters, numbers, and underscores only in the rule name. |
- Terraform Registry β
azurerm_redis_firewall_rule - Terraform Registry β
azurerm_redis_cache - Sibling module β
terraform-azurerm-redis-cache - Sibling module β
terraform-azurerm-private-endpoint(private-access alternative) - This module's
SCOPE.md
π "Infrastructure as Code should be standardized, consistent, and secure."