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:
Portabilidad: no necesitamos una cuenta de nube.
Reproducibilidad: la definición completa del clúster vive en código.
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 KubernetesRequisitos
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└── .gitignoreEn 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=localLos 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.changedPrimero instala Ansible:
sudo apt updatesudo apt install -y ansibleDespués ejecuta el playbook:
cd didgii-local-k8s-platform/ansibleansible-playbook -i inventory.ini setup-host.ymlSi 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 dockerValida las herramientas:
docker infoterraform versionkind versionkubectl version --clienthelm versionNo 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 |
|---|---|---|
|
| Portainer |
|
| Argo CD en una fase posterior |
|
| 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/ansibleansible-playbook -i inventory.ini manage-kind-cluster.yml -e action=applyValida el resultado:
kubectl cluster-info --context kind-didgii-labkubectl get nodes -o widekubectl get pods --all-namespacesdocker ps --filter label=io.x-k8s.kind.cluster=didgii-labEl estado esperado es:
didgii-lab-control-plane Ready control-planedidgii-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=180sOpcional: 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 5mAbre:
https://localhost:9443Si el laboratorio se encuentra en una VM y configuraste 0.0.0.0, utiliza:
https://IP_DEL_HOST:9443Obtén el setup token inicial sin publicarlo:
kubectl logs deployment/portainer \ --namespace portainer \ --since=10m | grep setup_tokenPortainer limita el tiempo disponible para completar la configuración inicial. Si la instalación expira, reinicia el Deployment:
kubectl rollout restart deployment/portainer -n portainerkubectl rollout status deployment/portainer -n portainer --timeout=120sSolución de problemas
Docker solo funciona con sudo
Comprueba los grupos efectivos de tu usuario:
idgetent group dockerSi tu cuenta pertenece al grupo docker, pero la sesión actual todavía no lo reconoce, vuelve a iniciar sesión o ejecuta:
newgrp dockerLos workers permanecen en NotReady
Empieza revisando los recursos del host:
free -hdocker stats --no-streamkubectl describe node didgii-lab-workerkubectl get pods -A -o wideLa 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 -lntpdocker 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-clusterterraform apply -replace=kind_cluster.labEste proceso elimina la información guardada dentro del clúster. Úsalo únicamente cuando puedas reconstruir el laboratorio.
Destruye el laboratorio
cd ../../ansibleansible-playbook -i inventory.ini manage-kind-cluster.yml -e action=destroyAl 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 authorityPreguntas 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.
Comentarios