Learn

Creare una Azure Function con Terraform per inviare notifiche tramite SendGrid

19 Novembre 2024 - ~ 12 minuti di lettura

Hai mai desiderato automatizzare l’invio di notifiche email ogni volta che un file viene caricato su uno Storage Account di Azure? Se sì, sei nel posto giusto.

In questo tutorial, ti guiderò nella creazione di un’infrastruttura completa su Azure utilizzando Terraform. Configureremo una Azure Function che si attiverà automaticamente ogni volta che un file viene caricato in uno Storage Account. La funzione utilizzerà un trigger per rilevare i nuovi file, elaborerà l’evento e invierà un’email tramite SendGrid per notificarti in tempo reale. Il tutto sarà gestito come “infrastructure-as-code”.
La cosa migliore? Tutto il codice necessario è già pronto e disponibile su GitHub.

Obiettivo del tutorial: cosa faremo

L’obiettivo è semplice: vogliamo inviare una notifica via email ogni volta che un file viene caricato su uno Storage Account di Azure. Per farlo, utilizzeremo Terraform per creare e configurare le risorse su Azure e scriveremo una funzione utilizzando Node.js che gestirà l’invio delle email grazie a DendGrid.

Ecco una panoramica dei servizi che configureremo con un diagramma dell’infrastruttura:

  • Azure Storage Account: Creeremo due Storage Account, uno per l’archiviazione dei file caricati dall’utente e uno per la gestione della funzione Azure.
  • Azure KeyVault: Utilizzeremo il KeyVault per memorizzare in modo sicuro l’API Key di SendGrid.
  • Azure Function: Creeremo una Azure Function che si attiva ogni volta che un file viene caricato. La funzione invierà un’email di notifica utilizzando

Prerequisiti: cosa ti serve

Prima di iniziare, assicurati di avere:

  • Terraform installato
  • Node.js installato (puoi scaricarlo da qui).
  • Azure CLI installata (puoi scaricarla da qui).
  • Un account Azure (se non lo hai ancora, puoi crearne uno gratuitamente qui).
  • Un account SendGrid con un’API Key attiva (se non sai da dove iniziare, questo link ti può aiutare).
  • Dimestichezza con Node.js, CLI di Azure, i comandi di Terraform e l’interfaccia web di Azure.

Creiamo la nostra infrastruttura con Terraform

Cominciamo con la parte “terra-formante”. Terraform è uno strumento fantastico che ti permette di definire l’infrastruttura come codice, e lo useremo per creare tutto quello di cui abbiamo bisogno su Azure.

La struttura del progetto Terraform: non obbligatoria, ma utile !

Prima di dedicarci al codice, è importante capire che la struttura che sto per mostrarti non è l’unico modo per organizzare un progetto Terraform. In effetti, puoi semplificarlo ulteriormente e ottenere comunque lo stesso risultato.

Ad esempio, puoi combinare tutti i componenti principali (Resource Group, Storage Account, Azure Function) in un unico file main.tf senza utilizzare moduli o separare le risorse in file distinti solo quando la complessità del progetto cresce.

Nomenclatura dei file: convenzioni, non regole !

In Terraform, i nomi dei file come provider.tf o variables.tf sono convenzioni comuni, ma non obbligatorie. Puoi inserire queste configurazioni in file con nomi diversi o unire tutto in un unico file main.tf. Tuttavia, c’è un’eccezione: se decidessi di utilizzare un file per definire valori predefiniti per le variabili, dovresti nominarlo “terraform.tfvars” affinché Terraform lo possa caricare automaticamente. A parte questo, la scelta del nome dei file serve principalmente a migliorare l’organizzazione e la leggibilità del codice, specialmente in progetti complessi.

Perché preferire una struttura più organizzata?

Sebbene sia possibile semplificare il codice, ci sono diversi vantaggi nell’organizzare un progetto Terraform in modo modulare e separando le risorse in file distinti.

Il file main.tf che abbiamo creato nel nostro progetto principale gestisce la configurazione delle risorse chiave e richiama i moduli quando necessario. Questa organizzazione aiuta a mantenere il codice chiaro e gestibile, soprattutto quando il progetto cresce in complessità:

  1. Migliore visibilità e gestione: Separare le risorse principali in un file tf ti permette di avere una visione d’insieme dell’infrastruttura. Se devi apportare modifiche significative, sai esattamente dove guardare.
  2. Isolamento delle risorse: Utilizzando più Storage Account (uno per i file caricati e uno per la funzione), ottieni una separazione delle responsabilità e una gestione delle risorse più granulare. Questo può migliorare la sicurezza e facilitare la gestione delle autorizzazioni.

Moduli: vantaggi, riutilizzo e manutenibilità

L’utilizzo di un modulo come storage-notifier per gestire la Azure Function ha numerosi vantaggi:

  1. Riutilizzabilità: Un modulo è essenzialmente un pacchetto di codice Terraform che può essere riutilizzato in diversi contesti. Ad esempio, se desiderassi implementare lo stesso sistema in un’altra parte della tua infrastruttura, potresti semplicemente riutilizzare il modulo senza dover riscrivere tutto da capo.
  2. Manutenibilità: Con il codice organizzato in moduli, puoi apportare modifiche localizzate senza rischiare di influire negativamente su altre parti del progetto. Ad esempio, se dovessi aggiornare la configurazione della funzione, lo faresti all’interno del modulo senza dover modificare il file “main.tf” principale.
  3. Scalabilità: Quando il tuo progetto diventa più complesso, avere moduli separati ti permette di scalare facilmente l’infrastruttura, aggiungendo nuove funzionalità o replicando parti esistenti senza aumentare la complessità del codice principale.
  4. Chiarezza: Separando le logiche complesse in moduli, è più facile capire cosa fa ogni parte del progetto, anche per qualcuno che non ha mai visto il codice.
  5. Testabilità: Puoi testare i moduli in isolamento, assicurandoti che funzionino correttamente prima di integrarli nel progetto principale.
  6. Collaborazione: In team di sviluppo, la modularità permette a diversi membri del team di lavorare su parti diverse del progetto senza conflitti, migliorando la collaborazione.

Se vuoi approfondire questo argomento e scoprire le migliori pratiche per organizzare il codice Terraform, ti consiglio di leggere questo articolo.

 


 

Esploriamo il codice: ogni pezzo al suo posto

Dopo aver discusso delle varie opzioni per strutturare il progetto Terraform, è ora di entrare nel vivo di questo tutorial e spiegare nel dettaglio il codice che compone la nostra infrastruttura. Ogni blocco di codice ha un ruolo specifico e contribuisce al funzionamento complessivo del sistema. Esaminiamoli uno per uno.

provider.tf

Il file provider.tf specifica i provider Terraform che useremo per interagire con le risorse di Azure. Senza questa parte, Terraform non saprebbe come comunicare con Azure.

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~>3.0"
    }
    random = {
      source  = "hashicorp/random"
      version = "~>3.0"
    }
  }
}

provider "azurerm" {
  features {}
}

  • azurerm: Questo è il provider principale che ci consente di creare e gestire risorse su Azure.
  • random: Utilizziamo questo provider per generare identificatori casuali che ci aiutano a evitare conflitti di nomi nelle risorse.

La sezione features {} è richiesta per abilitare le funzionalità del provider Azure. La versione specificata garantisce che utilizziamo una versione compatibile e stabile del provider.

main.tf

Nel file “main.tf” definiamo la maggior parte delle risorse che compongono l’infrastruttura. Questo file crea il Resource Group, gli Storage Account, il KeyVault e richiama il modulo che gestisce la Azure Function.

data "azurerm_client_config" "current" {}

resource "random_id" "rnd" {
  byte_length = 4
}
  • data “azurerm_client_config” “current”: Qui vengono recuperate le informazioni sulla configurazione del client corrente (ad esempio, l’ID del tenant e l’ID dell’utente connesso), che saranno utilizzate in seguito per configurare correttamente le risorse.
  • random_id “rnd”: Questo blocco genera un identificatore casuale che usiamo per creare nomi unici per le risorse, evitando conflitti nei nomi su Azure.

Creazione del Resource Group

Tutte le risorse Azure devono appartenere a un Resource Group. Per nominare il Resource Group utilizziamo l’identificatore `random_id.rnd.hex` visto precedentemente.

resource "azurerm_resource_group" "rg" {
  location = var.location
  name     = "${var.rg_name}-${random_id.rnd.hex}"
}
  • location: Questa variabile definisce la regione in cui le risorse saranno create (ad esempio, “West Europe”).
  • name: Il nome del Resource Group è composto da un prefisso (var.rg_name) e dall’identificatore casuale (random_id.rnd.hex), assicurandone l’univocità.

Creazione degli Storage Account

Creiamo due Storage Account: uno per i file caricati dall’utente (“landarea”) e uno per l’archiviazione della funzione Azure (“fn”).

resource "azurerm_storage_account" "landarea" {
  name                     = "landarea${random_id.rnd.hex}"
  resource_group_name      = azurerm_resource_group.rg.name
  location                 = azurerm_resource_group.rg.location
  account_tier             = "Standard"
  account_replication_type = "GRS"
}

resource "azurerm_storage_account" "fn" {
  name                     = "fnstorage${random_id.rnd.hex}"
  resource_group_name      = azurerm_resource_group.rg.name
  location                 = var.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

Vediamo a cosa servono i due Storage Account:

  • landarea: Destinato a ospitare i file caricati dall’utente. La replica geografica (“GRS”) offre ridondanza globale.
  • fn: Utilizzato dalla Azure Function per archiviare i file necessari al suo funzionamento. La replica locale (LRS) è sufficiente per questa funzione.

Creazione del Container nello Storage Account

Dopo aver creato lo Storage Account “landarea”, è necessario definire un contenitore (“storage container”) all’interno di questo account. Il contenitore è essenzialmente una cartella virtuale in cui i file saranno archiviati.

resource "azurerm_storage_container" "landarea" {
  name                  = "landarea"
  storage_account_name  = azurerm_storage_account.landarea.name
  container_access_type = "private"
}

Creazione del KeyVault

Azure KeyVault è utilizzato per gestire in sicurezza le chiavi e i secrets, come l’API Key di SendGrid.

resource "azurerm_key_vault" "keyvault" {
  name                        = "vault-${random_id.rnd.hex}"
  location                    = var.location
  resource_group_name         = azurerm_resource_group.rg.name
  enabled_for_disk_encryption = true
  tenant_id                   = data.azurerm_client_config.current.tenant_id
  soft_delete_retention_days  = 7
  purge_protection_enabled    = false
  sku_name                    = "standard"
}
  • “name”: Definisce il nome del KeyVault, reso univoco dall’aggiunta di un identificatore casuale (rnd.hex). Questo aiuta a evitare conflitti di nome in Azure.
  • “location”: Specifica la regione di Azure in cui il KeyVault verrà creato, utilizzando il valore passato dalla variabile location.
  • “resource_group_name”: Indica il Resource Group al quale il KeyVault apparterrà, garantendo che tutte le risorse siano raggruppate in modo logico.
  • “enabled_for_disk_encryption”: Abilita l’uso del KeyVault per criptare i dischi, aumentando la sicurezza dei dati sensibili.
  • “tenant_id”: Associa il KeyVault al tenant Azure corretto, utilizzando l’ID del tenant recuperato dinamicamente tramite la configurazione del client.
  • “soft_delete_retention_days”: Configura il numero di giorni per i quali i secrets eliminati rimarranno nel sistema prima di essere definitivamente rimossi, permettendo di recuperare dati cancellati accidentalmente.
  • “purge_protection_enabled”: Specifica se abilitare o meno la protezione dalla purga dei secrets. In questo caso, è impostato su false, il che significa che i secrets possono essere eliminati definitivamente una volta scaduto il periodo di soft delete.
  • “sku_name”: Determina il livello di servizio del KeyVault. standard è sufficiente per la maggior parte degli scenari, ma Azure offre anche un livello premium con funzionalità più avanzate come la gestione delle chiavi hardware-backed.

Definizione delle policy di accesso al KeyVault

Dopo aver creato il KeyVault, dobbiamo definire le politiche di accesso (access policies) per specificare chi può fare cosa.

resource "azurerm_key_vault_access_policy" "default" {
  key_vault_id = azurerm_key_vault.keyvault.id
  tenant_id    = data.azurerm_client_config.current.tenant_id
  object_id    = data.azurerm_client_config.current.object_id
  key_permissions = ["Get"]

  secret_permissions = [
    "Get", "List", "Set", "Delete", "Purge"
  ]
  storage_permissions = ["Get"]
}
  • key_vault_id: Collega la policy al KeyVault appena creato.
  • tenant_id e object_id: Questi valori identificano il tenant di Azure e l’utente o il servizio che ha accesso al KeyVault.
  • key_permissions: Specifica i permessi per le chiavi criptografiche. In questo caso, l’accesso è limitato al permesso “Get” (recupero della chiave).
  • secret_permissions: Definisce i permessi per i secrets all’interno del KeyVault, permettendo operazioni come recuperare, elencare, impostare, eliminare e purgare i secrets.
  • storage_permissions: Specifica i permessi relativi alle chiavi di crittografia dei blob storage, in questo caso limitato alla lettura (“Get”).

Creazione del Service Plan per la Azure Function

Per eseguire la Azure Function, abbiamo bisogno di un (Service Plan). Questo piano definisce le risorse di calcolo (CPU, memoria) disponibili per la funzione.

resource "azurerm_service_plan" "fn" {
  name                = "azure-functions-service-plan"
  resource_group_name = azurerm_resource_group.rg.name
  location            = var.location
  os_type             = "Windows"
  sku_name            = "Y1"
}
  • name: Semplicemente, il nome del piano di servizio.
  • resource_group_name: Collega il piano di servizio al Resource Group creato in precedenza.
  • location: Specifica la regione di Azure in cui il piano di servizio sarà creato.
  • os_type: Indica il sistema operativo utilizzato dalla funzione. In questo caso, Windows.
  • sku_name: Specifica il tipo di piano di servizio. “Y1” è un piano di servizio di tipo “Consumption”, che addebita solo per il tempo di esecuzione effettivo della funzione, ottimizzando i costi.

variables.tf

Il file variables.tf è fondamentale per rendere la tua infrastruttura Terraform flessibile e riutilizzabile. Definendo variabili per parametri come nomi, location, chiavi API e altre configurazioni, possiamo facilmente adattare la stessa infrastruttura a diversi ambienti (sviluppo, staging, produzione) senza dover modificare il codice principale.

variable "rg_name" {
  type        = string
  default     = null
  description = "Resource group name"
}

variable "location" {
  type        = string
  default     = null
  description = "Location"
}

variable "sendgrid_api_key" {
  type        = string
  default     = null
  description = "SendGrid API key"
}

variable "mail_sender" {
  type        = string
  default     = null
  description = "Email address for the sender"
}

variable "mail_receiver" {
  type        = string
  default     = null
  description = "Email address for the receiver"
}

Vediamo ora il ruolo di ciascuna di queste variabili:

  • “rg_name”: Questa variabile definisce il nome del Resource Group. Il nome può essere specificato al momento dell’esecuzione diTerraform, rendendo il progetto adattabile a diversi ambienti o clienti.
  • “location”: La location di Azure in cui verranno create le risorse (ad esempio, “West Europe”). Usare una variabile permette di distribuire la stessa infrastruttura in regioni diverse senza cambiare il codice.
  • “sendgrid_api_key”: Questa variabile contiene l’API Key di SendGrid necessaria per l’invio di email. Definirla come variabile migliora la sicurezza, evitando di hardcodare la chiave nel codice.
  • “mail_sender” e “mail_receiver”: Queste variabili definiscono gli indirizzi email del mittente e del destinatario delle notifiche. Utilizzare variabili per questi campi permette di configurare facilmente diversi scenari di notifica a seconda dell’ambiente.

Passaggio delle variabili al modulo

Nel main.tf, passiamo le variabili ad alto livello come input al modulo fn_storage_notifier. Ecco come viene fatto:

module "fn_storage_notifier" {
  source                        = "modules/storage-notifier"
  location                      = azurerm_storage_account.landarea.location
  rg_name                       = azurerm_resource_group.rg.name
  key_vault_id                  = azurerm_key_vault.keyvault.id
  fn_service_plan               = azurerm_service_plan.fn.id
  fn_storage_account_name       = azurerm_storage_account.fn.name
  fn_storage_account_access_key = azurerm_storage_account.fn.primary_access_key
  sendgrid_api_key              = var.sendgrid_api_key
  mail_sender                   = var.mail_sender
  mail_receiver                 = var.mail_receiver
  landarea_storage_connstring   = azurerm_storage_account.landarea.primary_connection_string

  depends_on = [azurerm_key_vault_access_policy.default]
}
  • “location”: La variabile location, definita nel file tf principale, viene passata al modulo per determinare in quale regione di Azure verranno distribuite le risorse del modulo, inclusa la Azure Function.
  • “rg_name”: Questa variabile fornisce al modulo il nome del Resource Group creato nel tf, assicurando che tutte le risorse definite nel modulo siano associate al gruppo di risorse corretto.
  • “key_vault_id”: L’ID del KeyVault è passato al modulo per permettere alla funzione di accedere in modo sicuro ai secrets (come l’API Key di SendGrid) gestiti dal KeyVault.
  • “fn_service_plan”: Questa variabile contiene l’ID del piano di servizio (Service Plan) creato nel tf.Viene utilizzata dal modulo per configurare la Azure Function su questo piano, che definisce le risorse di calcolo (come CPU e memoria) disponibili per l’esecuzione della funzione.
  • “fn_storage_account_name” e “fn_storage_account_access_key”: Queste variabili contengono rispettivamente il nome dello Storage Account e la chiave di accesso ad esso. Sono necessarie affinché la funzione Azure possa accedere al suo Storage Account per archiviare e recuperare i file necessari.
  • “sendgrid_api_key”: La variabile sendgrid_api_key contiene l’API Key di SendGrid, passata al modulo affinché la funzione possa utilizzare questa chiave per autenticarsi e inviare email di notifica.
  • “mail_sender” e “mail_receiver”: Queste variabili contengono gli indirizzi email del mittente e del destinatario delle notifiche. Sono utilizzate dal modulo per configurare correttamente i dettagli di invio delle email.
  • “landarea_storage_connstring”: Questa variabile contiene la stringa di connessione dello Storage Account landarea, dove vengono caricati i file. Il modulo utilizza questa connessione per monitorare i nuovi caricamenti e attivare le notifiche.
  • “depends_on”: Questa impostazione assicura che le risorse all’interno del modulo vengano create solo dopo che le politiche di accesso al KeyVault sono state correttamente applicate, garantendo che la funzione Azure abbia accesso sicuro ai secrets.

Queste variabili vengono utilizzate all’interno del modulo per configurare correttamente la Azure Function.

output.tf

Per ottenere un feedback immediato dopo l’esecuzione di Terraform, utilizziamo il file output.tf per esportare e visualizzare le informazioni chiave.

output "resource_group_name" {
  value = azurerm_resource_group.rg.name
}
  • resource_group_name: Questo output fornisce il nome del Resource Group creato, utile per ulteriori operazioni o debugging.

Codice del modulo storage-notifier

Il file “main.tf” del modulo “storage-notifier” è il cuore della configurazione della Azure Function che invierà notifiche email tramite SendGrid quando vengono caricati file sullo Storage Account “landarea”.

  1. Definizione della (User-Assigned Identity)

La prima risorsa che creiamo è una User-Assigned Managed Identity, che consente alla Azure Function di autenticarsi in modo sicuro su Azure e accedere a risorse come il KeyVault.

resource "azurerm_user_assigned_identity" "fn-identity" {
  name                = "fn-notifier-identity-${random_id.rnd.hex}"
  resource_group_name = var.rg_name
  location            = var.location
}
  • “azurerm_user_assigned_identity”: Crea una identità gestita dall’utente, necessaria per consentire alla funzione Azure di operare con privilegi specifici all’interno dell’ecosistema Azure senza la necessità di utilizzare credenziali statiche.
  • “name”: Il nome dell’identità include un identificatore casuale (rnd.hex) per garantire l’unicità.
  • “resource_group_name e location”: Specificano il Resource Group e la regione Azure in cui l’identità sarà creata, utilizzando i valori passati dalla configurazione principale.
  1. Politica di Accesso al Key Vault per la Funzione

La funzione Azure deve poter accedere ai secrets archiviati nel KeyVault, come l’API Key di SendGrid. Per questo, configuriamo una politica di accesso specifica.

resource "azurerm_key_vault_access_policy" "function" {
  key_vault_id = var.key_vault_id
  tenant_id    = data.azurerm_client_config.current.tenant_id
  object_id    = azurerm_user_assigned_identity.fn-identity.principal_id

  secret_permissions = ["Get"]
}
  • “azurerm_key_vault_access_policy”: Definisce i permessi di accesso per la User-Assigned Managed Identity al KeyVault.
  • “key_vault_id”: Identifica il KeyVault al quale questa policy si applica, utilizzando l’ID passato dalla configurazione principale.
  • “tenant_id e object_id”: Specificano l’ID del tenant di Azure e l’ID dell’identità appena creata, definendo chi può eseguire azioni sui secrets del KeyVault.
  • “secret_permissions”: In questo caso, l’identità ha il permesso di recuperare (Get) i secrets necessari per la funzione, come l’API Key di SendGrid.
  1. “Imballaggio” del Codice della Funzione

Il codice della funzione, scritto in Node.js, deve essere preparato in un file zip prima di essere distribuito. Utilizziamo una risorsa di tipo archive_file per questo scopo.

data "archive_file" "function_app_package" {
  type        = "zip"
  output_path = "/tmp/notifier/function-source.zip"
  source_dir  = "modules/storage-notifier/function-source/"
}
  • “archive_file”: Questa risorsa crea un archivio .zip contenente tutti i file del codice sorgente della funzione Azure.
  • “type”: Specifica il formato dell’archivio, in questo caso .zip.
  • “source_dir”: Indica la directory in cui si trova il codice sorgente della funzione, che viene inclusa nell’archivio.
  • “output_path”: Definisce il percorso di destinazione dove il file .zip sarà salvato.
  1. Calcolo dell’Hash del file .zip

Questo passaggio consente di gestire il deployment in modo efficiente, ridistribuendo la funzione solo se il contenuto del codice è cambiato.

resource "terraform_data" "function_app_package_md5" {
  input = data.archive_file.function_app_package.output_md5
}
  • “terraform_data”: Questo blocco calcola l’hash MD5 dell’archivio zip. Se l’hash cambia,Terraform capisce che il codice è stato modificato e ridistribuisce la funzione.
  • “input”: Utilizza l’MD5 calcolato dal blocco archive_file come input.
  1. Memorizzazione dell’API Key di SendGrid nel Key Vault

Per garantire la sicurezza, l’API Key di SendGrid viene archiviata nel KeyVault.

resource "azurerm_key_vault_secret" "sendgrid_key" {
  name         = "sendgrid-api-key"
  value        = var.sendgrid_api_key
  key_vault_id = var.key_vault_id
  depends_on   = [azurerm_key_vault_access_policy.function]
}
  • “azurerm_key_vault_secret”: Memorizza l’API Key di SendGrid come un secret nel KeyVault.
  • “name”: Il nome del secret nel KeyVault.
  • “value”: Il valore dell’API Key, passato dalla configurazione principale.
  • “key_vault_id”: Indica il KeyVault in cui memorizzare il secret.
  • “depends_on”: Garantisce che il secret venga creato solo dopo che la politica di accesso è stata applicata.
  1. Creazione della Azure Function

Infine, creiamo la Azure Function vera e propria, configurando tutte le impostazioni necessarie per il suo corretto funzionamento.

resource "azurerm_windows_function_app" "fn" {
  name                       = "notifier-${random_id.rnd.hex}"
  resource_group_name        = var.rg_name
  location                   = var.location
  storage_account_name       = var.fn_storage_account_name
  storage_account_access_key = var.fn_storage_account_access_key
  service_plan_id            = var.fn_service_plan
  zip_deploy_file            = data.archive_file.function_app_package.output_path

  lifecycle {
    replace_triggered_by = [terraform_data.function_app_package_md5]
  }

  site_config {
    application_stack {
      node_version = "~18"
    }
  }

  identity {
    type        = "UserAssigned"
    identity_ids = [azurerm_user_assigned_identity.fn-identity.id]
  }

  key_vault_reference_identity_id = azurerm_user_assigned_identity.fn-identity.id

  app_settings = {
    landarea_STORAGE = var.landarea_storage_connstring
    MAIL_RECEIVER    = var.mail_receiver
    MAIL_SENDER      = var.mail_sender
    SENDGRID_API_KEY = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.sendgrid_key.id})"
  }
}
  • “azurerm_windows_function_app”: Crea la funzione Azure specifica per Windows.
  • “name: Nome univoco della funzione.
  • “resource_group_name e location”: Indicano in quale Resource Group e regione di Azure sarà distribuita la funzione.
  • “storage_account_name” e “storage_account_access_key”: Collegano la funzione allo Storage Account, specificando il nome e la chiave di accesso.
  • “service_plan_id”: Specifica il piano di servizio su cui verrà eseguita la funzione, determinando le risorse di calcolo disponibili.
  • “zip_deploy_file”: Percorso del file zip contenente il codice sorgente della funzione.
  • “lifecycle”: La funzione viene ridistribuita automaticamente se l’MD5 del file zip cambia, grazie al blocco replace_triggered_by.
  • “site_config”: Configura lo stack applicativo della funzione, in questo caso specificando la versione di Node.js.
  • “identity” e “key_vault_reference_identity_id”: Configurano l’identità gestita che la funzione utilizza per accedere in modo sicuro al KeyVault.
  • “app_settings”: Definisce le variabili di ambiente per la funzione, inclusi i dettagli di configurazione per lo Storage Account e SendGrid. L’API Key di SendGrid viene recuperata in modo sicuro dal Key Vault tramite la sintassi @Microsoft.KeyVault.

Perché Creare un File variables.tf nel Modulo?

Il file variables.tf nel modulo storage-notifier è essenziale perché definisce le variabili che il modulo si aspetta di ricevere dalla configurazione principale. Questo file serve a:

  1. Chiarezza: Definire chiaramente quali input il modulo richiede, rendendo il modulo auto-documentante e più facile da utilizzare.
  2. Flessibilità: Permettere di riutilizzare il modulo in contesti diversi, passando valori differenti per le variabili senza dover modificare il codice interno del
  3. Sicurezza: Isolare la configurazione del modulo, garantendo che le variabili sensibili come chiavi API o connessioni a servizi esterni vengano gestite in modo sicuro e appropriato.

In pratica, il file variables.tf consente di mantenere il modulo indipendente e riutilizzabile, rendendo la configurazione dell’infrastruttura più modulare e facilmente manutenibile.

Ora che abbiamo terminato con Terraform, concentriamoci sul progetto Node.js nella Cartella function-source

Il progetto contenuto nella cartella function-source è il nucleo della funzionalità che permette di inviare notifiche email tramite SendGrid ogni volta che un file viene caricato in uno Storage Account di Azure. Questo progetto sfrutta il framework di Azure Functions e la libreria SendGrid per implementare la logica necessaria.

Il file notifier.js contiene la logica principale della Azure Function. Vediamo in dettaglio come funziona.

const { app } = require('@azure/functions');
const sendgrid = require('@sendgrid/mail');

sendgrid.setApiKey(process.env.SENDGRID_API_KEY || '');

async function sendMail(fileName, uri) {
    await sendgrid.send({
        to: process.env.MAIL_RECEIVER,
        from: process.env.MAIL_SENDER,
        subject: 'New file on the bucket',
        text: `Azure[1]: The new file "${fileName}" was uploaded to the bucket (Uri: "${uri}") just now`,
    });
    console.log(`Mail sent`);
}

app.storageBlob('notifier', {
    path: 'landarea/{name}',
    connection: 'landarea_STORAGE',
    handler: async (blob, context) => {
        context.log(`[landarea_STORAGE]: Storage blob function processed blob "${context.triggerMetadata.name}" with size ${blob.length} bytes`);
        await sendMail(context.triggerMetadata.name, context.triggerMetadata.uri);
    }
});

Spiegazione del Codice

  • Importazione delle Librerie:

◦ “@azure/functions”: Questa libreria consente gestire le Azure Functions tramite le relative API.

◦ “@sendgrid/mail”: La libreria ufficiale di SendGrid per l’invio di email attraverso le sue API.

  • Configurazione di SendGrid:
    • “setApiKey”: L’API Key di SendGrid viene impostata utilizzando la variabile d’ambiente SENDGRID_API_KEY. Se questa variabile non è definita (ad esempio durante lo sviluppo locale), può essere fornita una chiave API di default, anche se per sicurezza è sempre meglio evitare di hardcodare le chiavi API nel codice.
  • Funzione sendMail:
    • “sendMail(fileName, uri)”: Questa funzione invia un’email utilizzando SendGrid. L’email contiene informazioni sul nuovo file caricato, come il nome del file e l’URI (Uniform Resource Identifier) del file all’interno dello Storage Account.
    • “to”, “from”, “subject, text”: Questi campi definiscono i dettagli dell’email. Gli indirizzi email di destinatario e mittente vengono letti dalle variabili d’ambiente MAIL_RECEIVER e MAIL_SENDER.
  • Definizione della Funzione BlobTrigger:
    • “app.storageBlob”: Qui viene definita una Azure Function con trigger su Blob Storage. Ogni volta che un file (blob) viene caricato nel percorso specificato (“landarea/{name}”), questa funzione viene eseguita.
    • “path”: Specifica il percorso monitorato all’interno dello Storage Account. “{name}” è un parametro che corrisponde al nome del file caricato.
    • “connection”: Indica la stringa di connessione allo Storage Account (“landarea_STORAGE”), che è configurata come variabile d’ambiente.
    • “handler(blob, context)”: La funzione di gestione (handler) viene eseguita quando viene rilevato un nuovo blob. Questa funzione registra un log dell’operazione e chiama “sendMail” per inviare un’email con i dettagli del file caricato.

package.json

Il file package.json descrive le dipendenze e le configurazioni del progetto.

{
  "name": "notifier",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "start": "func start",
    "test": "echo \"No tests yet...\""
  },
  "dependencies": {
    "@azure/functions": "^4.0.0",
    "@sendgrid/mail": "^8.1.1"
  },
  "devDependencies": {},
  "main": "src/functions/*.js"
}
  • “name” e “version”: Specificano il nome e la versione del progetto.
  • “scripts”:

“start”:Avvia il progetto utilizzando Azure Functions CoreTools (func start), utile per il debugging e il testing locale.

“test”: Un placeholder per eventuali test, attualmente non implementato.

  • “dependencies”:

“@azure/functions”: Libreria necessaria per sviluppare e gestire Azure Functions.

“@sendgrid/mail”: Libreria di SendGrid utilizzata per inviare email.

  • “main”: Indica il percorso principale dove si trovano le funzioni. Sebbene il valore src/functions/*.js sembri un pattern che punta a file JavaScript, nel tuo progetto la funzione principale si trova direttamente in js.

Comandi da eseguire

Per creare l’infrastruttura e avviare la funzione, segui questi passaggi:

  1. Installa le dipendenze: Prima di inizializzare e applicare Terraform, assicurati di eseguire “npm install” all’interno della cartella “function-source” per installare tutte le dipendenze necessarie:
cd modules/storage-notifier/function-source
    npm install
  1. Configura le variabili d’ambiente: Prima di inizializzare e applicare Terraform, è necessario configurare le variabili d’ambiente richieste: export TF_VAR_rg_name=”nome del tuo resource group”
export TF_VAR_rg_name="nome del tuo resource group"
    export TF_VAR_location=" la regione in cui le risorse saranno create"
    export TF_VAR_sendgrid_api_key="la chiave di SendGrid"
    export TF_VAR_mail_sender="l'email del mittente usata per inviare la notifica, registrata su SendGrid"
    export TF_VAR_mail_receiver="l'email dove desideri ricevere la notifica"
  1. Autenticati con Azure CLI: Assicurati di essere autenticato nel tuo account Azure:
    az login
  2. Inizializza Terraform:
    terraform init
  3. Applica la configurazioneTerraform:
    terraform apply

Durante l’applicazione, Terraform utilizzerà le variabili d’ambiente che hai configurato per creare e configurare le risorse su Azure.

  1. Distruggi le risorse create:
    terraform destroy

Questo comando eliminerà tutte le risorse create daTerraform.

 

Conclusioni

Ed eccoci qua! In pochi passaggi, hai creato un sistema completamente automatizzato che invia notifiche email ogni volta che un file viene caricato su uno Storage Account Azure. Sebbene si tratti di un esempio semplice, rappresenta un ottimo punto di partenza per familiarizzare conTerraform e i servizi Azure.

Spero che questa guida ti sia stata utile, e se vuoi esplorare il progetto completo, ti ricordo che lo puoi trovare su GitHub.

Articolo scritto da