In un post precedente avevo trattato l'uso dei domini e dei certificati SSL in AWS. In un post più antico avevo scritto alcune informazioni riguardante l'uso di Ingress e Kubernetes. E arrivato il momento di unire il tutto?
Avviare Kubernetes in AWS
Per avere un cluster Kubernetes avviato in AWS ci sono tre possibili metodi:
- Aws Console dal browser
- Da terminale con il comando eksctl
- Terraform
Tralascio il primo metodo solo per gusto personale perché lo trovo scomodo e troppo spesso mi ha portato ad errori.
Con il secondo metodo, comando eksctl, si può creare un Cluster per Kubernetes in AWS con questo comando da terminale:
eksctl create cluster \ --name my-cluster \ --version 1.22 \ --region eu-south-1 \ --nodegroup-name linux-nodes-for-k8s \ --node-type t3.small \ --nodes 1
E per cancellarlo:
eksctl delete cluster --name my-cluster
Tempo medio per la creazione? (Tempo misurato con il comando time in Bash):
real 15m15.657s user 0m0.000s sys 0m0.000s
Per la cancellazione:
real 8m35.827s user 0m0.015s sys 0m0.031s
La comodità con questo comando è che si avrà già la configurazione attiva sul computer da cui è lanciata e si potrà iniziare a lavorare con Kubernetes immediatamente:
kubectl get nodes ...
Terraform
Ormai ho scritto parecchi post a riguardo ed ormai è il mio metodo preferito per la creazione di un cluster EKS in AWS. Anche per questo post ho creato un repository in Github con il codice sorgente, e nella directory apposita si può trovare i vari file che creeranno tutte le risorse necessarie.
I passaggi, per chi già conosce Terraform sono i soliti:
terraform init terraform plan terraform apply -auto-approve
Ovviamente prima di lanciare questi comandi si dev'essere certi di avere configurato l'accesso ad AWS correttamente sulla propria macchina con il comando:
aws configure
Tempi di creazione nel mio caso:
real 10m49.006s user 0m0.047s sys 0m0.031s
Per cancellare il cluster con il comando terraform destroy -auto-approve:
real 9m16.021s user 0m0.000s sys 0m0.046s
Come spiegato nel post precedente, per poter poi utilizzare il cluster Kubernetes creato in AWS con questo metodo si può usare questo comando:
aws eks --region eu-south-1 update-kubeconfig --name K8sDemo
Che mi permette di eseguire comandi come kubectl...
Domini e certificati
Qualsiasi scelta si è fatta per la creazione del cluster per Kubernetes, ora si deve controllare di avere un dominio e un certificato SSL. Le varie operazioni per come configurare questi due risorse in AWS ne ho scritto fin troppo nel post precedente, quindi non aggiungerò nulla, anche perché tutte le operazioni sono semplicissime. Per proseguire è necessario solo il nome del dominio - nel mio caso example.com - che, ricordo, è ovviamente fittizio (ne sto usando un altro sempre .com), che il certificato SSL abbia lo stesso nome del dominio e che sia creato nella Region dove è stato avviato il cluster di Kubernetes - nel mio caso eu-south-1 (Milan).
Applicazioni web di esempio
Per questo esempio creerò due Pod in due Namespace, per semplicità a e b. Utilizzando ancora Terraform:
resource "kubernetes_namespace" "namespace-apache" { metadata { name = var.namespace_apache # <- a } } resource "kubernetes_namespace" "namespace-nginx" { metadata { name = var.namespace_nginx # <- b } }
Il primo contenente Nginx e la sua home page di default:
resource "kubernetes_deployment" "nginx-1" { metadata { name = "nginx-1" namespace = kubernetes_namespace.namespace-nginx.metadata.0.name } spec { replicas = 1 selector { match_labels = { app = "nginx-1" } } template { metadata { labels = { app = "nginx-1" } } spec { container { image = "nginx:1.14.2" name = "nginx-1" } } } } } resource "kubernetes_service" "nginx-1-ingress" { metadata { name = "nginx-1-ingress" namespace = kubernetes_namespace.namespace-nginx.metadata.0.name } spec { selector = { app = "nginx-1" } port { port = 80 } cluster_ip = "None" type ="ClusterIP" } depends_on = [kubernetes_deployment.nginx-1] }
Creo inoltre un Service di tipo ClusterIp che utilizzerò poi con Ingress. Queste risorse sono create all'interno del Namespace a.
La seconda web application si basa su Apache. In questo caso ho personalizzato la home page perché la pagina di default - It works - è fin troppo banale. Semplificando il tutto ho creato il contenuto della home page in un oggetto ConfigMap:
locals { html_home_page = <<EOF <html> <head> <style>body {font-family:verdana;text-align:center}</style> </head> <body> <h1>My Apache home page</h1> Simple example with content in config map </body> </html> EOF } resource "kubernetes_config_map" "html-apache-content" { metadata { name = "html-apache-content" namespace = kubernetes_namespace.namespace-apache.metadata.0.name # <- B } binary_data = { "HomePage" = base64encode(local.html_home_page) } }
E il codice per il deploy di questa pagina con Apache:
resource "kubernetes_deployment" "apache-1" { metadata { name = "apache-1" namespace = kubernetes_namespace.namespace-apache.metadata.0.name } spec { replicas = 1 selector { match_labels = { app = "apache-1" } } template { metadata { labels = { app = "apache-1" } } spec { container { image = "httpd:2.4.54-alpine3.16" name = "apache-1" volume_mount { name = "config-volume-apache" mount_path = "/usr/local/apache2/htdocs/" read_only = true } } volume { name = "config-volume-apache" config_map { name = "html-apache-content" items { key = "HomePage" path = "index.html" } } } } } } depends_on = [] } resource "kubernetes_service" "apache-1-ingress" { metadata { name = "apache-1-ingress" namespace = kubernetes_namespace.namespace-apache.metadata.0.name } spec { selector = { app = "apache-1" } port { port = 80 } cluster_ip = "None" type ="ClusterIP" } depends_on = [kubernetes_deployment.apache-1] }
Il tutto sarà creato nel Namespace b.
Ingress e Load Balancer in AWS
A questo link sono presenti informazioni e link su Ingress per Kubernetes. Tralasciando le informazioni di cui avevo già parlato in passato, c'è una sezione di una pagina dal link precedente che spiega come installare Ingress con AWS. In modo semplice si deve solo avviare lo script per Kubernetes con una riga di comando:
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.2.1/deploy/static/provider/aws/deploy.yaml
Interessanti e utili le informazioni per la creazione di Ingress per la comunicazione con TLS e certificati, che è proprio quello che voglio:
Volendo usare solo Terraform ora ho due soluzioni: utilizzare provider come kubectl (non il comando) che permette di includere e installare i classici Manifest yaml di Kubernetes, oppure convertire il codice presente nel file Yaml nella versione per Terraform. Io ho scelto la seconda - più avanti farò qualche commento su questo argomento.
Come spiegato nella documentazione per l'uso dei certificati TLS devo inserire l'AWS ARN del mio certificato e il CIDR della mia VPC. Ottengo queste informazioni con le Resource Data apposite:
data "aws_route53_zone" "myzone" { name = var.domain_name } data "aws_acm_certificate" "cert" { domain = var.domain_name } data "aws_eks_cluster" "k8sData" { name = var.cluster_name } data "aws_vpc" "selected" { id = data.aws_eks_cluster.k8sData.vpc_config[0].vpc_id }
Che utilizzerò nel file convertito:
... data = { allow-snippet-annotations = "true" http-snippet = "server {\n listen 2443;\n return 308 https://$host$request_uri;\n}\n" proxy-real-ip-cidr = data.aws_vpc.selected.cidr_block use-forwarded-headers = "true" } ... annotations = { "service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout" = "60" "service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled" = "true" "service.beta.kubernetes.io/aws-load-balancer-ssl-cert" = data.aws_acm_certificate.cert.arn "service.beta.kubernetes.io/aws-load-balancer-ssl-ports" = "https" "service.beta.kubernetes.io/aws-load-balancer-type" = "nlb" }
Se lanciassi ora la configurazione di Terraform dovrei trovare in AWS un nuovo Load Balancer con il certificato configurato correttamente:
Notare che con il codice di quella documentazione viene creato un Network Load Balancer e non un Application Load Balancer. Per il risultato finale non cambia molto, ma è sempre meglio saperlo.
Ingress in Kubernetes
Accantono un attimo il Load Balancer in AWS e passo a scrivere il codice per la configurazione di Ingress:
resource "kubernetes_ingress_v1" "api-ingress-nginx" { metadata { name = "api-ingress-nginx" annotations = { "kubernetes.io/ingress.class" = "nginx-ingress" "nginx.ingress.kubernetes.io/rewrite-target" = "/" } namespace = "default" } spec { rule { host = var.domain_name # <- example.com http { path { path = "/" backend { service { name = kubernetes_service.apache-service.metadata.0.name port { number = 80 } } } } } } rule { host = "nginx.${var.domain_name}" # <- nginx.example.com http { path { path = "/" backend { service { name = kubernetes_service.nginx-service.metadata.0.name port { number = 80 } } } } } } } depends_on = [ kubectl_manifest.ingressmanifest ] }
Ho creato due sezioni rule in spec, una per ogni servizio creato (apache/nginx). Nella prima rule ho inserito il dominio principale (example.com) in modo che punti al Pod dove gira Apache, mentre il dominio di terzo livello (nginx) punterà al Pod dove è in esecuzione Nginx e la sua pagina di default.
Avendo creato le due web application in due Namespace diversi, così come i loro Service a cui devo fare riferimento, non posso inserire direttamente il loro nome nella configurazione perché Ingress può solo vedere Service nel suo stesso Namespace. Un passaggio aggiuntivo è necessario. Nel Namespace Default creo altre due Service di tipo ExternalName, nel quale creo un redirect al Service ClusterIp all'interno dei Namespace:
resource "kubernetes_service" "nginx-service" { metadata { name = "nginx-service" } spec { type = "ExternalName" external_name = "${kubernetes_service.nginx-1-ingress.metadata.0.name}.${kubernetes_namespace.namespace-nginx.metadata.0.name}.svc.cluster.local" } } ... resource "kubernetes_service" "apache-service" { metadata { name = "apache-service" } spec { type = "ExternalName" external_name = "${kubernetes_service.apache-1-ingress.metadata.0.name}.${kubernetes_namespace.namespace-apache.metadata.0.name}.svc.cluster.local" } }
In external ho inserito il dominio completo di Namespace:
{name service}.{namespace}.svc.cluster.localster.local
In questo modo in Ingress posso puntare al nome di questo Service superando il limite prima descritto.
Piccola parentesi su questo oggetto: l'uso dei Service di tipo Externalname è utile anche nel caso si volesse creare un Endpoint esterno da chiamare da tutti i Pod all'interno del Cluster in modo che sia facilmente modificabile senza dover mettere mano a tutte le configurazioni degli oggetti creati. Esempio:
apiVersion: v1 kind: Service metadata: name: my-service namespace: prod spec: type: ExternalName externalName: www.google.com
Ora da qualsiasi Pod potrei richiamare questo comando per ottenere come risultato il contenuto della pagina di Google:
curl my-servicemy-service
In futuro si potrebbe modificare il nome del dominio finale in www.altromotorediricerca.it in questo service, e si avrà la sicurezza che tutti i Pod automaticamente chiameranno il nuovo dominio quando utilizzeranno my-service. Chiudo la parentesi.
Prima di configurare i Domini in AWS con Terraform, ecco in questo grafico com'è configurato il tutto:
Il Service di tipo Load Balancer in Kubernetes è in grado di creare autonomamente un Load Balancer in AWS, da qui potrebbe nascere la domanda del perché complicarsi la vita con l'utilizzo di Ingress. La risposta è semplice è intuibile: per motivi economici. Ogni oggetto di tipo Load Balancer in AWS ha un costo anche per la sola attivazione: supponendo di avere decine di Service da esporre in Internet si può immagine non solo il costo, ma l'inutilità di avere un oggetto di questo tipo per ogni Service interno in Kubernetes. Con Ingress si risolve questo problema visto che tutte le richieste passeranno per un unico Load Balancer e, grazie ad un unico file di configurazione, si potrà avere l'esatta configurazione di tutti i Service in modo chiaro in un'unica risorsa.
Gestione dei domini
Niente di nuovo da quello che avevo già fatto nel post precedente. L'unica variante è che non utilizzerò oggetti creati in CloudFront ma il Load Balancer prima creato. Ora ho bisogno delle informazioni da questo oggetto. Anche in questo caso Terraform mi viene in aiuto:
data "aws_lb" "ingress-load-balancer" { tags = { "kubernetes.io/cluster/${var.cluster_name}" = "owned" } depends_on = [ kubectl_manifest.ingressmanifest ] }
Ora posso completare l'ultimo passo:
resource "aws_route53_record" "www-apache" { zone_id = data.aws_route53_zone.myzone.zone_id name = var.domain_name # <- example.com type = "A" alias { name = data.aws_lb.ingress-load-balancer.dns_name zone_id = data.aws_lb.ingress-load-balancer.zone_id evaluate_target_health = false } } resource "aws_route53_record" "www-nginx" { zone_id = data.aws_route53_zone.myzone.zone_id name = "nginx.${var.domain_name}" # <- nginx.example.com type = "A" alias { name = data.aws_lb.ingress-load-balancer.dns_name zone_id = data.aws_lb.ingress-load-balancer.zone_id evaluate_target_health = false } }
Ora non mi rimane che controllare che le pagine siano raggiungibili. Richiamo l'Url:
https://example.com
E il browser:
Ora controllo la pagina generata da Nginx:
Entrambe le pagine sono protette in https. Ok, funziona.
Convertire il codice Yaml di Kubernetes in Terraform
Alcuni suggerimenti dati dalla mia esperienza per la conversione dei file in Yaml al formato Terraform. Personalmente se la conversione riguarda pochi file la faccio a mano facendomi aiutare dall'Intellisense di VSCode.
Per il file di Ingress preso in considerazione sopra, in cui sono presenti molte Resource e oltre cinquecento righe di codice, ho fatto affidamento per la prima conversione al tool k2tf. C'è subito da dire che non è perfetto come Tool, perché spesso non converte certe configurazioni dimenticando alcune sezioni senza dare warning o errori a riguardo. Consiglio si deve fare una revisione completa del codice prodotto per essere certi che la conversione sia corretta.
Il suo utilizzo è semplice e può gestire qualsiasi file Yaml per Kubernetes, ma può essere usato anche con le risorse già presenti nel Cluster. Per esempio, con questo comando prendo un Service esistente:
kubectl get svc/kube-dns -n kube-system -o yaml
Avrò questo output:
apiVersion: v1 kind: Service metadata: annotations: prometheus.io/port: "9153" prometheus.io/scrape: "true" creationTimestamp: "2022-05-11T19:15:39Z" labels: k8s-app: kube-dns kubernetes.io/cluster-service: "true" kubernetes.io/name: CoreDNS name: kube-dns namespace: kube-system resourceVersion: "275" uid: d6c08f56-6016-4101-814f-7e8210d4dfee spec: clusterIP: 10.96.0.10 clusterIPs: - 10.96.0.10 internalTrafficPolicy: Cluster ipFamilies: - IPv4 ipFamilyPolicy: SingleStack ports: - name: dns port: 53 protocol: UDP targetPort: 53 - name: dns-tcp port: 53 protocol: TCP targetPort: 53 - name: metrics port: 9153 protocol: TCP targetPort: 9153 selector: k8s-app: kube-dns sessionAffinity: None type: ClusterIP status: loadBalancer: {}
Salvato in un file lo posso convertire per Terraform:
k2tf -f input.yaml -o output.tf
Con questo risultato:
resource "kubernetes_service" "kube_dns" { metadata { name = "kube-dns" namespace = "kube-system" labels = { k8s-app = "kube-dns" "kubernetes.io/cluster-service" = "true" "kubernetes.io/name" = "CoreDNS" } annotations = { "prometheus.io/port" = "9153" "prometheus.io/scrape" = "true" } } spec { port { name = "dns" protocol = "UDP" port = 53 target_port = "53" } port { name = "dns-tcp" protocol = "TCP" port = 53 target_port = "53" } port { name = "metrics" protocol = "TCP" port = 9153 target_port = "9153" } selector = { k8s-app = "kube-dns" } cluster_ip = "10.96.0.10" cluster_i_ps = ["10.96.0.10"] type = "ClusterIP" session_affinity = "None" ip_families = ["IPv4"] } }
E' già presente un errore se si guarda bene il codice: cluster_i_ps invece di cluster_ips.
Consiglio di non prendere troppo alla leggera la conversione di configurazioni già esistenti da Yaml nel formato di Terraform perché si potrebbero trovare anomalie. Faccio un esempio: ecco un semplice file Yaml che crea un nuovo Namespace e al suo interno crea un Pod:
kind: Namespace apiVersion: v1 metadata: name: test labels: name: test --- apiVersion: v1 kind: Pod metadata: name: mywebapp1 namespace: test labels: role: webserver-role app: nginx spec: containers: - name: webserver1 image: nginx:1.2
kubectl da questo file creerà le risorse sequenzialmente: innanzitutto il Namespace, quindi al suo interno il Pod con Nginx. Ipotizzando lo stesso file in Terraform:
provider "kubernetes" { config_path = "~/.kube/config" } resource "kubernetes_namespace" "test" { metadata { name = "test" labels = { name = "test" } } } resource "kubernetes_pod" "mywebapp_1" { metadata { name = "mywebapp1" namespace = "test" labels = { app = "nginx" role = "webserver-role" } } spec { container { name = "webserver1" image = "nginx:1.23.0" } } }
Le due risorse saranno create in parallelo ma potrebbe succedere che il Pod venga creato in un Namespace che ancora non esiste con conseguente errore. Questo è solo un esempio perché la creazione del Namespace è molto veloce, ma per altre risorse potrebbe comportare errori e problemi casuali - lo dico perché ho avuto esperienza in merito. In questo caso è sempre meglio intervenire preventivamente con le dipendenze implicite o esplicite come spiegato nel primo post dedicato a Terraform. Ecco il codice qui sopra scritto in modo più sicuro:
resource "kubernetes_pod" "mywebapp_1" { metadata { name = "mywebapp1" namespace = kubernetes_namespace.test.metadata[0].name labels = { app = "nginx" role = "webserver-role" } } }
Con questa dipendenza implicita il Pod sarà sempre gestito dopo la creazione del Namespace. La differenza è visibile anche dal log nella console di Terraform. Nel primo caso:
kubernetes_namespace.test: Creating... kubernetes_pod.mywebapp_1: Creating... kubernetes_namespace.test: Creation complete after 0s [id=test] kubernetes_pod.mywebapp_1: Creation complete after 3s [id=test/mywebapp1]
I due oggetti sono creati in parallelo. Con la dipendenza implicita invece la creazione è sequenziale:
kubernetes_namespace.test: Creating... kubernetes_namespace.test: Creation complete after 0s [id=test] kubernetes_pod.mywebapp_1: Creating... kubernetes_pod.mywebapp_1: Creation complete after 3s [id=test/mywebapp1]
Se si controlla il mio file convertito in Terraform incluso nella demo, ho dovuto fare moltissime modifiche a riguardo con l'aggiunta delle dipendenze in modo che tutte le Resource coinvolte venissero create nell'ordine corretto. Ma ne valeva la pena?
Conclusioni
Quello mostrato in questo post è la modalità semplificata per la configurazione di Ingress in AWS. C'è una procedura più complessa e personalizzabile, ma personalmente non padroneggiandola mi astengo nel divulgarla.
Non era lo scopo di questo post, ma avrei potuto inoltre aggiungere CloudFront come layer aggiuntivo tra il dominio e il LoadBalancer in modo da poter distribuire velocemente, grazie alla CDN di AWS, il contenuto statico di queste due pagine. Utilità? Nessuna per questa demo.
Non rimane che postare il link dove trovare il codice qui utilizzato.
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
- 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