Skip to content

Commit

Permalink
feat: avm/res/cache/redis: add support for AKV secrets export (#4138)
Browse files Browse the repository at this point in the history
## Description
Closes #4137

Add support for AKV secrets export by introducing a new
`secretsExportConfiguration` input param as per
https://azure.github.io/Azure-Verified-Modules/specs/bcp/res/interfaces/#secrets-export.

In addition to the primary and secondary access key, I also added
support for connection strings in the form `rediss://...`, which is
supported by most Redis clients:

-
[Lettuce](https://github.com/redis/lettuce/wiki/Redis-URI-and-connection-details)
- [Jedis](https://redis.io/docs/latest/develop/clients/jedis/connect/)
-
[node-redis](https://github.com/redis/node-redis/blob/master/docs/client-configuration.md#createclient-configuration)
-
[redis-py](https://redis-py.readthedocs.io/en/stable/connections.html#redis.Redis.from_url)

| Secret key name | Secret value |
|--------|--------|
| `secretsExportConfiguration.primaryAccessKeyName` | Primary access key
|
| `secretsExportConfiguration.primaryConnectionStringName` | TLS-enabled
primary connection string in [Redis URI
form](https://www.iana.org/assignments/uri-schemes/prov/rediss):
`rediss://:<primaryaccesskey>@<hostname>:6380` |
| `secretsExportConfiguration.secondaryAccessKeyName` | Secondary access
key |
| `secretsExportConfiguration.secondaryConnectionStringName` |
TLS-enabled secondary connection string in [Redis URI
form](https://www.iana.org/assignments/uri-schemes/prov/rediss):
`rediss://:<secondaryaccesskey>@<hostname>:6380` |

Usage example:
```bicep
module redis 'br/public:avm/res/cache/redis:<version>' = {
  name: 'redisDeployment'
  params: {
    // Required parameters
    name: 'kvref'
    // Non-required parameters
    location: '<location>'
    secretsExportConfiguration: {
      keyVaultResourceId: '<keyVaultResourceId>'
      primaryAccessKeyName: 'custom-primaryAccessKey-name'
      primaryConnectionStringName: 'custom-primaryConnectionString-name'
      secondaryAccessKeyName: 'custom-secondaryAccessKey-name'
      secondaryConnectionStringName: 'custom-secondaryConnectionString-name'
    }
  }
}
```

## Pipeline Reference

Ran the `kv-secrets` E2E test locally and passed:


![image](https://github.com/user-attachments/assets/240800ba-292d-4346-b04a-9471061599dc)


| Pipeline |
| -------- |
|          |

## Type of Change

<!-- Use the checkboxes [x] on the options that are relevant. -->

- [ ] Update to CI Environment or utilities (Non-module affecting
changes)
- [x] Azure Verified Module updates:
- [ ] Bugfix containing backwards-compatible bug fixes, and I have NOT
bumped the MAJOR or MINOR version in `version.json`:
- [x] Someone has opened a bug report issue, and I have included "Closes
#{bug_report_issue_number}" in the PR description.
- [ ] The bug was found by the module author, and no one has opened an
issue to report it yet.
- [x] Feature update backwards compatible feature updates, and I have
bumped the MINOR version in `version.json`.
- [ ] Breaking changes and I have bumped the MAJOR version in
`version.json`.
  - [ ] Update to documentation

## Checklist

- [x] I'm sure there are no other open Pull Requests for the same
update/change
- [x] I have run `Set-AVMModule` locally to generate the supporting
module files.
- [ ] My corresponding pipelines / checks run clean and green without
any errors or warnings

<!-- Please keep up to date with the contribution guide at
https://aka.ms/avm/contribute/bicep -->

---------

Co-authored-by: hundredacres <[email protected]>
  • Loading branch information
JeffreyCA and hundredacres authored Jan 22, 2025
1 parent 9e9b912 commit 9a0b5af
Show file tree
Hide file tree
Showing 8 changed files with 608 additions and 10 deletions.
167 changes: 161 additions & 6 deletions avm/res/cache/redis/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ This module deploys a Redis Cache.
| `Microsoft.Cache/redis/accessPolicyAssignments` | [2024-04-01-preview](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Cache/2024-04-01-preview/redis/accessPolicyAssignments) |
| `Microsoft.Cache/redis/linkedServers` | [2024-03-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Cache/2024-03-01/redis/linkedServers) |
| `Microsoft.Insights/diagnosticSettings` | [2021-05-01-preview](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Insights/2021-05-01-preview/diagnosticSettings) |
| `Microsoft.KeyVault/vaults/secrets` | [2023-07-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.KeyVault/2023-07-01/vaults/secrets) |
| `Microsoft.Network/privateEndpoints` | [2023-11-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Network/2023-11-01/privateEndpoints) |
| `Microsoft.Network/privateEndpoints/privateDnsZoneGroups` | [2023-11-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Network/2023-11-01/privateEndpoints/privateDnsZoneGroups) |

Expand All @@ -36,9 +37,10 @@ The following section provides usage examples for the module, which were used to
- [Using only defaults](#example-1-using-only-defaults)
- [Using EntraID authentication](#example-2-using-entraid-authentication)
- [Using large parameter set](#example-3-using-large-parameter-set)
- [Passive Geo-Replicated Redis Cache](#example-4-passive-geo-replicated-redis-cache)
- [WAF-aligned](#example-5-waf-aligned)
- [Deploying with a key vault reference to save secrets](#example-3-deploying-with-a-key-vault-reference-to-save-secrets)
- [Using large parameter set](#example-4-using-large-parameter-set)
- [Passive Geo-Replicated Redis Cache](#example-5-passive-geo-replicated-redis-cache)
- [WAF-aligned](#example-6-waf-aligned)

### Example 1: _Using only defaults_

Expand Down Expand Up @@ -222,7 +224,94 @@ param redisConfiguration = {
</details>
<p>

### Example 3: _Using large parameter set_
### Example 3: _Deploying with a key vault reference to save secrets_

This instance deploys the module saving all its secrets in a key vault.


<details>

<summary>via Bicep module</summary>

```bicep
module redis 'br/public:avm/res/cache/redis:<version>' = {
name: 'redisDeployment'
params: {
// Required parameters
name: 'kvref'
// Non-required parameters
location: '<location>'
secretsExportConfiguration: {
keyVaultResourceId: '<keyVaultResourceId>'
primaryAccessKeyName: 'custom-primaryAccessKey-name'
primaryConnectionStringName: 'custom-primaryConnectionString-name'
secondaryAccessKeyName: 'custom-secondaryAccessKey-name'
secondaryConnectionStringName: 'custom-secondaryConnectionString-name'
}
}
}
```

</details>
<p>

<details>

<summary>via JSON parameters file</summary>

```json
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
// Required parameters
"name": {
"value": "kvref"
},
// Non-required parameters
"location": {
"value": "<location>"
},
"secretsExportConfiguration": {
"value": {
"keyVaultResourceId": "<keyVaultResourceId>",
"primaryAccessKeyName": "custom-primaryAccessKey-name",
"primaryConnectionStringName": "custom-primaryConnectionString-name",
"secondaryAccessKeyName": "custom-secondaryAccessKey-name",
"secondaryConnectionStringName": "custom-secondaryConnectionString-name"
}
}
}
}
```

</details>
<p>

<details>

<summary>via Bicep parameters file</summary>

```bicep-params
using 'br/public:avm/res/cache/redis:<version>'
// Required parameters
param name = 'kvref'
// Non-required parameters
param location = '<location>'
param secretsExportConfiguration = {
keyVaultResourceId: '<keyVaultResourceId>'
primaryAccessKeyName: 'custom-primaryAccessKey-name'
primaryConnectionStringName: 'custom-primaryConnectionString-name'
secondaryAccessKeyName: 'custom-secondaryAccessKey-name'
secondaryConnectionStringName: 'custom-secondaryConnectionString-name'
}
```

</details>
<p>

### Example 4: _Using large parameter set_

This instance deploys the module with most of its features enabled.

Expand Down Expand Up @@ -623,7 +712,7 @@ param zones = [
</details>
<p>

### Example 4: _Passive Geo-Replicated Redis Cache_
### Example 5: _Passive Geo-Replicated Redis Cache_

This instance deploys the module with geo-replication enabled.

Expand Down Expand Up @@ -763,7 +852,7 @@ param zoneRedundant = false
</details>
<p>

### Example 5: _WAF-aligned_
### Example 6: _WAF-aligned_

This instance deploys the module in alignment with the best-practices of the Azure Well-Architected Framework.

Expand Down Expand Up @@ -1051,6 +1140,7 @@ param zones = [
| [`replicasPerMaster`](#parameter-replicaspermaster) | int | The number of replicas to be created per primary. |
| [`replicasPerPrimary`](#parameter-replicasperprimary) | int | The number of replicas to be created per primary. Needs to be the same as replicasPerMaster for a Premium Cluster Cache. |
| [`roleAssignments`](#parameter-roleassignments) | array | Array of role assignments to create. |
| [`secretsExportConfiguration`](#parameter-secretsexportconfiguration) | object | Key vault reference and secret settings for the module's secrets export. |
| [`shardCount`](#parameter-shardcount) | int | The number of shards to be created on a Premium Cluster Cache. |
| [`skuName`](#parameter-skuname) | string | The type of Redis cache to deploy. |
| [`staticIP`](#parameter-staticip) | string | Static IP address. Optionally, may be specified when deploying a Redis cache inside an existing Azure Virtual Network; auto assigned by default. |
Expand Down Expand Up @@ -2000,6 +2090,69 @@ The principal type of the assigned principal ID.
```
- MinValue: 1

### Parameter: `secretsExportConfiguration`

Key vault reference and secret settings for the module's secrets export.

- Required: No
- Type: object
- MinValue: 1

**Required parameters**

| Parameter | Type | Description |
| :-- | :-- | :-- |
| [`keyVaultResourceId`](#parameter-secretsexportconfigurationkeyvaultresourceid) | string | The resource ID of the key vault where to store the secrets of this module. |

**Optional parameters**

| Parameter | Type | Description |
| :-- | :-- | :-- |
| [`primaryAccessKeyName`](#parameter-secretsexportconfigurationprimaryaccesskeyname) | string | The primaryAccessKey secret name to create. |
| [`primaryConnectionStringName`](#parameter-secretsexportconfigurationprimaryconnectionstringname) | string | The primaryConnectionString secret name to create. |
| [`secondaryAccessKeyName`](#parameter-secretsexportconfigurationsecondaryaccesskeyname) | string | The secondaryAccessKey secret name to create. |
| [`secondaryConnectionStringName`](#parameter-secretsexportconfigurationsecondaryconnectionstringname) | string | The secondaryConnectionString secret name to create. |

### Parameter: `secretsExportConfiguration.keyVaultResourceId`

The resource ID of the key vault where to store the secrets of this module.

- Required: Yes
- Type: string
- MinValue: 1

### Parameter: `secretsExportConfiguration.primaryAccessKeyName`

The primaryAccessKey secret name to create.

- Required: No
- Type: string
- MinValue: 1

### Parameter: `secretsExportConfiguration.primaryConnectionStringName`

The primaryConnectionString secret name to create.

- Required: No
- Type: string
- MinValue: 1

### Parameter: `secretsExportConfiguration.secondaryAccessKeyName`

The secondaryAccessKey secret name to create.

- Required: No
- Type: string
- MinValue: 1

### Parameter: `secretsExportConfiguration.secondaryConnectionStringName`

The secondaryConnectionString secret name to create.

- Required: No
- Type: string
- MinValue: 1

### Parameter: `shardCount`

The number of shards to be created on a Premium Cluster Cache.
Expand Down Expand Up @@ -2090,6 +2243,7 @@ If the zoneRedundant parameter is true, replicas will be provisioned in the avai

| Output | Type | Description |
| :-- | :-- | :-- |
| `exportedSecrets` | | A hashtable of references to the secrets exported to the provided Key Vault. The key of each reference is each secret's name. |
| `hostName` | string | Redis hostname. |
| `location` | string | The location the resource was deployed into. |
| `name` | string | The name of the Redis Cache. |
Expand All @@ -2107,6 +2261,7 @@ This section gives you an overview of all local-referenced module files (i.e., o
| Reference | Type |
| :-- | :-- |
| `br/public:avm/res/network/private-endpoint:0.7.1` | Remote reference |
| `br/public:avm/utl/types/avm-common-types:0.4.1` | Remote reference |

## Notes

Expand Down
2 changes: 1 addition & 1 deletion avm/res/cache/redis/linked-servers/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"_generator": {
"name": "bicep",
"version": "0.32.4.45862",
"templateHash": "9818219089257499387"
"templateHash": "3113734842268106241"
},
"name": "Redis Cache Linked Servers",
"description": "This module connects a primary and secondary Redis Cache together for geo-replication."
Expand Down
73 changes: 73 additions & 0 deletions avm/res/cache/redis/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ param accessPolicies accessPolicyType[] = []
@description('Optional. Array of access policy assignments.')
param accessPolicyAssignments accessPolicyAssignmentType[] = []

@description('Optional. Key vault reference and secret settings for the module\'s secrets export.')
param secretsExportConfiguration secretsExportConfigurationType?

var availabilityZones = skuName == 'Premium'
? zoneRedundant ? !empty(zones) ? zones : pickZones('Microsoft.Cache', 'redis', location, 3) : []
: []
Expand Down Expand Up @@ -356,6 +359,52 @@ module redis_geoReplication 'linked-servers/main.bicep' = if (!empty(geoReplicat
dependsOn: redis_privateEndpoints
}

module secretsExport 'modules/keyVaultExport.bicep' = if (secretsExportConfiguration != null) {
name: '${uniqueString(deployment().name, location)}-secrets-kv'
scope: resourceGroup(
split((secretsExportConfiguration.?keyVaultResourceId ?? '//'), '/')[2],
split((secretsExportConfiguration.?keyVaultResourceId ?? '////'), '/')[4]
)
params: {
keyVaultName: last(split(secretsExportConfiguration.?keyVaultResourceId ?? '//', '/'))
secretsToSet: union(
[],
contains(secretsExportConfiguration!, 'primaryAccessKeyName')
? [
{
name: secretsExportConfiguration!.primaryAccessKeyName
value: redis.listKeys().primaryKey
}
]
: [],
contains(secretsExportConfiguration!, 'primaryConnectionStringName')
? [
{
name: secretsExportConfiguration!.primaryConnectionStringName
value: 'rediss://:${redis.listKeys().primaryKey}@${redis.properties.hostName}:6380'
}
]
: [],
contains(secretsExportConfiguration!, 'secondaryAccessKeyName')
? [
{
name: secretsExportConfiguration!.secondaryAccessKeyName
value: redis.listKeys().secondaryKey
}
]
: [],
contains(secretsExportConfiguration!, 'secondaryConnectionStringName')
? [
{
name: secretsExportConfiguration!.secondaryConnectionStringName
value: 'rediss://:${redis.listKeys().secondaryKey}@${redis.properties.hostName}:6380'
}
]
: []
)
}
}

@description('The name of the Redis Cache.')
output name string = redis.name

Expand Down Expand Up @@ -391,6 +440,12 @@ output privateEndpoints array = [
}
]

import { secretsOutputType } from 'br/public:avm/utl/types/avm-common-types:0.4.1'
@description('A hashtable of references to the secrets exported to the provided Key Vault. The key of each reference is each secret\'s name.')
output exportedSecrets secretsOutputType = (secretsExportConfiguration != null)
? toObject(secretsExport.outputs.secretsSet, secret => last(split(secret.secretResourceId, '/')), secret => secret)
: {}

// =============== //
// Definitions //
// =============== //
Expand Down Expand Up @@ -583,3 +638,21 @@ type accessPolicyAssignmentType = {
@description('Required. Name of the access policy to be assigned.')
accessPolicyName: string
}

@export()
type secretsExportConfigurationType = {
@description('Required. The resource ID of the key vault where to store the secrets of this module.')
keyVaultResourceId: string

@description('Optional. The primaryAccessKey secret name to create.')
primaryAccessKeyName: string?

@description('Optional. The primaryConnectionString secret name to create.')
primaryConnectionStringName: string?

@description('Optional. The secondaryAccessKey secret name to create.')
secondaryAccessKeyName: string?

@description('Optional. The secondaryConnectionString secret name to create.')
secondaryConnectionStringName: string?
}
Loading

0 comments on commit 9a0b5af

Please sign in to comment.