Blog DevOps

Azure infrastructure managée : déployer vos applications | Padok

Rédigé par Quentin Richard | 22 déc. 2022 13:34:00

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.