Ecco altre mie annotazioni personali. Dopo il post precedente dove ho solo introdotto Terraform ora provo ad alzare l'asticella. Obbiettivo che voglio ottenere in questo post: avviare le tre istanze di MongoDb e le due web application in Kubernetes con Terraform ma all'interno di un cluster Kubernetes in AWS (EKS) anch'esso creato con Terraform.
Questa volta ho bisogno di questi tool:
- Il tool terraform (versione >=1.0)
- aws cli (versione >=2.0)
- Account attivo in AWS
Dal post precedente si dovrebbe già avere installato il tool terraform nella macchina locale. aws cli lo si può scaricare da qui. Naturalmente è necessario avere un account attivo in AWS e configurato perché possa essere usato con il comando aws da terminale. Per completare questo passo è sufficiente lanciare il comando:
aws configure
E inserire i codici dell'utente creato dalla console web di AWS. Alla fine, per verificare che tutto funzioni, da terminale lanciare il comando:
$ aws sts get-caller-identity { "UserId": "AEDAXXXXXXXXXXXXX228Z", "Account": "xxxxxxxxxxxx", "Arn": "arn:aws:iam::xxxxxxxxxxxx:user/xxxxxxxxxx" }
Che dovrebbe dare in risultato simile all'esempio sopra. Premessa prima di continuare: anche se si sta utilizzando un account di prova in AWS che dà molti servizi gratuiti il primo anno, EKS non è tra questi.
Si comincia... EKS
Innanzitutto il codice utilizzato in Terraform nel post precedente per la creazione delle risorse rimarrà quasi immutato. Le uniche differenze saranno nella modalità di autenticazione di Kubernetes e Helm per accedere al cluster in AWS. Completamente nuovo è il codice in Terraform per la creazione delle risorse in AWS di un cluster per Kubernetes. Utilizzando AWS, inoltre, non utilizzerò il tipo NodePort per i service di Kubernetes per le web application, ma il Load Balancer. EKS è il servizio che mette a disposizione AWS per la creazione di un cluster Kubernetes nel Cloud. Permette la creazione di macchine virtuale (EC2) da utilizzare nel cluster per l'esecuzione dei Pod e offre anche la possibilità di usare la modalità Fargate per poterli farli girare in un ambiante Serverless. Inoltre è in grado di collegare in modo autonomo la creazione dei Load Balancer di Kubernetes con l'omonimo servizio si AWS, così come i dischi EBS che saranno creati e gestiti da AWS come Persistent Volume all'interno del cluster, con la possibilità anche di selezionare la tipologia di servizio per esigenze prestazionali o di capacità. Il suo utilizzo da console web all'inizio può appare problematico, perché necessita della creazione manuale di due Role per il suo utilizzo (Cluster Role e Node Role). Infine, si deve configurare il numero e il tipo di macchine da avviare e collegare al cluster in un Node Group. Per esigenze particolari si possono avviare più Node Group con ognuna la sua tipologia di macchine.
Personalmente ho sempre trovato l'uso del comando da terminale eksctl più comodo per la creazione (e distruzione) dei cluster Kubernetes in AWS/EKS - questione di gusti. Ora utilizzerà una nuova via: Terraform.
Come nello scorso post cercherà di mantenere una struttura più pulita possibile con il nome dei file consigliati.
versions.tf
Come spiegato nello scorso post, qui inserisco i provider che utilizzerò:
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" } aws = { source = "hashicorp/aws" version = ">= 4.0.0" } } }
Confrontato con lo stesso file precedente ho solo aggiunto il provider per AWS.
variables.tf
In questo file ho aggiunto parecchie variabili per personalizzare la creazione di tutte le risorse. Oltre al nome del namespace per Kubernetes, ho inserito anche il nome della nuova VPC (vpc_name) e la regione (aws_region) in cui sarà creato il tutto in AWS. Inoltre per il mio scopo creerò due Node Group. Nel primo creerà tre macchine nel quale avvierò le istanze di MongoDB, e nel secondo userò una sola macchina virtuale nella quale avvierò le due web application. Per specificarne il tipo e il numero:
variable "aws_region" { description = "Aws region" type = string default = "eu-south-1" } ... variable "vpc_name" { description = "VPC name" type = string default = "AZ-vpc" } variable "cluster_name" { description = "K8s cluster name" type = string default = "K8sDemo" } variable "cluster_version" { description = "Cluster version" type = string default = "1.21" } variable "worker_group_mongodb_instance_type" { description = "Worker group mongodb instance type" type = string default = "t3.small" } variable "worker_group_mongodb_desidered_size" { description = "Worker group mongodb desidered size" type = number default = 3 } variable "worker_group_webapp_instance_type" { description = "Worker group webapp instance type" type = string default = "t3.small" } variable "worker_group_webapp_desidered_size" { description = "Worker group webapp desidered size" type = number default = 1 }
providers.tf
provider "aws" { region = var.aws_region } data "aws_availability_zones" "available" {}
In questo caso configuro AWS comunicando la region che userà (eu-south-1, Milano) e con data chiedo a Terraform di poter richiedere le informazioni da AWS. Questo è necessario quando si devono gestire o fare riferimento a risorse già presenti. Per esempio, nel prossimo script (di uso solo didattico) per Terraform non creerò nulla ma richiederò solo informazioni sulla VPC in AWS nella region eu-south-1:
provider "aws" { region = "eu-south-1" } data "aws_availability_zones" "available" {} data "aws_vpc" "selected" {} data "aws_subnets" "example" { filter { name = "vpc-id" values = [data.aws_vpc.selected.id] } } data "aws_subnet" "example" { for_each = toset(data.aws_subnets.example.ids) id = each.value } #################### output "aws-vailable-zones" { description = "AWS Zones" value = data.aws_availability_zones.available } output "vpc-default" { description = "AWS Zones" value = data.aws_vpc.selected } output "subnet_cidr_blocks" { value = [for s in data.aws_subnet.example : s.id] }
Nelle sezioni data ho inserito le richieste delle informazioni che voglio da AWS: VPC, zone, subnet. L'output dopo il comando terraform apply sarà:
aws-vailable-zones = { "all_availability_zones" = tobool(null) "exclude_names" = toset(null) /* of string */ "exclude_zone_ids" = toset(null) /* of string */ "filter" = toset(null) /* of object */ "group_names" = toset([ "eu-south-1", ]) "id" = "eu-south-1" "names" = tolist([ "eu-south-1a", "eu-south-1b", "eu-south-1c", ]) "state" = tostring(null) "zone_ids" = tolist([ "eus1-az1", "eus1-az2", "eus1-az3", ]) } subnet_cidr_blocks = [ "subnet-4e668127", "subnet-7216170a", "subnet-ee99b9a4", ] vpc-default = { "arn" = "arn:aws:ec2:eu-south-1:############:vpc/vpc-876582ee" "cidr_block" = "172.31.0.0/16" "cidr_block_associations" = tolist([ { "association_id" = "vpc-cidr-assoc-421afd2b" "cidr_block" = "172.31.0.0/16" "state" = "associated" }, ]) "default" = true "dhcp_options_id" = "dopt-8f6780e6" "enable_dns_hostnames" = true "enable_dns_support" = true "filter" = toset(null) /* of object */ "id" = "vpc-876582ee" "instance_tenancy" = "default" "ipv6_association_id" = "" "ipv6_cidr_block" = "" "main_route_table_id" = "rtb-791cfb10" "owner_id" = "############" "state" = tostring(null) "tags" = tomap({}) }
E potrò utilizzare questi dati ovunque mi servano, come nel prossimo file.
1-vpc.tf
Con il codice seguente creo una nuova VPC:
module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "3.2.0" name = var.vpc_name cidr = "10.0.0.0/16" azs = data.aws_availability_zones.available.names private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] public_subnets = ["10.0.4.0/24", "10.0.5.0/24", "10.0.6.0/24"] enable_nat_gateway = true single_nat_gateway = true enable_dns_hostnames = true tags = { "kubernetes.io/cluster/${var.cluster_name}" = "shared" } public_subnet_tags = { "kubernetes.io/cluster/${var.cluster_name}" = "shared" "kubernetes.io/role/elb" = "1" } private_subnet_tags = { "kubernetes.io/cluster/${var.cluster_name}" = "shared" "kubernetes.io/role/internal-elb" = "1" } }
In questa VPC ho definito il CIDR che dovrà gestire come le Subnet sottostanti (notare l'uso del data per l'inserimento delle zone disponibile nell'oggetto azs). Creo tre Subnet private e tre pubbliche e un Gateway in modo che dalle Subnet private, dove farò girare le mie macchine per Kubernetes, si potrà accedere a Internet. Nelle tre sezioni di TAGS inserisco delle property necessarie perché possano essere utilizzate da EKS.
2-security-groups.tf
Per i due Node Group che creerò nel mio cluster devo specificare i rispettivi Security Group. Non avendo esigenze particolari li creo senza inserire nessun permesso:
resource "aws_security_group" "worker_group_mgmt_one" { name_prefix = "worker_group_mgmt_one" vpc_id = module.vpc.vpc_id } resource "aws_security_group" "worker_group_mgmt_two" { name_prefix = "worker_group_mgmt_two" vpc_id = module.vpc.vpc_id } resource "aws_security_group" "all_worker_mgmt" { name_prefix = "all_worker_management" vpc_id = module.vpc.vpc_id }
3-eks.tf
All'interno di questo file vengono create le due Role necessarie per la creazione del cluster:
resource "aws_iam_role" "demo" { name = "eks-cluster-${var.cluster_name}" assume_role_policy = jsonencode({ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "eks.amazonaws.com" }, "Action": "sts:AssumeRole" } ] }) } resource "aws_iam_role" "nodes" { name = "eks-node-group-nodes-${var.cluster_name}" assume_role_policy = jsonencode({ Statement = [{ Action = "sts:AssumeRole" Effect = "Allow" Principal = { Service = "ec2.amazonaws.com" } }] Version = "2012-10-17" }) }
Quindi queste Role vengono assegnate al cluster creato in questa sezione:
resource "aws_eks_cluster" "demo" { name = var.cluster_name role_arn = aws_iam_role.demo.arn version = var.cluster_version vpc_config { subnet_ids = module.vpc.private_subnets } depends_on = [aws_iam_role_policy_attachment.demo-AmazonEKSClusterPolicy] }
Un cluster senza macchine collegate è inutile. In EKS, come scritto sopra, è necessario creare almeno un Node Group. Nel mio esempio ne creo due, ecco solo la definizione per le macchine per MongoDb (la definizione per le due web application è simile):
resource "aws_eks_node_group" "mongodb-nodes" { cluster_name = aws_eks_cluster.demo.name node_group_name = "mongodb-nodes" node_role_arn = aws_iam_role.nodes.arn subnet_ids = module.vpc.private_subnets capacity_type = "ON_DEMAND" instance_types = [var.worker_group_mongodb_instance_type] scaling_config { desired_size = var.worker_group_mongodb_desidered_size max_size = var.worker_group_mongodb_desidered_size min_size = var.worker_group_mongodb_desidered_size } update_config { max_unavailable = 1 } labels = { role = "mongodb" name = "mongodb" } tags = { name = "mongodb1" } depends_on = [ aws_iam_role_policy_attachment.nodes-AmazonEKSWorkerNodePolicy, aws_iam_role_policy_attachment.nodes-AmazonEKS_CNI_Policy, aws_iam_role_policy_attachment.nodes-AmazonEC2ContainerRegistryReadOnly, ] }
A parte l'assegnazione alla VPC creata, alla tipologia e numero di macchine da creare, ho inserito anche le Labels - role = "mongodb" - che utilizzerò per l'assegnazione dei Pod a queste macchine, come mostrerò in seguito.
Nel file ho inserito anche questa sezione:
data "aws_eks_cluster_auth" "demo" { name = var.cluster_name }
Che sarà usato nel prossimo file.
4-kubernetes.tf
In questo file configuro i provider per Kubernetes e per Helm. Nel post precedente lo avevo fatto inserendo il path alla directory .kube, utilizzando AWS devo utilizzare:
provider "kubernetes" { host = aws_eks_cluster.demo.endpoint token = data.aws_eks_cluster_auth.demo.token cluster_ca_certificate = base64decode(aws_eks_cluster.demo.certificate_authority.0.data) } provider "helm" { kubernetes { host = aws_eks_cluster.demo.endpoint token = data.aws_eks_cluster_auth.demo.token cluster_ca_certificate = base64decode(aws_eks_cluster.demo.certificate_authority.0.data) } }
Ed ecco l'uso di aws_eks_cluster_auth grazie al quale riesco ad avere le informazioni necessarie per gestire da Terraform la connessione a Kubernetes.
I file successivi sono gli stessi mostrati nel post precedente a parte piccole modifiche, come l'assegnazione dei Pod alle macchine volute. Nel caso di MongoDb per l'assegnazione dei Pod al Node Group con la Label role="mongodb" ho inserito questo codice:
set { name = "nodeAffinityPreset.type" value = "hard" } set { name = "nodeAffinityPreset.key" value = "role" } set { name = "nodeAffinityPreset.values[0]" value = "mongodb" }
Come in quella documentazione, in Kubernetes ho a disposizione due metodi per l'Affinity ai Node (per la preferenza su quale node creare il Pod). In type, avendo inserito hard, l'Affinity è obbligatoria, mentre con Soft essa è solo una preferenza. Quest'ultimo metodo è comodo in caso il Node dove gira il Pod debba andare offline per qualsiasi ragione. Con la modalità hard il Pod non potrà mai essere ricreato perché l'unico Node con l'Affinity desiderata dal Pod non è disponibile, mentre con soft, non trovando il Node con l'Affinity desiderata sarà scelto un altro con le risorse libere necessarie. Nella configurazione sopra ho inserito l'Affinity per i Node che hanno una Label con la key role e come valore mongodb.
Nel caso delle web application, le macchine nel Node Group avranno la Label role="webapp". Nel blocco Deployment ho aggiunto questo codice:
... spec { affinity { node_affinity { required_during_scheduling_ignored_during_execution { node_selector_term { match_expressions { key = "role" operator = "In" values = ["webapp"] } } } } } volume { ...
Che equivale alla regola scritta sopra per il chart di Helm per MongoDb.
outputs.tf
Siccome ho demandato ad AWS la creazione dei Load Balancer, con questo file posso mostrare gli URL che dovrò utilizzare per accedere alle due web application:
output "mongodb-express-dns-name" { description = "MongoDb password" value = "http://${kubernetes_service.mongoexpress-service.status[0].load_balancer[0].ingress[0].hostname}" } output "testdb-dns-name" { description = "Api rest port" value = "http://${kubernetes_service.webapp-service.status[0].load_balancer[0].ingress[0].hostname}/api/info" }
Avviare il tutto
Arrivato a questo punto è il momento di avviare la creazione di tutte queste risorse in AWS. In sequenza:
terraform init
Per il download dei provider necessari.
terraform plan
Per verificare che la sintassi sia corretta. E solo infine:
terraform apply -auto-approve
Questa volta la procedura sarà piuttosto lunga. Saranno necessari circa due minuti per la creazione della VPC, quindi dai cinque ai dieci minuti per la creazione del cluster EKS, e dai tre ai cinque minuti per la creazione dei due Node Group. Dopodiché inizierà la configurazione dei Pod e delle altre risorse necessarie all'interno di Kubernetes. E solo infine ecco l'output voluto:
Outputs: mongodb-express-dns-name = "http://a56ec8e93f6c542ebbce34d149748ae2-93b4730aa8841fd1.elb.eu-south-1.amazonaws.com" mongodb-password = <sensitive> namespace = "test-blog-2" testdb-dns-name = "http://a2a8417bd547347aa807c2af01b99b40-e8ef85a0505fb6f1.elb.eu-south-1.amazonaws.com/api/info"
Non rimane che provare questi url. Per mongo-express:
Per la web api:
Se si riceve errore quando si richiedono le due web application non si deve avere fretta. Il collegamento dal Load Balancer e le due web application in Kubernetes non è mai immediato e l'attesa è quasi sempre di qualche minuto.
Se volessi utilizzare Kubernetes in AWS da locale con il classico comando kubectl, dovrei usare questo comando da terminale:
aws eks --region eu-south-1 update-kubeconfig --name K8sDemo
Dove K8sDemo è il nome del cluster EKS creato in AWS (e inserito nelle variabili nei file per Terraform). Ultimo controllo che i Pod siano stati inseriti sulle giuste macchine:
$ kubectl get po -n test-blog-2 -o wide NAME READY STATUS RESTARTS AGE IP NODE mongodb-easy-0 1/1 Running 0 22m 10.0.2.81 ip-10-0-2-234.eu-south-1.compute.internal mongodb-easy-1 1/1 Running 0 22m 10.0.1.149 ip-10-0-1-233.eu-south-1.compute.internal mongodb-easy-2 1/1 Running 0 21m 10.0.3.90 ip-10-0-3-143.eu-south-1.compute.internal showdb-858c68d8b4-zld98 1/1 Running 0 9m21s 10.0.3.13 ip-10-0-3-251.eu-south-1.compute.internal testdb-5454db7d9d-t6594 1/1 Running 0 9m21s 10.0.3.161 ip-10-0-3-251.eu-south-1.compute.internal
I tre Pod di MongoDb sono avviati sulle tre macchine dedicate, così come le due web application sono avviate entrambe su una sola macchina.
Controllato il buon funzionamento, distruggo il tutto con il comando:
terraform destroy -auto-approve
E in una decina di minuti tutto dovrebbe essere cancellato. In qualche caso può apparire questo errore:
Error: context deadline exceeded
Sembra che sia dovuto ad un timeout del provider di AWS durante la cancellazione, e il più delle volte basta rilanciare il comando qui sopra per completare la cancellazione. In altri casi, piuttosto rari, si deve intervenire manualmente cancellando le risorse direttamente dalla console web di AWS.
IAM Role ai Pod
Tra le feature del cluster di Kubernetes in AWS c'è la possibilità di assegnare delle IAM Role ai singoli Pod. Questo consente di poter accedere a risorse di AWS senza dover utilizzare direttamente delle credenziali che creerebbero poi il problema sulla sicurezza della loro gestione. Tale possibilità non la mostrerò in questo post perché ci sono ancora dei dettagli che non mi sono chiari - mancanza di tempo per approfondire la cosa, come scusa funziona sempre. Comunque maggiori info qui e nel link al video a fine post.
Usare altri Cloud
Se volessi usare un altro cloud le modifiche sono solo a livello di avvio e configurazione del cluster di Kubernetes. Nel mio caso, non essendoci la VPC così come in AWS, potrei cancellare completamente i file 1, 2 e 3, e modificare parzialmente il 4 con la configurazione di LKE (il cluster di Kubernetes in Linode) con queste modifiche:
terraform { required_providers { linode = { source = "linode/linode" } } } # Configure the Linode Provider provider "linode" { token = "################################################################" } resource "linode_lke_cluster" "mycluster" { label = "my-cluster" k8s_version = "1.23" region = "eu-central" tags = ["prod"] control_plane { high_availability = false } pool { type = "g6-standard-1" count = 4 } } resource "local_file" "config" { content_base64 = linode_lke_cluster.mycluster.kubeconfig filename = "${path.module}/config.txt" } provider "kubernetes" { config_path = local_file.config.filename } resource "kubernetes_namespace" "test" { metadata { name = "my-test-123455" } }
Qui ho dovuto salvare il file di configurazione per l'accesso al cluster in un file locale (grazie alla resource di Terraform local_file) il cui percorso completo l'ho inserito come parametro in config_path (lo stesso file lo potrò utilizzare poi per accedere al cluster da locale).
Sicurezza
Come mostrato nel post precedente l'inserimento di password all'interno di Terraform è sempre problematico. Mi è stato suggerito di utilizzare i vari servizi di Secret dei vari cloud, in modo che è possibile creare manualmente un Secret contenente le credenziali poi da usare nella creazione di servizi che poi dovrebbero utilizzarle. Nel caso della mia demo la cosa non risolve il problema. Ipotizzando di avere un Secret in AWS dal nome example, è possibile leggerne il contenuto con questo codice in Terraform:
terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 3.0" } } } # Configure the AWS Provider provider "aws" { region = "eu-south-1" } data "aws_secretsmanager_secret" "byname" { name = "example" } data "aws_secretsmanager_secret_version" "secretversion" { secret_id = data.aws_secretsmanager_secret.byname.id } output "example1" { description = "Secret Metadata" value = data.aws_secretsmanager_secret.byname } output "example2" { description = "Secret Content" value = jsondecode(data.aws_secretsmanager_secret_version.secretversion.secret_string) sensitive = true }
Il problema è che leggendo i file di stato creato da Terraform vedrei il contenuto in chiaro:
"instances": [ { "schema_version": 0, "attributes": { "arn": "arn:aws:secretsmanager:eu-south-1:838080890745:secret:example-pcyzfF", "id": "arn:aws:secretsmanager:eu-south-1:838080890745:secret:example-pcyzfF|AWSCURRENT", "secret_binary": "", "secret_id": "arn:aws:secretsmanager:eu-south-1:838080890745:secret:example-pcyzfF", "secret_string": "{\n \"test1\": \"1234\",\n \"test2\": \"5678\"\n}", "version_id": "35fb8d94-b96c-4419-92ef-fd7e324e3470", "version_stage": "AWSCURRENT", "version_stages": [ "AWSCURRENT" ] }, "sensitive_attributes": [] } ]
La soluzione è utilizzare servizi appositi per la gestione dello State, visto che lo State Storage su S3 permette anche la crittografazione del contenuto:
https://www.terraform.io/language/settings/backends/s3#encrypt
Critiche e apprezzamenti di Terraform con il Cloud
In passato ho avuto a che fare con chi vendeva Terraform come passepartout per tutti i Cloud. Come se costruiti dei file per la configurazione di risorse da utilizzare in AWS bastasse modificare il provider per poterli utilizzare con altri Cloud (Azure, Google, etc...). Ovviamente non così - e sarebbe da pazzi crederlo, almeno attualmente. E' sufficiente vedere il solo esempio sopra, dove ho voluto utilizzare gli stessi file di Terraform con due Cloud differenti, in cui ho dovuto stravolgere quasi tutti i file - a parte Kubernetes, anche se avrei dovuto rimuovere le Affinity. Questo sconvolgimento sarebbe stato altresì completo anche se avessi usato la controparte di Azure per la costruzione di una rete interna VNet. Lasciando perdere questa facile critica, di contro, è da apprezzare la possibilità di avere uno strumento unico per la creazione di risorse completamente differenti su piattaforme agli antipodi (esagerando). Senza strumenti come Terraform, per la creazione di risorse su AWS e Azure, avrei dovuto utilizzare il comando da terminale aws (lo stesso usato all'inizio del post - aws configure) e il comando az di Azure. Inoltre lo scambio di informazioni tra i due Cloud con quei comandi porterebbe ad una complicazione inutile che Terraform risolve facilmente. Ergo: Terraform promosso.
Fine
Ecco il repository con il codice. Qui un video illuminante su quanto esposto che mi ha aiutato a risolvere alcuni dubbi. Ci saranno altri post dedicati a Terraform? Perché no? Magari con qualcosa di più simpatico e utile - forse.
Per inserire un commento, devi avere un account.
Fai il login e torna a questa pagina, oppure registrati alla nostra community.
- AWS, EKS e Fluent Bit, il 22 luglio 2022 alle 20:01
- AWS, EKS, OIDC: accedere alla risorse di AWS da Kubernetes, il 15 luglio 2022 alle 19:53
- AWS, EKS, gestione domini e TLS con Ingress, l'8 luglio 2022 alle 20:00
- Gestione dei domini e certificati in AWS con Terraform, il 25 giugno 2022 alle 09:54
- Terraform, Vue.js, Lambda in Net6 e Aws CloudFront, il 3 giugno 2022 alle 10:56
- Terraform, Vue.js e Aws CloudFront, il 26 maggio 2022 alle 19:46