Crea un laboratorio Kubernetes local con kind y Terraform

Crea un laboratorio Kubernetes multinodo reproducible en Ubuntu con kind, Terraform y Ansible, configuración automática y puertos estables.

Este post está disponible en: English

Crear un clúster Kubernetes local es sencillo cuando solo necesitas un entorno temporal. El reto empieza cuando quieres que sea reproducible, accesible desde otra computadora y fácil de reconstruir sin repetir una larga lista de comandos manuales.

En esta primera parte prepararemos un host Ubuntu con Ansible y crearemos un clúster Kubernetes de tres nodos con Terraform y kind. El resultado será un laboratorio pequeño que podremos destruir y reconstruir de forma consistente mientras agregamos un registro privado, una API y una interfaz propias, GitOps y observabilidad en los próximos artículos.

El objetivo no es presentar kind como una plataforma de producción. Buscamos un entorno práctico y económico para aprender, probar automatización y reproducir flujos de trabajo de Kubernetes.

Al terminar tendremos:

  • Un host Ubuntu preparado automáticamente con Ansible.

  • Docker, Terraform, kubectl, Helm y kind instalados.

  • Un nodo de control y dos workers.

  • El kubeconfig disponible en ~/.kube/config.

  • Puertos estables reservados para los próximos servicios.

  • Flujos repetibles para crear, validar y destruir el laboratorio.

  • Una instalación opcional de Portainer para inspeccionar el clúster visualmente.

Arquitectura

Host Ubuntu
|
v
Preparación con Ansible
|
v
Terraform
|
v
+--------------+
| Clúster kind |
+--------------+
|
+-------------------+-------------------+-------------------+
| | | |
v v v v
Control plane Worker 1 Worker 2 Portainer (opcional)

Ansible prepara el sistema operativo. Terraform controla el ciclo de vida del clúster. kind ejecuta los nodos Kubernetes como contenedores Docker. Esta separación le da una responsabilidad clara a cada herramienta y facilita mucho el diagnóstico de problemas.

¿Por qué kind?

kind significa Kubernetes IN Docker y ejecuta los nodos de Kubernetes como contenedores. Fue creado principalmente para probar Kubernetes, pero también funciona muy bien en laboratorios locales porque es rápido, permite topologías multinodo y se puede reconstruir fácilmente.

Para este proyecto nos ofrece tres ventajas:

  1. Portabilidad: no necesitamos una cuenta de nube.

  2. Reproducibilidad: la definición completa del clúster vive en código.

  3. Bajo costo operativo: podemos destruir el laboratorio sin dejar recursos de nube ejecutándose.

Este entorno es para desarrollo y aprendizaje. Para producción normalmente usaríamos un servicio administrado como Amazon EKS, Azure Kubernetes Service o Google Kubernetes Engine, o una distribución diseñada para instalaciones on-premises.

¿Por qué combinar Ansible y Terraform?

Las dos herramientas tienen algunas funciones similares, pero en este laboratorio cada una tiene una responsabilidad concreta:

Herramienta

Responsabilidad

Ansible

Instalar y configurar los requisitos del host

Terraform

Crear, actualizar y destruir el clúster kind

Docker

Ejecutar los contenedores que funcionan como nodos

kubectl

Inspeccionar y validar Kubernetes

Helm

Instalar aplicaciones de plataforma en las siguientes fases

Nuestro flujo queda así:

Ubuntu → Ansible → Terraform → kind → cargas de Kubernetes

Requisitos

Siempre que sea posible, utiliza un host limpio con Ubuntu 22.04 o 24.04.

Recursos recomendados:

Recurso

Mínimo

Recomendado

CPU

4 núcleos

6 o más núcleos

Memoria

8 GB

16 GB

Disco libre

30 GB

50 GB

Ocho gigabytes son suficientes para el clúster base y algunas cargas pequeñas. Harbor, las herramientas de observabilidad y nuestras aplicaciones necesitarán más espacio. Durante mis pruebas, la falta de memoria hizo que los workers cambiaran a NotReady, así que recomiendo 16 GB para completar toda la serie.

También necesitas:

  • Acceso a Internet para descargar paquetes e imágenes.

  • Un usuario con permisos de sudo.

  • Git para clonar el proyecto.

Estructura del repositorio

Crea la siguiente estructura:

didgii-local-k8s-platform/
├── ansible/
├── inventory.ini
├── setup-host.yml
└── manage-kind-cluster.yml
├── terraform/
└── kind-cluster/
├── versions.tf
├── main.tf
├── variables.tf
└── outputs.tf
└── .gitignore

En los próximos artículos agregaremos los directorios de la aplicación, Harbor y GitOps.

Paso 1: crea el inventario de Ansible

Crea ansible/inventory.ini:

[local]
localhost ansible_connection=local

Los playbooks se ejecutan directamente en el host Ubuntu, así que no necesitamos una conexión SSH.

Paso 2: prepara Ubuntu con Ansible

Crea ansible/setup-host.yml:

---
- name: Prepare the Didgii Kubernetes lab host
hosts: local
become: true
gather_facts: true
vars:
target_user: "{{ ansible_env.SUDO_USER | default(ansible_user_id, true) }}"
system_arch: "{{ 'amd64' if ansible_architecture == 'x86_64' else 'arm64' }}"
kind_version: "v0.33.0"
tasks:
- name: Validate the host architecture
ansible.builtin.assert:
that:
- ansible_architecture in ['x86_64', 'aarch64', 'arm64']
fail_msg: "This playbook currently supports AMD64 and ARM64 hosts."
- name: Install prerequisite packages
ansible.builtin.apt:
name:
- ca-certificates
- curl
- git
- gnupg
- unzip
state: present
update_cache: true
- name: Create the APT keyring directory
ansible.builtin.file:
path: /etc/apt/keyrings
state: directory
mode: "0755"
- name: Download the Docker signing key
ansible.builtin.get_url:
url: https://download.docker.com/linux/ubuntu/gpg
dest: /etc/apt/keyrings/docker.asc
mode: "0644"
- name: Configure the Docker repository
ansible.builtin.apt_repository:
repo: >-
deb [arch={{ system_arch }} signed-by=/etc/apt/keyrings/docker.asc]
https://download.docker.com/linux/ubuntu
{{ ansible_distribution_release }} stable
filename: docker
state: present
update_cache: true
- name: Install Docker
ansible.builtin.apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-buildx-plugin
- docker-compose-plugin
state: present
- name: Start and enable Docker
ansible.builtin.systemd_service:
name: docker
state: started
enabled: true
- name: Add the local user to the Docker group
ansible.builtin.user:
name: "{{ target_user }}"
groups: docker
append: true
register: docker_group
- name: Download the HashiCorp signing key
ansible.builtin.get_url:
url: https://apt.releases.hashicorp.com/gpg
dest: /etc/apt/keyrings/hashicorp.asc
mode: "0644"
- name: Configure the HashiCorp repository
ansible.builtin.apt_repository:
repo: >-
deb [signed-by=/etc/apt/keyrings/hashicorp.asc]
https://apt.releases.hashicorp.com
{{ ansible_distribution_release }} main
filename: hashicorp
state: present
update_cache: true
- name: Install Terraform
ansible.builtin.apt:
name: terraform
state: present
- name: Download kubectl
ansible.builtin.get_url:
url: >-
https://dl.k8s.io/release/{{ lookup('ansible.builtin.url',
'https://dl.k8s.io/release/stable.txt') | trim }}/bin/linux/{{ system_arch }}/kubectl
dest: /usr/local/bin/kubectl
mode: "0755"
- name: Download kind
ansible.builtin.get_url:
url: "https://kind.sigs.k8s.io/dl/{{ kind_version }}/kind-linux-{{ system_arch }}"
dest: /usr/local/bin/kind
mode: "0755"
- name: Download the Helm installer
ansible.builtin.get_url:
url: https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
dest: /tmp/get_helm.sh
mode: "0700"
- name: Install Helm
ansible.builtin.command:
cmd: /tmp/get_helm.sh
creates: /usr/local/bin/helm
- name: Remove the Helm installer
ansible.builtin.file:
path: /tmp/get_helm.sh
state: absent
- name: Explain the Docker group change
ansible.builtin.debug:
msg: >-
Docker group membership changed. Log out and back in before continuing.
when: docker_group.changed

Primero instala Ansible:

sudo apt update
sudo apt install -y ansible

Después ejecuta el playbook:

cd didgii-local-k8s-platform/ansible
ansible-playbook -i inventory.ini setup-host.yml

Si el usuario acaba de agregarse al grupo de Docker, cierra la sesión y vuelve a entrar. Para actualizar temporalmente la sesión actual también puedes ejecutar:

newgrp docker

Valida las herramientas:

docker info
terraform version
kind version
kubectl version --client
helm version

No continúes hasta que docker info funcione sin sudo.

Paso 3: configura el provider de Terraform

Crea terraform/kind-cluster/versions.tf:

terraform {
required_version = ">= 1.5.0"
required_providers {
kind = {
source = "tehcyx/kind"
version = "~> 0.11.0"
}
}
}
provider "kind" {}

El origen del provider es importante: Terraform no publica un provider oficial llamado hashicorp/kind. Si se omite o se declara incorrectamente, terraform init puede intentar descargar un provider que no existe.

Paso 4: define las variables del clúster

Crea terraform/kind-cluster/variables.tf :

variable "cluster_name" {
description = "Name of the local kind cluster"
type = string
default = "didgii-lab"
}
variable "kubeconfig_path" {
description = "Path where kind writes the kubeconfig"
type = string
default = "~/.kube/config"
}
variable "extra_port_mappings" {
description = "Ports published from the kind control-plane container"
type = list(object({
container_port = number
host_port = number
listen_address = string
protocol = string
}))
default = [
{
container_port = 30779
host_port = 9443
listen_address = "127.0.0.1"
protocol = "TCP"
},
{
container_port = 30443
host_port = 8443
listen_address = "127.0.0.1"
protocol = "TCP"
},
{
container_port = 30080
host_port = 8080
listen_address = "127.0.0.1"
protocol = "TCP"
}
]
}

Los puertos reservados son:

Puerto del host

NodePort

Uso previsto

9443

30779

Portainer

8443

30443

Argo CD en una fase posterior

8080

30080

Aplicación de ejemplo

Al usar 127.0.0.1, los puertos solo están disponibles desde el host Ubuntu.

Si el laboratorio se ejecuta dentro de una VM remota y necesitas entrar desde tu laptop, cambia listen_address a 0.0.0.0. Esto expone los puertos en todas las interfaces del host, así que debes proteger la VM con un firewall y nunca publicar las interfaces administrativas directamente en Internet.

Los port mappings se aplican cuando kind crea el contenedor del nodo. Si los cambias después, tendrás que recrear el clúster.

Paso 5: crea el clúster multinodo

Crea terraform/kind-cluster/main.tf :

resource "kind_cluster" "lab" {
name = var.cluster_name
wait_for_ready = true
kubeconfig_path = pathexpand(var.kubeconfig_path)
kind_config {
kind = "Cluster"
api_version = "kind.x-k8s.io/v1alpha4"
node {
role = "control-plane"
dynamic "extra_port_mappings" {
for_each = var.extra_port_mappings
content {
container_port = extra_port_mappings.value.container_port
host_port = extra_port_mappings.value.host_port
listen_address = extra_port_mappings.value.listen_address
protocol = extra_port_mappings.value.protocol
}
}
}
node {
role = "worker"
}
node {
role = "worker"
}
}
}

Terraform creará tres contenedores Docker: un control plane y dos workers. Esta topología permite practicar scheduling, fallas de nodos y distribución de cargas, pero no convierte un laboratorio de un solo host en una plataforma de alta disponibilidad. Si falla Ubuntu o Docker, todo el clúster deja de estar disponible.

Crea terraform/kind-cluster/outputs.tf:

output "cluster_name" {
description = "Name of the kind cluster"
value = kind_cluster.lab.name
}
output "kubeconfig_path" {
description = "Kubeconfig generated for the cluster"
value = kind_cluster.lab.kubeconfig_path
}

Paso 6: orquesta Terraform con Ansible

Crea ansible/manage-kind-cluster.yml :

---
- name: Manage the Didgii kind cluster
hosts: local
connection: local
gather_facts: false
vars:
action: apply
terraform_directory: "{{ playbook_dir }}/../terraform/kind-cluster"
tasks:
- name: Validate the requested action
ansible.builtin.assert:
that:
- action in ['apply', 'destroy']
fail_msg: "Use -e action=apply or -e action=destroy."
- name: Initialize Terraform
ansible.builtin.command:
cmd: terraform init -input=false
chdir: "{{ terraform_directory }}"
when: action == 'apply'
- name: Check Terraform formatting
ansible.builtin.command:
cmd: terraform fmt -check -recursive
chdir: "{{ terraform_directory }}"
when: action == 'apply'
- name: Validate Terraform
ansible.builtin.command:
cmd: terraform validate
chdir: "{{ terraform_directory }}"
when: action == 'apply'
- name: Create the cluster
ansible.builtin.command:
cmd: terraform apply -auto-approve -input=false
chdir: "{{ terraform_directory }}"
when: action == 'apply'
- name: Destroy the cluster
ansible.builtin.command:
cmd: terraform destroy -auto-approve -input=false
chdir: "{{ terraform_directory }}"
when: action == 'destroy'

Terraform continúa siendo el propietario del ciclo de vida del clúster. Ansible nos ofrece un punto de entrada consistente y después coordinará las demás capas de la plataforma en el orden correcto.

Paso 7: crea y valida el clúster

Ejecuta:

cd didgii-local-k8s-platform/ansible
ansible-playbook -i inventory.ini manage-kind-cluster.yml -e action=apply

Valida el resultado:

kubectl cluster-info --context kind-didgii-lab
kubectl get nodes -o wide
kubectl get pods --all-namespaces
docker ps --filter label=io.x-k8s.kind.cluster=didgii-lab

El estado esperado es:

didgii-lab-control-plane Ready control-plane
didgii-lab-worker Ready <none>
didgii-lab-worker2 Ready <none>

Espera hasta que todos los nodos estén listos:

kubectl wait \
--for=condition=Ready \
nodes \
--all \
--timeout=180s

Opcional: instala Portainer

Portainer no es necesario para operar Kubernetes, pero ofrece una referencia visual útil mientras aprendemos cómo se relacionan los recursos.

helm repo add portainer https://portainer.github.io/k8s/
helm repo update
helm upgrade --install portainer \
portainer/portainer \
--namespace portainer \
--create-namespace \
--set service.type=NodePort \
--set service.httpsNodePort=30779 \
--wait \
--timeout 5m

Abre:

https://localhost:9443

Si el laboratorio se encuentra en una VM y configuraste 0.0.0.0, utiliza:

https://IP_DEL_HOST:9443

Obtén el setup token inicial sin publicarlo:

kubectl logs deployment/portainer \
--namespace portainer \
--since=10m | grep setup_token

Portainer limita el tiempo disponible para completar la configuración inicial. Si la instalación expira, reinicia el Deployment:

kubectl rollout restart deployment/portainer -n portainer
kubectl rollout status deployment/portainer -n portainer --timeout=120s

Solución de problemas

Docker solo funciona con sudo

Comprueba los grupos efectivos de tu usuario:

id
getent group docker

Si tu cuenta pertenece al grupo docker, pero la sesión actual todavía no lo reconoce, vuelve a iniciar sesión o ejecuta:

newgrp docker

Los workers permanecen en NotReady

Empieza revisando los recursos del host:

free -h
docker stats --no-stream
kubectl describe node didgii-lab-worker
kubectl get pods -A -o wide

La presión de memoria es común cuando varios nodos y aplicaciones comparten una VM pequeña. Aumenta la memoria disponible antes de realizar cambios aleatorios en Kubernetes.

Un puerto ya está ocupado

Identifica el proceso antes de recrear el clúster:

sudo ss -lntp
docker ps --format 'table {{.Names}}\t{{.Ports}}'

Selecciona otro host_port o detén correctamente el servicio que lo utiliza. No mates procesos hasta entender quién los inició.

Los cambios en los puertos no aparecen

extraPortMappings forma parte de la configuración de los contenedores Docker. Terraform debe recrear el clúster para aplicar los cambios:

cd ../terraform/kind-cluster
terraform apply -replace=kind_cluster.lab

Este proceso elimina la información guardada dentro del clúster. Úsalo únicamente cuando puedas reconstruir el laboratorio.

Destruye el laboratorio

cd ../../ansible
ansible-playbook -i inventory.ini manage-kind-cluster.yml -e action=destroy

Al destruir el clúster también se eliminan sus workloads y los datos almacenados localmente por Kubernetes. La configuración de Terraform permanece en Git, así que podremos crear el entorno nuevamente.

¿Qué sigue?

Ya tenemos una base Kubernetes reproducible, pero todavía no contamos con nuestra plataforma de aplicaciones.

En la segunda parte instalaremos Harbor como registro privado de contenedores, crearemos nuestra propia autoridad certificadora, configuraremos HTTPS y haremos que Docker y cada nodo de kind confíen en el registro. También reproduciremos y solucionaremos uno de los errores más comunes en registros locales:

x509: certificate signed by unknown authority

Preguntas frecuentes

¿kind es adecuado para producción?

No. kind está diseñado principalmente para probar Kubernetes y funciona muy bien en desarrollo local, CI y laboratorios. Para cargas reales debes utilizar una plataforma orientada a producción.

¿Por qué crear tres nodos en un solo host?

Nos permite practicar scheduling y comportamiento multinodo. No proporciona alta disponibilidad a nivel de host porque todos los nodos siguen ejecutándose en la misma máquina Ubuntu.

¿Por qué usar Terraform en lugar de ejecutar únicamente comandos de kind?

Terraform proporciona un ciclo de vida declarativo y permite revisar en código la topología, la ubicación del kubeconfig y los puertos publicados.

¿Por qué debo recrear el clúster al cambiar los puertos?

kind implementa esos mappings como puertos publicados de los contenedores Docker. Docker los asigna cuando se crea el contenedor de cada nodo.

Referencias

Comentarios