惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

GbyAI
GbyAI
阮一峰的网络日志
阮一峰的网络日志
G
Google Developers Blog
J
Java Code Geeks
Blog — PlanetScale
Blog — PlanetScale
大猫的无限游戏
大猫的无限游戏
云风的 BLOG
云风的 BLOG
Vercel News
Vercel News
L
LangChain Blog
Hugging Face - Blog
Hugging Face - Blog
T
The Blog of Author Tim Ferriss
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Stack Overflow Blog
Stack Overflow Blog
P
Proofpoint News Feed
腾讯CDC
博客园_首页
博客园 - 聂微东
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
M
MIT News - Artificial intelligence
WordPress大学
WordPress大学
D
DataBreaches.Net
Microsoft Security Blog
Microsoft Security Blog
有赞技术团队
有赞技术团队
博客园 - 叶小钗

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
De Cero a Kubernetes Parte 1: Infraestructura con Terrafo...
jesus manriq · 2026-05-20 · via DEV Community

jesus manrique

K8s Terraform ArgoCD — Header

Serie: De Cero a Kubernetes — Parte 1 · Parte 2 · Parte 3 · Parte 4 · Parte 5


Esta es la primera parte de una guía de 5 entregas donde vamos a construir, desde cero absoluto, un cluster Kubernetes completo con Terraform, desplegar un backend con base de datos, dos frontends independientes, y gestionarlo todo con GitOps usando ArgoCD.

No es un tutorial de "haz clic aquí y cruza los dedos". Vamos a entender cada pieza. Al final de la serie tendrás un cluster real corriendo en la nube, con TLS automático, deployments reproducibles, y todo el código versionado en GitHub.

Lo que construiremos en esta Parte 1:

  • 3 VMs (1 control-plane + 2 workers) en Hetzner Cloud con Terraform
  • Red privada entre VMs, firewalls con puertos Kubernetes, SSH keys
  • containerd como container runtime en cada nodo
  • kubeadm init en el control-plane, kubeadm join en los workers
  • Calico como CNI (Container Network Interface)
  • Todo automatizado vía cloud-init — en 5 minutos tienes cluster

¿Por qué kubeadm y no un servicio managed?

EKS, GKE y AKS son excelentes. Pero cuando usas un managed service, no tocas el control-plane, no ves etcd, no configuras el CNI, no entiendes qué pasa cuando un nodo no se une al cluster.

kubeadm es la herramienta oficial de Kubernetes para bootstrappear clusters. Es lo que usan los ingenieros que construyen plataformas. Al terminar esta serie, sabrás exactamente qué hace cada componente y podrás reproducir este cluster en cualquier proveedor: Hetzner, AWS EC2, GCP Compute Engine, o tus propias máquinas físicas.


Arquitectura de infraestructura

┌──────────────────────────────────────────────────────┐
│                  Hetzner Cloud                        │
│                                                      │
│  ┌─────────────────────┐  ┌──────────┐  ┌──────────┐ │
│  │   control-plane     │  │ worker-0 │  │ worker-1 │ │
│  │   (CX21, 2vCPU/4GB) │  │(CX21)    │  │(CX21)    │ │
│  │                     │  │          │  │          │ │
│  │ api-server          │  │ kubelet  │  │ kubelet  │ │
│  │ etcd                │  │ containerd│ │ containerd│ │
│  │ controller-manager  │  │ Calico   │  │ Calico   │ │
│  │ scheduler           │  │          │  │          │ │
│  │ kubelet             │  │          │  │          │ │
│  │ containerd          │  │          │  │          │ │
│  │ Calico              │  │          │  │          │ │
│  └────────┬────────────┘  └────┬─────┘  └────┬─────┘ │
│           │                    │             │       │
│           └────────────────────┼─────────────┘       │
│                                │                     │
│                    ┌───────────▼───────────┐         │
│                    │   Red privada         │         │
│                    │   (10.0.0.0/16)      │         │
│                    └───────────────────────┘         │
│                                                      │
│   Firewall: SSH(22) + k8s API(6443) + NodePorts      │
│   + Calico networking (179/tcp, 4789/udp)            │
└──────────────────────────────────────────────────────┘

Enter fullscreen mode Exit fullscreen mode


Estructura del proyecto Terraform

Creamos un directorio infra/ en nuestro monorepo:

infra/
├── main.tf              # Providers, recursos principales
├── variables.tf         # Variables de entrada
├── outputs.tf           # Outputs: IPs, kubeconfig
├── terraform.tfvars     # Valores (no commitear secretos)
├── versions.tf          # Versiones de providers
├── cloud-init/
│   ├── control-plane.yaml
│   └── worker.yaml
└── .gitignore

Enter fullscreen mode Exit fullscreen mode


Paso 1: Configurar providers y variables

versions.tf — Bloqueamos versiones para reproducibilidad:

terraform {
  required_version = ">= 1.9.0"

  required_providers {
    hcloud = {
      source  = "hetznercloud/hcloud"
      version = "~> 1.47"
    }
    tls = {
      source  = "hashicorp/tls"
      version = "~> 4.0"
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

main.tf — El esqueleto:

provider "hcloud" {
  token = var.hcloud_token
}

# Red privada
resource "hcloud_network" "k8s" {
  name     = "k8s-network"
  ip_range = "10.0.0.0/16"
}

resource "hcloud_network_subnet" "k8s" {
  network_id   = hcloud_network.k8s.id
  type         = "cloud"
  network_zone = "eu-central"
  ip_range     = "10.0.1.0/24"
}

Enter fullscreen mode Exit fullscreen mode


Paso 2: SSH keys

resource "tls_private_key" "k8s" {
  algorithm = "ED25519"
}

resource "hcloud_ssh_key" "k8s" {
  name       = "k8s-cluster-key"
  public_key = tls_private_key.k8s.public_key_openssh
}

Enter fullscreen mode Exit fullscreen mode

Terraform guarda la clave privada en el state. Puedes extraerla con terraform output -raw ssh_private_key.


Paso 3: Firewall con los puertos exactos de Kubernetes

No abras todo. Kubernetes necesita puertos específicos:

resource "hcloud_firewall" "k8s" {
  name = "k8s-firewall"

  # SSH desde cualquier IP (o tu IP fija para producción)
  rule {
    direction  = "in"
    protocol   = "tcp"
    port       = "22"
    source_ips = ["0.0.0.0/0"]
  }

  # Kubernetes API (control-plane)
  rule {
    direction  = "in"
    protocol   = "tcp"
    port       = "6443"
    source_ips = ["0.0.0.0/0"]
  }

  # NodePort range (30000-32767 para ingress y servicios)
  rule {
    direction  = "in"
    protocol   = "tcp"
    port       = "30000-32767"
    source_ips = ["0.0.0.0/0"]
  }

  # Calico BGP (179/tcp) y VXLAN (4789/udp)
  rule {
    direction  = "in"
    protocol   = "tcp"
    port       = "179"
    source_ips = ["10.0.0.0/16"]
  }
  rule {
    direction  = "in"
    protocol   = "udp"
    port       = "4789"
    source_ips = ["10.0.0.0/16"]
  }

  # Comunicación interna completa entre nodos del cluster
  rule {
    direction  = "in"
    protocol   = "tcp"
    port       = "10250-10256"
    source_ips = ["10.0.0.0/16"]
  }
}

Enter fullscreen mode Exit fullscreen mode

Explicación de cada puerto:

  • 6443 → API server. kubectl y kubeadm hablan con este endpoint
  • 30000-32767 → Rango NodePort. El ingress nginx se expone por aquí mientras no tengamos LoadBalancer
  • 179/tcp → BGP de Calico para compartir rutas entre nodos
  • 4789/udp → VXLAN de Calico para encapsular tráfico de pods entre nodos
  • 10250 → Kubelet API (logs, exec, métricas)
  • 10256 → kube-proxy health check
  • 10257-10259 → Controller manager y scheduler (solo en control-plane)

Paso 4: Las VMs — el corazón del cluster

# Control-plane
resource "hcloud_server" "control_plane" {
  name        = "k8s-control-plane"
  server_type = "cx21"
  image       = "ubuntu-24.04"
  location    = "nbg1"
  ssh_keys    = [hcloud_ssh_key.k8s.id]
  firewall_ids = [hcloud_firewall.k8s.id]

  network {
    network_id = hcloud_network.k8s.id
    ip         = "10.0.1.10"
  }

  # cloud-init ejecuta todo el bootstrap al primer boot
  user_data = templatefile("${path.module}/cloud-init/control-plane.yaml", {
    pod_network_cidr = "192.168.0.0/16"
  })

  depends_on = [hcloud_network_subnet.k8s]
}

# Workers
resource "hcloud_server" "worker" {
  count = 2

  name         = "k8s-worker-${count.index}"
  server_type  = "cx21"
  image        = "ubuntu-24.04"
  location     = "nbg1"
  ssh_keys     = [hcloud_ssh_key.k8s.id]
  firewall_ids = [hcloud_firewall.k8s.id]

  network {
    network_id = hcloud_network.k8s.id
    ip         = "10.0.1.${20 + count.index}"
  }

  user_data = templatefile("${path.module}/cloud-init/worker.yaml", {
    control_plane_ip = "10.0.1.10"
    # El token se genera en el cloud-init del control-plane
    # y se comparte vía output de Terraform
  })

  depends_on = [hcloud_server.control_plane]
}

Enter fullscreen mode Exit fullscreen mode


Paso 5: cloud-init — la magia que automatiza todo

Aquí es donde sucede la instalación real de Kubernetes. cloud-init ejecuta estos scripts en el primer boot de cada VM.

cloud-init/control-plane.yaml:

#cloud-config
hostname: control-plane
package_update: true
package_upgrade: true

packages:
  - containerd
  - apt-transport-https
  - ca-certificates
  - curl
  - gnupg

write_files:
  # Configuración de containerd con systemd como cgroup driver
  - path: /etc/containerd/config.toml
    permissions: '0644'
    content: |
      version = 2
      [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
        runtime_type = "io.containerd.runc.v2"
        [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
          SystemdCgroup = true

  # Modprobe overlay y br_netfilter para networking de pods
  - path: /etc/modules-load.d/k8s.conf
    content: |
      overlay
      br_netfilter

  # Parámetros de kernel para networking
  - path: /etc/sysctl.d/k8s.conf
    content: |
      net.bridge.bridge-nf-call-iptables  = 1
      net.bridge.bridge-nf-call-ip6tables = 1
      net.ipv4.ip_forward                 = 1

runcmd:
  # 1. Cargar módulos de kernel y sysctl
  - modprobe overlay
  - modprobe br_netfilter
  - sysctl --system

  # 2. Desactivar swap (Kubernetes lo requiere)
  - swapoff -a
  - sed -i '/swap/d' /etc/fstab

  # 3. Reiniciar containerd con la nueva config
  - systemctl restart containerd
  - systemctl enable containerd

  # 4. Agregar repo de Kubernetes y instalar kubeadm, kubelet, kubectl
  - |
    curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.31/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
    echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.31/deb/ /" > /etc/apt/sources.list.d/kubernetes.list
    apt-get update
    apt-get install -y kubelet kubeadm kubectl
    apt-mark hold kubelet kubeadm kubectl

  # 5. Inicializar el cluster
  - |
    kubeadm init \
      --pod-network-cidr=${pod_network_cidr} \
      --apiserver-advertise-address=10.0.1.10 \
      --upload-certs \
      | tee /root/kubeadm-init.log

  # 6. Configurar kubeconfig para root
  - mkdir -p /root/.kube
  - cp /etc/kubernetes/admin.conf /root/.kube/config

  # 7. Instalar Calico
  - |
    kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.28.1/manifests/tigera-operator.yaml --kubeconfig=/etc/kubernetes/admin.conf
    kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.28.1/manifests/custom-resources.yaml --kubeconfig=/etc/kubernetes/admin.conf

  # 8. Generar token de join para workers y guardarlo
  - kubeadm token create --print-join-command > /root/join-command.sh
  - chmod 644 /root/join-command.sh

final_message: "Control-plane bootstrap completado en $UPTIME segundos"

Enter fullscreen mode Exit fullscreen mode

cloud-init/worker.yaml:

#cloud-config
hostname: worker
package_update: true
package_upgrade: true

packages:
  - containerd
  - apt-transport-https
  - ca-certificates
  - curl
  - gnupg

write_files:
  - path: /etc/containerd/config.toml
    permissions: '0644'
    content: |
      version = 2
      [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
        runtime_type = "io.containerd.runc.v2"
        [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
          SystemdCgroup = true

  - path: /etc/modules-load.d/k8s.conf
    content: |
      overlay
      br_netfilter

  - path: /etc/sysctl.d/k8s.conf
    content: |
      net.bridge.bridge-nf-call-iptables  = 1
      net.bridge.bridge-nf-call-ip6tables = 1
      net.ipv4.ip_forward                 = 1

runcmd:
  - modprobe overlay
  - modprobe br_netfilter
  - sysctl --system
  - swapoff -a
  - sed -i '/swap/d' /etc/fstab
  - systemctl restart containerd
  - systemctl enable containerd

  # Instalar kubeadm, kubelet, kubectl
  - |
    curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.31/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
    echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.31/deb/ /" > /etc/apt/sources.list.d/kubernetes.list
    apt-get update
    apt-get install -y kubelet kubeadm kubectl
    apt-mark hold kubelet kubeadm kubectl

  # Esperar que el join-command esté disponible
  # (esto se resuelve con el script de abajo)

final_message: "Worker bootstrap completado en $UPTIME segundos. Ejecuta join manual luego del init del control-plane."

Enter fullscreen mode Exit fullscreen mode


Paso 6: Unir los workers al cluster

Como el join-command se genera en el control-plane durante el bootstrap, necesitamos que los workers lo obtengan después. Usamos un recurso null_resource en Terraform o lo hacemos manual la primera vez:

# Desde tu máquina local, después del terraform apply:
scp root@<control-plane-ip>:/root/join-command.sh .
scp join-command.sh root@<worker-0-ip>:/root/
ssh root@<worker-0-ip> "bash /root/join-command.sh"
# Repetir para worker-1

Enter fullscreen mode Exit fullscreen mode

En producción esto se automatiza con Vault o un bucket S3 compartido. Para el tutorial, este paso manual de 2 minutos es suficiente y te deja ver exactamente qué pasa.


Paso 7: Verificación

$ terraform apply
# ... (2-3 minutos para crear VMs + cloud-init)

$ ssh root@<control-plane-ip>

root@control-plane:~# kubectl get nodes
NAME               STATUS   ROLES           AGE   VERSION
k8s-control-plane  Ready    control-plane   5m    v1.31.0
k8s-worker-0       Ready    <none>          2m    v1.31.0
k8s-worker-1       Ready    <none>          2m    v1.31.0

root@control-plane:~# kubectl get pods -A
NAMESPACE          NAME                                      READY   STATUS    RESTARTS   AGE
calico-system      calico-kube-controllers-xxxxx             1/1     Running   0          4m
calico-system      calico-node-xxxxx                         1/1     Running   0          4m
calico-system      calico-node-xxxxx                         1/1     Running   0          2m
calico-system      calico-node-xxxxx                         1/1     Running   0          2m
kube-system        coredns-xxxxx                             1/1     Running   0          5m
kube-system        coredns-xxxxx                             1/1     Running   0          5m
kube-system        etcd-control-plane                        1/1     Running   0          5m
kube-system        kube-apiserver-control-plane              1/1     Running   0          5m
kube-system        kube-controller-manager-control-plane     1/1     Running   0          5m
kube-system        kube-proxy-xxxxx                          1/1     Running   0          5m
kube-system        kube-scheduler-control-plane              1/1     Running   0          5m

Enter fullscreen mode Exit fullscreen mode

Tres nodos Ready, todos los pods del sistema corriendo. Cluster listo.


Outputs de Terraform

output "control_plane_ip" {
  value = hcloud_server.control_plane.ipv4_address
}

output "worker_ips" {
  value = [for w in hcloud_server.worker : w.ipv4_address]
}

output "ssh_private_key" {
  value     = tls_private_key.k8s.private_key_pem
  sensitive = true
}

output "kubeconfig" {
  value     = "${path.module}/kubeconfig.yaml"  # lo copiamos con un provisioner
  sensitive = true
}

Enter fullscreen mode Exit fullscreen mode


Costos reales

Recurso Precio mensual
3× CX21 (2vCPU, 4GB RAM, 40GB SSD) €11.97
Red privada €0.00
Firewall €0.00
Tráfico (20TB incluido) €0.00
Total ≈ €12/mes

Menos de $13 USD al mes por un cluster Kubernetes real con control-plane dedicado. Compáralo con EKS que cobra $0.10/hora solo por el control-plane ($72/mes) más las VMs aparte.


Qué aprendiste en esta parte

  • Estructura de un proyecto Terraform profesional para Kubernetes
  • Puertos exactos que Kubernetes necesita (y por qué cada uno)
  • cloud-init como herramienta de automatización de bootstrap
  • containerd como container runtime (no Docker — Kubernetes deprecó Docker en v1.24)
  • kubeadm init y los flags críticos (pod-network-cidr, advertise-address)
  • Calico como CNI para networking de pods entre nodos
  • El concepto de control-plane + workers y qué corre en cada uno

En la Parte 2, tomaremos este cluster vacío y lo convertiremos en una plataforma lista para producción: namespaces, ingress controller con TLS automático, storage persistente, y RBAC. Todo con Terraform, sin un solo kubectl apply manual.


En Guayoyo Tech diseñamos y desplegamos arquitecturas Kubernetes como esta para empresas reales. Terraform, kubeadm, GitOps, CI/CD — lo armamos contigo, sin humo y sin vendor lock-in. Si quieres que tu infraestructura se parezca a lo que acabas de leer, hablemos gratis 15 minutos. Tu cluster, tu nube, tus reglas — nosotros ponemos la ingeniería.