Best practice per l'utilizzo di Terraform

Questo documento fornisce linee guida e suggerimenti per uno sviluppo efficace di Terraform da parte di più membri del team e flussi di lavoro.

Questa guida non è un'introduzione a Terraform. Per un'introduzione all'utilizzo di Terraform con Google Cloud, consulta la guida introduttiva all'utilizzo di Terraform.

Linee guida generali su stile e struttura

I suggerimenti seguenti riguardano lo stile e la struttura di base delle tue configurazioni Terraform. I suggerimenti si applicano ai moduli Terraform riutilizzabili e alle configurazioni principali.

Seguire una struttura di moduli standard

  • I moduli Terraform devono seguire la struttura dei moduli standard.
  • Avvia ogni modulo con un file main.tf, dove si trovano le risorse per impostazione predefinita.
  • In ogni modulo, includi un file README.md in formato Markdown. Nel file README.md, includi la documentazione di base sul modulo.
  • Inserisci gli esempi in una cartella examples/, con una sottodirectory separata per ogni esempio. Per ogni esempio, includi un file README.md dettagliato.
  • Crea raggruppamenti logici di risorse con i propri file e nomi descrittivi, come network.tf, instances.tf o loadbalancer.tf.
    • Evita di assegnare a ogni risorsa il proprio file. Raggruppa le risorse in base al loro scopo condiviso. Ad esempio, combina google_dns_managed_zone e google_dns_record_set in dns.tf.
  • Nella directory root del modulo, includi solo i file di metadati di Terraform (*.tf) e dei repository (come README.md e CHANGELOG.md).
  • Inserisci eventuale documentazione aggiuntiva in una sottodirectory docs/.

Adotta una convenzione di denominazione

  • Assegna un nome a tutti gli oggetti di configurazione utilizzando trattini bassi per delimitare più parole. Questa prassi garantisce la coerenza con la convenzione di denominazione per i tipi di risorse, i tipi di origini dati e altri valori predefiniti. Questa convenzione non si applica agli argomenti dei nomi.

    Consiglio:

    resource "google_compute_instance" "web_server" {
      name = "web-server"
    }
    

    Sconsigliato:

    resource "google_compute_instance" "web-server" {
      name = "web-server"
    }
    
  • Per semplificare i riferimenti a una risorsa che è l'unico del suo tipo (ad esempio, un singolo bilanciatore del carico per un intero modulo), denomina la risorsa main.

    • Ci vuole più lavoro mentale per ricordare some_google_resource.my_special_resource.id invece di some_google_resource.main.id.
  • Per distinguere risorse dello stesso tipo l'una dall'altra (ad esempio, primary e secondary), fornisci nomi significativi per le risorse.

  • Imposta i nomi delle risorse come singoli.

  • Non ripetere il tipo di risorsa nel nome della risorsa, Ad esempio:

    Consiglio:

    resource "google_compute_global_address" "main" { ... }
    

    Sconsigliato:

    resource "google_compute_global_address" "main_global_address" { … }
    

Utilizza le variabili con attenzione

  • Dichiara tutte le variabili in variables.tf.
  • Assegna alle variabili nomi descrittivi pertinenti al loro utilizzo o scopo:
    • Gli input, le variabili locali e gli output che rappresentano valori numerici, come dimensioni del disco o dimensioni della RAM, devono essere denominati con unità (ad esempio ram_size_gb). Le API Google Cloud non hanno unità standard, pertanto la denominazione delle variabili con unità rende chiara l'unità di input prevista per i gestori della configurazione.
    • Per le unità di archiviazione, utilizza i prefissi delle unità binarie (potenze di 1024): kibi, mebi e gibi. Per tutte le altre unità di misura, utilizza prefissi di unità decimali (potenze pari a 1000): kilo, mega e giga. Questo utilizzo corrisponde a quello all'interno di Google Cloud.
    • Per semplificare la logica condizionale, assegna alle variabili booleane nomi positivi, ad esempio enable_external_access.
  • Le variabili devono avere descrizioni. Le descrizioni sono incluse automaticamente nella documentazione generata automaticamente di un modulo pubblicato. Le descrizioni aggiungono ulteriore contesto per i nuovi sviluppatori che i nomi descrittivi non sono in grado di fornire.
  • Assegna alle variabili tipi definiti.
  • Se opportuno, fornisci i valori predefiniti:
    • Per le variabili con valori indipendenti dall'ambiente (ad esempio le dimensioni del disco), fornisci i valori predefiniti.
    • Per le variabili con valori specifici dell'ambiente (ad esempio project_id), non fornire valori predefiniti. In questo modo, il modulo di chiamata deve fornire valori significativi.
  • Utilizza i valori predefiniti vuoti per le variabili (ad esempio stringhe o elenchi vuoti) solo se lasci vuota la variabile è una preferenza valida che le API sottostanti non rifiutano.
  • Utilizza con prudenza le variabili. parametrizza solo i valori che devono variare per ogni istanza o ambiente. Quando decidi se esporre una variabile, assicurati di avere un caso d'uso concreto per la modifica della variabile. Se esiste solo una piccola possibilità che possa essere necessaria una variabile, non esporla.
    • L'aggiunta di una variabile con un valore predefinito è compatibile con le versioni precedenti.
    • La rimozione di una variabile non è compatibile con le versioni precedenti.
    • Nei casi in cui un valore letterale viene riutilizzato in più posizioni, puoi utilizzare un valore locale senza esporlo come variabile.

Esponi output

  • Organizza tutti gli output in un file outputs.tf.
  • Fornisci descrizioni significative per tutti gli output.
  • Descrizioni dell'output dei documenti nel file README.md. Genera automaticamente descrizioni sul commit con strumenti come terraform-docs.
  • Visualizza tutti i valori utili che i moduli principali potrebbero dover fare riferimento o condividere. In particolare per i moduli open source o molto utilizzati, esponi tutti gli output potenzialmente di consumo.
  • Non trasmettere gli output direttamente tramite le variabili di input, perché così facendo impedisci che vengano aggiunte correttamente al grafico delle dipendenze. Per assicurarti che vengano create dipendenze implicite, assicurati che restituisca gli attributi di riferimento dalle risorse. Anziché fare riferimento direttamente a una variabile di input per un'istanza, trasmetti l'attributo come mostrato qui:

    Consiglio:

    output "name" {
      description = "Name of instance"
      value       = google_compute_instance.main.name
    }
    

    Sconsigliato:

    output "name" {
      description = "Name of instance"
      value       = var.name
    }
    

Utilizzare le origini dati

  • Inserisci le origini dati accanto alle risorse che le fanno riferimento. Ad esempio, se stai recuperando un'immagine da utilizzare nell'avvio di un'istanza, posizionala accanto all'istanza anziché raccogliere risorse dati nel proprio file.
  • Se il numero di origini dati diventa elevato, valuta la possibilità di spostarle in un file data.tf dedicato.
  • Per recuperare i dati relativi all'ambiente attuale, utilizza l'interpolazione della variabile o della risorsa.

Limitare l'utilizzo di script personalizzati

  • Utilizza gli script solo se necessario. Lo stato delle risorse create tramite script non viene preso in considerazione né gestito da Terraform.
    • Se possibile, evita gli script personalizzati. Usali solo quando le risorse Terraform non supportano il comportamento desiderato.
    • Tutti gli script personalizzati utilizzati devono avere un motivo chiaramente documentato per la presenza e, idealmente, un piano di ritiro.
  • Terraform può chiamare script personalizzati tramite provisioner, incluso il provisioner local-exec.
  • Inserisci gli script personalizzati richiamati da Terraform in una directory scripts/.

Includi gli script di supporto in una directory separata

  • Organizza gli script helper che non sono chiamati da Terraform in una directory helpers/.
  • Script helper per i documenti nel file README.md con spiegazioni e chiamate di esempio.
  • Se gli script di supporto accettano gli argomenti, fornisci il controllo degli argomenti e l'output --help.

Inserisci i file statici in una directory separata

  • I file statici a cui Terraform fa riferimento ma che non esegue (ad esempio gli script di avvio caricati nelle istanze di Compute Engine) devono essere organizzati in una directory files/.
  • Inserisci file HereDocumenti lunghi in file esterni, separati dai loro HCL. Utilizza la funzione file() come riferimento.
  • Per i file letti con la funzione templatefile di Terraform, utilizza l'estensione del file .tftpl.
    • I modelli devono essere inseriti in una directory templates/.

Proteggi le risorse stateful

Per le risorse stateful, come i database, assicurati che sia abilitata la protezione da eliminazione. Ad esempio:

resource "google_sql_database_instance" "main" {
  name = "primary-instance"
  settings {
    tier = "D0"
  }

  lifecycle {
    prevent_destroy = true
  }
}

Utilizzare la formattazione integrata

Tutti i file Terraform devono essere conformi agli standard di terraform fmt.

Limitare la complessità delle espressioni

  • Limita la complessità di una singola espressione interpolata. Se sono necessarie molte funzioni in una singola espressione, valuta la possibilità di suddividerla in più espressioni utilizzando valori locali.
  • Non avere più di un'operazione ternaria su una sola riga. Utilizza invece più valori locali per creare la logica.

Utilizza count per i valori condizionali

Per creare l'istanza di una risorsa in modo condizionale, utilizza il meta-argomento count. Ad esempio:

variable "readers" {
  description = "..."
  type        = list
  default     = []
}

resource "resource_type" "reference_name" {
  // Do not create this resource if the list of readers is empty.
  count = length(var.readers) == 0 ? 0 : 1
  ...
}

Fai attenzione quando utilizzi le variabili specificate dall'utente per impostare la variabile count per le risorse. Se viene fornito un attributo di risorsa per una variabile di questo tipo (come project_id) e la risorsa non esiste ancora, Terraform non può generare un piano. Terraform, invece, segnala l'errore value of count cannot be computed. In questi casi, utilizza una variabile enable_x separata per calcolare la logica condizionale.

Utilizza for_each per le risorse iterate

Se vuoi creare più copie di una risorsa basata su una risorsa di input, utilizza il meta-argomento for_each.

Pubblicare moduli in un registro

Moduli riutilizzabili

Per i moduli destinati al riutilizzo, utilizza le seguenti linee guida in aggiunta a quelle precedenti.

Attiva le API richieste nei moduli

I moduli Terraform possono attivare qualsiasi servizio richiesto utilizzando la risorsa google_project_service o il modulo project_services. L'inclusione dell'attivazione dell'API semplifica le dimostrazioni.

  • Se l'attivazione dell'API è inclusa in un modulo, l'attivazione dell'API deve essere disabilitabile mediante l'esposizione di una variabile enable_apis che per impostazione predefinita è true.
  • Se l'attivazione dell'API è inclusa in un modulo, l'attivazione dell'API deve impostare disable_services_on_destroy su false, poiché questo attributo può causare problemi quando si lavora con più istanze del modulo.

    Ad esempio:

    module "project-services" {
      source  = "terraform-google-modules/project-factory/google//modules/project_services"
      version = "~> 12.0"
    
      project_id  = var.project_id
      enable_apis = var.enable_apis
    
      activate_apis = [
        "compute.googleapis.com",
        "pubsub.googleapis.com",
      ]
      disable_services_on_destroy = false
    }
    

Includi un file dei proprietari

Per tutti i moduli condivisi, includi un file OWNERS (o CODEOWNERS su GitHub), che documenta chi è responsabile del modulo. Prima che una richiesta pull venga unita, un proprietario deve approvarla.

Rilascia versioni con tag

A volte i moduli richiedono modifiche che provocano errori ed è necessario comunicare gli effetti agli utenti in modo che possano aggiungere le configurazioni a una versione specifica.

Assicurati che i moduli condivisi siano conformi a SemVer v2.0.0 quando vengono rilasciate o contrassegnate nuove versioni.

Quando fai riferimento a un modulo, usa un vincolo della versione per bloccare la versione principale. Ad esempio:

module "gke" {
  source  = "terraform-google-modules/kubernetes-engine/google"
  version = "~> 20.0"
}

Non configurare provider o backend

I moduli condivisi non devono configurare provider o backend. Configura invece provider e backend nei moduli principali.

Per i moduli condivisi, definisci le versioni minime richieste del provider in un blocco required_providers, come segue:

terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = ">= 4.0.0"
    }
  }
}

Se non diversamente dimostrato, presupponi che le nuove versioni del provider funzionino.

Esponi le etichette come variabile

Consenti flessibilità nell'etichettatura delle risorse attraverso l'interfaccia del modulo. Valuta la possibilità di fornire una variabile labels con il valore predefinito di una mappa vuota, come segue:

variable "labels" {
  description = "A map of labels to apply to contained resources."
  default     = {}
  type        = "map"
}

Esponi output per tutte le risorse

Le variabili e gli output consentono di dedurre le dipendenze tra moduli e risorse. Senza output, gli utenti non possono ordinare correttamente il modulo in relazione alle loro configurazioni Terraform.

Per ogni risorsa definita in un modulo condiviso, includi almeno un output che faccia riferimento alla risorsa.

Utilizza sottomoduli incorporati per una logica complessa

  • I moduli in linea consentono di organizzare moduli Terraform complessi in unità più piccole e deduplicare le risorse comuni.
  • Posiziona i moduli incorporati in modules/$modulename.
  • Tratta i moduli incorporati come privati e non devono essere utilizzati da moduli esterni, a meno che non sia indicato diversamente nella documentazione del modulo condiviso.
  • Terraform non monitora le risorse sottoposte a refactoring. Se inizi con diverse risorse nel modulo di primo livello e poi le invii nei sottomoduli, Terraform cerca di ricreare tutte le risorse sottoposte a refactoring. Per mitigare questo comportamento, utilizza i blocchi moved durante il refactoring.
  • Gli output definiti dai moduli interni non vengono esposti automaticamente. Per condividere gli output dei moduli interni, esportali di nuovo.

Moduli principali Terraform

Le configurazioni radice (moduli root) sono le directory di lavoro da cui esegui l'interfaccia a riga di comando di Terraform. Assicurati che le configurazioni root siano conformi agli standard seguenti (e alle precedenti linee guida di Terraform, se applicabili). I suggerimenti espliciti per i moduli principali prevalgono sulle linee guida generali.

Riduci al minimo il numero di risorse in ogni modulo principale

È importante evitare che un'unica configurazione radice diventi troppo grande, con troppe risorse archiviate nella stessa directory e nello stesso stato. Tutte le risorse di una determinata configurazione radice vengono aggiornate ogni volta che viene eseguito Terraform. Ciò può causare l'esecuzione lenta se troppe risorse sono incluse in un singolo stato. Regola generale: non includere più di 100 risorse (e idealmente solo poche decine) in un singolo stato.

Usa directory separate per ogni applicazione

Per gestire applicazioni e progetti in modo indipendente l'uno dall'altro, inserisci le risorse per ogni applicazione e progetto nelle rispettive directory Terraform. Un servizio potrebbe rappresentare una particolare applicazione o un servizio comune come il networking condiviso. Nidifica tutto il codice Terraform per un determinato servizio in una directory (incluse le sottodirectory).

Suddividi le applicazioni in sottodirectory specifiche dell'ambiente

Quando esegui il deployment dei servizi in Google Cloud, suddividi la configurazione Terraform per il servizio in due directory di primo livello: una directory modules che contiene la configurazione effettiva del servizio e una directory environments contenente le configurazioni principali per ciascun ambiente.

-- SERVICE-DIRECTORY/
   -- OWNERS
   -- modules/
      -- <service-name>/
         -- main.tf
         -- variables.tf
         -- outputs.tf
         -- provider.tf
         -- README
      -- ...other…
   -- environments/
      -- dev/
         -- backend.tf
         -- main.tf

      -- qa/
         -- backend.tf
         -- main.tf

      -- prod/
         -- backend.tf
         -- main.tf

Usa directory di ambiente

Per condividere il codice tra ambienti, fai riferimento ai moduli. In genere, potrebbe essere un modulo del servizio che include la configurazione Terraform condivisa di base per il servizio. Nei moduli di servizio, esegui come hardcoded gli input comuni e richiedi solo input specifici dell'ambiente come variabili.

Ogni directory di ambiente deve contenere i seguenti file:

  • Un file backend.tf, che dichiara la posizione dello stato del backend di Terraform (in genere Cloud Storage)
  • Un file main.tf che crea un'istanza del modulo del servizio

Ogni directory di ambiente (dev, qa, prod) corrisponde a un'area di lavoro Terraform predefinita ed esegue il deployment di una versione del servizio nell'ambiente. Queste aree di lavoro isolano le risorse specifiche dell'ambiente nei rispettivi contesti. Utilizza solo l'area di lavoro predefinita.

Non è consigliabile avere più aree di lavoro CLI all'interno di un ambiente per i seguenti motivi:

  • Può essere difficile esaminare la configurazione in ogni area di lavoro.
  • Non è consigliabile avere un unico backend condiviso per più aree di lavoro perché il backend condiviso diventa un single point of failure se viene utilizzato per la separazione dell'ambiente.
  • Sebbene il riutilizzo del codice sia possibile, diventa più difficile leggere il codice dover cambiare in base alla variabile dell'area di lavoro corrente (ad esempio, terraform.workspace == "foo" ? this : that).

Per ulteriori informazioni, consulta le seguenti risorse:

Esponi output tramite lo stato remoto

Assicurati di visualizzare output utili delle istanze di modulo da un modulo principale.

Ad esempio, il seguente snippet di codice passa attraverso l'output dell'ID progetto dell'istanza del modulo di fabbrica del progetto come output del modulo principale.

# Project root module
terraform {
  backend "gcs" {
    bucket  = "BUCKET"
  }
}

module "project" {
  source  = "terraform-google-modules/project-factory/google"
  ...
}

output "project_id" {
  value       = module.project.project_id
  description = "The ID of the created project"
}

Altri ambienti e applicazioni Terraform possono fare riferimento solo a output a livello di modulo principale.

Utilizzando lo stato remoto, puoi fare riferimento agli output del modulo principale. Per consentire l'utilizzo da parte di altre app dipendenti per la configurazione, assicurati di esportare nello stato remoto le informazioni relative agli endpoint di un servizio.

# Networks root module
data "terraform_remote_state" "network_project" {
  backend = "gcs"

  config = {
    bucket = "BUCKET"
  }
}

module "vpc" {
  source  = "terraform-google-modules/network/google"
  version = "~> 9.0"

  project_id   = data.terraform_remote_state.network_project.outputs.project_id
  network_name = "vpc-1"
  ...
}

A volte, ad esempio quando si chiama un modulo di servizio condiviso dalle directory degli ambienti, è opportuno esportare nuovamente l'intero modulo figlio, come segue:

output "service" {
  value       = module.service
  description = "The service module outputs"
}

Blocca le versioni del provider secondario

Nei moduli principali, dichiara ciascun provider e bloccalo su una versione secondaria. Ciò consente l'upgrade automatico alle nuove release delle patch, mantenendo comunque un target solido. Per coerenza, assegna al file delle versioni il nome versions.tf.

terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 4.0.0"
    }
  }
}

Archivia variabili in un file tfvars

Per i moduli principali, fornisci le variabili utilizzando un file delle variabili .tfvars. Per coerenza, assegna un nome ai file delle variabili terraform.tfvars.

Non specificare le variabili utilizzando opzioni della riga di comando alternative o var-files o var='key=val'. Le opzioni della riga di comando sono temporanee e facili da dimenticare. È più prevedibile utilizzare un file di variabili predefinite.

Controlla in .terraform.lock.hcl file

Per i moduli principali, il file di blocco delle dipendenze .terraform.lock.hcl deve essere archiviato nel controllo del codice sorgente. Ciò consente di monitorare e rivedere le modifiche nelle selezioni del fornitore per una determinata configurazione.

Comunicazione tra configurazioni

Un problema comune che si verifica quando si utilizza Terraform è il modo in cui condividere informazioni tra diverse configurazioni Terraform (possibilmente gestite da team diversi). In genere, le informazioni possono essere condivise tra le configurazioni senza richiedere che siano archiviate in un'unica directory di configurazione (o anche in un solo repository).

Il metodo consigliato per condividere informazioni tra le diverse configurazioni Terraform è utilizzare lo stato remoto per fare riferimento ad altri moduli principali. Cloud Storage o Terraform Enterprise sono i backend di stato preferiti.

Per eseguire query sulle risorse non gestite da Terraform, utilizza le origini dati del provider Google. Ad esempio, l'account di servizio Compute Engine predefinito può essere recuperato utilizzando un'origine dati. Non utilizzare origini dati per eseguire query sulle risorse gestite da un'altra configurazione Terraform. In questo modo si possono creare dipendenze implicite su strutture e nomi delle risorse che le normali operazioni di Terraform potrebbero involontariamente interrompere.

Utilizzo delle risorse Google Cloud

Le best practice per il provisioning delle risorse Google Cloud con Terraform sono integrate nei moduli Cloud Foundation Toolkit gestiti da Google. In questa sezione vengono ribadite alcune di queste best practice.

Crea immagini di macchine virtuali

In generale, ti consigliamo di creare immagini di macchine virtuali utilizzando uno strumento come Packer. Terraform deve quindi avviare le macchine solo utilizzando le immagini preconfigurate.

Se le immagini precompilate non sono disponibili, Terraform può trasferire nuove macchine virtuali a uno strumento di gestione della configurazione con un blocco provisioner. Ti consigliamo di evitare questo metodo e di utilizzarlo solo come ultima risorsa. Per ripulire lo stato precedente associato all'istanza, i provisioner che richiedono una logica di annullamento devono utilizzare un blocco provisioner con when = destroy.

Terraform deve fornire informazioni sulla configurazione delle VM alla gestione della configurazione con i metadati delle istanze.

Gestisci Identity and Access Management

Durante il provisioning delle associazioni IAM con Terraform, sono disponibili diverse risorse:

  • google_*_iam_policy (ad esempio, google_project_iam_policy)
  • google_*_iam_binding (ad esempio, google_project_iam_binding)
  • google_*_iam_member (ad esempio, google_project_iam_member)

google_*_iam_policy e google_*_iam_binding creano associazioni IAM autorevoli, in cui le risorse Terraform fungono da unica fonte attendibile per le autorizzazioni che possono essere assegnate alla risorsa pertinente.

Se le autorizzazioni cambiano al di fuori di Terraform, Terraform nell'esecuzione successiva sovrascrive tutte le autorizzazioni per rappresentare il criterio come definito nella configurazione. Questo potrebbe avere senso per le risorse che sono interamente gestite da una specifica configurazione Terraform, ma significa che i ruoli gestiti automaticamente da Google Cloud vengono rimossi, con potenziali ripercussioni sulla funzionalità di alcuni servizi.

Per evitare che questo accada, ti consigliamo di utilizzare le risorse google_*_iam_member direttamente o il modulo IAM di Google.

Controllo delle versioni

Come per altre forme di codice, archivia il codice dell'infrastruttura nel controllo della versione per preservare la cronologia e consentire rollback semplici.

Utilizza una strategia di diramazione predefinita

Per tutti i repository che contengono codice Terraform, utilizza la seguente strategia per impostazione predefinita:

  • Il ramo main è il ramo di sviluppo principale e rappresenta il codice approvato più recente. Il ramo main è protetto.
  • Lo sviluppo avviene sui rami di funzionalità e correzione di bug che si diramano dal ramo main.
    • Assegna un nome ai rami di funzionalità feature/$feature_name.
    • Assegna un nome ai rami per la correzione di bug fix/$bugfix_name.
  • Quando la correzione di una funzionalità o di un bug è completa, uniscila di nuovo al ramo main con una richiesta di pull.
  • Per evitare conflitti di unione, ribasa i rami prima di unirli.

Utilizza i rami di ambiente per le configurazioni radice

Per i repository che includono configurazioni radice di cui viene eseguito il deployment direttamente in Google Cloud, è necessaria una strategia di implementazione sicura. Ti consigliamo di avere un ramo separato per ogni ambiente. Pertanto, le modifiche alla configurazione di Terraform possono essere promosse unendo le modifiche tra i diversi rami.

Ramo separato per ogni ambiente

Consenti visibilità ampia

Rendi il codice sorgente e i repository di Terraform ampiamente visibili e accessibili all'interno delle organizzazioni di ingegneria, per i proprietari dell'infrastruttura (ad es. gli SRE) e gli stakeholder dell'infrastruttura (ad esempio gli sviluppatori). Ciò garantisce che gli stakeholder dell'infrastruttura possano avere una migliore comprensione dell'infrastruttura da cui dipendono.

Incoraggia gli stakeholder dell'infrastruttura a inviare richieste di unione nell'ambito del processo di richiesta di modifica.

Non eseguire mai il commit dei secret

Non eseguire mai il commit dei secret nel controllo del codice sorgente, anche nella configurazione di Terraform. Caricali invece in un sistema come Secret Manager e utilizzali come riferimento utilizzando le origini dati.

Tieni presente che questi valori sensibili potrebbero comunque finire nel file di stato e anche essere esposti come output.

Organizza i repository in base ai confini dei team

Sebbene sia possibile utilizzare directory separate per gestire i confini logici tra le risorse, i confini dell'organizzazione e la logistica determinano la struttura del repository. In generale, segui il principio di progettazione secondo il quale le configurazioni con requisiti di approvazione e gestione diversi vengono separate in repository di controllo del codice sorgente diversi. Per illustrare questo principio, ecco alcune possibili configurazioni di repository:

  • Un repository centrale: in questo modello, tutto il codice Terraform è gestito centralmente da un unico team della piattaforma. Questo modello funziona al meglio se è presente un team dedicato all'infrastruttura responsabile della gestione di tutta il cloud e che approva eventuali modifiche richieste da altri team.

  • Repository in team: in questo modello, ogni team è responsabile del proprio repository Terraform, in cui gestisce tutto ciò che riguarda l'infrastruttura di sua proprietà. Ad esempio, il team per la sicurezza potrebbe avere un repository in cui vengono gestiti tutti i controlli di sicurezza e i team addetti alle applicazioni potrebbero disporre ciascuno del proprio repository Terraform per eseguire il deployment e gestire l'applicazione.

    L'organizzazione dei repository in base ai confini dei team è la struttura migliore per la maggior parte degli scenari aziendali.

  • Repository disaccoppiati: in questo modello, ogni componente Terraform logico viene suddiviso nel proprio repository. Ad esempio, il networking potrebbe avere un repository dedicato e potrebbe esistere un repository della fabbrica di progetto separato per la creazione e la gestione dei progetti. Questo approccio funziona al meglio in ambienti altamente decentralizzati, in cui le responsabilità spesso si spostano da un team all'altro.

Struttura del repository di esempio

Puoi combinare questi principi per suddividere la configurazione di Terraform tra diversi tipi di repository:

  • Di base
  • Specifici per applicazione e team
Repository di base

Un repository di base che contiene i principali componenti centrali, come cartelle o IAM dell'organizzazione. Questo repository può essere gestito dal team Cloud centrale.

  • In questo repository, includi una directory per ogni componente principale (ad esempio cartelle, reti e così via).
  • Nelle directory dei componenti, includi una cartella separata per ogni ambiente (che riflette le indicazioni sulla struttura della directory menzionate in precedenza).

Struttura del repository di base

Repository per applicazioni e team

Esegui il deployment di repository specifici per le applicazioni e per i team separatamente per ogni team al fine di gestire la propria configurazione Terraform unica e specifica per l'applicazione.

Struttura dei repository specifica per le applicazioni e i team

Suite operativa

La protezione dell'infrastruttura dipende dall'avere un processo stabile e sicuro per l'applicazione degli aggiornamenti di Terraform.

Pianifica sempre prima

Genera sempre prima un piano per le esecuzioni di Terraform. Salva il piano in un file di output. Dopo l'approvazione del proprietario dell'infrastruttura, esegui il piano. Anche quando gli sviluppatori stanno prototipando le modifiche in locale, devono generare un piano ed esaminare le risorse da aggiungere, modificare ed eliminare prima di applicare il piano.

Implementare una pipeline automatizzata

Per garantire un contesto di esecuzione coerente, esegui Terraform tramite strumenti automatizzati. Se un sistema di compilazione (come Jenkins) è già in uso e ampiamente adottato, utilizzalo per eseguire automaticamente i comandi terraform plan e terraform apply. Se non sono disponibili sistemi esistenti, adotta Cloud Build o Terraform Cloud.

Utilizza le credenziali dell'account di servizio per l'integrazione continua

Quando Terraform viene eseguito da una macchina in una pipeline CI/CD, deve ereditare le credenziali dell'account di servizio dal servizio che esegue la pipeline. Dove possibile, esegui pipeline CI su Google Cloud perché Cloud Build, Google Kubernetes Engine o Compute Engine inseriscono le credenziali senza scaricare le chiavi degli account di servizio.

Per le pipeline che vengono eseguite al di fuori di Google Cloud, preferisci la federazione delle identità per i carichi di lavoro per ottenere le credenziali senza scaricare le chiavi degli account di servizio.

Evita di importare risorse esistenti

Se possibile, evita di importare risorse esistenti (utilizzando terraform import), perché così facendo potrebbe essere difficile comprendere appieno la provenienza e la configurazione delle risorse create manualmente. Crea invece nuove risorse con Terraform ed elimina quelle precedenti.

Nei casi in cui l'eliminazione di risorse vecchie crei lavoro eccessivo, utilizza il comando terraform import con approvazione esplicita. Dopo aver importato una risorsa in Terraform, gestiscila esclusivamente con Terraform.

Google offre uno strumento che puoi usare per importare le tue risorse Google Cloud nello stato di Terraform. Per maggiori informazioni, consulta Importare le risorse Google Cloud nello stato di Terraform.

Non modificare manualmente lo stato di Terraform

Il file di stato di Terraform è fondamentale per mantenere la mappatura tra la configurazione Terraform e le risorse Google Cloud. La corruzione può causare gravi problemi dell'infrastruttura. Quando sono necessarie modifiche allo stato di Terraform, utilizza il comando terraform state.

Controlla regolarmente i blocchi della versione

Il blocco delle versioni garantisce la stabilità, ma impedisce l'incorporamento di correzioni di bug e altri miglioramenti nella configurazione. Pertanto, esamina regolarmente i PIN della versione per Terraform, provider e moduli Terraform.

Per automatizzare questo processo, utilizza uno strumento come Dependabot.

Utilizza le credenziali predefinite dell'applicazione durante l'esecuzione in locale

Quando gli sviluppatori eseguono l'iterazione locale della configurazione di Terraform, devono eseguire l'autenticazione eseguendo gcloud auth application-default login per generare le credenziali predefinite dell'applicazione. Non scaricare le chiavi degli account di servizio, perché le chiavi scaricate sono più difficili da gestire e sicure.

Impostare gli alias su Terraform

Per semplificare lo sviluppo locale, puoi aggiungere alias al tuo profilo della shell dei comandi:

  • alias tf="terraform"
  • alias terrafrom="terraform"

Sicurezza

Per funzionare, Terraform richiede un accesso sensibile alla tua infrastruttura cloud. Seguire queste best practice per la sicurezza può aiutare a ridurre al minimo i rischi associati e a migliorare la sicurezza generale del cloud.

Usa stato remoto

Per i clienti di Google Cloud, consigliamo di utilizzare il backend dello stato di Cloud Storage. Questo approccio blocca lo stato per consentire la collaborazione come team. Separa inoltre lo stato e tutte le informazioni potenzialmente sensibili dal controllo della versione.

Assicurati che solo gli amministratori del sistema di compilazione e con privilegi elevati possano accedere al bucket utilizzato per lo stato remoto.

Per impedire il commit accidentale dello stato di sviluppo nel controllo del codice sorgente, utilizza gitignore per i file di stato di Terraform.

Stato crittografia

Sebbene i bucket Google Cloud siano criptati at-rest, puoi utilizzare chiavi di crittografia fornite dal cliente per fornire un ulteriore livello di protezione. Per farlo, utilizza la variabile di ambiente GOOGLE_ENCRYPTION_KEY. Anche se nel file di stato non deve essere presente alcun secret, cripta sempre lo stato come misura di difesa aggiuntiva.

Non archiviare i secret nello stato

In Terraform esistono molte risorse e provider di dati che archiviano i valori secret in testo non crittografato nel file di stato. Se possibile, evita di archiviare i secret nello stato. Di seguito sono riportati alcuni esempi di provider che archiviano i segreti in testo non crittografato:

Contrassegna output sensibili

Anziché tentare di criptare manualmente i valori sensibili, affidati al supporto integrato di Terraform per la gestione degli stati sensibili. Quando esporti valori sensibili nell'output, assicurati che i valori siano contrassegnati come sensibili.

Assicura la separazione dei compiti

Se non puoi eseguire Terraform da un sistema automatizzato a cui gli utenti non hanno accesso, rispetta una separazione dei compiti separando autorizzazioni e directory. Ad esempio, un progetto di rete corrisponde a un utente o a un account di servizio Terraform di rete il cui accesso è limitato a questo progetto.

Esegui i controlli preliminari

Quando esegui Terraform in una pipeline automatizzata, usa uno strumento come gcloud terraform vet per verificare l'output del piano in base ai criteri prima dell'applicazione. In questo modo è possibile rilevare le regressioni della sicurezza prima che si verifichino.

Esegui controlli continui

Dopo l'esecuzione del comando terraform apply, esegui i controlli di sicurezza automatici. Questi controlli possono contribuire a garantire che l'infrastruttura non passi in uno stato non sicuro. Di seguito sono riportati strumenti validi per questo tipo di controlli:

Test

A volte i test di moduli e configurazioni Terraform seguono convenzioni e pattern diversi rispetto al test del codice dell'applicazione. Sebbene il test del codice dell'applicazione comporta principalmente il test della logica di business delle applicazioni stesse, il test completo del codice dell'infrastruttura richiede il deployment di risorse cloud reali per ridurre al minimo il rischio di errori di produzione. Quando si eseguono i test di Terraform, bisogna tenere conto di alcuni aspetti:

  • L'esecuzione di un test Terraform crea, modifica ed elimina l'infrastruttura reale, quindi i test possono essere costosi e dispendiosi in termini di tempo.
  • Non puoi semplicemente testare le unità di un'architettura end-to-end. L'approccio migliore è suddividere l'architettura in moduli e testarli singolarmente. I vantaggi di questo approccio includono uno sviluppo iterativo più rapido grazie a un runtime dei test più rapido, costi ridotti per ogni test e una riduzione delle probabilità di errori nei test da fattori fuori dal tuo controllo.
  • Se possibile, evita di riutilizzare lo stato. Potrebbero verificarsi situazioni in cui esegui test con configurazioni che condividono i dati con altre configurazioni, ma idealmente ogni test dovrebbe essere indipendente e non dovrebbe riutilizzare lo stato nei vari test.

Utilizza prima metodi di test meno costosi

Esistono diversi metodi che puoi utilizzare per testare Terraform. In ordine crescente di costo, tempo di esecuzione e profondità, includono quanto segue:

  • Analisi statica: consente di testare la sintassi e la struttura della configurazione senza eseguire il deployment di risorse, utilizzando strumenti come compilatori, linter e prove. Per farlo, utilizza terraform validate.
  • Test di integrazione dei moduli: per assicurarti che i moduli funzionino correttamente, testa i singoli moduli isolati. Il test di integrazione per i moduli prevede il deployment del modulo in un ambiente di test e la verifica della creazione delle risorse previste. Esistono diversi framework di test che semplificano la scrittura di test, ad esempio:
  • Test end-to-end: estendendo l'approccio dei test di integrazione a un intero ambiente, puoi confermare che più moduli funzionano insieme. In questo approccio, esegui il deployment di tutti i moduli che compongono l'architettura in un ambiente di test aggiornato. Idealmente, l'ambiente di test è il più simile possibile all'ambiente di produzione. È costosa, ma garantisce la massima certezza che le modifiche non interrompano il tuo ambiente di produzione.

Inizia in piccolo

Assicurati che i test si sviluppino reciprocamente in modo iterativo. Valuta la possibilità di eseguire prima test più piccoli e poi di testarli più complessi utilizzando un approccio fail fast.

Randomizza gli ID progetto e i nomi delle risorse

Per evitare conflitti di denominazione, assicurati che le configurazioni abbiano un ID progetto univoco a livello globale e nomi di risorse non sovrapposti all'interno di ogni progetto. A questo scopo, utilizza gli spazi dei nomi per le risorse. Terraform ha un provider casuale integrato per questa operazione.

Usa un ambiente separato per i test

Durante il test, vengono create ed eliminate molte risorse. Assicurati che l'ambiente sia isolato dai progetti di sviluppo o di produzione per evitare eliminazioni accidentali durante la pulizia delle risorse. L'approccio migliore consiste nel creare una cartella o un progetto nuovo per ogni test. Per evitare errori di configurazione, valuta la possibilità di creare account di servizio specifici per ogni esecuzione del test.

Esegui la pulizia di tutte le risorse

Testare il codice dell'infrastruttura significa che stai eseguendo il deployment di risorse effettive. Per evitare addebiti, valuta la possibilità di implementare un passaggio di pulizia.

Per eliminare tutti gli oggetti remoti gestiti da una determinata configurazione, utilizza il comando terraform destroy. Alcuni framework di test prevedono una fase di pulizia integrata. Ad esempio, se utilizzi Terratest, aggiungi defer terraform.Destroy(t, terraformOptions) al tuo test. Se utilizzi Kitchen-Terraform, elimina l'area di lavoro utilizzando terraform kitchen delete WORKSPACE_NAME.

Dopo aver eseguito il comando terraform destroy, esegui anche ulteriori procedure di pulizia per rimuovere le risorse che Terraform non è riuscito a eliminare. Per farlo, elimina i progetti utilizzati per l'esecuzione del test o utilizza uno strumento come il modulo project_cleanup.

Ottimizza tempo di esecuzione dei test

Per ottimizzare i tempi di esecuzione del test, segui questi approcci:

  • Esegui test in parallelo. Alcuni framework di test supportano l'esecuzione contemporanea di più test Terraform.
    • Ad esempio, con Terratest puoi farlo aggiungendo t.Parallel() dopo la definizione della funzione di test.
  • Esegui i test per fasi. Separa i test in configurazioni indipendenti che possono essere testate separatamente. Questo approccio elimina la necessità di attraversare tutte le fasi durante l'esecuzione di un test e accelera il ciclo di sviluppo iterativo.
    • Ad esempio, in Kitchen-Terraform, suddividi i test in suite separate. Durante l'iterazione, esegui ciascuna suite in modo indipendente.
    • Allo stesso modo, utilizzando Terratest, puoi eseguire ogni fase del test con stage(t, STAGE_NAME, CORRESPONDING_TESTFUNCTION). Imposta le variabili di ambiente che indicano quali test eseguire. Ad esempio, SKIPSTAGE_NAME="true".
    • Il framework per i test dei progetti supporta l'esecuzione graduale.

Passaggi successivi