k8s learning notes

Prerrequisitos, necesitas tener instalado kubernetes en tu maquina local. Voy a usar minikube para aprender k8s.

Instalar

1
2
3
4
5
6
7
8
> minikube start
😄 minikube v1.34.0 en Microsoft Windows 11 Pro 10.0.26100.2605 Build 26100.2605
✨ Controlador docker seleccionado automáticamente. Otras opciones: hyperv, ssh
📌 Using Docker Desktop driver with root privileges
👍 Starting "minikube" primary control-plane node in "minikube" cluster
🚜 Pulling base image v0.0.45 ...
💾 Descargando Kubernetes v1.31.0 ...
> kubectl

Ahora esta up and running.

Vamos a ver que nodos tenemos y que pods tenemos.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> kubectl get nodes
NAME STATUS ROLES AGE VERSION
docker-desktop Ready control-plane 3d10h v1.27.2
> kubectl get pods -A
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system coredns-5d78c9869d-7692x 1/1 Running 3 (20m ago) 3d10h
kube-system coredns-5d78c9869d-bj8g5 1/1 Running 3 (20m ago) 3d10h
kube-system etcd-docker-desktop 1/1 Running 3 (20m ago) 3d10h
kube-system kube-apiserver-docker-desktop 1/1 Running 3 (20m ago) 3d10h
kube-system kube-controller-manager-docker-desktop 1/1 Running 3 (20m ago) 3d10h
kube-system kube-proxy-7fdd8 1/1 Running 3 (20m ago) 3d10h
kube-system kube-scheduler-docker-desktop 1/1 Running 3 (20m ago) 3d10h
kube-system storage-provisioner 1/1 Running 6 (20m ago) 3d10h
kube-system vpnkit-controller 1/1 Running 3 (20m ago) 3d10h

Aquí podemos ver todos los pods que están corriendo en el clúster.

Instalar Calico

Para ver cómo funcionan los plugin de red en k8s, voy a instalar Calico.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
> minikube delete -p minikube
🔥 Eliminando "minikube" en docker...
🔥 Eliminando contenedor "minikube" ...
🔥 Eliminando C:\Users\whistler092\.minikube\machines\minikube...
💀 Removed all traces of the "minikube" cluster.
> minikube start -p demo --network-plugin=cni --cni=calico -p demo
😄 minikube v1.34.0 en Microsoft Windows 11 Pro 10.0.26100.2605 Build 26100.2605
✨ Controlador docker seleccionado automáticamente. Otras opciones: hyperv, ssh
❗ With --network-plugin=cni, you will need to provide your own CNI. See --cni flag as a user-friendly alternative
📌 Using Docker Desktop driver with root privileges
👍 Starting "minikube" primary control-plane node in "minikube" cluster
🚜 Pulling base image v0.0.45 ...
🔥 Creating docker container (CPUs=2, Memory=8100MB) ...
❗ Failing to connect to https://registry.k8s.io/ from inside the minikube container
💡 To pull new external images, you may need to configure a proxy: https://minikube.sigs.k8s.io/docs/reference/networking/proxy/
🐳 Preparando Kubernetes v1.31.0 en Docker 27.2.0...
▪ Generando certificados y llaves
▪ Iniciando plano de control
▪ Configurando reglas RBAC...
🔗 Configurando CNI Calico ...
🔎 Verifying Kubernetes components...
▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🌟 Complementos habilitados: storage-provisioner, default-storageclass
🏄 Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default

Ahora el nuevo clúster está corriendo con Calico como plugin de red. Verifiquemos

1
2
3
4
5
6
7
8
9
10
11
> kubectl get pods -A
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system calico-kube-controllers-7fbd86d5c5-29tkl 1/1 Running 0 58s
kube-system calico-node-z7kr2 1/1 Running 0 58s
kube-system coredns-6f6b679f8f-22ggj 1/1 Running 0 58s
kube-system etcd-minikube 1/1 Running 0 65s
kube-system kube-apiserver-minikube 1/1 Running 0 63s
kube-system kube-controller-manager-minikube 1/1 Running 0 63s
kube-system kube-proxy-gnljs 1/1 Running 0 58s
kube-system kube-scheduler-minikube 1/1 Running 0 63s
kube-system storage-provisioner 1/1 Running 1 (57s ago) 61s

Hay muchos plugin de CNI, en este caso se cambió el CNI de minikube a Calico. Esto nos permitirá crear network policies.

Deploying a simple app

He usado los materiales del curso, que pueden ser accedidos públicamente desde aquí

https://github.com/LinkedInLearning/kubernetes-microservices-3808182

Vamos a revisar el archivo kubernetes-microservices-3808182\02_03\learning-resources-api.yaml que contiene la definición de un deployment y un service.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: learning-resources
labels:
app: learning-resources
spec:
replicas: 3
selector:
matchLabels:
app: learning-resources
template:
metadata:
labels:
app: learning-resources
spec:
containers:
- name: learning-resources-container
image: kimschles/learning-resources:latest
imagePullPolicy: Always
ports:
- containerPort: 3000
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
---
apiVersion: v1
kind: Service
metadata:
name: learning-service
labels:
app: learning-resources
spec:
selector:
app: learning-resources
ports:
- protocol: TCP
port: 80
targetPort: 3000
type: ClusterIP

Este yaml file llamado learning-resources, podemos verlo aquí

1
2
3
4
metadata:
name: learning-resources
labels:
app: learning-resources

Contiene un deployment y un service. Se pueden diferenciar por el kind

1
kind: Service | Deployment

También tenemos otro yaml llamado echo-server.yaml que contiene un deployment y un service.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: echo-server
spec:
replicas: 2
selector:
matchLabels:
app: echo-server
template:
metadata:
labels:
app: echo-server
spec:
containers:
- image: ealen/echo-server:latest
imagePullPolicy: Always
name: echo-server
ports:
- containerPort: 80
env:
- name: PORT
value: "80"
---
apiVersion: v1
kind: Service
metadata:
name: echo-service
spec:
selector:
app: echo-server
type: NodePort
ports:
- name: echo
port: 80
targetPort: 80
nodePort: 30076
protocol: TCP

Ahora Creemos los recursos

1
2
3
4
5
6
> kubectl apply -f learning-resources-api.yaml
deployment.apps/learning-resources created
service/learning-service created
> kubectl apply -f .\echo-server.yaml
deployment.apps/echo-server created
service/echo-service created

Ahora podemos ver los pods y los servicios

1
2
3
4
5
6
7
> kubectl get pods
NAME READY STATUS RESTARTS AGE
echo-server-6b4cb79dc5-gtjxj 1/1 Running 0 3m20s
echo-server-6b4cb79dc5-x67c8 1/1 Running 0 3m20s
learning-resources-5d986c647d-flxz9 1/1 Running 0 4m29s
learning-resources-5d986c647d-g5tn9 1/1 Running 0 4m29s
learning-resources-5d986c647d-zp6l6 1/1 Running 0 4m29s
1
2
3
4
5
> kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
echo-service NodePort 10.103.79.0 <none> 80:30076/TCP 4m
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 46m
learning-service ClusterIP 10.102.219.228 <none> 80/TCP 5m9s

Aquí se desplegaron dos servicios de backend, uno de tipo NodePort y otro de tipo ClusterIP. Ahora vamos a desplegar el servicio de Frontend para poder interactuar con estos microservicios.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
---
apiVersion: v1
kind: Namespace
metadata:
name: frontend
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
namespace: frontend
labels:
app: frontend-ui
spec:
replicas: 3
selector:
matchLabels:
app: frontend-ui
template:
metadata:
labels:
app: frontend-ui
spec:
containers:
- name: frontend-container
image: kimschles/frontend:latest
imagePullPolicy: Always
ports:
- containerPort: 4173
env:
- name: PUBLIC_K8S_SERVICE_URL
value: "http://learning-service.default.svc.cluster.local"
---
apiVersion: v1
kind: Service
metadata:
name: frontend-service
namespace: frontend
spec:
selector:
app: frontend-ui
ports:
- port: 80
targetPort: 4173
type: LoadBalancer

Con este yaml file, vamos a desplegar un servicio de frontend que se va a comunicar con el servicio de backend llamado learning-service.default.svc.cluster.local. Vamos a desplegarlo

1
2
3
4
> kubectl apply -f frontend.yaml
namespace/frontend created
deployment.apps/frontend created
service/frontend-service created

Hay una curiosidad aquí. Si damos kubectl get pods no vamos a ver los pods del namespace frontend.
En el yaml anterior tenemos una propiedad nueva

1
2
3
kind: Namespace
metadata:
name: frontend

Esto es una definición de namespace, por lo tanto, si damos el comando de get pods no va a estar en el contexto actual. Para cambiar el contexto, podemos hacerlo de la siguiente manera

1
2
3
4
5
> kubectl get pods -n frontend
NAME READY STATUS RESTARTS AGE
frontend-5d5d6f9c79-hg2fv 1/1 Running 0 2m37s
frontend-5d5d6f9c79-jzj4c 1/1 Running 0 2m37s
frontend-5d5d6f9c79-kc57n 1/1 Running 0 2m37s

Ya podemos ver las 3 replicas

1
2
3
> kubectl get svc -n frontend
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
frontend-service LoadBalancer 10.109.19.66 <pending> 80:30134/TCP 3m12s

Hasta el momento se ha instalado el minikube, se ha creado un clúster y se cambió el CNI de kindnet a Calico. Se han desplegado 3 microservicios, uno de tipo NodePort, otro de tipo ClusterIP y otro de tipo LoadBalancer. Se ha creado un namespace y se han desplegado 3 réplicas de un servicio en ese namespace.

Vamos a ver que significa eso de tener diferentes tipos de servicios para permitir que los pods se comuniquen entre sí.

K8s puede crear un servicio con una IP estática y un nombre. Y cuando un request llega a este servicio, el usa un balanceador de carga para redirigir el tráfico a los pods cuyas IPs están cambiando constantemente. Así que el servicio es un punto de entrada para los pods, uno no debe intentar conectarse a las IPs directas de los pods.

https://kubernetes.io/docs/concepts/services-networking/service/

Diferentes tipos de servicios

ClusterIP

Este es el servicio que está usando el servicio de frontend. Este servicio es accesible solo dentro del clúster, esto siendo definido por el specs.type ClusterIP y no es accesible desde afuera del clúster, aunque se puede acceder a este servicio desde cualquier namespace. El spec.selector es el que define que pods van a ser asociados por este servicio.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
---
apiVersion: v1
kind: Service
metadata:
name: learning-service
labels:
app: learning-resources
spec:
selector:
app: learning-resources
ports:
- protocol: TCP
port: 80
targetPort: 3000
type: ClusterIP

Este es el servicio por defecto cuando no se define en el yaml.

Ejemplo de uso de ClusterIP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Pod
metadata:
name: busybox
namespace: default
spec:
containers:
- name: busybox
image: busybox:glibc
command:
- sleep
- "3600"
imagePullPolicy: IfNotPresent
restartPolicy: Always

Vamos a usar una herramienta llamada busybox. Es bastante útil para hacer debugging en k8s.

Busybox

1
2
3
4
5
6
7
8
9
> kubectl apply -f .\busybox.yaml
pod/busybox created
> kubectl get pods
NAME READY STATUS RESTARTS AGE
busybox 1/1 Running 0 2m7s
echo-server-6b4cb79dc5-gtjxj 1/1 Running 0 5h1m
echo-server-6b4cb79dc5-x67c8 1/1 Running 0 5h1m
learning-resources-5d986c647d-flxz9 1/1 Running 0 5h2m
learning-resources-5d986c647d-g5tn9 1/1 Running 0 5h2m

Accedamos al pod de busybox

1
2
> kubectl exec -it busybox -- sh
/ #

En otra terminal, vamos a buscar cual es la IP del pod de learning-resources

1
2
3
4
5
6
7
8
> kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
busybox 1/1 Running 0 4m22s 10.244.120.75 minikube <none> <none>
echo-server-6b4cb79dc5-gtjxj 1/1 Running 0 5h3m 10.244.120.71 minikube <none> <none>
echo-server-6b4cb79dc5-x67c8 1/1 Running 0 5h3m 10.244.120.70 minikube <none> <none>
learning-resources-5d986c647d-flxz9 1/1 Running 0 5h5m 10.244.120.67 minikube <none> <none>
learning-resources-5d986c647d-g5tn9 1/1 Running 0 5h5m 10.244.120.68 minikube <none> <none>
learning-resources-5d986c647d-zp6l6 1/1 Running 0 5h5m 10.244.120.69 minikube <none> <none>

La IP del servicio learning-resources-5d986c647d-flxz9 es 10.244.120.67. Ahora vamos a hacer un request a este servicio

1
2
3
/ # wget -O- 10.244.120.67
Connecting to 10.244.120.67 (10.244.120.67:80)
wget: can't connect to remote host (10.244.120.67): Connection refused

Como se está intentando acceder a la IP del pod directamente, porque él está corriendo sobre un puerto en específico. La IP del pod está en el puerto 30000

1
2
3
4
5
6
7
/ # wget -O- 10.244.120.67:3000
Connecting to 10.244.120.67:3000 (10.244.120.67:3000)
writing to stdout
{"Intermediate Level Kubernetes Learning Resources":[{"id" ...
100% |**************************************************************************************| 8189 0:00:00 ETA
written to stdout
/ #

¡Funciono! Aunque no deberíamos hacer eso porque las IPs de los pods varían, por lo cual se va a hacer este request al servicio.

Vamos a obtener la lista de los servicios actualmente creados.

1
2
3
4
5
> kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
echo-service NodePort 10.103.79.0 <none> 80:30076/TCP 5h13m
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 5h56m
learning-service ClusterIP 10.102.219.228 <none> 80/TCP 5h14m

Volvamos al pod de busybox e intentemos hacer el wget solamente especificando el nombre del servicio. No IP address.

1
2
3
4
5
6
/ # wget -O- learning-service
Connecting to learning-service (10.102.219.228:80)
writing to stdout
{"Intermediate Level Kubernetes Learning Resources":[{"id": ...
100% |**************************************************************************************| 8189 0:00:00 ETA
written to stdout

El DNS pudo resolver la dirección IP del servicio y redirigir el tráfico a los pods.

Hay otra forma de hacer esto, y es con el DNS completo.

1
2
3
4
5
6
/ # / # wget -O- http://learning-service.default.svc.cluster.local
Connecting to learning-service.default.svc.cluster.local (10.102.219.228:80)
writing to stdout
{"Intermediate Level Kubernetes Learning Resources ...
100% |**************************************************************************************| 8189 0:00:00 ETA
written to stdout

learning-service.default.svc.cluster.local se compone del

  • nombre del servicio,
  • seguido por el namespace default,
  • el tipo de k8s object svc
  • y el clúster domain.
    Esto es una forma de acceder a los servicios dentro del clúster.

Nodeport

Nodeport es un tiempo de servicio que ayuda a exponer un grupo de ports a internet.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
---
apiVersion: v1
kind: Service
metadata:
name: echo-service
spec:
selector:
app: echo-server
ports:
- name: echo
port: 80
targetPort: 80
nodePort: 30076
protocol: TCP
type: NodePort

Cualquier pod que tenga el label de spec.selector.app echo-service, va a recibir tráfico desde este servicio. En específico, este servicio usa las direcciones IP de los nodos de k8s, ósea que cualquiera que sepa las direcciones IPs de los nodos de k8s, puede hacer un request a estos servicios.

Este servicio tiene algunas implicaciones de seguridad, debe de ser usado con cuidado.

NodePort

Ejemplo de uso de NodePort

Vamos a explorar el servicio de echo-server, para eso volvamos al busybox pod.

Pero antes de eso, vamos a ver la IP del pod echo-server

1
2
3
4
5
6
7
8
> kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
busybox 1/1 Running 1 (10m ago) 70m 10.244.120.75 minikube <none> <none>
echo-server-6b4cb79dc5-gtjxj 1/1 Running 0 6h9m 10.244.120.71 minikube <none> <none>
echo-server-6b4cb79dc5-x67c8 1/1 Running 0 6h9m 10.244.120.70 minikube <none> <none>
learning-resources-5d986c647d-flxz9 1/1 Running 0 6h11m 10.244.120.67 minikube <none> <none>
learning-resources-5d986c647d-g5tn9 1/1 Running 0 6h11m 10.244.120.68 minikube <none> <none>
learning-resources-5d986c647d-zp6l6 1/1 Running 0 6h11m 10.244.120.69 minikube <none> <none>

Ahora sí, vamos al pod de busybox

1
2
3
4
5
6
> kubectl exec -it busybox -- sh
/ # wget -O- 10.244.120.70
Connecting to 10.244.120.71 (10.244.120.71:80)
writing to stdout
{"host":{"hostname":"10.244.120.71","ip":"::ffff:10.244.120.75","ips":[]},"http":{"method":"GET","baseUrl":"","originalUrl":"/","protocol":"http"},"request":{"params":{"0":"/"},"query":{},"cookies":{},"body":{},"headers":{"host":"10.244.120.71","user-agent":"Wget","connection":"close"}},"environment":{"PATH":"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","HOSTNAME":"echo-server-6b4cb79dc5-gtjxj","PORT":"80","LEARNING_SERVICE_PORT":"tcp://10.102.219.228:80","ECHO_SERVICE_SERVICE_HOST":"10.103.79.0","ECHO_SERVICE_SERVICE_PORT_ECHO":"80","ECHO_SERVICE_PORT":"tcp://10.103.79.0:80","KUBERNETES_SERVICE_PORT_HTTPS":"443","KUBERNETES_PORT_443_TCP":"tcp://10.96.0.1:443","KUBERNETES_PORT_443_TCP_ADDR":"10.96.0.1","LEARNING_SERVICE_SERVICE_PORT":"80","LEARNING_SERVICE_PORT_80_TCP_PROTO":"tcp","LEARNING_SERVICE_PORT_80_TCP_ADDR":"10.102.219.228","ECHO_SERVICE_SERVICE_PORT":"80","ECHO_SERVICE_PORT_80_TCP_PORT":"80","KUBERNETES_PORT":"tcp://10.96.0.1:443","KUBERNETES_PORT_443_TCP_PROTO":"tcp","LEARNING_SERVICE_PORT_80_TCP":"tcp://10.102.219.228:80","ECHO_SERVICE_PORT_80_TCP":"tcp://10.103.79.0:80","KUBERNETES_SERVICE_PORT":"443","KUBERNETES_PORT_443_TCP_PORT":"443","LEARNING_SERVICE_SERVICE_HOST":"10.102.219.228","LEARNING_SERVICE_PORT_80_TCP_PORT":"80","ECHO_SERVICE_PORT_80_TCP_PROTO":"tcp","ECHO_SERVICE_PORT_80_TCP- 100% |**************************************************************************************| 1465 0:00:00 ETA
written to stdout

El pod nos respondió bien, pero como sabemos, esto no es una buena práctica. Para eso vamos a hacer un request a la IP del servicio en sí. Pero para eso vamos a tener que buscar la dirección IP del node de k8s.

1
2
3
>  kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
minikube Ready control-plane 6h57m v1.31.0 192.168.49.2 <none> Ubuntu 22.04.4 LTS 5.15.167.4-microsoft-standard-WSL2 docker://27.2.0

La IP del nodo es 192.168.49.2. Ahora vamos a hacer un request a la IP del nodo desde el busybox.

1
2
3
/ # wget -O-  192.168.49.2
Connecting to 192.168.49.2 (192.168.49.2:80)
wget: can't connect to remote host (192.168.49.2): Connection refused

No podemos conectarnos, vamos a revisar el archivo yaml del servicio de echo-server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: echo-server
spec:
replicas: 2
selector:
matchLabels:
app: echo-server
template:
metadata:
labels:
app: echo-server
spec:
containers:
- image: ealen/echo-server:latest
imagePullPolicy: Always
name: echo-server
ports:
- containerPort: 80
env:
- name: PORT
value: "80"
---
apiVersion: v1
kind: Service
metadata:
name: echo-service
spec:
selector:
app: echo-server
type: NodePort
ports:
- name: echo
port: 80
targetPort: 80
nodePort: 30076
protocol: TCP

El servicio está corriendo en el puerto 80, pero el puerto que se está exponiendo es el 30076. Vamos a hacer un request a este puerto.

1
2
3
4
5
/ # wget -O-  192.168.49.2:30076
Connecting to 192.168.49.2:30076 (192.168.49.2:30076)
writing to stdout
{"host":{"hostname":"192.168.49.2","ip":"::ffff:192.168.49.2","ips":[]},"http":{"method":"GET","baseUrl":"","originalUrl":"/","protocol":"http"},"request":{"params":{"0":"/"},"query":{},"cookies":{},"body":{},"headers":{"host":"192.168.49.2:30076","user-agent":"Wget","connection":"close"}},"environment":{"PATH":"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","HOSTNAME":"echo-server-6b4cb79dc5-gtjxj","PORT":"80","LEARNING_SERVICE_PORT":"tcp://10.102.219.228:80","ECHO_SERVICE_SERVICE_HOST":"10.103.79.0","ECHO_SERVICE_SERVICE_PORT_ECHO":"80","ECHO_SERVICE_PORT":"tcp://10.103.79.0:80","KUBERNETES_SERVICE_PORT_HTTPS":"443","KUBERNETES_PORT_443_TCP":"tcp://10.96.0.1:443","KUBERNETES_PORT_443_TCP_ADDR":"10.96.0.1","LEARNING_SERVICE_SERVICE_PORT":"80","LEARNING_SERVICE_PORT_80_TCP_PROTO":"tcp","LEARNING_SERVICE_PORT_80_TCP_ADDR":"10.102.219.228","ECHO_SERVICE_SERVICE_PORT":"80","ECHO_SERVICE_PORT_80_TCP_PORT":"80","KUBERNETES_PORT":"tcp://10.96.0.1:443","KUBERNETES_PORT_443_TCP_PROTO":"tcp","LEARNING_SERVICE_PORT_80_TCP":"tcp://10.102.219.228:80","ECHO_SERVICE_PORT_80_TCP":"tcp://10.103.79.0:80","KUBERNETES_SERVICE_PORT":"443","KUBERNETES_PORT_443_TCP_PORT":"443","LEARNING_SERVICE_SERVICE_HOST":"10.102.219.228","LEARNING_SERVICE_PORT_80_TCP_PORT":"80","ECHO_SERVICE_PORT_80_TCP_PROTO":"tcp","ECHO_SERVICE_PORT_80_- 100% |**************************************************************************************| 1468 0:00:00 ETA
written to stdout

Pudimos hacer el GET request al node de k8s y obtuvimos el response esperado del pod. Pero vamos a hacer al servicio en sí.

1
2
3
4
5
/ # wget -O- echo-service
Connecting to echo-service (10.103.79.0:80)
writing to stdout
{"host":{"hostname":"echo-service","ip":"::ffff:10.244.120.75","ips":[]},"http":{"method":"GET","baseUrl":"","originalUrl":"/","protocol":"http"},"request":{"params":{"0":"/"},"query":{},"cookies":{},"body":{},"headers":{"host":"echo-service","user-agent":"Wget","connection":"close"}},"environment":{"PATH":"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","HOSTNAME":"echo-server-6b4cb79dc5-x67c8","PORT":"80","KUBERNETES_SERVICE_HOST":"10.96.0.1","KUBERNETES_PORT":"tcp://10.96.0.1:443","ECHO_SERVICE_SERVICE_PORT":"80","KUBERNETES_PORT_443_TCP":"tcp://10.96.0.1:443","LEARNING_SERVICE_SERVICE_PORT":"80","LEARNING_SERVICE_PORT_80_TCP_PORT":"80","ECHO_SERVICE_SERVICE_HOST":"10.103.79.0","ECHO_SERVICE_PORT_80_TCP_ADDR":"10.103.79.0","ECHO_SERVICE_PORT":"tcp://10.103.79.0:80","ECHO_SERVICE_PORT_80_TCP":"tcp://10.103.79.0:80","KUBERNETES_SERVICE_PORT":"443","KUBERNETES_PORT_443_TCP_ADDR":"10.96.0.1","LEARNING_SERVICE_PORT_80_TCP":"tcp://10.102.219.228:80","LEARNING_SERVICE_PORT_80_TCP_PROTO":"tcp","ECHO_SERVICE_SERVICE_PORT_ECHO":"80","LEARNING_SERVICE_PORT_80_TCP_ADDR":"10.102.219.228","ECHO_SERVICE_PORT_80_TCP_PROTO":"tcp","ECHO_SERVICE_PORT_80_TCP_PORT":"80","KUBERNETES_SERVICE_PORT_HTTPS":"443","KUBERNETES_PORT_443_TCP_PROTO":"tcp","KUBERNETES_PORT_443_TCP_PORT":"443","LEARNING_SERVICE_SERVICE_HOST":"10.102.21- 100% |**************************************************************************************| 1463 0:00:00 ETA
written to stdout

Ahora usemos el DNS name. http://echo-service.default.svc.cluster.local

1
2
3
4
/ # wget -O- http://echo-service.default.svc.cluster.local
Connecting to echo-service.default.svc.cluster.local (10.103.79.0:80)
writing to stdout
{"host":{"hostname":"echo-service.de...

Loadbalancer

Este servicio es accesible desde afuera del clúster. Este servicio crea un balanceador de carga en la nube y asigna una IP publica a este servicio.

1
2
3
4
5
6
7
8
9
10
11
12
13
---
apiVersion: v1
kind: Service
metadata:
name: frontend
namespace: frontend
spec:
selector:
app: frontend-ui
ports:
- port: 80
targetPort: 4173
type: LoadBalancer

Ejemplo de uso de LoadBalancer

Para este ejemplo usaremos el siguiente yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
---
apiVersion: v1
kind: Namespace
metadata:
name: frontend
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
namespace: frontend
labels:
app: frontend-ui
spec:
replicas: 3
selector:
matchLabels:
app: frontend-ui
template:
metadata:
labels:
app: frontend-ui
spec:
containers:
- name: frontend-container
image: kimschles/frontend:latest
imagePullPolicy: Always
ports:
- containerPort: 4173
env:
- name: PUBLIC_K8S_SERVICE_URL
value: "http://learning-service.default.svc.cluster.local"
---
apiVersion: v1
kind: Service
metadata:
name: frontend-service
namespace: frontend
spec:
selector:
app: frontend-ui
ports:
- port: 80
targetPort: 4173
type: LoadBalancer

En este yaml, se va a crear un nuevo namespace llamado frontend, y se va a desplegar un servicio de frontend llamado frontend-ui con 3 réplicas, y en las variables de env del contenedor frontend-container se ve la URL que se va a comunicar con el servicio de backend llamado learning-service.default.svc.cluster.local.

Finalmente, el servicio llamado frontend-service va a ser de tipo LoadBalancer. Vamos a desplegarlo.

1
2
3
4
5
6
7
8
> kubectl apply -f .\frontend-ui.yaml
namespace/frontend unchanged
deployment.apps/frontend unchanged
service/frontend-service unchanged

> kubectl get svc -n frontend
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
frontend-service LoadBalancer 10.109.19.66 <pending> 80:30134/TCP 6h22m

Como podemos ver, este servicio está pendiente de asignar una IP pública. Si se está corriendo en un clúster en la nube, este servicio va a asignar una IP pública. Cuando el tráfico llegue por esa IP, eso se va a entrar al clúster, después a los servicios y finalmente a los pods. Este servicio es muy útil para exponer servicios a internet.

Vamos a usar una variante de busybox para hacer un request a este servicio.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Pod
metadata:
name: busybox-ui
namespace: frontend
spec:
containers:
- name: busybox
image: busybox:glibc
command:
- sleep
- "3600"
imagePullPolicy: IfNotPresent
restartPolicy: Always

Creémoslo e intentemos obtener información de los pods usando el nombre del servicio.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
> kubectl apply -f .\busybox-ui.yaml
pod/busybox-ui created
> kubectl -n frontend exec -it busybox-ui -- sh
/ #
/ # wget -O- frontend-service
Connecting to frontend-service (10.109.19.66:80)
writing to stdout
<!DOCTYPE html>
<html lang="en">
...
</body>

- 100% |**************************************************************************************| 12511 0:00:00 ETA
written to stdout
/ # wget -O- http://frontend-service.frontend.svc.cluster.local
Connecting to frontend-service.frontend.svc.cluster.local (10.109.19.66:80)
writing to stdout
<!DOCTYPE html>
...
</body>

- 100% |**************************************************************************************| 12511 0:00:00 ETA
written to stdout

Funciono, pudimos hacer un request al servicio de frontend desde el pod de busybox. Vamos a revisar de nuevo el servicio

1
2
3
 kubectl get svc -n frontend
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
frontend-service LoadBalancer 10.109.19.66 <pending> 80:30134/TCP 6h30m

Si hubiéramos configurado un servicio apropiadamente en un clúster en la nube, este servicio hubiera asignado una IP pública. Pero hay una manera de hacerlo en minikube.

How to access applications running within minikube

1
2
3
4
5
6
7
> minikube tunnel
✅ Tunnel successfully started

📌 NOTE: Please do not close this terminal as this process must stay alive for the tunnel to be accessible ...

❗ Access to ports below 1024 may fail on Windows with OpenSSH clients older than v8.1. For more information, see: https://minikube.sigs.k8s.io/docs/handbook/accessing/#access-to-ports-1024-on-windows-requires-root-permission
🏃 Starting tunnel for service frontend-service.

Ahora vamos a revisar el servicio y ahora vemos que hay una external-ip asignada.

1
2
3
 kubectl get svc -n frontend
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
frontend-service LoadBalancer 10.109.19.66 127.0.0.1 80:30134/TCP 6h33m

La página cargo con LoadBalancer

Actualmente, la página frontend cargada obtiene la información del servicio de backend.

Otra forma de verlo es con port-forwarding, vamos a obtener el nombre de los pods y vamos a hacer port-forwarding.

1
2
3
4
5
6
7
8
9
10
11
> kubectl get pods -n frontend
NAME READY STATUS RESTARTS AGE
busybox-ui 1/1 Running 0 11m
frontend-5d5d6f9c79-hg2fv 1/1 Running 0 6h37m
frontend-5d5d6f9c79-jzj4c 1/1 Running 0 6h37m
frontend-5d5d6f9c79-kc57n 1/1 Running 0 6h37m
> kubectl port-forward frontend-5d5d6f9c79-hg2fv 5478 4173 -n frontend
Forwarding from 127.0.0.1:5478 -> 5478
Forwarding from [::1]:5478 -> 5478
Forwarding from 127.0.0.1:4173 -> 4173
Forwarding from [::1]:4173 -> 4173

Si abrimos http://127.0.0.1:4173/ en el navegador, vamos a ver la página cargada.

ExternalName

Este servicio es un alias para un servicio externo. Este servicio no tiene un selector, pero tiene un campo externalName que es el nombre del servicio externo.

1
2
3
4
5
6
7
8
9
---
apiVersion: v1
kind: Service
metadata:
name: database-service
namespace: prod
spec:
externalName: my.postgres.database.com
type: ExternalName

En vez de hacer load balancing del tráfico de los pods dentro del clúster, lo que hace este servicio es redirigir el tráfico a un nombre de DNS, que por lo general esta fuera del clúster.

En este caso hay un servicio llamado database-service, que vive en el namespace de prod, y cuando el tráfico es enviado a ese servicio se va a redirigir el tráfico a my.postgres.database.com.

Network Policies

Son una serie de reglas que te permiten controlar el flujo del tráfico a nivel de IP address o a nivel de puerto.

Ingress o Egress

  • Ingress: Controla el tráfico que entra a los pods
  • Egress: Controla el tráfico que sale de los pods

Revisemos un ejemplo de network policy. Esta politica va a denegar todo el tráfico de entrada y salida. Vamos a crear un archivo llamado deny-all.yaml

1
2
3
4
5
6
7
8
9
10
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress

Ejecutemos el archivo

1
2
3
4
5
> kubectl apply -f .\deny-all.yaml
networkpolicy.networking.k8s.io/deny-all created
> kubectl get networkpolicy
NAME POD-SELECTOR AGE
deny-all <none> 10s

Esto se va a ejecutar a todos los pods porque no hay un selector asociado.

Ahora miremos un ejemplo de un network policy que va a permitir tráfico inbound de todos los pods en el mismo namespace.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
---
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
name: allow-from-learning-resources
spec:
podSelector:
matchLabels:
app: echo-server
ingress:
- from:
- podSelector:
matchLabels:
app: learning-resources
ports:
- port: 80
1
2
3
4
5
6
> kubectl apply -f .\allow-ingress.yaml
networkpolicy.networking.k8s.io/allow-from-learning-resources created
> kubectl get networkpolicy
NAME POD-SELECTOR AGE
allow-from-learning-resources app=echo-server 3s
deny-all <none> 4m22s

En la configuración anterior se está permitiendo el tráfico de entrada de los pods que tengan el label app: learning-resources al servicio de echo-server en el puerto 80.

Ahora vamos a crear un network policy que va a permitir el tráfico de egress desde la aplicación frontend-ui. Vamos a crear un archivo llamado allow-egress.yaml con el siguiente contenido

1
2
3
4
5
6
7
8
9
10
11
12
13
14
---
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
name: allow-egress
namespace: frontend
spec:
podSelector:
matchLabels:
app: frontend-ui
egress:
- to:
- ipBlock:
cidr: 172.11.0.0/20
1
2
3
4
5
> kubectl apply -f .\allow-egress.yaml
networkpolicy.networking.k8s.io/allow-egress created
> kubectl get networkpolicy -n frontend
NAME POD-SELECTOR AGE
allow-egress app=frontend-ui 4m53s

Ahora vamos a reiniciar el clúster de minikube para poder hacer nuevas pruebas.

1
> minikube delete -p demo

Ingress

¿Como el tráfico llega el Clúster?

Por defecto, los pods solamente responden a solicitudes de tráfico que llegan de otros pods dentro del mismo clúster. El k8s ingress y egress controllers aceptan y enrutan el tráfico externo a los servicios del clúster.

Un objecto Ingress es parte de k8s y crea una dirección IP de acceso público. Así que cuando se envía una petición HTTP o HTTPs a esa dirección IP, el controlador de Ingress a enviar el tráfico al Ingress Controller, este va a revisar la lista de reglas del firewall configuradas y enrutara el tráfico al servicio correcto en funcion a esas reglas.

  • El objeto Ingress solamente define las reglas
  • El ingress controller es el que las aplica, siendo un proxy reverso y un balanceador de carga.

Cuando los Objetos ingress son parte del Core de k8s, los controladores de Ingress son implementaciones de terceros, similares a los CNIs. Por eso son plugins que se pueden instalar en el clúster.

Ingress controllers populares

  • Ingress-nginx
  • Traefik
  • HAProxy

ingress controllers

vamos a ver un ejemplo de un objeto Ingress en el que usa ingress nginx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$1
spec:
rules:
- host: lil-microservices.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: frontend-ui
port:
number: 8080

En este ejemplo, el tipo sera determinado por kind: Ingress, y en annotations se definio que se usara el nginx.ingress. El objeto Ingress va a enrutar el tráfico que llega a lil-microservices.com al servicio frontend-ui en el puerto 8080.

En vez de obtener tráfico en el clúster usando NodePort o LoadBalancer, en frente de cada microservicio, K8s ingress te permite tener un punto de entrada para tu clúster. Esto te da más flexibilidad.

Service Mesh

Hay un punto en que implementar network policies se vuelve complicado. Por ejemplo, si se tiene un clúster con 1000 pods, y se quiere permitir que solo 10 de esos pods se comuniquen con un servicio en específico, se va a tener que escribir 1000 reglas de network policy. Esto se vuelve complicado.

Un Service Mesh es una capa de infraestructura que maneja la comunicacion entre los microservicios. Manejara todos los aspectos de Service Discovery. El proposito de un Service Mesh es:

  • Manejar la comunicacion entre los microservicios
  • Seguridad y control de trafico
  • Observabilidad y monitoreo

Hay varias implementaciones de Service Mesh

  • Istio
  • Linkerd
  • HashiCorp Consul

Algunas de paga como

  • AWS App Mesh
  • Google Anthos
  • Open Service Mesh (OSM) de Microsoft

El downside de un Service Mesh es que es una capa adicional de infraestructura que se tiene que manejar. Pero es una buena opcion si se tiene un clúster grande con muchos microservicios.