El presente proyecto parte de la necesidad que tuve hace unos meses de instalar un clúster Kubernetes de manera on-premise (bare metal) para un trabajo particular. En mi opinión, el despliegue de Kubernetes desde cero es complejo, toma tiempo y requiere de mucha habilidad y conocimiento técnico de parte del administrador.
Es por ello que una vez logré realizarlo con éxito, decidí recopilar todas la tareas y configuraciones necesarias en un proyecto Ansible, de manera que pudiera reutilizar el procedimiento cuando lo necesitase, además de quedar documentado para el futuro.
En el presente artículo trato de explicar el procedimiento de instalación de un clúster Kubernetes en Linux usando Ansible, el despliegue de aplicaciones simples, así como también el uso de comandos para verficar el correcto funcionamiento de nodos, deployments, servicios, ingress, etc. en Kubernetes.
Resumen
🔰 Requerimientos Iniciales
Para la ejecución del proyecto Ansible necesitaremos como mínimo 3 hosts Linux (Ansible, Master y Worker), los cuales tendrán las siguientes funciones y características:
Servidor | Función | Software | Hardware |
---|---|---|---|
Ansible | Controlador Ansible desde el cual se realizará el despliegue. | Centos 7/8 Rocky Linux 8 Debe tener instalado Ansible y GIT | 2 CPU 2 GB RAM |
Master | Realizará la función de Kubernetes Master | Centos 7/8 Rocky Linux 8 Instalación Limpia | 2 CPU 4 GB RAM |
Worker | Realizará la función de Kubernetes Worker. Podemos desplegar 1 o más. | Centos 7/8 Rocky Linux 8 Instalación Limpia | 2 CPU 4 GB RAM |
NFS (Opcional) | Ejecutará la función de Servidor NFS Podemos reutilizar el host Master para esta función | Centos 7/8 Rocky Linux 8 Instalación Limpia | 2 CPU 2 GB RAM |
- Para todos los servidores podemos usar CentOS 7/8 o Rocky Linux 8.
- El servidor Ansible funcionará como controlador y debe tener Ansible y el cliente de GIT instalados:
[adminUsername@localhost ~]$ sudo yum install epel-release
[adminUsername@localhost ~]$ sudo yum install ansible
[adminUsername@localhost ~]$ sudo yum install git
[adminUsername@localhost ~]$
[adminUsername@localhost ~]$ git --version
git version 2.27.0
[adminUsername@localhost ~]$ ansible --version
ansible 2.9.25
- Para los servidores Master, Worker y NFS, el sistema operativo debe estar recién instalado, actualizado y sin paquetes ni software adicional.
- Para el presente proyecto se recomienda desplegar un sólo servidor Master y NFS. Podemos desplegar tantos servidores Worker como deseemos y los recursos nos lo permitan.
🌉 Infraestructura de Despliegue
Todas los servidores se encontrarán conectados al mismo segmento de red (en este caso 10.0.1.0/24) según la imagen:

Ya con los servidores preparados, procederemos a explicar el procedimiento de despliegue:
En el host Ansible descargaremos el proyecto Ansible con git, personalizaremos algunos archivos, configuraremos el acceso a los hosts Kubernetes (master, workers y nfs) y lanzaremos el despliegue sobre dichos servidores.
🧐 Estructura del Proyecto Ansible
El proyecto Ansible que utilizaremos se encuentra disponible en el siguiente repositorio GitHub: https://github.com/jruizcampos/kubernetes-deploy
Procedemos a clonar el repositorio:
[adminUsername@localhost ~]$ git clone https://github.com/jruizcampos/kubernetes-deploy.git
Cloning into 'kubernetes-deploy'...
remote: Enumerating objects: 730, done.
remote: Counting objects: 100% (730/730), done.
remote: Compressing objects: 100% (432/432), done.
remote: Total 730 (delta 341), reused 513 (delta 140), pack-reused 0
Receiving objects: 100% (730/730), 158.54 KiB | 418.00 KiB/s, done.
Resolving deltas: 100% (341/341), done.
Ingresamos al proyecto, carpeta ansible y revisamos su contenido:
[adminUsername@localhost ~]$ cd kubernetes-deploy/ansible/
[adminUsername@localhost ansible]$ ls -lh
total 40K
-rw-rw-r--. 1 john john 171 Nov 8 05:51 01-configuraciones-comunes.yml
-rw-rw-r--. 1 john john 150 Nov 8 05:51 02-configurando-nfs.yml
-rw-rw-r--. 1 john john 204 Nov 8 05:51 03-configuraciones-master-worker.yml
-rw-rw-r--. 1 john john 163 Nov 8 05:51 04-configurando-master.yml
-rw-rw-r--. 1 john john 166 Nov 8 05:51 05-configurando-workers.yml
-rw-rw-r--. 1 john john 155 Nov 8 05:51 06-tests-cluster.yml
-rwxrwxr-x. 1 john john 228 Nov 8 05:51 deploy.sh
-rw-rw-r--. 1 john john 173 Nov 8 05:51 despliegue-aplicacion.yml
-rw-rw-r--. 1 john john 755 Nov 8 05:51 despliegue-kubernetes.yml
drwxrwxr-x. 2 john john 141 Nov 8 05:51 group_vars
-rw-rw-r--. 1 john john 157 Nov 8 05:51 hosts
drwxrwxr-x. 9 john john 106 Nov 8 05:51 roles
De la lista, los 2 playbooks principales son despliegue-kubernetes.yml y despliegue-aplicacion.yml. Estos 2 playbooks llaman o usan todos los demás archivos del proyecto según el siguiente esquema:

El playbook despliegue-kubernetes.yml se encarga del despliegue del clúster Kubernetes en sí, y el playbook despliegue-aplicacion.yml instala de manera opcional un par de aplicaciones de ejemplo sobre el clúster ya instalado.
El script bash deploy.sh es sólo una manera cómoda de llamar a ambos playbooks de manera secuencial.
🎚️ Personalizando el Proyecto
Antes de realizar el despliegue, debemos editar un par de archivos con valores acordes a nuestra infraestructura en particular.
📄 Archivo ansible/hosts:
[all:vars]
# Usuario linux para conectarse a los hosts
ansible_user=adminUsername
# Definimos el host master y su ip
[master]
master1 ansible_host=10.0.1.21
# Definimos los hosts worker y sus ips
[workers]
worker1 ansible_host=10.0.1.51
# worker2 ansible_host=10.0.1.52
# .
# .
# .
# worker[n] ansible_host=10.0.1.n
# Definimos el host NFS y su ip
[nfs]
nfs1 ansible_host=10.0.1.21
Este archivo debemos modificarlo de acuerdo a lo siguiente:
ansible_user=adminUsername
En esta sección colocaremos en ansible_user el Usuario Linux que Ansible utilizará para conectarse a los hosts [master], [workers] y [nfs] (en este caso adminUsername). Debemos colocar el que nosotros hayamos configurado en dichos sistemas.
[master]
master1 ansible_host=10.0.1.21
En esta sección colocaremos en ansible_host la dirección ip del host master1 (en este caso 10.0.1.21).
[workers]
worker1 ansible_host=10.0.1.51
worker2 ansible_host=10.0.1.52
...
En esta sección colocaremos en ansible_host la dirección ip de los hosts worker1, worker2, etc. En este caso 10.0.1.51, 10.0.1.52, etc.
[nfs]
nfs1 ansible_host=10.0.1.21
En esta sección colocaremos en ansible_host la dirección ip del host nfs. En este caso 10.0.1.21 (estamos reutilizando el master para la función nfs).
📄 Archivo ansible/group_vars/master.yaml
# Red de Docker configurada en el master. La obtenemos a través de los facts
docker_network: "{{ ansible_docker0.ipv4.network }}/16"
# Aquí definimos la red por defecto para los pods. Podemos cambiarla si así lo deseamos.
pod_network: "10.255.0.0/16"
# Aquí definimos que tipo de SDN queremos desplegar Calico o Flannel
sdn: flannel
# sdn: calico
En este archivo modificaremos parámetros relacionados a la instalación de Kubernetes de acuerdo a lo siguiente:
pod_network: "10.255.0.0/16"
En esta sección colocaremos en pod_network el segmento de red de los pods en formato CIDR. Este valor por lo general no se cambia y se usa el que está por defecto: 10.255.0.0/16
sdn: calico
En esta sección colocaremos en sdn el tipo de SDN a utilizar en la instalación de Kubernetes: calico o flannel (en minúsculas).
La elección de Calico o Flannel depende de la infraestructura subyacente sobre la cual estamos instalando Kubernetes:
- Si estamos realizando una instalación on-premise (bare-metal) se recomienda usar Calico puesto que es más completo y personalizable (aunque Flannel también funcionará).
- Si estamos realizando una instalación sobre máquinas virtuales desplegadas en un proveedor de cloud como Azure, debemos usar Flannel puesto que los proveedores de cloud por defecto bloquean a nivel de red ciertos puertos y protocolos necesarios para el correcto funcionamiento de Calico.
🔐 Configurando los permisos de Ansible
🔑 Configurando el acceso directo por SSH a los hosts
Antes de proceder con el despliegue, debemos configurar el acceso por SSH desde el host Ansible hacia los hosts master y workers sin necesidad de ingresar usuario y contraseña.
Para ello, en nuestro host Ansible debemos contar con las llaves (keys) SSH privada y pública de nuestro usuario Linux. En caso no se encuentren creadas, podemos hacerlo con el comando ssh-keygen:
[adminUsername@localhost ~]$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/adminUsername/.ssh/id_rsa):
Created directory '/home/adminUsername/.ssh'.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/adminUsername/.ssh/id_rsa.
Your public key has been saved in /home/adminUsername/.ssh/id_rsa.pub.
Ahora procederemos a copiar nuestra llave pública a los hosts master y worker:
[localhost ~]$ ssh-copy-id -i ~/.ssh/id_rsa.pub adminUsername@10.0.1.21
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/home/adminUsername/.ssh/id_rsa.pub"
The authenticity of host '10.0.1.21 (10.0.1.21)' can't be established.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
adminUsername@10.0.1.21's password:
Number of key(s) added: 1
[localhost ~]$ ssh-copy-id -i ~/.ssh/id_rsa.pub adminUsername@10.0.1.51
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/home/adminUsername/.ssh/id_rsa.pub"
The authenticity of host '10.0.1.51 (10.0.1.51)' can't be established.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
adminUsername@10.0.1.51's password:
Number of key(s) added: 1
De esta manera, ya podemos iniciar sesión en dichos servidores sin necesidad de usar usuario y clave. Podemos verificarlo con los comandos a continuación:
[adminUsername@localhost ~]$ ssh adminUsername@10.0.1.21
[adminUsername@localhost ~]$
[adminUsername@localhost ~]$ ssh adminUsername@10.0.1.51
[adminUsername@localhost ~]$
⭐ Configurando visudo en los hosts Kubernetes
Adicionalmente, debemos habilitar en los hosts master, workers y nfs que el usuario Linux pueda escalar privilegios mediante sudo pero sin solicitar la contraseña.
Para ello, en cada servidor ejecutamos el comando visudo como root:
[adminUsername@localhost ~]$ sudo visudo
Agregamos la siguiente línea al final del archivo y guardamos:
## Same thing without a password
adminUsername ALL=(ALL) NOPASSWD: ALL
Ahora si probamos escalar privilegios como superusuario (root), veremos que ya no se nos pide la contraseña:
[adminUsername@localhost ~]$ sudo su
[root@localhost adminUsername]#
🏗️ Despliegue del Clúster Kubernetes
Ya estamos listos para realizar el despliegue de nuestro clúster Kubernetes.
⚙️ Instalando Kubernetes
Ingresamos a la carpeta kubernetes-deploy/ansible y ejecutamos lo siguiente:
[localhost ansible]$ ansible-playbook -i hosts despliegue-kubernetes.yml
Mientras se ejecuta el playbook, se mostrarán una a una las tareas que se van realizando sobre los hosts. A continuación un resumen:
PLAY [Realizando configuraciones comunes a todos los nodos] ***********************
TASK [Gathering Facts] ************************************************************
ok: [master1]
ok: [nfs1]
ok: [worker1]
TASK [common : Configuramos el hostname] ******************************************
skipping: [nfs1]
changed: [worker1]
changed: [master1]
TASK [common : Actualizando los paquetes del Sistema] *****************************
changed: [worker1]
changed: [master1]
changed: [nfs1]
TASK [master_workers : Instalamos Docker] *****************************************
changed: [worker1]
changed: [master1]
TASK [master_workers : Instalamos Kubernetes] *************************************
changed: [master1]
changed: [worker1]
TASK [master : debug] *************************************************************
ok: [master1] => {
"msg": "Se ejecutara: kubeadm init --apiserver-advertise-address 10.0.1.21 --pod-network-cidr 10.255.0.0/16"
}
TASK [master : Mostrando la salida de la ejecución del kubeadm init] **************
ok: [master1] => {
"msg": [
"[init] Using Kubernetes version: v1.22.3",
"[preflight] Running pre-flight checks",
"[preflight] Pulling images required for setting up a Kubernetes cluster",
"Your Kubernetes control-plane has initialized successfully!",
"You should now deploy a pod network to the cluster.",
"Run \"kubectl apply -f [podnetwork].yaml\" with one of the options listed at:",
" https://kubernetes.io/docs/concepts/cluster-administration/addons/",
"",
"Then you can join any number of worker nodes by running the following on each as root:",
"",
"kubeadm join 10.0.1.21:6443 --token xxxxxx.xxxxxxxxxxxxxxxx \\",
"\t--discovery-token-ca-cert-hash sha256:fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff "
]
}
TASK [master : Extrayendo el hash CRT CA del Master] ******************************
changed: [master1]
TASK [master : Extrayendo el token del Master en formato JSON] ********************
changed: [master1]
TASK [master : Instalamos el operador de Tigera] **********************************
changed: [master1]
TASK [master : Instalamos la SDN Calico] ******************************************
changed: [master1]
TASK [master : Mostramos el resultado de la instalación de la SDN Calico] *********
ok: [master1] => {
"msg": [
"installation.operator.tigera.io/default created"
]
}
TASK [master : Desplegando el Nginx Ingress Controller] ***************************
changed: [master1]
TASK [master : Mostramos el resultado de la instalación Nginx Ingress Controller] *
ok: [master1] => {
"msg": [
"namespace/ingress-nginx created",
"serviceaccount/ingress-nginx created",
"configmap/ingress-nginx-controller created",
"clusterrole.rbac.authorization.k8s.io/ingress-nginx created",
"clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx created",
"role.rbac.authorization.k8s.io/ingress-nginx created",
"rolebinding.rbac.authorization.k8s.io/ingress-nginx created",
"service/ingress-nginx-controller-admission created",
"service/ingress-nginx-controller created",
"deployment.apps/ingress-nginx-controller created",
"ingressclass.networking.k8s.io/nginx created",
"serviceaccount/ingress-nginx-admission created",
"role.rbac.authorization.k8s.io/ingress-nginx-admission created",
"job.batch/ingress-nginx-admission-create created",
"job.batch/ingress-nginx-admission-patch created"
]
}
PLAY [Realizando configuraciones en los Workers] **********************************
TASK [workers : debug] ************************************************************
ok: [worker1] => {
"msg": "La IP del master es: 10.0.1.21"
}
TASK [workers : Habilitamos los puertos para unirse al clúster 10250] *************
changed: [worker1]
TASK [workers : Habilitamos los puertos para unirse al clúster 30000 32767] *******
changed: [worker1]
TASK [workers : Unimos el Worker al clúster Kubernetes] ***************************
changed: [worker1]
TASK [workers : Resultado de la unión del Worker al clúster] **********************
ok: [worker1] => {
"msg": [
"[preflight] Running pre-flight checks",
"[preflight] Reading configuration from the cluster...",
"[kubelet-start] Writing kubelet configuration to file \"/var/lib/kubelet/config.yaml\"",
"[kubelet-start] Writing kubelet environment file with flags to file \"/var/lib/kubelet/kubeadm-flags.env\"",
"[kubelet-start] Starting the kubelet",
"[kubelet-start] Waiting for the kubelet to perform the TLS Bootstrap...",
"This node has joined the cluster:",
"* Certificate signing request was sent to apiserver and a response was received.",
"* The Kubelet was informed of the new secure connection details.",
"Run 'kubectl get nodes' on the control-plane to see this node join the cluster."
]
}
TASK [tests : Mostrando información del Cluster recién desplegado] ****************
ok: [master1] => {
"msg": "\"--> SE CULMINÓ EL DESPLIEGUE DEL CLÚSTER DE KUBERNETES\"\n\"['\\x1b[0;32mKubernetes control plane\\x1b[0m is running at \\x1b[0;33mhttps://10.0.1.21:6443\\x1b[0m', '\\x1b[0;32mCoreDNS\\x1b[0m is running at \\x1b[0;33mhttps://10.0.1.21:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy\\x1b[0m', '', \"To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.\"]\"\n"
}
TASK [tests : Ejecutando kubectl get svc] *****************************************
changed: [master1]
TASK [tests : Mostramos el resultado de kubectl get svc] **************************
ok: [master1] => {
"msg": [
"NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE",
"default kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 45s",
"ingress-nginx ingress-nginx-controller NodePort 10.97.92.38 <none> 80:30614/TCP,443:31329/TCP 32s",
"ingress-nginx ingress-nginx-controller-admission ClusterIP 10.101.84.254 <none> 443/TCP 32s",
"kube-system kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP,9153/TCP 42s"
]
}
Al finalizar la ejecución del playbook, podemos verificar si todas las tareas terminaron correctamente o si algunas fallaron:
PLAY RECAP ************************************************************************
master1: ok=71 changed=50 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0
nfs1 : ok=2 changed=2 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
worker1: ok=39 changed=28 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
💡 Instalando las Aplicaciones de ejemplo
Con el clúster Kubernetes ya instalado, podemos proceder a instalar las aplicaciones de ejemplo. Para ello ejecutamos:
[localhost ansible]$ ansible-playbook -i hosts despliegue-aplicacion.yml
Al igual que con el playbook anterior, se mostrarán una a una las tareas que se van realizando sobre el host Kubernetes master:
PLAY [Realizando el Despliegue de la aplicación sobre Kubernetes] *****************
TASK [Gathering Facts] ************************************************************
ok: [master1]
TASK [app : Copiando los archivos de las Aplicaciones] ****************************
changed: [master1] => (item=mongodb-secret.yaml)
changed: [master1] => (item=mongodb-cm.yaml)
changed: [master1] => (item=mongodb.yaml)
changed: [master1] => (item=mongo-express.yaml)
changed: [master1] => (item=ajedrez-app.yaml)
changed: [master1] => (item=myingress.yaml)
TASK [app : Desplegando las Aplicaciones] *****************************************
changed: [master1] => (item=mongodb-secret.yaml)
changed: [master1] => (item=mongodb-cm.yaml)
changed: [master1] => (item=mongodb.yaml)
changed: [master1] => (item=mongo-express.yaml)
changed: [master1] => (item=ajedrez-app.yaml)
changed: [master1] => (item=myingress.yaml)
TASK [app : Mostramos el resultado del despliegue de las Aplicaciones] ************
ok: [master1] => (item={'cmd': 'kubectl apply -f /root/mongodb-secret.yaml', 'stdout': 'secret/mongodb-secret created', 'stderr': '', 'rc': 0, 'start': '2021-11-10 16:21:33.595560', 'end': '2021-11-10 16:21:33.895110', 'delta': '0:00:00.299550', 'changed': True, 'invocation': {'module_args': {'_raw_params': 'kubectl apply -f /root/mongodb-secret.yaml', '_uses_shell': True, 'warn': True, 'stdin_add_newline': True, 'strip_empty_ends': True, 'argv': None, 'chdir': None, 'executable': None, 'creates': None, 'removes': None, 'stdin': None}}, 'stdout_lines': ['secret/mongodb-secret created'], 'stderr_lines': [], 'failed': False, 'item': 'mongodb-secret.yaml', 'ansible_loop_var': 'item'}) => {
"msg": [
"secret/mongodb-secret created"
]
}
ok: [master1] => (item={'cmd': 'kubectl apply -f /root/mongodb-cm.yaml', 'stdout': 'configmap/mongodb-configmap created', 'stderr': '', 'rc': 0, 'start': '2021-11-10 16:21:34.264286', 'end': '2021-11-10 16:21:34.618406', 'delta': '0:00:00.354120', 'changed': True, 'invocation': {'module_args': {'_raw_params': 'kubectl apply -f /root/mongodb-cm.yaml', '_uses_shell': True, 'warn': True, 'stdin_add_newline': True, 'strip_empty_ends': True, 'argv': None, 'chdir': None, 'executable': None, 'creates': None, 'removes': None, 'stdin': None}}, 'stdout_lines': ['configmap/mongodb-configmap created'], 'stderr_lines': [], 'failed': False, 'item': 'mongodb-cm.yaml', 'ansible_loop_var': 'item'}) => {
"msg": [
"configmap/mongodb-configmap created"
]
}
ok: [master1] => (item={'cmd': 'kubectl apply -f /root/mongodb.yaml', 'stdout': 'deployment.apps/mongodb-deployment created\nservice/mongodb-service created', 'stderr': '', 'rc': 0, 'start': '2021-11-10 16:21:34.943632', 'end': '2021-11-10 16:21:35.427144', 'delta': '0:00:00.483512', 'changed': True, 'invocation': {'module_args': {'_raw_params': 'kubectl apply -f /root/mongodb.yaml', '_uses_shell': True, 'warn': True, 'stdin_add_newline': True, 'strip_empty_ends': True, 'argv': None, 'chdir': None, 'executable': None, 'creates': None, 'removes': None, 'stdin': None}}, 'stdout_lines': ['deployment.apps/mongodb-deployment created', 'service/mongodb-service created'], 'stderr_lines': [], 'failed': False, 'item': 'mongodb.yaml', 'ansible_loop_var': 'item'}) => {
"msg": [
"deployment.apps/mongodb-deployment created",
"service/mongodb-service created"
]
}
ok: [master1] => (item={'cmd': 'kubectl apply -f /root/mongo-express.yaml', 'stdout': 'deployment.apps/mongo-express created\nservice/mongo-ex-service created', 'stderr': '', 'rc': 0, 'start': '2021-11-10 16:21:37.110115', 'end': '2021-11-10 16:21:40.956915', 'delta': '0:00:03.846800', 'changed': True, 'invocation': {'module_args': {'_raw_params': 'kubectl apply -f /root/mongo-express.yaml', '_uses_shell': True, 'warn': True, 'stdin_add_newline': True, 'strip_empty_ends': True, 'argv': None, 'chdir': None, 'executable': None, 'creates': None, 'removes': None, 'stdin': None}}, 'stdout_lines': ['deployment.apps/mongo-express created', 'service/mongo-ex-service created'], 'stderr_lines': [], 'failed': False, 'item': 'mongo-express.yaml', 'ansible_loop_var': 'item'}) => {
"msg": [
"deployment.apps/mongo-express created",
"service/mongo-ex-service created"
]
}
ok: [master1] => (item={'cmd': 'kubectl apply -f /root/ajedrez-app.yaml', 'stdout': 'deployment.apps/ajedrez-app created\nservice/ajedrez-app-service created', 'stderr': '', 'rc': 0, 'start': '2021-11-10 16:21:42.890756', 'end': '2021-11-10 16:21:44.295909', 'delta': '0:00:01.405153', 'changed': True, 'invocation': {'module_args': {'_raw_params': 'kubectl apply -f /root/ajedrez-app.yaml', '_uses_shell': True, 'warn': True, 'stdin_add_newline': True, 'strip_empty_ends': True, 'argv': None, 'chdir': None, 'executable': None, 'creates': None, 'removes': None, 'stdin': None}}, 'stdout_lines': ['deployment.apps/ajedrez-app created', 'service/ajedrez-app-service created'], 'stderr_lines': [], 'failed': False, 'item': 'ajedrez-app.yaml', 'ansible_loop_var': 'item'}) => {
"msg": [
"deployment.apps/ajedrez-app created",
"service/ajedrez-app-service created"
]
}
ok: [master1] => (item={'cmd': 'kubectl apply -f /root/myingress.yaml', 'stdout': 'ingress.networking.k8s.io/myingress created', 'stderr': '', 'rc': 0, 'start': '2021-11-10 16:21:46.202769', 'end': '2021-11-10 16:21:49.322063', 'delta': '0:00:03.119294', 'changed': True, 'invocation': {'module_args': {'_raw_params': 'kubectl apply -f /root/myingress.yaml', '_uses_shell': True, 'warn': True, 'stdin_add_newline': True, 'strip_empty_ends': True, 'argv': None, 'chdir': None, 'executable': None, 'creates': None, 'removes': None, 'stdin': None}}, 'stdout_lines': ['ingress.networking.k8s.io/myingress created'], 'stderr_lines': [], 'failed': False, 'item': 'myingress.yaml', 'ansible_loop_var': 'item'}) => {
"msg": [
"ingress.networking.k8s.io/myingress created"
]
}
Terminada la ejecución del playbook, podemos verificar si todas las tareas terminaron satisfactoriamente o si alguna falló:
PLAY RECAP ***********************************************************************
master1: ok=6 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
👌 Pruebas de estado y acceso a las Aplicaciones
En esta sección verificaremos el estado del clúster Kubernetes que acabamos de instalar y el acceso a las aplicaciones de prueba.
✅ Verificando el estado del Clúster Kubernetes
Nos conectamos al nodo master y como usuario root ejecutamos el comando kubectl cluster-info
. La salida nos muestra información del clúster Kubernetes recién instalado como la URL del Control Plane y del CoreDNS:
[root@master1 ~]# kubectl cluster-info
Kubernetes control plane is running at https://10.0.1.21:6443
CoreDNS is running at https://10.0.1.21:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
Ahora ejecutaremos kubectl get nodes -o wide
. Este comando nos mostrará la lista de nodos que forman parte del clúster, su estado y dirección IP:
[root@master1 ~]# kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP
master1.jruizcampos.com Ready control-plane,master 57m v1.22.3 10.0.1.21
worker1.jruizcampos.com Ready <none> 57m v1.22.3 10.0.1.51
Ejecutamos kubectl get deployments -o wide
y kubectl get pods -o wide
. Con estos comandos verificamos si las aplicaciones de prueba que desplegamos se llegaron a inicializar de manera correcta:
[root@master1 ~]# kubectl get deployments -o wide
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES
ajedrez-app 1/1 1 1 34m ajedrez-app chess
mongo-express 1/1 1 1 34m mongo-express mongo-express
mongodb-deployment 1/1 1 1 34m mongodb mongo
[root@master1 ~]# kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP
ajedrez-app-8459955f76-699sm 1/1 Running 0 26m 10.255.78.76
mongo-express-78fcf796b8-dgbhl 1/1 Running 0 26m 10.255.78.75
mongodb-deployment-8f6675bc5-fc9gn 1/1 Running 0 26m 10.255.78.74
En los resultados mostrados se verifica que todos los despliegues de aplicación se realizaron correctamente y están listos (READY 1/1).
🖥️ Acceso a las aplicaciones directamente en los Nodos
Las aplicaciones de prueba desplegadas son 2: Un juego de ajedrez web basado en PHP y una base de datos mongodb con su gestor web mongo-express.
El acceso a estas aplicaciones es a través de servicios configurados en Kubernetes como tipo NodePort. Para ver el estado de los servicios ejcutamos kubectl get svc
:
[root@master1 ~]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ajedrez-app-service NodePort 10.96.76.52 <none> 8082:31236/TCP 57m
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 62m
mongo-ex-service NodePort 10.106.141.185 <none> 8081:32622/TCP 57m
mongodb-service ClusterIP 10.103.213.238 <none> 27017/TCP 57m
Al ser de tipo NodePort, para acceder a un servicio en particular debemos ingresar a la dirección ip de cualquier nodo del clúster y en el puerto devuelto por el comando anterior (resaltado en naranja).
Por ejemplo para acceder a la aplicación de ajedrez en el nodo master: http://10.0.1.21:31236

Accediendo al ajedrez en el nodo worker: http://10.0.1.51:31236

Ahora accederemos a la segunda aplicación, el gestor web mongo-express a través del nodo master http://10.0.1.21:32622 :

🚦 Acceso a las aplicaciones usando un Ingress Controller
La otra forma de publicar una aplicación Kubernetes es usando un Ingress Controller. En este caso, al momento de realizar el despliegue de Kubernetes se instaló el ingress controller de Nginx. Podemos verificarlo ejecutando kubectl get svc -n ingress-nginx
:
[root@master1 ~]# kubectl get svc -n ingress-nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
ingress-nginx-controller NodePort 10.107.49.177 <none> 80:30575/TCP,443:32507/TCP
ingress-nginx-controller-admission ClusterIP 10.104.4.66 <none> 443/TCP
En el resultado mostrado podemos ver que el ingress controller escucha en el puerto 30575 para http y 32507 para https. Estos valores nos servirán más adelante.
Como ya contamos con un servicio de ingress controller instalado en el clúster, nos queda desplegar la configuración de ingress específica para la publicación de nuestros servicios.
Cuando realizamos el despliegue de las aplicaciones con el archivo despliegue-aplicacion.yml, se desplegó también la configuración de ingress especificada en myingress.yaml. Podemos verificarlo ejecutando kubectl get ingress
:
[root@master1 ~]# kubectl get ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
myingress <none> jruizcampos.com 10.0.1.51 80 3h35m
Podemos revisar más a detalle esta configuración con el comando kubectl describe ingress myingress
:
[root@master1 ~]# kubectl describe ingress myingress
Name: myingress
Namespace: default
Address: 10.0.1.51
Default backend: default-http-backend:80 (<error: endpoints "default-http-backend" not found>)
Rules:
Host Path Backends
---- ---- --------
jruizcampos.com
/mongodb mongo-ex-service:8081 (10.255.1.6:8081)
/ajedrez ajedrez-app-service:8082 (10.255.1.7:80)
Annotations: kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/add-base-url: true
nginx.ingress.kubernetes.io/rewrite-target: /
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Sync 42s (x3 over 3h35m) nginx-ingress-controller Scheduled for sync
La configuración nos indica que podemos acceder a la aplicación ajedrez usando el dominio jruizcampos.com y la ruta /ajedrez.
Adicionalmente debemos agregar a la URL de acceso el número de puerto en el que escucha el Ingress Controller, que como vimos al inicio de la sección es 30575 (http) por lo que nuestra URL quedaría así: http://jruizcampos.com:30575/ajedrez

Para que el acceso por dominio funcione, debemos tener previamente configurado en nuestro DNS que el dominio (en este caso jruizcampos.com) apunte a la dirección ip del nodo master (en este caso 10.0.1.21).
En mi caso y al ser una instalación de prueba, he realizado la configuración DNS agregando la siguiente línea al final del archivo hosts de mi equipo:
10.0.1.21 jruizcampos.com
Bueno, este artículo ha quedado más extenso de lo previsto. Espero les haya sido de utilidad. Nos vemos pronto!