Skip to content

Commit 174bc8f

Browse files
authored
Add new Terraform module for Azure AppConfiguration (#1105)
1 parent fb9caa2 commit 174bc8f

File tree

21 files changed

+1293
-76
lines changed

21 files changed

+1293
-76
lines changed

.changeset/metal-comics-clap.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"azure_app_configuration": patch
3+
---
4+
5+
Initial version

apps/website/docs/azure/app-configuration/azure-app-configuration.md

Lines changed: 32 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -12,74 +12,31 @@ exploiting the hot reload capabilities.
1212

1313
## Configuring the resource via Terraform
1414

15-
You can use the following Terraform code to create an Azure App Configuration
16-
instance. Make sure to adapt the naming conventions and resource group according
17-
to your environment. The example below creates a standard
15+
You can use the Terraform module
16+
[`azure_app_configuration`](https://registry.terraform.io/modules/pagopa-dx/azure-app-configuration/azurerm/latest)
17+
to create an Azure App Configuration instance. The module usage - showed in the
18+
example below - creates a standard
1819
[SKU App Configuration](https://azure.microsoft.com/en-us/pricing/details/app-configuration/)
19-
instance with system-assigned identity, private endpoint connectivity, and
20+
instance with private endpoint connectivity, Entra ID authentication, and
2021
[purge protection enabled](https://learn.microsoft.com/en-us/azure/azure-app-configuration/howto-recover-deleted-stores-in-azure-app-configuration).
2122

2223
```hcl
2324
24-
resource "azurerm_app_configuration" "example" {
25-
name = provider::azuredx::resource_name(merge(
26-
var.naming_config,
27-
{
28-
name = "demo",
29-
resource_type = "app_configuration",
30-
})
31-
)
32-
resource_group_name = local.resource_group_name
33-
location = local.environment.location
34-
35-
identity {
36-
type = "SystemAssigned"
37-
}
38-
39-
sku = "standard" # others are free and premium
40-
data_plane_proxy_authentication_mode = "Pass-through"
41-
local_auth_enabled = false
25+
module "appcs" {
26+
source = "pagopa-dx/azure-app-configuration/azurerm"
27+
version = "~> 0.0"
4228
43-
public_network_access = "Disabled"
44-
purge_protection_enabled = true
45-
46-
tags = local.tags
47-
}
29+
environment = local.environment
30+
resource_group_name = var.resource_group_name
4831
49-
data "azurerm_private_dns_zone" "appconfig" {
50-
name = "privatelink.azconfig.io"
51-
resource_group_name = var.private_dns_zone_resource_group_name
52-
}
32+
subnet_pep_id = data.azurerm_subnet.pep.id
5333
54-
resource "azurerm_private_endpoint" "app_config" {
55-
name = provider::azuredx::resource_name(merge(
56-
var.naming_config,
57-
{
58-
name = "demo",
59-
resource_type = "app_configuration_private_endpoint",
60-
})
61-
)
62-
location = local.environment.location
63-
resource_group_name = local.resource_group_name
64-
subnet_id = var.subnet_pep_id
65-
66-
private_service_connection {
67-
name = provider::azuredx::resource_name(merge(
68-
var.naming_config,
69-
{
70-
name = "demo",
71-
resource_type = "app_configuration_private_endpoint",
72-
})
73-
)
74-
private_connection_resource_id = azurerm_app_configuration.example.id
75-
is_manual_connection = false
76-
subresource_names = ["configurationStores"]
34+
virtual_network = {
35+
name = local.virtual_network.name
36+
resource_group_name = local.virtual_network.resource_group_name
7737
}
7838
79-
private_dns_zone_group {
80-
name = "private-dns-zone-group"
81-
private_dns_zone_ids = [data.azurerm_private_dns_zone.appconfig.id]
82-
}
39+
private_dns_zone_resource_group_name = data.azurerm_resource_group.network.name
8340
8441
tags = local.tags
8542
}
@@ -88,7 +45,7 @@ module "roles" {
8845
source = "pagopa-dx/azure-role-assignments/azurerm"
8946
version = "~> 1.3"
9047
91-
principal_id = module.test_app.app_service.app_service.principal_id
48+
principal_id = module.test_app.app_service.app_service.principal_id # example application which needs to access App Configuration
9249
subscription_id = data.azurerm_subscription.current.subscription_id
9350
9451
app_config = [
@@ -124,28 +81,27 @@ provider "azurerm" {
12481
If your application has sensitive application settings (secrets), the
12582
AppConfiguration instance should be configured to retrieve those secrets from
12683
Azure Key Vault, to make them available to the application. The authentication
127-
via identities between AppConfiguration and KeyVault is managed via Terraform:
84+
via identities between AppConfiguration and KeyVault is managed by the module
85+
[`azure_app_configuration`](https://registry.terraform.io/modules/pagopa-dx/azure-app-configuration/azurerm/latest),
86+
which optionally accepts a KeyVault reference:
12887

12988
```hcl
13089
131-
module "roles" {
132-
source = "pagopa-dx/azure-role-assignments/azurerm"
133-
version = "~> 1.3"
13490
135-
principal_id = azurerm_app_configuration.example.identity[0].principal_id
136-
subscription_id = data.azurerm_subscription.current.subscription_id
91+
module "appcs_with_kv" {
92+
source = "pagopa-dx/azure-app-configuration/azurerm"
93+
version = "~> 0.0"
13794
138-
app_config = [
139-
{
140-
name = azurerm_app_configuration.example.name
141-
resource_group_name = azurerm_app_configuration.example.resource_group_name
142-
has_rbac_support = true # or false if KeyVault is using Access Policies
143-
description = "Complete access to app configuration control plane and data"
144-
roles = {
145-
secrets = "Reader" # Allow AppConfiguration to read secrets from KeyVault
146-
}
147-
}
148-
]
95+
...
96+
97+
key_vault = {
98+
subscription_id = data.azurerm_subscription.current.subscription_id
99+
name = azurerm_key_vault.kv.name
100+
resource_group_name = azurerm_key_vault.kv.resource_group_name
101+
has_rbac_support = true # or false if KeyVault uses Access Policies
102+
}
103+
104+
tags = local.tags
149105
}
150106
151107
```
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# azure_app_configuration
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# DX - Azure App Configuration
2+
3+
![Terraform Module Downloads](https://img.shields.io/terraform/module/dm/pagopa-dx/azure-app-configuration/azurerm?logo=terraform&label=downloads&cacheSeconds=5000&link=https%3A%2F%2Fregistry.terraform.io%2Fmodules%2Fpagopa-dx%2Fazure-app-configuration%2Fazurerm%2Flatest)
4+
5+
This module deploys an Azure App Configuration, opinionated setup in terms of networking setup and access mode.
6+
7+
## Features
8+
9+
- **App Configuration**: An Azure App Configuration instance to retrieve application settings, secrets and feature flags
10+
- **Private Endpoint**: To allow only private incoming connections
11+
12+
## Use cases Comparison
13+
14+
| Use case | Description | SLA | Guaranteed Throughput | Request Quota | Soft Delete | Geo Replication |
15+
| --------- | ------------------------------------------------- | ------ | ------------------------------- | --------------- | ----------- | --------------- |
16+
| default | Tier for production workfloads | 99.95% | 300 RPS (read) - 60 RPS (write) | 30.000 per hour | Yes | Yes |
17+
| developer | Tier for experimentation or development scenarios | - | - | 6.000 per hour | No | No |
18+
19+
### Allowed Sizes
20+
21+
The SKU name is determined by the use case, but if you want to override it, you can set the `size` variable when `use_case` is set to `default`.
22+
The allowed sizes are:
23+
24+
- `standard`
25+
- `premium`
26+
27+
## Usage Example
28+
29+
For examples of how to use this module, refer to the [examples](https://github.com/pagopa-dx/terraform-azurerm-azure-app-configuration/tree/main/examples) folder in the module repository.
30+
31+
<!-- BEGIN_TF_DOCS -->
32+
## Requirements
33+
34+
| Name | Version |
35+
|------|---------|
36+
| <a name="requirement_azurerm"></a> [azurerm](#requirement\_azurerm) | ~> 4.0 |
37+
| <a name="requirement_pagopa-dx"></a> [pagopa-dx](#requirement\_pagopa-dx) | ~> 0.8 |
38+
39+
## Modules
40+
41+
| Name | Source | Version |
42+
|------|--------|---------|
43+
| <a name="module_app_roles"></a> [app\_roles](#module\_app\_roles) | pagopa-dx/azure-role-assignments/azurerm | ~> 1.3 |
44+
| <a name="module_appconfig_team_roles"></a> [appconfig\_team\_roles](#module\_appconfig\_team\_roles) | pagopa-dx/azure-role-assignments/azurerm | ~> 1.3 |
45+
46+
## Resources
47+
48+
| Name | Type |
49+
|------|------|
50+
| [azurerm_app_configuration.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/app_configuration) | resource |
51+
| [azurerm_private_endpoint.app_config](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/private_endpoint) | resource |
52+
| [azurerm_private_dns_zone.appconfig](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/private_dns_zone) | data source |
53+
54+
## Inputs
55+
56+
| Name | Description | Type | Default | Required |
57+
|------|-------------|------|---------|:--------:|
58+
| <a name="input_authorized_teams"></a> [authorized\_teams](#input\_authorized\_teams) | Object containing lists of principal IDs (Azure AD object IDs) of product teams to be granted read or write permissions on the App Configuration. These represent the teams within the organization that need access to this resource." | <pre>object({<br/> writers = optional(list(string), []),<br/> readers = optional(list(string), [])<br/> })</pre> | <pre>{<br/> "readers": [],<br/> "writers": []<br/>}</pre> | no |
59+
| <a name="input_environment"></a> [environment](#input\_environment) | Values which are used to generate resource names and location short names. They are all mandatory except for domain, which should not be used only in the case of a resource used by multiple domains. | <pre>object({<br/> prefix = string<br/> env_short = string<br/> location = string<br/> domain = optional(string)<br/> app_name = string<br/> instance_number = string<br/> })</pre> | n/a | yes |
60+
| <a name="input_key_vaults"></a> [key\_vaults](#input\_key\_vaults) | Optionally, integrate App Configuration with a one or more existing Key Vault for secrets retrieval.<br/> Set `has_rbac_support` to true if the referenced Key Vault uses RBAC model for access control.<br/> Use `app_principal_ids` to set application principal IDs to be granted access to the Key Vault. | <pre>list(object({<br/> name = string<br/> resource_group_name = string<br/> has_rbac_support = bool<br/> app_principal_ids = list(string)<br/> }))</pre> | `null` | no |
61+
| <a name="input_private_dns_zone_resource_group_name"></a> [private\_dns\_zone\_resource\_group\_name](#input\_private\_dns\_zone\_resource\_group\_name) | Name of the resource group containing the private DNS zone for private endpoints. Default is the resource group of the virtual network. | `string` | `null` | no |
62+
| <a name="input_resource_group_name"></a> [resource\_group\_name](#input\_resource\_group\_name) | The name of the resource group where resources will be deployed. | `string` | n/a | yes |
63+
| <a name="input_size"></a> [size](#input\_size) | "App Configuration SKU. Allowed values: 'standard', 'premium'. If not set, it will be determined by the use\_case." | `string` | `null` | no |
64+
| <a name="input_subnet_pep_id"></a> [subnet\_pep\_id](#input\_subnet\_pep\_id) | ID of the subnet hosting private endpoints. | `string` | n/a | yes |
65+
| <a name="input_subscription_id"></a> [subscription\_id](#input\_subscription\_id) | Subscription Id of the involved resources | `string` | n/a | yes |
66+
| <a name="input_tags"></a> [tags](#input\_tags) | A map of tags to assign to the resources. | `map(any)` | n/a | yes |
67+
| <a name="input_use_case"></a> [use\_case](#input\_use\_case) | Allowed values: 'default', 'development'. | `string` | `"default"` | no |
68+
| <a name="input_virtual_network"></a> [virtual\_network](#input\_virtual\_network) | Virtual network where the subnet will be created. | <pre>object({<br/> name = string<br/> resource_group_name = string<br/> })</pre> | n/a | yes |
69+
70+
## Outputs
71+
72+
| Name | Description |
73+
|------|-------------|
74+
| <a name="output_endpoint"></a> [endpoint](#output\_endpoint) | The service endpoint URL |
75+
| <a name="output_id"></a> [id](#output\_id) | The ID of the Azure Cosmos DB account. |
76+
| <a name="output_name"></a> [name](#output\_name) | The name of the Azure Cosmos DB account. |
77+
| <a name="output_principal_id"></a> [principal\_id](#output\_principal\_id) | The system-assigned managed identity pricipal id |
78+
| <a name="output_resource_group_name"></a> [resource\_group\_name](#output\_resource\_group\_name) | The name of the resource group containing the Azure Cosmos DB account. |
79+
<!-- END_TF_DOCS -->
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
resource "azurerm_app_configuration" "this" {
2+
name = provider::pagopa-dx::resource_name(merge(local.naming_config, { resource_type = "app_configuration" }))
3+
resource_group_name = var.resource_group_name
4+
location = var.environment.location
5+
6+
identity {
7+
type = "SystemAssigned"
8+
}
9+
10+
sku = local.appcs.sku_name
11+
data_plane_proxy_authentication_mode = "Pass-through"
12+
local_auth_enabled = false
13+
14+
public_network_access = "Disabled"
15+
purge_protection_enabled = local.use_case_features.purge_protection_enabled
16+
17+
tags = local.tags
18+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
data "azurerm_private_dns_zone" "appconfig" {
2+
name = "privatelink.azconfig.io"
3+
resource_group_name = local.private_dns_zone.resource_group_name
4+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
locals {
2+
# Distinct principal IDs from all Key Vaults
3+
distinct_app_principals = var.key_vaults != null ? toset(flatten([
4+
for kv in var.key_vaults : kv.app_principal_ids
5+
])) : toset([])
6+
7+
# For each distinct principal, collect all Key Vaults where they're listed
8+
app_assignments = {
9+
for principal_id in local.distinct_app_principals :
10+
principal_id => {
11+
principal_id = principal_id
12+
key_vaults = [
13+
for kv in var.key_vaults :
14+
{
15+
name = kv.name
16+
resource_group_name = kv.resource_group_name
17+
has_rbac_support = kv.has_rbac_support
18+
}
19+
if contains(kv.app_principal_ids, principal_id)
20+
]
21+
}
22+
}
23+
24+
# App Configuration role assignments for authorized teams
25+
appconfig_role_assignments = merge(
26+
{
27+
for idx, principal_id in var.authorized_teams.writers : "${principal_id}|writer" => {
28+
principal_id = principal_id
29+
role = "writer"
30+
}
31+
},
32+
{
33+
for idx, principal_id in var.authorized_teams.readers : "${principal_id}|reader" => {
34+
principal_id = principal_id
35+
role = "reader"
36+
}
37+
}
38+
)
39+
}
40+
41+
# Assign both Key Vault and App Configuration roles to application principals
42+
# Each principal gets:
43+
# - Roles for all Key Vaults where they are listed
44+
# - A single reader role for the App Configuration
45+
module "app_roles" {
46+
for_each = local.app_assignments
47+
48+
source = "pagopa-dx/azure-role-assignments/azurerm"
49+
version = "~> 1.3"
50+
51+
subscription_id = var.subscription_id
52+
principal_id = each.value.principal_id
53+
54+
key_vault = [
55+
for kv in each.value.key_vaults : {
56+
name = kv.name
57+
resource_group_name = kv.resource_group_name
58+
has_rbac_support = kv.has_rbac_support
59+
description = "Allow application to read Key Vault secrets"
60+
roles = {
61+
secrets = "reader"
62+
}
63+
}
64+
]
65+
66+
app_config = [{
67+
name = azurerm_app_configuration.this.name
68+
resource_group_name = azurerm_app_configuration.this.resource_group_name
69+
description = "Allow application to read App Configuration settings"
70+
role = "reader"
71+
}]
72+
}
73+
74+
module "appconfig_team_roles" {
75+
for_each = local.appconfig_role_assignments
76+
77+
source = "pagopa-dx/azure-role-assignments/azurerm"
78+
version = "~> 1.3"
79+
80+
subscription_id = var.subscription_id
81+
principal_id = each.value.principal_id
82+
83+
app_config = [
84+
{
85+
name = azurerm_app_configuration.this.name
86+
resource_group_name = azurerm_app_configuration.this.resource_group_name
87+
description = "Allow team to access App Configuration"
88+
role = each.value.role
89+
}
90+
]
91+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
locals {
2+
tags = merge(var.tags, { ModuleSource = "DX", ModuleVersion = try(jsondecode(file("${path.module}/package.json")).version, "unknown"), ModuleName = try(jsondecode(file("${path.module}/package.json")).name, "unknown") })
3+
4+
naming_config = {
5+
prefix = var.environment.prefix,
6+
environment = var.environment.env_short,
7+
location = var.environment.location
8+
domain = var.environment.domain,
9+
name = var.environment.app_name,
10+
instance_number = tonumber(var.environment.instance_number),
11+
}
12+
13+
use_cases = {
14+
default = {
15+
sku = "standard"
16+
purge_protection_enabled = true
17+
}
18+
development = {
19+
sku = "developer"
20+
purge_protection_enabled = false
21+
}
22+
}
23+
24+
use_case_features = local.use_cases[var.use_case]
25+
26+
private_dns_zone = {
27+
resource_group_name = var.private_dns_zone_resource_group_name == null ? var.virtual_network.resource_group_name : var.private_dns_zone_resource_group_name
28+
}
29+
30+
appcs = {
31+
sku_name = var.size != null ? var.size : local.use_case_features.sku
32+
private_endpoint_name = provider::pagopa-dx::resource_name(merge(local.naming_config, { resource_type = "app_configuration_private_endpoint" }))
33+
}
34+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
terraform {
2+
required_providers {
3+
azurerm = {
4+
source = "hashicorp/azurerm"
5+
version = "~> 4.0"
6+
}
7+
pagopa-dx = {
8+
source = "pagopa-dx/azure"
9+
version = "~> 0.8"
10+
}
11+
}
12+
}

0 commit comments

Comments
 (0)