Publié le 22 décembre 2022, mis à jour le 29 octobre 2024.
Suite à nos multiples déploiements d’applications web dans le cloud, nous avons identifié des patterns dans les besoins de ressources et de configurations.
Découvrez l’infrastructure utilisant des ressources managées publiques que nous recommandons dans un environnement Azure à travers des modules open source créés en interne de Theodo Cloud.
Notre Architecture
L’infrastructure Azure que nous proposons pour déployer votre application web repose principalement sur l’utilisation de ressources publiques.
L’objectif est de vous permettre de créer l’infrastructure minimale pour faire fonctionner votre application web ; à savoir un backend, un frontend ainsi qu’une gestion des secrets. Nous déploierons par ailleurs des ressources transverses pouvant être utilisées par plusieurs de vos ressources applicatives.
Sans plus attendre, plongeons-nous dans l’architecture proposée.
Infrastructure transverse
Les ressources dites “transverses” ont pour objectif d’être utilisées par plusieurs applications au sein d’une même souscription. Les recommandations et Best Practices Azure incitent à déployer certaines ressources critiques pour l’ensemble de l’environnement applicatif.
C’est pourquoi nous avons un groupe de ressource dédié, dans lequel se situeront :
- Un Log Analytics Workspace lié à un Storage Account de Backup, qui récoltera l’ensemble des logs et metrics de l’infrastructure et des applications déployées
- Un Container Registry lié à un Keyvault pour stocker les images applicatives déployées sur les différents App Services de l’environnement.
Infrastructure applicative
Le groupe de ressources applicatif est dédié à une application de notre souscription pour un environnement donné. Il pourra être dupliqué autant que nécessaire selon le nombre d’applications à déployer.
Nous déployons ici une application intégrant un frontend statique et un backend conteneurisé ayant le besoin de se connecter à une base de données et d’utiliser des secrets. Cette application doit être exposée sur internet pour permettre à vos utilisateurs d’y accéder.
Pour répondre à ce besoin nous avons donc déployé :
- Frontend - Un Storage Account avec un conteneur web
- Backend - Un App Service Linux ayant le droit de pull des images de conteneurs sur le Container Registry
- Database - Une base de données PosgreSQL
- Secrets - Un Keyvault dédié dans lequel seront stockés les accès à la base de données et autres clés secrètes
- Routage - Une Application Gateway chargée d’effectuer les redirections de requêtes entre le frontend et le backend
Intégration
Aujourd’hui, le modèle d’architecture Hub & Spokes est largement utilisé dans les infrastructures Azure. Notre proposition d’infrastructure pour le déploiement d’une application web s’intègre parfaitement à ce paradigme. Il sera très facile d’exposer l’application au travers d’un load balancer frontal tel qu’une Azure Frontdoor. On pourra aussi déployer l’ensemble des ressources dans des réseaux virtuels dédiés.
Ainsi, vous n’aurez pas à chambouler l’ensemble de votre infrastructure pour y implémenter votre nouvelle application.
Notre Guide d’implémentation
Dans la suite de cet article, nous allons expliquer pas à pas comment implémenter cette infrastructure via terraform, à l’aide des modules open source de la librairie Theodo Cloud.
Infrastructure transverse
Nous allons, dans un premier temps, déployer les ressources transverses de notre infrastructure.
Déployez tout d’abord le groupe de ressources dédié.
azurerm_resource_group "transverse" {
name = "transverse"
location = "francecentral"
}
Nous voulons ensuite mettre en place les ressources permettant de monitorer l’ensemble de notre infrastructure. Nous voulons également avoir un backup de nos données. Pour cela nous utiliserons deux modules open source de la librairie : terraform-azurerm-logger
et terraform-azurerm-storage-account
.
module "logger" {
source = "git@github.com:padok-team/terraform-azurerm-logger.git?ref=v0.2.0"
resource_group = azurerm_resource_group.transverse
name = "logger"
}
module "logger_backup" {
source = "git@github.com:padok-team/terraform-azurerm-storage-account.git?ref=v0.2.1"
resource_group = azurerm_resource_group.transverse
name = "logger-backup"
}
resource "azurerm_log_analytics_data_export_rule" "this" {
name = "logger-data-exporter"
resource_group_name = azurerm_resource_group.transverse.name
workspace_resource_id = module.logger[0].azurerm_log_analytics_workspace_id
destination_resource_id = module.backup[0].this.id
table_names = [
... # What you want to export
]
enabled = true
}
Enfin, nous allons déployer notre Container Registry et le Keyvault qui lui est associé. Cela permettra de stocker et récupérer les images conteneurisées des applications. Nous utiliserons pour cela les modules terraform-azurerm-keyvault
et terraform-azurerm-acr
de la librairie open source.
module "acr_keyvault" {
source = "git@github.com:/padok-team/terraform-azurerm-keyvault?ref=v0.2.0"
name = "acr-keyvault"
resource_group = azurerm_resource_group.transverse
sku_name = "standard"
}
resource "azurerm_key_vault_key" "acr_encryption" {
name = "acr-encryption"
key_vault_id = module.acr_keyvault.akv_id
key_type = "RSA"
key_size = 2048
key_opts = [
"decrypt",
"encrypt",
"sign",
"unwrapKey",
"verify",
"wrapKey",
]
}
module "acr" {
source = "git@github.com:/padok-team/terraform-azurerm-acr?ref=v0.4.0"
name = "acr"
resource_group_name = azurerm_resource_group.transverse.name
location = azurerm_resource_group.transverse.location
# Encryption at rest
encryption_key_vault_id = module.acr_keyvault.akv_id
encryption_key_vault_key_id = azurerm_key_vault_key.acr_encryption.id
network_default_action = "Allow"
public_network_access_enabled = true
}
Notre zone transverse est maintenant terminée et pourra être utilisée par différentes applications de notre infrastructure.
Infrastructure applicative
Pour cette seconde partie nous allons déployer l’ensemble des ressources nécessaires au bon fonctionnement de l’application web dans notre infrastructure.
Nous déployons dans un premier temps un groupe de ressources pour l’application.
azurerm_resource_group "app" {
name = "app"
location = "francecentral"
}
Notre application web ayant besoin d’une base de données ainsi que d’accéder à des secrets, nous allons donc utiliser les modules terraform-azurerm-postgresql-server
et terraform-azurerm-keyvault
de la librairie open source.
module "app_keyvault" {
source = "git@github.com:/padok-team/terraform-azurerm-keyvault?ref=v0.2.0"
name = "acr-keyvault"
resource_group = azurerm_resource_group.app
sku_name = "standard"
}
module "pgsql-server" {
source = "git@github.com:padok-team/terraform-azurerm-postgresql-server?ref=v0.2.0"
name = "pgsql-server"
resource_group_name = azurerm_resource_group.app.name
location = azurerm_resource_group.app.location
administrator_login = "admin"
}
# Store secret in the keyvault
resource "azurerm_key_vault_secret" "database_admin_password" {
name = "DATABASE-ADMINISTRATOR-LOGIN-PASSWORD"
key_vault_id = module.app_keyvault.akv_id
value = module.application_database.this.administrator_login_password
}
L’application web ayant besoin d’un frontend statique et d’un backend conteneurisé nous allons tout d’abord déployer les ressources nécessaires. Nous utiliserons les modules terraform-azurerm-storage-account
et terraform-azurerm-app-service-container-linux
de la librairie open source.
De plus, nous allons permettre au backend de pouvoir accéder aux images du Container Registry ainsi que de pouvoir lire les secrets d’un Keyvault.
module "backend" {
source = "git@github.com:padok-team/terraform-azurerm-app-service-container-linux.git?ref=v0.1.3"
name = "backend"
resource_group = azurerm_resource_group.app
app_settings = {
DOCKER_REGISTRY_SERVER_URL = "https://${module.acr.this.login_server}"
DATABASE_HOSTNAME = module.application_database.this.fqdn
DATABASE_ADMINISTRATOR_LOGIN_PASSWORD = "@Microsoft.KeyVault(VaultName=${module.app_keyvault.akv_name};SecretName=${azurerm_key_vault_secret.database_admin_password.name})"
DATABASE_USERNAME = module.application_database.this.administrator_login
}
enable_auth_settings = false
client_cert_enabled = false
site_config_override = {
acr_use_managed_identity_credentials = true
}
}
resource "azurerm_role_assignment" "backend_acr_pull" {
scope = module.acr.this.id
role_definition_name = "AcrPull"
principal_id = module.backend.this.identity[0].principal_id
}
resource "azurerm_role_assignment" "backend_keyvault_secret_user" {
scope = module.app_keyvault.akv_id
role_definition_name = "Key Vault Secrets User"
principal_id = module.backend.this.identity[0].principal_id
}
module "frontend" {
source = "git@github.com:padok-team/terraform-azurerm-storage-account.git?ref=v0.2.1"
name = "frontend"
resource_group = azurerm_resource_group.app
static_website_enabled = true
static_website_index_document = "index.html"
static_website_error_404_document = "index.html"
network_rules_default_action = "Allow"
}
Nous avons maintenant besoin d’une Application Gateway placée devant le storage account et l’app service afin d’effectuer le routage des requêtes entrantes. Ce sera la porte d’entrée de notre infrastructure.
Afin de permettre une communication https dans notre infrastructure, nous allons également déclarer un certificat dans le keyvault qui sera utilisé pour le chiffrement TLS et passé en paramètre à l’application gateway.
resource "azurerm_user_assigned_identity" "application_gateway" {
location = azurerm_resource_group.app.name
resource_group_name = azurerm_resource_group.app.location
name = "app-gateway"
}
resource "azurerm_role_assignment" "app_gateway_secret_officer" {
scope = module.app_keyvault.akv_id
role_definition_name = "Key Vault Secrets Officer"
principal_id = azurerm_user_assigned_identity.application_gateway.principal_id
}
resource "azurerm_application_gateway" "this" {
name = "app_gateway"
resource_group_name = azurerm_resource_group.app.name
location = azurerm_resource_group.app.location
sku {
name = "Standard_v2"
tier = "Standard_v2"
capacity = 2
}
identity {
type = "UserAssigned"
identity_ids = [azurerm_user_assigned_identity.application_gateway.id]
}
#----------------------------------------------------------------
# COMMON
#----------------------------------------------------------------
frontend_ip_configuration {
name = "frontend-ip-configuration"
public_ip_address = xxx
}
frontend_port {
name = "http"
port = 80
}
frontend_port {
name = "https"
port = 443
}
http_listener {
name = "http"
frontend_ip_configuration_name = "frontend-ip-configuration"
frontend_port_name = "https"
protocol = "Http"
}
http_listener {
name = "https"
frontend_ip_configuration_name = "frontend-ip-configuration"
frontend_port_name = "https"
protocol = "Https"
ssl_certificate_name = "certificate"
}
ssl_certificate {
name = "certificate"
key_vault_secret_id = "xxx"
}
request_routing_rule {
name = "http-to-https"
http_listener_name = "http"
redirect_configuration_name = "http-to-https"
rule_type = "Basic"
}
# Request Configuration for Https
redirect_configuration {
name = "http-to-https"
target_listener_name = "https"
redirect_type = "Permanent"
include_path = true
include_query_string = true
}
request_routing_rule {
name = "http-to-https"
rule_type = "PathBasedRouting"
http_listener_name = "https"
url_path_map_name = "url-path-map"
}
url_path_map {
name = "url-path-map"
default_backend_address_pool_name = "app-static-frontend"
default_backend_http_settings_name = "app-static-frontend"
default_redirect_configuration_name = null
default_rewrite_rule_set_name = null
path_rule {
name = "app-backend"
paths = ["/api/*"]
backend_address_pool_name = "app-backend"
backend_http_settings_name = "app-backend"
redirect_configuration_name = null
rewrite_rule_set_name = null
firewall_policy_id = null
}
}
#----------------------------------------------------------------
# FRONTEND
#----------------------------------------------------------------
# Backend pool for frontend storage account
backend_address_pool {
name = "app-static-frontend"
fqdns = [module.frontend.this.primary_web_host]
}
backend_http_settings {
name = "app-static-frontend"
cookie_based_affinity = "Disabled"
path = "/"
port = 443
protocol = "Https"
request_timeout = 60
probe_name = "app-static-frontend"
pick_host_name_from_backend_address = true
}
probe {
name = "app-static-frontend"
protocol = "Https"
path = "/index.html"
host = module.forntend.this.primary_web_host
port = 443
unhealthy_threshold = 3
interval = 30
timeout = 30
}
# #----------------------------------------------------------------
# # BACKEND
# #----------------------------------------------------------------
backend_address_pool {
name = "app-backend"
fqdns = [module.backend.this.default_site_hostname]
}
backend_http_settings {
name = "app-backend"
cookie_based_affinity = "Disabled"
path = "/"
port = 443
protocol = "Https"
request_timeout = 60
probe_name = "app-backend"
pick_host_name_from_backend_address = true
}
probe {
name = "app-backend"
protocol = "Https"
path = "/"
host = module.backend.this.default_site_hostname
port = 443
unhealthy_threshold = 3
interval = 30
timeout = 30
}
depends_on = [
azurerm_role_assignment.app_gateway_secret_officer
]
}
Conclusion
Les modules et ressources présentés vous permettront d’implémenter une infrastructure managée publique pour déployer et exposer vos applications web.
Cette proposition d’infrastructure s’inscrit totalement dans une architecture stand-alone mais est aussi adaptée à un environnement de type hub & spokes.
Enfin, de nombreuses évolutions sont possibles, il serait par exemple intéressant de privatiser l’ensemble des ressources applicatives de cette infrastructure pour améliorer la sécurité de celle-ci. Vous pouvez d’ailleurs retrouver cet article sur la privatisation d’une infrastructure hub & spokes Azure sur le blog Theodo Cloud.