diff --git a/docs/changelog.md b/docs/changelog.md index 77ec75aff8..f0a6ec011d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -34,6 +34,9 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers - App Configuration: - Check that replica locations are in allowed regions by @BernieWhite. [#3441](https://github.com/Azure/PSRule.Rules.Azure/issues/3441) + - Azure Cache for Redis: + - Check for legacy Azure Cache for Redis instances by @BenjaminEngeset. + [#3605](https://github.com/Azure/PSRule.Rules.Azure/issues/3605) - Managed Instance for Apache Cassandra: - Check that Managed Instance for Apache Cassandra clusters have availability zones enabled by @BenjaminEngeset. [#3592](https://github.com/Azure/PSRule.Rules.Azure/issues/3592) diff --git a/docs/en/rules/Azure.Redis.MigrateAMR.md b/docs/en/rules/Azure.Redis.MigrateAMR.md new file mode 100644 index 0000000000..dbc4c26b5a --- /dev/null +++ b/docs/en/rules/Azure.Redis.MigrateAMR.md @@ -0,0 +1,100 @@ +--- +reviewed: 2025-11-23 +severity: Important +pillar: Operational Excellence +category: OE:05 Infrastructure as code +resource: Azure Cache for Redis +resourceType: Microsoft.Cache/redis +online version: https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.Redis.MigrateAMR/ +--- + +# Migrate to Azure Managed Redis + +## SYNOPSIS + +Azure Cache for Redis is being retired. Migrate to Azure Managed Redis. + +## DESCRIPTION + +Microsoft has announced the retirement timeline for Azure Cache for Redis across all SKUs. +The recommended replacement going forward is Azure Managed Redis. + +Azure Cache for Redis (Basic, Standard, Premium) will be retired according to the following timeline: + +- Creation blocked for new customers: April 1, 2026. +- Creation blocked for existing customers: October 1, 2026. +- Retirement Date: September 30, 2028. +- Instances will be disabled starting October 1, 2028. + +To avoid service disruption, migrate your workloads to Azure Managed Redis. + +## RECOMMENDATION + +Plan and execute migration from Azure Cache for Redis to Azure Managed Redis before the retirement dates to avoid service disruption. + +## EXAMPLES + +### Configure with Bicep + +To deploy resource that pass this rule: + +- Create resources of type `Microsoft.Cache/redisEnterprise` and an Azure Managed Redis SKU, such as: + - `Balanced_*` + - `MemoryOptimized_*` + - `ComputeOptimized_*` + +For example: + +```bicep +resource primary 'Microsoft.Cache/redisEnterprise@2025-07-01' = { + name: name + location: location + properties: { + highAvailability: 'Enabled' + publicNetworkAccess: 'Disabled' + } + sku: { + name: 'Balanced_B10' + } +} +``` + +### Configure with Azure template + +To deploy resource that pass this rule: + +- Create resources of type `Microsoft.Cache/redisEnterprise` and an Azure Managed Redis SKU, such as: + - `Balanced_*` + - `MemoryOptimized_*` + - `ComputeOptimized_*` + +For example: + +```json +{ + "type": "Microsoft.Cache/redisEnterprise", + "apiVersion": "2025-07-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "properties": { + "highAvailability": "Enabled", + "publicNetworkAccess": "Disabled" + }, + "sku": { + "name": "Balanced_B10" + } +} +``` + +## NOTES + +Redis Enterprise and Enterprise Flash used the `Microsoft.Cache/redisEnterprise` resource type. +Redis Enterprise and Enterprise Flash SKUs `Enterprise_*` and `EnterpriseFlash_*` are also deprecated. + +## LINKS + +- [OE:05 Infrastructure as code](https://learn.microsoft.com/azure/architecture/framework/devops/automation-infrastructure) +- [Azure Cache for Redis retirement: What to know and how to prepare](https://techcommunity.microsoft.com/blog/azure-managed-redis/azure-cache-for-redis-retirement-what-to-know-and-how-to-prepare/4458721) +- [Azure Cache for Redis retirement FAQ](https://learn.microsoft.com/azure/azure-cache-for-redis/retirement-faq) +- [Azure Managed Redis documentation](https://learn.microsoft.com/azure/azure-cache-for-redis/managed-redis/managed-redis-overview) +- [Azure deployment reference](https://learn.microsoft.com/azure/templates/microsoft.cache/redisenterprise) diff --git a/docs/examples/resources/amr.bicep b/docs/examples/resources/amr.bicep new file mode 100644 index 0000000000..49ce229c74 --- /dev/null +++ b/docs/examples/resources/amr.bicep @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Bicep documentation examples + +@minLength(1) +@maxLength(63) +@sys.description('The name of the resource.') +param name string + +@sys.description('The location resources will be deployed.') +param location string = resourceGroup().location + +@sys.description('The location of a secondary replica.') +param secondaryLocation string = location + +// An example Azure Managed Redis instance with availability zones. +resource primary 'Microsoft.Cache/redisEnterprise@2025-07-01' = { + name: name + location: location + properties: { + highAvailability: 'Enabled' + publicNetworkAccess: 'Disabled' + } + sku: { + name: 'Balanced_B10' + } +} + +// An example secondary replica in an alternative region. +resource secondary 'Microsoft.Cache/redisEnterprise@2025-07-01' = { + name: name + location: secondaryLocation + properties: { + highAvailability: 'Enabled' + publicNetworkAccess: 'Disabled' + } + sku: { + name: 'Balanced_B10' + } +} + +// An example database replicated across the primary and secondary instances. +resource database 'Microsoft.Cache/redisEnterprise/databases@2025-07-01' = { + parent: primary + name: 'default' + properties: { + clientProtocol: 'Encrypted' + evictionPolicy: 'VolatileLRU' + clusteringPolicy: 'OSSCluster' + deferUpgrade: 'NotDeferred' + modules: [ + { + name: 'RedisJSON' + } + ] + persistence: { + aofEnabled: false + rdbEnabled: true + rdbFrequency: '12h' + } + accessKeysAuthentication: 'Disabled' + geoReplication: { + groupNickname: 'group' + linkedDatabases: [ + { + id: resourceId('Microsoft.Cache/redisEnterprise/databases', secondary.name, 'default') + } + ] + } + } +} diff --git a/docs/examples/resources/amr.json b/docs/examples/resources/amr.json new file mode 100644 index 0000000000..748e199303 --- /dev/null +++ b/docs/examples/resources/amr.json @@ -0,0 +1,97 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "6517319720095351040" + } + }, + "parameters": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 63, + "metadata": { + "description": "The name of the resource." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "The location resources will be deployed." + } + }, + "secondaryLocation": { + "type": "string", + "defaultValue": "[parameters('location')]", + "metadata": { + "description": "The location of a secondary replica." + } + } + }, + "resources": [ + { + "type": "Microsoft.Cache/redisEnterprise", + "apiVersion": "2025-07-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "properties": { + "highAvailability": "Enabled", + "publicNetworkAccess": "Disabled" + }, + "sku": { + "name": "Balanced_B10" + } + }, + { + "type": "Microsoft.Cache/redisEnterprise", + "apiVersion": "2025-07-01", + "name": "[parameters('name')]", + "location": "[parameters('secondaryLocation')]", + "properties": { + "highAvailability": "Enabled", + "publicNetworkAccess": "Disabled" + }, + "sku": { + "name": "Balanced_B10" + } + }, + { + "type": "Microsoft.Cache/redisEnterprise/databases", + "apiVersion": "2025-07-01", + "name": "[format('{0}/{1}', parameters('name'), 'default')]", + "properties": { + "clientProtocol": "Encrypted", + "evictionPolicy": "VolatileLRU", + "clusteringPolicy": "OSSCluster", + "deferUpgrade": "NotDeferred", + "modules": [ + { + "name": "RedisJSON" + } + ], + "persistence": { + "aofEnabled": false, + "rdbEnabled": true, + "rdbFrequency": "12h" + }, + "accessKeysAuthentication": "Disabled", + "geoReplication": { + "groupNickname": "group", + "linkedDatabases": [ + { + "id": "[resourceId('Microsoft.Cache/redisEnterprise/databases', parameters('name'), 'default')]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Cache/redisEnterprise', parameters('name'))]", + "[resourceId('Microsoft.Cache/redisEnterprise', parameters('name'))]" + ] + } + ] +} \ No newline at end of file diff --git a/src/PSRule.Rules.Azure/en/PSRule-rules.psd1 b/src/PSRule.Rules.Azure/en/PSRule-rules.psd1 index 2aa982b189..b6c103ef3b 100644 --- a/src/PSRule.Rules.Azure/en/PSRule-rules.psd1 +++ b/src/PSRule.Rules.Azure/en/PSRule-rules.psd1 @@ -131,4 +131,5 @@ ResourceHasNoTags = "The resource does not have any tags. Expected tags: {0}." ActiveSecurityAlerts = "There are {0} active security alerts of high or medium severity." KeyValueShouldNotContainSecrets = "The key value '{0}' property should not contain secrets." + CacheRedisMigrateAMR = "Azure Cache for Redis is being retired. Migrate to Azure Managed Redis." } diff --git a/src/PSRule.Rules.Azure/rules/Azure.Redis.Rule.ps1 b/src/PSRule.Rules.Azure/rules/Azure.Redis.Rule.ps1 index caba1003df..4e53f43fb6 100644 --- a/src/PSRule.Rules.Azure/rules/Azure.Redis.Rule.ps1 +++ b/src/PSRule.Rules.Azure/rules/Azure.Redis.Rule.ps1 @@ -123,6 +123,11 @@ Rule 'Azure.Redis.Version' -Ref 'AZR-000347' -Type 'Microsoft.Cache/redis' -Tag ).Reason($LocalizedData.AzureCacheRedisVersion) } +# Synopsis: Azure Cache for Redis is being retired. Migrate to Azure Managed Redis. +Rule 'Azure.Redis.MigrateAMR' -Ref 'AZR-000533' -Type 'Microsoft.Cache/redis' -Tag @{ release = 'GA'; ruleSet = '2025_12'; 'Azure.WAF/pillar' = 'Operational Excellence'; } { + $Assert.Fail($LocalizedData.CacheRedisMigrateAMR) +} + #region Helper functions function global:GetCacheMemory { diff --git a/tests/PSRule.Rules.Azure.Tests/Azure.Redis.Tests.ps1 b/tests/PSRule.Rules.Azure.Tests/Azure.Redis.Tests.ps1 index 44bbcbeaac..dede38e472 100644 --- a/tests/PSRule.Rules.Azure.Tests/Azure.Redis.Tests.ps1 +++ b/tests/PSRule.Rules.Azure.Tests/Azure.Redis.Tests.ps1 @@ -353,6 +353,18 @@ Describe 'Azure.Redis' -Tag 'Redis' { $ruleResult.TargetName | Should -BeIn 'redis-R'; $ruleResult.Length | Should -Be 1; } + + It 'Azure.Redis.MigrateAMR' { + $filteredResult = $result | Where-Object { $_.RuleName -eq 'Azure.Redis.MigrateAMR' }; + + # Fail - all instances should fail + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' }); + $ruleResult | Should -Not -BeNullOrEmpty; + $ruleResult.Length | Should -Be 12; + $ruleResult.TargetName | Should -BeIn 'redis-A', 'redis-B', 'redis-C', 'redis-D', 'redis-E', 'redis-F', 'redis-G', 'redis-H', 'redis-I', 'redis-J', 'redis-Q', 'redis-R'; + + $ruleResult[0].Reason | Should -BeExactly "Azure Cache for Redis is being retired. Migrate to Azure Managed Redis."; + } } Context 'With Configuration Option' -Tag 'Configuration' {