Photo by Joshua Harris on Unsplash
Come esporre più applicazioni su Amazon EKS con un singolo Application Load Balancer
If you're looking for the English version of this article, click here.
Esporre i microservizi verso Internet con AWS
Uno dei momenti decisivi nella costruzione di un'applicazione a microservizi è decidere come esporre gli endpoint in modo che un client o un'API possa inviare richieste e ottenere risposte.
Solitamente ogni microservizio ha il suo endpoint. Ad esempio, ogni suffisso alla fine di un indirizzo web punterà a un microservizio diverso:
www.example.com/service1 > microservice1
www.example.com/service2 > microservice2
www.example.com/service3 > microservice3
...
Questo tipo di instradamento è noto come path-based routing.
Questo approccio ha l'importante vantaggio di avere un basso costo ed essere semplice anche quando si espongono decine di microservizi.
In AWS, sia Application Load Balancer (ALB) che Amazon API Gateway supportano questa funzione. Pertanto, con un singolo ALB o un singolo API Gateway è possibile esporre i microservizi in esecuzione come container con Amazon EKS o Amazon ECS o come funzioni serverless con AWS Lambda.
Recentemente AWS ha proposto una soluzione per esporre microservizi orchestrati da EKS tramite un Application Load Balancer. La loro soluzione è basata sull'utilizzo di NodePort esposte da Kubernetes.
Io invece voglio proporre una soluzione diversa che sfrutta il VPC CNI add-on del cluster EKS e consente ai pod di agganciarsi automaticamente al proprio target group, senza quindi l'utilizzo di NodePort.
Inoltre, nel mio caso d'uso l'Application Load Balancer è gestito in modo indipendente rispetto a EKS, ovvero non è Kubernetes a detenerne il controllo. Questo consente di poter utilizzare anche altri tipi di routing basandosi sulle possibili condizioni da configurare sul load balancer; ad esempio, potremmo avere un certificato SSL con più di un dominio (SNI) e basare il routing delle richieste non solo sul path ma anche sul dominio.
Configurazione dei componenti
Il codice mostrato qui è parziale. L'esempio completo è consultabile qui.
EKS cluster
In questo articolo il cluster EKS è un prerequisito e si dà per scontato che sia già installato. Se vuoi, puoi leggere come installare un cluster EKS con Terraform nel mio articolo sull'autoscaling. Nel repository si trova un esempio completo.
VPC CNI add-on
Il VPC CNI (Container Network Interface) add-on consente di assegnare automaticamente un indirizzo IP della VPC direttamente ad un pod all'interno del cluster EKS.
Poiché vogliamo che i pod si auto-registrino sul proprio target group (che è una risorsa fuori da Kubernetes e interna alla VPC), l'utilizzo di questo add-on è indispensabile. La sua installazione è integrata su EKS in modo nativo, come spiegato qui.
AWS Load Balancer Controller plugin
AWS Load Balancer Controller è un controller che aiuta a gestire un Elastic Load Balancer per un cluster Kubernetes.
Normalmente viene utilizzato per il provisioning di un Application Load Balancer, come risorsa Ingress, oppure di un Network Load Balancer come risorsa Service.
Nel nostro caso il provisioning non è richiesto, perché il nostro Application Load Balancer è gestito in modo indipendente. Utilizzeremo però un altro tipo di componente installato dalla CRD per far sì che i pod si registrino al loro target group.
Questo plugin non è incluso nell'installazione di EKS, pertanto va installato seguendo le istruzioni dalla documentazione AWS.
Se usi Terraform, come me, puoi considerare di usare un modulo per l'installazione del plugin:
module "load_balancer_controller" {
source = "DNXLabs/eks-lb-controller/aws"
version = "0.6.0"
cluster_identity_oidc_issuer = module.eks_cluster.cluster_oidc_issuer_url
cluster_identity_oidc_issuer_arn = module.eks_cluster.oidc_provider_arn
cluster_name = module.eks_cluster.cluster_id
namespace = "kube-system"
create_namespace = false
}
Load Balancer e Security Group
Con Terraform creo un Application Load Balancer nelle subnet pubbliche della nostra VPC, ed il suo Security Group. La VPC è la stessa in cui è installato il cluster EKS.
resource "aws_lb" "alb" {
name = "${local.name}-alb"
internal = false
load_balancer_type = "application"
subnets = module.vpc.public_subnets
enable_deletion_protection = false
security_groups = [aws_security_group.alb.id]
}
resource "aws_security_group" "alb" {
name = "${local.name}-alb-sg"
description = "Allow ALB inbound traffic"
vpc_id = module.vpc.vpc_id
tags = {
"Name" = "${local.name}-alb-sg"
}
ingress {
description = "allowed IPs"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "allowed IPs"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
}
}
E' importante ricordarsi di autorizzare questo Security Group come sorgente nelle regole in ingresso del Security Group dei nodi del cluster.
A questo punto creo i target group a cui i pod si agganceranno. In questo esempio ne uso due:
resource "aws_lb_target_group" "alb_tg1" {
port = 8080
protocol = "HTTP"
target_type = "ip"
vpc_id = module.vpc.vpc_id
tags = {
Name = "${local.name}-tg1"
}
health_check {
path = "/"
}
}
resource "aws_lb_target_group" "alb_tg2" {
port = 9090
protocol = "HTTP"
target_type = "ip"
vpc_id = module.vpc.vpc_id
tags = {
Name = "${local.name}-tg2"
}
health_check {
path = "/"
}
}
L'ultima configurazione sull'Application Load Balancer è la definizione dei listener, che contengono le regole di instradamento del traffico.
La regola di default sui listener, che viene fornita in risposta a richieste che non soddisfano nessuna delle altre regole, nel mio caso è di rifiutare il traffico; la inserisco come misura di sicurezza:
resource "aws_lb_listener" "alb_listener_http" {
load_balancer_arn = aws_lb.alb.arn
port = "80"
protocol = "HTTP"
default_action {
type = "fixed-response"
fixed_response {
content_type = "text/plain"
message_body = "Internal Server Error"
status_code = "500"
}
}
}
resource "aws_lb_listener" "alb_listener_https" {
load_balancer_arn = aws_lb.alb.arn
port = "443"
protocol = "HTTPS"
certificate_arn = aws_acm_certificate.certificate.arn
ssl_policy = "ELBSecurityPolicy-2016-08"
default_action {
type = "fixed-response"
fixed_response {
content_type = "text/plain"
message_body = "Internal Server Error"
status_code = "500"
}
}
}
Le regole vere e proprie vengono quindi associate ai listener. Il listener sulla porta 80 ha una semplice redirect verso il listener HTTPS. Il listener sulla porta 443 ha delle regole di instradano il traffico a seconda del path:
resource "aws_lb_listener_rule" "alb_listener_http_rule_redirect" {
listener_arn = aws_lb_listener.alb_listener_http.arn
priority = 100
action {
type = "redirect"
redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
condition {
host_header {
values = local.all_domains
}
}
}
resource "aws_lb_listener_rule" "alb_listener_rule_forwarding_path1" {
listener_arn = aws_lb_listener.alb_listener_https.arn
priority = 100
action {
type = "forward"
target_group_arn = aws_lb_target_group.alb_tg1.arn
}
condition {
host_header {
values = local.all_domains
}
}
condition {
path_pattern {
values = [local.path1]
}
}
}
resource "aws_lb_listener_rule" "alb_listener_rule_forwarding_path2" {
listener_arn = aws_lb_listener.alb_listener_https.arn
priority = 101
action {
type = "forward"
target_group_arn = aws_lb_target_group.alb_tg2.arn
}
condition {
host_header {
values = local.all_domains
}
}
condition {
path_pattern {
values = [local.path2]
}
}
}
Utilizzo su Kubernetes
Una volta che la configurazione su AWS è completa, l'utilizzo di questa tecnica su EKS è semplicissimo! E' sufficiente inserire una risorsa di tipo TargetGroupBinding per ogni deployment/service che vogliamo esporre sul load balancer tramite il target group.
Vediamo un esempio. Poniamo che ho un deployment con un service:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
selector:
matchLabels:
app.kubernetes.io/name: nginx
replicas: 1
template:
metadata:
labels:
app.kubernetes.io/name: nginx
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app.kubernetes.io/name: nginx
spec:
selector:
app.kubernetes.io/name: nginx
ports:
- port: 8080
targetPort: 80
protocol: TCP
La sola configurazione da aggiungere è fatta in questo modo:
apiVersion: elbv2.k8s.aws/v1beta1
kind: TargetGroupBinding
metadata:
name: nginx
spec:
serviceRef:
name: nginx
port: 8080
targetGroupARN: "arn:aws:elasticloadbalancing:eu-south-1:123456789012:targetgroup/tf-20220726090605997700000002/a6527ae0e19830d2"
Da questo momento in poi ogni nuovo pod che afferisce al deployment associato a quel service si auto-registrerà sul target group indicato. Per testarlo, basta far scalare il numero di repliche del deployment:
kubectl scale deployment nginx --replicas 5
e nel giro di qualche secondo gli IP dei nuovi pod saranno presenti come target del target group.