Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/metal-comics-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"azure_app_configuration": patch
---

Initial version
108 changes: 32 additions & 76 deletions apps/website/docs/azure/app-configuration/azure-app-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,74 +12,31 @@ exploiting the hot reload capabilities.

## Configuring the resource via Terraform

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

```hcl

resource "azurerm_app_configuration" "example" {
name = provider::azuredx::resource_name(merge(
var.naming_config,
{
name = "demo",
resource_type = "app_configuration",
})
)
resource_group_name = local.resource_group_name
location = local.environment.location

identity {
type = "SystemAssigned"
}

sku = "standard" # others are free and premium
data_plane_proxy_authentication_mode = "Pass-through"
local_auth_enabled = false
module "appcs" {
source = "pagopa-dx/azure-app-configuration/azurerm"
version = "~> 0.0"

public_network_access = "Disabled"
purge_protection_enabled = true

tags = local.tags
}
environment = local.environment
resource_group_name = var.resource_group_name

data "azurerm_private_dns_zone" "appconfig" {
name = "privatelink.azconfig.io"
resource_group_name = var.private_dns_zone_resource_group_name
}
subnet_pep_id = data.azurerm_subnet.pep.id

resource "azurerm_private_endpoint" "app_config" {
name = provider::azuredx::resource_name(merge(
var.naming_config,
{
name = "demo",
resource_type = "app_configuration_private_endpoint",
})
)
location = local.environment.location
resource_group_name = local.resource_group_name
subnet_id = var.subnet_pep_id

private_service_connection {
name = provider::azuredx::resource_name(merge(
var.naming_config,
{
name = "demo",
resource_type = "app_configuration_private_endpoint",
})
)
private_connection_resource_id = azurerm_app_configuration.example.id
is_manual_connection = false
subresource_names = ["configurationStores"]
virtual_network = {
name = local.virtual_network.name
resource_group_name = local.virtual_network.resource_group_name
}

private_dns_zone_group {
name = "private-dns-zone-group"
private_dns_zone_ids = [data.azurerm_private_dns_zone.appconfig.id]
}
private_dns_zone_resource_group_name = data.azurerm_resource_group.network.name

tags = local.tags
}
Expand All @@ -88,7 +45,7 @@ module "roles" {
source = "pagopa-dx/azure-role-assignments/azurerm"
version = "~> 1.3"

principal_id = module.test_app.app_service.app_service.principal_id
principal_id = module.test_app.app_service.app_service.principal_id # example application which needs to access App Configuration
subscription_id = data.azurerm_subscription.current.subscription_id

app_config = [
Expand Down Expand Up @@ -124,28 +81,27 @@ provider "azurerm" {
If your application has sensitive application settings (secrets), the
AppConfiguration instance should be configured to retrieve those secrets from
Azure Key Vault, to make them available to the application. The authentication
via identities between AppConfiguration and KeyVault is managed via Terraform:
via identities between AppConfiguration and KeyVault is managed by the module
[`azure_app_configuration`](https://registry.terraform.io/modules/pagopa-dx/azure-app-configuration/azurerm/latest),
which optionally accepts a KeyVault reference:

```hcl

module "roles" {
source = "pagopa-dx/azure-role-assignments/azurerm"
version = "~> 1.3"

principal_id = azurerm_app_configuration.example.identity[0].principal_id
subscription_id = data.azurerm_subscription.current.subscription_id
module "appcs_with_kv" {
source = "pagopa-dx/azure-app-configuration/azurerm"
version = "~> 0.0"

app_config = [
{
name = azurerm_app_configuration.example.name
resource_group_name = azurerm_app_configuration.example.resource_group_name
has_rbac_support = true # or false if KeyVault is using Access Policies
description = "Complete access to app configuration control plane and data"
roles = {
secrets = "Reader" # Allow AppConfiguration to read secrets from KeyVault
}
}
]
...

key_vault = {
subscription_id = data.azurerm_subscription.current.subscription_id
name = azurerm_key_vault.kv.name
resource_group_name = azurerm_key_vault.kv.resource_group_name
has_rbac_support = true # or false if KeyVault uses Access Policies
}

tags = local.tags
}

```
1 change: 1 addition & 0 deletions infra/modules/azure_app_configuration/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# azure_app_configuration
79 changes: 79 additions & 0 deletions infra/modules/azure_app_configuration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# DX - Azure App Configuration

![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)

This module deploys an Azure App Configuration, opinionated setup in terms of networking setup and access mode.

## Features

- **App Configuration**: An Azure App Configuration instance to retrieve application settings, secrets and feature flags
- **Private Endpoint**: To allow only private incoming connections

## Use cases Comparison

| Use case | Description | SLA | Guaranteed Throughput | Request Quota | Soft Delete | Geo Replication |
| --------- | ------------------------------------------------- | ------ | ------------------------------- | --------------- | ----------- | --------------- |
| default | Tier for production workfloads | 99.95% | 300 RPS (read) - 60 RPS (write) | 30.000 per hour | Yes | Yes |
| developer | Tier for experimentation or development scenarios | - | - | 6.000 per hour | No | No |

### Allowed Sizes

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`.
The allowed sizes are:

- `standard`
- `premium`

## Usage Example

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.

<!-- BEGIN_TF_DOCS -->
## Requirements

| Name | Version |
|------|---------|
| <a name="requirement_azurerm"></a> [azurerm](#requirement\_azurerm) | ~> 4.0 |
| <a name="requirement_pagopa-dx"></a> [pagopa-dx](#requirement\_pagopa-dx) | ~> 0.8 |

## Modules

| Name | Source | Version |
|------|--------|---------|
| <a name="module_app_roles"></a> [app\_roles](#module\_app\_roles) | pagopa-dx/azure-role-assignments/azurerm | ~> 1.3 |
| <a name="module_appconfig_team_roles"></a> [appconfig\_team\_roles](#module\_appconfig\_team\_roles) | pagopa-dx/azure-role-assignments/azurerm | ~> 1.3 |

## Resources

| Name | Type |
|------|------|
| [azurerm_app_configuration.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/app_configuration) | resource |
| [azurerm_private_endpoint.app_config](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/private_endpoint) | resource |
| [azurerm_private_dns_zone.appconfig](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/private_dns_zone) | data source |

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <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 |
| <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 |
| <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 |
| <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 |
| <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 |
| <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 |
| <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 |
| <a name="input_subscription_id"></a> [subscription\_id](#input\_subscription\_id) | Subscription Id of the involved resources | `string` | n/a | yes |
| <a name="input_tags"></a> [tags](#input\_tags) | A map of tags to assign to the resources. | `map(any)` | n/a | yes |
| <a name="input_use_case"></a> [use\_case](#input\_use\_case) | Allowed values: 'default', 'development'. | `string` | `"default"` | no |
| <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 |

## Outputs

| Name | Description |
|------|-------------|
| <a name="output_endpoint"></a> [endpoint](#output\_endpoint) | The service endpoint URL |
| <a name="output_id"></a> [id](#output\_id) | The ID of the Azure Cosmos DB account. |
| <a name="output_name"></a> [name](#output\_name) | The name of the Azure Cosmos DB account. |
| <a name="output_principal_id"></a> [principal\_id](#output\_principal\_id) | The system-assigned managed identity pricipal id |
| <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. |
<!-- END_TF_DOCS -->
18 changes: 18 additions & 0 deletions infra/modules/azure_app_configuration/appcs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
resource "azurerm_app_configuration" "this" {
name = provider::pagopa-dx::resource_name(merge(local.naming_config, { resource_type = "app_configuration" }))
resource_group_name = var.resource_group_name
location = var.environment.location

identity {
type = "SystemAssigned"
}

sku = local.appcs.sku_name
data_plane_proxy_authentication_mode = "Pass-through"
local_auth_enabled = false

public_network_access = "Disabled"
purge_protection_enabled = local.use_case_features.purge_protection_enabled

tags = local.tags
}
4 changes: 4 additions & 0 deletions infra/modules/azure_app_configuration/data.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
data "azurerm_private_dns_zone" "appconfig" {
name = "privatelink.azconfig.io"
resource_group_name = local.private_dns_zone.resource_group_name
}
91 changes: 91 additions & 0 deletions infra/modules/azure_app_configuration/iam.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
locals {
# Distinct principal IDs from all Key Vaults
distinct_app_principals = var.key_vaults != null ? toset(flatten([
for kv in var.key_vaults : kv.app_principal_ids
])) : toset([])

# For each distinct principal, collect all Key Vaults where they're listed
app_assignments = {
for principal_id in local.distinct_app_principals :
principal_id => {
principal_id = principal_id
key_vaults = [
for kv in var.key_vaults :
{
name = kv.name
resource_group_name = kv.resource_group_name
has_rbac_support = kv.has_rbac_support
}
if contains(kv.app_principal_ids, principal_id)
]
}
}

# App Configuration role assignments for authorized teams
appconfig_role_assignments = merge(
{
for idx, principal_id in var.authorized_teams.writers : "${principal_id}|writer" => {
principal_id = principal_id
role = "writer"
}
},
{
for idx, principal_id in var.authorized_teams.readers : "${principal_id}|reader" => {
principal_id = principal_id
role = "reader"
}
}
)
}

# Assign both Key Vault and App Configuration roles to application principals
# Each principal gets:
# - Roles for all Key Vaults where they are listed
# - A single reader role for the App Configuration
module "app_roles" {
for_each = local.app_assignments

source = "pagopa-dx/azure-role-assignments/azurerm"
version = "~> 1.3"

subscription_id = var.subscription_id
principal_id = each.value.principal_id

key_vault = [
for kv in each.value.key_vaults : {
name = kv.name
resource_group_name = kv.resource_group_name
has_rbac_support = kv.has_rbac_support
description = "Allow application to read Key Vault secrets"
roles = {
secrets = "reader"
}
}
]

app_config = [{
name = azurerm_app_configuration.this.name
resource_group_name = azurerm_app_configuration.this.resource_group_name
description = "Allow application to read App Configuration settings"
role = "reader"
}]
}

module "appconfig_team_roles" {
for_each = local.appconfig_role_assignments

source = "pagopa-dx/azure-role-assignments/azurerm"
version = "~> 1.3"

subscription_id = var.subscription_id
principal_id = each.value.principal_id

app_config = [
{
name = azurerm_app_configuration.this.name
resource_group_name = azurerm_app_configuration.this.resource_group_name
description = "Allow team to access App Configuration"
role = each.value.role
}
]
}
34 changes: 34 additions & 0 deletions infra/modules/azure_app_configuration/locals.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
locals {
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") })

naming_config = {
prefix = var.environment.prefix,
environment = var.environment.env_short,
location = var.environment.location
domain = var.environment.domain,
name = var.environment.app_name,
instance_number = tonumber(var.environment.instance_number),
}

use_cases = {
default = {
sku = "standard"
purge_protection_enabled = true
}
development = {
sku = "developer"
purge_protection_enabled = false
}
}

use_case_features = local.use_cases[var.use_case]

private_dns_zone = {
resource_group_name = var.private_dns_zone_resource_group_name == null ? var.virtual_network.resource_group_name : var.private_dns_zone_resource_group_name
}

appcs = {
sku_name = var.size != null ? var.size : local.use_case_features.sku
private_endpoint_name = provider::pagopa-dx::resource_name(merge(local.naming_config, { resource_type = "app_configuration_private_endpoint" }))
}
}
12 changes: 12 additions & 0 deletions infra/modules/azure_app_configuration/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
pagopa-dx = {
source = "pagopa-dx/azure"
version = "~> 0.8"
}
}
}
Loading