Terraform e Kubernetes

di Andrea Zani, in terraform,

Terraform è uno dei più popolari tool per l'IAC (Infrastructure As Code). Semplificando, Terraform è un unico eseguibile che, leggendo dei file di testo nel formato HCL (HashiCorp Configuration Language) si interfaccia con dei provider appositi per la costruzione di risorse per le più svariate piattaforme. Esistono provider per pressocché tutti i Cloud, ma anche per altre piattaforme come Kubernetes, Consul, Helm, Splunk, etc...

Attualmente i provider ufficiali sono 35, quelli verificati 203, quelli della community 1832 (molti di essi abbandonati o inaffidabili). Scopo di questi provider è convertire le configurazioni scritte in HCL e inviarle al servizio apposito per la gestione di risorse. Nel caso del Cloud, avendo un provider (ufficiale) per AWS, è possibile definire in HCL l'infrastruttura di cui si ha necessità e sarà poi Terraform con il provider a pensare al resto, dall'aggiunta, alla modifica, alla cancellazione. E' possibile gestire il Multi Cloud con un unico strumento visto che si potrebbe creare un'infrastruttura in AWS e Azure contemporaneamente con un solo comando. Inoltre, potendo Terraform leggere le informazioni delle strutture create grazie ai provider, può inviare informazioni più Cloud - esempio banale, creato un database Sql di Azure è possibile inviare IP/DNS con le credenziali ad una Lambda creata contemporaneamente in AWS che necessita dell'accesso a quel database.

Pure Kubernetes?

Esiste un provider anche per Kubernetes. Confesso che l'ho iniziato a utilizzare da poco tempo perché ero purtroppo scettico dei vantaggi che avrebbe potuto darmi la gestione delle risorse in Kubernetes con Terraform. E posso dire di essermi quasi ricreduto. Solo perché questo post è pubblico, un po' di ABC.

Innanzitutto per provare il tutto sono necessari questi strumenti:

  • terraform (tool scaricabile nelle varie versioni per tutti i sistemi operativi, almeno la versione 1.0) scaricabile da qui.
  • Kubernetes avviato e funzionante (Docker Desktop per Windows, Minukube etc...)

Nel public registry di Terraform cerco il provider per Kubernetes:

https://registry.terraform.io/providers/hashicorp/kubernetes/latest

E' disponibile anche il link al progetto in Github di questo provider con informazioni utili, tra cui anche esempi e documentazione sul suo utilizzo. Quindi creo la prima parte del file per terraform (main.tf):

terraform { 
    required_providers { 
      kubernetes = { 
        source  = "hashicorp/kubernetes" 
        version = ">= 2.0.0" 
      } 
  } 
}

provider "kubernetes" { 
    config_path = "~/.kube/config" 
}

La sintassi di HCL è simile al JSON ma ha caratteristiche tutte sue e discutibili. Tralasciando polemiche inutili nella prima parte - required_providers - specifico i provider che voglio utilizzare e la versione (opzionale). Quindi nella sezione provider posso inserire la configurazione che sarà utilizzata da questo provider; in questo caso ho inserito il il path dove solitamente Kubernetes inserisce i file (config, etc...) per poter essere poi utilizzato da tool come kubectl.

Da console avvio il comando terraform init che dovrebbe dare in risultato come il seguente:

> terraform init

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/kubernetes versions matching ">= 2.0.0"...
- Installing hashicorp/kubernetes v2.11.0...
- Installed hashicorp/kubernetes v2.11.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!
...

Ora è il momento di creare qualcosa in Kubernetes con Terraform. Per fare questo utilizzo il blocco Resource (che aggiungo al file precedente):

resource "kubernetes_namespace" "test" { 
    metadata { 
        name = "test-ABC" 
    } 
}

Con queste poche righe di codice voglio creare un nuovo namespace in Kubernetes. Ora da console controllo se tutto è corretto prima della creazione effettiva. Si utilizza il comando terraform plan:

> terraform plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # kubernetes_namespace.test will be created
  + resource "kubernetes_namespace" "test" {
      + id = (known after apply)

      + metadata {
          + generation       = (known after apply)
          + name             = "test-abc"
          + resource_version = (known after apply)
          + uid              = (known after apply)
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Se non ci sono messaggi di errore è il momento della creazione del nuovo namespace in Kubernetes con Terraform:

> terraform apply

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # kubernetes_namespace.test will be created
  + resource "kubernetes_namespace" "test" {
      + id = (known after apply)

      + metadata {
          + generation       = (known after apply)
          + resource_version = (known after apply)
          + uid              = (known after apply)
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

kubernetes_namespace.test: Creating...
kubernetes_namespace.test: Creation complete after 0s [id=test-abc]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
        

Durante la creazione viene chiesta conferma (è possibile aggiungere al comando terraform l'opzione -auto-approve per scavalcare questa richiesta). Infine faccio la verifica con kubectl:

> kubectl get ns
NAME              STATUS   AGE
default           Active   7d17h
kube-node-lease   Active   7d17h
kube-public       Active   7d17h
kube-system       Active   7d17h
test-abc          Active   5s

Se volessi distruggere la risorsa appena creata:

terraform destroy

Con il comando plan o prima della creazione effettiva si può notare come Terraform mostri informazioni sulle risorse che saranno gestite:

  + resource "kubernetes_namespace" "test" {
    + id = (known after apply)

    + metadata {
        + generation       = (known after apply)
        + resource_version = (known after apply)
        + uid              = (known after apply)
    }
}

Queste informazioni possono essere utilizzate anche da altre Resource create o per essere utilizzate direttamente. Per esempio, se avessi voluto visualizzarle alla fine della creazione avrei dovuto usare in Terraform una o più sezioni output (da aggiungere al file):

output "namespace-id" { 
  description = "Id per test-abc"  
  value = kubernetes_namespace.test.id
}

output "namespace-uid" { 
  description = "uid per test-abc"  
  value = kubernetes_namespace.test.metadata[0].uid
}

Ora posso richiamare ancora il comando apply (che non farà nessuna modifica) e visualizzerà le informazioni:

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

namespace-id = "test-abc"
namespace-uid = "5f0cc05f-8a8a-43fc-82e1-5349a86b20cf"

Il formalismo per prendere questi valori da una resource è [tipo_risorsa].[nome_risorsa].[property_risorsa]...

Questo apre un'importante caratteristica di Terraform: le dipendenze. Queste possono essere implicite o esplicite. Complicando leggermente l'esempio qui sopra, aggiungo un altro provider:

terraform { 
  required_providers { 
    kubernetes = { 
      source  = "hashicorp/kubernetes" 
      version = ">= 2.0.0" 
    } 
    random = { 
      source = "hashicorp/random" 
      version = ">= 3.0.0" 
    } 
  }
}

Il provider random è utile perché permette di creare stringhe random (utili per password), sequenze casuali per array, etc... Ora voglio aggiungere una stringa casuale al nuovo namespace in Annotation (dove posso inserire qualsiasi stringa, utile per il mio test). All'interno del provider Random sono presenti più tipi di Resource, io utilizzerò quello per creare una password (random_password):

resource "random_password" "mypassword" { 
  count = 1
  length = 16 
  special = true 
}

Una volta eseguito, mypassword conterrà una password di lunghezza 16 caratteri contenente anche caratteri speciali (info al provider).

Ora modifico la creazione del namespace:

resource "kubernetes_namespace" "test" { 
  metadata { 
    name = "test-abc-2" 
    annotations = { 
      custom_text = "Stringa random: ${random_password.mypassword.0.result}"
    } 
  }
}

In annotations ho inserito la key custom_text con la mia stringa casuale. Ora lancio il comando terminal apply e verifico in Kubernetes il risultato:

> kubectl describe ns test-abc-2 
Name:         test-abc-2
Labels:       kubernetes.io/metadata.name=test-abc-2
Annotations:  custom_text: Stringa random: #FAGUPP9jlO1Xbi@
Status:       Active

Non ho inserito come output il risultato della stringa casuale perché essendo creato con l'oggetto password potrebbe contenere informazioni importanti e Terraform bloccherebbe immediatamente il tutto. Inserendo nel mio file:

output "namespace-annotation" { 
  description = "annotation text"  
  value = kubernetes_namespace.test.metadata[0].annotations.custom_text
}

L'apply di Terraform visualizzerà questo errore:

Error: Output refers to sensitive values
?
?   on main.tf line 43:
?   43: output "namespace-annotation" {
?
? To reduce the risk of accidentally exporting sensitive data that was intended to be only internal, Terraform requires that any root module output containing sensitive data be explicitly   
? marked as sensitive, to confirm your intent.
?
? If you do intend to export this data, annotate the output value as sensitive by adding the following argument:
?     sensitive = true

Aggiungendo la property sensitive = true come suggerito nel messaggio di errore, output visualizzerà degli asterischi al posto del contenuto.

Questo esempio mostra una dipendenza implicita. Nella Resource per il namespace, dovendo utilizzare il risultato di un altro Resource (password) ha atteso il risultato di quest'ultima prima della sua creazione. Altrimenti Terraform tenta di creare sempre tutte le risorse in parallelo per velocizzare il tutto. Se, invece, le risorse non sono direttamente dipendenti si può dichiarare esplicitamente una o più dipendenze:

resource "kubernetes_namespace" "test" { 
  metadata { 
    name = "test-abc-2" 
    annotations = { 
      custom_text = "Stringa random: ${random_password.mypassword.0.result}"
    } 
  }
  depends_on = [random_password.mypassword]
}

Molto utile nel caso si debba avviare nel cloud un database e solo dopo la sua creazione avviare applicazioni che lo utilizzano.

Ritorno al passato

In questo vecchio mio post avevo avviato con solo i file di configurazione di Kubernetes tre istanze in replica set per MongoDb e due web application che lo utilizzavano come datasource. Sempre in quel post avevo mostrato la discreta complessità della configurazione di MongoDb in cui avevo inserito anche degli script in bash per controllare e impostare correttamente il tutto. Un modo molto più semplice per ottenere lo stesso risultato con MongoDb è l'utilizzo di Helm. Helm è un tool per il deployment per Kubernetes in cui sono presenti numerose configurazioni (charts) già pronte e funzionanti da utilizzare. E' presente anche un chart per MongoDb per la sua configurazione in replica set a questo link. Il tuo utilizzo semplifica quanto da me fatto in quel post e, volendo replicare di seguito quanto fatto allora con Terraform, perché non usare pure Helm visto che è tra i provider presenti?

Mettere ordine

Anche se è possibile creare un unico file in HCL per Terraform per creare il tutto, è consigliato la creazione di più file con specifici nomi per rendere più leggibile il tutto e per poter poi utilizzare la configurazione creata come modulo. In Terraform viene consigliata la creazione di almeno questi tre file:

  • main.tf
  • variables.tf
  • outputs.tf

In aggiunta è consigliata la presenza anche di questi file:

  • provider.tf
  • versions.tf
  • *.tfvars

variables.tf

E' il file dove inserire tutte le variabili che si vogliono utilizzare nel proprio script. Ecco un esempio reale questo tipo di file:

variable "namespace_name" {
  description = "Name for the namespace"
  type        = string
  default     = "test-blog"
}

variable "mongodb_username" {
  description = "username used for mongodb authentication"
  type        = string
  default     = "myuser"
}

variable "mongodb-express-port" {
  description = "port for mongoexpress web application"
  type        = number
  default     = 30165
}

variable "testdb-port" {
  description = "port for testdb web application"
  type        = number
  default     = 30164
}

description, default e type sono tutti opzionali. Nel caso type non fosse specificato sarà usato come tipo di variabile Any. Qui dettagli sulle specifiche di ogni attributo. In default è possibile inserire i valori voluti ma è possibile utilizzare il file *.tfvars apposito per la specifica dei valori da utilizzare. Per esempio, creando il file testing.tfvars con questo valori:

namespace_name=new_application
mongodb_username=az

Con il comando Terraform posso specificare l'utilizzo di quel file per l'assegnazione dei valori delle variabili:

terraform apply -var-file="testing.tfvars"

Questo permette la creazione di più file di configurazione da utilizzare con lo stesso modulo, come per la creazione di ambienti di test etc...

versions.tf

In questo file è possibile inserire tutti i provider da utilizzare. Ecco il mio esempio in cui includo tre provider:

terraform {
  required_providers {
    helm = {
      source  = "hashicorp/helm"
      version = ">= 2.0.0"
      }

    kubernetes = { 
      source  = "hashicorp/kubernetes" 
      version = ">= 2.0.0" 
    } 

    random = { 
      source = "hashicorp/random" 
      version = "3.0.1" 
    } 
  }
}

Ho inserito i tre provider che mi servono: per Helm, per Kubernetes e per i valori random con le relative versioni volute.

providers.tf

Qui eventuali configurazioni dei singoli provider:

provider "kubernetes" { 
  config_path = "~/.kube/config" 
} 

provider "helm" {
    kubernetes {
        config_path = "~/.kube/config"
    }
}

Nel caso di Kuberrnetes e Helm ho dovuto inserire il config_path dove il provider troverà le informazioni per l'accesso al cluster. Per la configurazione ho letto la documentazione ufficiale del provider dove si trovano i vari esempi di utilizzo.

outputs.tf

Come spiegato prima, qui vengono specificate le variabili di output del modulo da visualizzare:

output "mongodb-password" {
  description = "MongoDb password"
  value       = random_password.password[0].result
  sensitive = true
}

output "mongodb-express-port" {
  description = "MongoDb express port"
  value       = kubernetes_service.mongoexpress-service.spec[0].port[0].node_port
}

output "testdb-port" {
  description = "Api rest port"
  value       = kubernetes_service.testdb-service.spec[0].port[0].node_port
}

output "namespace" {
  description = "Namespace used"
  value       = kubernetes_namespace.test.metadata.0.name
}

Le due web application saranno esposte pubblicamente il servizio NodePort di Kubernetes, quindi ho solo la necessità di visualizzare le porte che saranno utilizzate (anche se sono presenti nel file variables.tf).

main.tf

Ora si comincia a fare sul serio. Qui possiamo inserire il contenuto reale per la creazione di tutte le risorse necessarie. Io inserirò la creazione di tre Resource:

resource "kubernetes_namespace" "test" { 
  metadata { 
    name = var.namespace_name
  } 
}

resource "random_password" "password" { 
  count = 1 
  length = 32
  special = false
} 

resource "kubernetes_secret" "credentials1" {
  metadata {
    name = "credentials1"
    namespace = kubernetes_namespace.test.metadata.0.name
  }

  data = {
    "username" = var.mongodb_username
    "mongodb-password" = random_password.password.0.result
    "mongodb-root-password" = random_password.password.0.result
    "mongodb-replica-set-key" = "ba436283462dcaada36367"
  }

  type = "Opaque"
  depends_on = [random_password.password]
}

Che farà in modo che Terraform crei un namespace nel cluster di Kubernetes dove inserirò tutte le risorse necessarie. Quindi con il provider random creo una password di 32 caratteri alfanumerici (senza caratteri speciali) che utilizzo per la creazione di un Secret in Kubernetes per l'inserimento delle credenziali per l'accesso a MongoDb. Tra queste due Resource c'è una dipendenza implicita, e il Secret non sarà creato fino a quando Random non creerà la password richiesta.

Come già scritto, l'uso di questi file è un semplice formalismo visto che, quando viene richiamato il comando Terraform, lui cerca in tutti i file *.tf della directory attuale. In progetti più corposi e reali qui si possono definire tutti i moduli che saranno utilizzati.

Ipotizzando che la struttura qui sopra sia in una sottodirectory di un progetto più grosso, dal file main.tf della root avrei potuto include il mio modulo con questo codice:

module "azkubernetestest" {
    source               = "./mia_directory_module" # path del mio modulo relativo alla root
    namespace_name       = "mio-namespace"
    mongodb_username     = "mongodbuser"
    mongodb-express-port = 30000
    testdb-port          = 30001
}

E il mio modulo e tutte le risorse sarebbero state create con le variabili passate nella sezione Module.

mongodb-helm.tf

In questo file ho inserito la configurazione di Helm per la creazione delle tre istanze di MongoDb in replica set. Come da documentazione ho inserito i parametri necessari per la creazione del numero di istanze volute e per il passaggio delle variabili:

resource "helm_release" "mongodb1" {
  name  = "mongodb-easy"

    repository = "https://charts.bitnami.com/bitnami"
    chart      = "mongodb"
    version    = "12.0.0"

  set {
    name = "global.namespaceOverride"
    value = kubernetes_namespace.test.metadata.0.name
  }

  set {
    name  = "architecture"
    value = "replicaset"
  }

  set {
    name  = "auth.rootUser"
    value = var.mongodb_username
  }

  set {
    name = "auth.existingSecret"
    value = "credentials1"
  }

  set {
    name = "auth.existingSecret"
    value = "credentials1"
  }

  set {
    name = "image.tag"
    value = "4.4.13-debian-10-r51"
  }

  set {
    name = "persistence.enabled"
    value = false
  }

  set {
    name = "arbiter.enabled"
    value = false
  }

  set {
    name = "replicaCount"
    value = 3
  }

  depends_on = [kubernetes_secret.credentials1]
}

Nella dichiarazione del chart è possibile inserire anche la versione desiderata. Helm permetta la configurazione degli oggetti in Kubernetes con le variabili, cosa che eseguo con la sintassi di Terraform nel quale inserisco anche il nome del namespace dove inserire le risorse create, il nome del Secret da cui prenderà le informazioni - auth.existingSecret - e infine ho dovuto inserire la dipendenza esplicita al Secret perché voglio che le istanze di MongoDb vengano create solo dopo la sua creazione.

mongo-express.tf

In questo file definisco il Deployment della web application mongo-express così come avevo fatto in quel post. Tutta la configurazione scritta all'epoca in yaml compatibile con il comando kubectl l'ho dovuta riscrivere nel formato accettato da Terraform e dal provider di Kubernetes:

resource "kubernetes_deployment" "mongoexpress" { 
  metadata { 
    name = "mongoexpress" 
    namespace = kubernetes_namespace.test.metadata.0.name
  } 

  spec { 
    replicas = 1 
    selector { 
      match_labels = { 
        app = "mongoexpress" 
      } 
    }
    template { 
      metadata { 
        labels = { 
          app = "mongoexpress" 
        } 
        annotations = { 
          custom_text = "Mongoexpress web application"
        } 
      } 
      spec {
        volume {
          name = "secretvolume"
          secret {
            secret_name = "credentials1"
          }
        } 
        container { 
          image = "mkucuk20/mongo-express" 
          name  = "mongoexpress" 
          resources {
            limits = {
              memory = "100Mi"
              #cpu = 
            }
          }
          env { 
            name= "ME_CONFIG_MONGODB_ADMINUSERNAME_FILE" 
            value="/etc/secretvolume/username" 
          } 
          env { 
            name= "ME_CONFIG_MONGODB_ADMINPASSWORD_FILE" 
            value = "/etc/secretvolume/mongodb-root-password"
          } 
          env { 
            name= "ME_CONFIG_MONGODB_SERVER" 
            value="mongodb-easy-0.mongodb-easy-headless,mongodb-easy-1.mongodb-easy-headless,mongodb-easy-2.mongodb-easy-headless" 
          }
          volume_mount {
            name = "secretvolume"
            read_only = true
            mount_path = "/etc/secretvolume"
          }
        } 
      } 
    } 
  } 
  depends_on = [helm_release.mongodb1, kubernetes_secret.credentials1]
} 

resource "kubernetes_service" "mongoexpress-service" {
  metadata {
    name = "mongoexpress-service"
    namespace = kubernetes_namespace.test.metadata.0.name
  }
  spec {
    selector = {
      app = "mongoexpress"
    }

    port {
      port        = 80
      target_port = 8081
      node_port = var.mongodb-express-port
    }

    type ="NodePort"
  }
  depends_on = [kubernetes_deployment.mongoexpress]
}

Non si discosta molto dalla versione originale, e anche in questo caso, anche se non strettamente necessario, ho inserito le dipendenze esplicite che mi permettono di creare questa web application SOLO quando le istanze di MongoDb sono effettivamente avviate. Nella seconda parte dello script, la creazione del service di tipo NodePort.

webapi.tf

Ecco l'ultima risorsa creata, come per mongo-express creo un deployment in Kubernetes per l'applicazione (web api rest) che avevo scritto qualche anno fa e che girava con Net Core 3.0:

resource "kubernetes_deployment" "testdb" { 
  metadata { 
    name = "testdb"
    namespace = kubernetes_namespace.test.metadata.0.name
  } 

  spec { 
    replicas = 1 
    selector { 
      match_labels = { 
        app = "testdb" 
      } 
    } 
    template { 
      metadata { 
        labels = { 
          app = "testdb" 
        } 
        annotations = { 
          custom_test = "Wep application rest to test MongoDb"
        } 
      } 
      spec { 
        volume {
          name = "secretvolume"
          secret {
            secret_name = "credentials1"
          }
        }
        
        container { 
          image = "sbraer/mongodbtest:v1" 
          name  = "testdb"
          port {
            protocol = "TCP"
            container_port = 5000
          } 
          resources {
            limits = {
              memory = "100Mi"
              #cpu = 
            }
          }
          env { 
            name= "MONGODB_SERVER_USERNAME_FILE" 
            value = "/etc/secretvolume/username"
          } 
          env { 
            name= "MONGODB_SERVER_PASSWORD_FILE" 
            value = "/etc/secretvolume/mongodb-root-password"
          } 
          env { 
            name= "MONGODB_SERVER_LIST" 
            value="mongodb-easy-0.mongodb-easy-headless,mongodb-easy-1.mongodb-easy-headless,mongodb-easy-2.mongodb-easy-headless" 
          } 
          env { 
            name= "MONGODB_REPLICA_SET" 
            value="rs0" 
          } 
          env { 
            name= "MONGODB_DATABASE_NAME" 
            value="MyDatabase" 
          } 
          env { 
            name= "MONGODB_BOOKS_COLLECTION_NAME" 
            value="MyTest" 
          } 
          env { 
            name= "TMPDIR" 
            value="/tmp" 
          } 
          volume_mount {
            name = "secretvolume"
            read_only = true
            mount_path = "/etc/secretvolume"
          }
        } 
      } 
    } 
  } 
  depends_on = [helm_release.mongodb1, kubernetes_secret.credentials1]
} 

resource "kubernetes_service" "testdb-service" {
  metadata {
    name = "testdb-service"
    namespace = kubernetes_namespace.test.metadata.0.name
  }
  spec {
    selector = {
      app = "testdb"
    }

    port {
      port        = 80
      target_port = 5000
      node_port = var.testdb-port
    }

    type = "NodePort"
  }
  depends_on = [kubernetes_deployment.testdb]
}

Nulla di nuovo, anche in questo caso avvio una web application e il relativo servizio NodePort per rendere pubblica l'applicazione.

Avviare il tutto

E' il momento di controllare il funzionamento. Come spiegato nell'esempio iniziale, primo passo è con l'opzione init:

> terraform init

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/random versions matching "3.0.1"...
- Finding hashicorp/helm versions matching ">= 2.0.0"...
- Finding hashicorp/kubernetes versions matching ">= 2.0.0"...
- Installing hashicorp/kubernetes v2.11.0...
- Installed hashicorp/kubernetes v2.11.0 (signed by HashiCorp)
- Installing hashicorp/random v3.0.1...
- Installed hashicorp/random v3.0.1 (signed by HashiCorp)
- Installing hashicorp/helm v2.5.1...
- Installed hashicorp/helm v2.5.1 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!
...

E' possibile eseguire il plan, e se tutto è corretto lanciare il comando:

terraform apply -auto-approve

Che creerà il tutto senza chiedere conferma (rimovuore l'opzione -auto-approve in caso si voglia proseguire solo dopo la conferma manuale). Alla fine delle operazioni, si dovrebbe avere un risultato come il seguente:

Apply complete! Resources: 8 added, 0 changed, 0 destroyed.

Outputs:

mongodb-express-port = 30165
mongodb-password = <sensitive>
namespace = "test-blog"
testdb-port = 30164

Sconsiglio di eseguire la creazione di tutte queste risorse su una macchina con poca potenza e con pochi gigabyte di memoria: si otterrebbero errori nella creazioni dei Pod per l'insufficiente quantitativo di ram o errori e riavvii degli stessi per la bassa potenza della CPU.

Se si è ottenuto il risultato come sopra, si può verificare da console interrogando direttamente Kubernetes:

> kubectl -n test-blog get all
NAME                               READY   STATUS    RESTARTS   AGE
pod/mongodb-easy-0                 1/1     Running   0          4m54s
pod/mongodb-easy-1                 1/1     Running   0          4m41s
pod/mongodb-easy-2                 1/1     Running   0          4m28s
pod/mongoexpress-bc6f47b7d-z6qk7   1/1     Running   0          4m13s
pod/testdb-c7465865d-h748w         1/1     Running   0          4m13s

NAME                            TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
service/mongodb-easy-headless   ClusterIP   None             <none>        27017/TCP      4m54s
service/mongoexpress-service    NodePort    10.110.210.160   <none>        80:30165/TCP   4m6s
service/testdb-service          NodePort    10.108.55.54     <none>        80:30164/TCP   4m10s

NAME                           READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/mongoexpress   1/1     1            1           4m14s
deployment.apps/testdb         1/1     1            1           4m14s

NAME                                     DESIRED   CURRENT   READY   AGE
replicaset.apps/mongoexpress-bc6f47b7d   1         1         1       4m14s
replicaset.apps/testdb-c7465865d         1         1         1       4m14s

NAME                            READY   AGE
statefulset.apps/mongodb-easy   3/3     4m54s

E prova finale, richiamare l'url: http://localhost:30164/api/info

Quindi è possibile provare anche mongo-express con il link: http://localhost:30165

Per eseguire un ulteriore test, così come feci nel vecchio post, creo un database di nome MyDatabase, al suo interno creo la collection MyTest e inserisco questo Document:

{
    "Name": "I promessi sposi",
    "Author": "Alessandro Manzoni",
    "Price":19.99,
    "Category": "Classici italiani"
}

Ora verifico dalla web api se posso leggere questi dati. Richiamo il link: http://localhost:30264/api/books

Soddisfatto del buon funzionamento, distruggo tutto con il comando:

terraform destroy -auto-approve

Conclusioni e critiche

In questo breve post non ho trattato un altro importante argomento di Terraform: il mantenimento dello stato. Tutti i comandi visti finora creano, nella stessa directory dove sono lanciati, dei file interni usati da Terraform per mantenere lo stato attuale delle risorse create. Ad iniziare dal file terraform.tfstate che si può aprire con un qualsiasi text editor e in cui si può trovare un JSON con tutte le informazioni, comprese password create dinamicamente, come nel mio caso:

"data": {
    "mongodb-password": "AB6bplfTd19N2ijzRqKIWdgQIjfXUblR",
    "mongodb-replica-set-key": "ba436283462dcaada36367",
    "mongodb-root-password": "AB6bplfTd19N2ijzRqKIWdgQIjfXUblR",
    "username": "myuser"
}

Oltre a questo file si può trovare anche il file .terraform.lock.hcl e la directory .terraform contenente altre informazioni dello stato e per il lock delle risorse create. La creazione di questi file è ovviamente necessaria (senza di essi è impossibile eseguire modifiche o cancellazioni di quanto è stato creato con Terraform), e il loro mantenimento in locale è corretto solo per prove locali come quella che ho appena fatto. Soluzione alternativa è inserire questi file in un repository Git in modo che sia sempre utilizzabile, ma questo non blocca la possibilità che più utenti possano lavorare sullo stesso progetto ed entrambi inseriscano modifiche non condivise. Soluzioni più funzionali sono l'uso di servizi appositi come il Terraform Cloud, oppure, con AWS, è possibile utilizzare S3 e DynamoDb per salvare tutte queste informazioni.

Infine alcune critiche: tutto ciò che è creato da Terraform dev'essere gestito da Terraform. Di questo dettaglio ci si accorge abbastanza velocemente quando, creata una risorsa, ci si mette poi mano manualmente per una rifinitura, e quindi si riutilizza Terraform che, non riuscirà (spesso) a riconoscere quella risorsa (avendola noi modificata a mano) e vorrà ricrearla da capo. Unica soluzione e risistemare la risorsa a mano com'era nello stato precedente e poi continuare.

E' inutile girarci intorno, il linguaggio di scripting usato da Terraform (l'HCL) è limitato. Innanzitutto non è possibile fare veri e propri IF su variabili o oggetti creati per includere solo le risorse volute. Ci sono dei trucchi utilizzabili come quello con la property count condizionale:

resource "kubernetes_namespace" "test" { 
  count = var.create_namespace_a ? 1 : 0
  metadata { 
    name = "namespace-a"
  } 
}

Ma non risolve tutti i problemi soprattutto in configurazioni complesse che vanno al di là di semplici demo come quella che ho mostrato qui. In futuro? Ormai ci ho perso speranza.

In un prossimo post si potrebbe aggiungere anche la creazione di un cluster Kubernetes in un Cloud e altre critiche? Al momento mi fermo qui con il link al codice mostrato in questo post.

Commenti

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Nella stessa categoria
I più letti del mese