Si las asignaturas de Sistemas Operativos y Redes os parecieron intensas, agarraros bien porque DevOps y Cloud Computing ha sido la joya de la corona; una auténtica maratón técnica que te cambia la perspectiva por completo. Aquí ya no vale decir eso de *"en mi máquina funciona"*. Si no está automatizado, si no pasa el linter, si no se monitoriza en tiempo real... simplemente no existe.
Quiero compartir con vosotros todo el camino recorrido a lo largo de este semestre en la UOC-Jesuïtes (2026), desglosando los tres grandes "Productos" prácticos que tuvimos que sudar, configurar y defender. Desde picar código en Go y empaquetarlo en contenedores minimalistas, hasta montar un pipeline completo de CI/CD que automatiza las revisiones de código en GitHub, acabando con el despliegue de infraestructura en la nube monitorizada hasta el último byte. ¡Vamos allá!
1. Producto 1: Preparación del Entorno (Go 1.25 + Docker Multi-Stage)
El primer gran reto fue sentar las bases de la filosofía DevOps: aislamiento, consistencia y eficiencia. En lugar de pelearnos con configuraciones locales pesadas que acaban ensuciando el sistema operativo, nos propusimos desarrollar y empaquetar un microservicio web utilizando Go 1.25 y Docker.
Debido a que era nuestro primer contacto con el entorno real de contenedores, decidimos enfocar el desarrollo de forma colaborativa pero organizada. Centralizamos toda la documentación técnica en un espacio de Notion del grupo y estructuramos el repositorio de GitHub con carpetas individuales específicas para cada uno de nosotros (`alex`, `andrey`, `davidov`).
package main
import (
"fmt"
"log"
"net/http"
"os"
)
func main() {
// Puerto configurable mediante una variable de entorno (útil en Docker)
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
mux := http.NewServeMux()
// 1) Servir la carpeta /static/ (imagen)
fs := http.FileServer(http.Dir("./static"))
mux.Handle("/static/", http.StripPrefix("/static/", fs))
// 2) Endpoint principal
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Buenas prácticas: método permitido y tipo de contenido
if r.Method != http.MethodGet {
http.Error(w, "Método no permitido", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, `
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Producto 1 - UOC</title>
</head>
<body style="text-align:center;">
<h1>Soy alumno de la UOC (davidov@uoc.edu)</h1>
<p>Producto 1: entorno Go + Docker</p>
<img src="/static/uoc.jpg" alt="Imagen UOC" width="900">
</body>
</html>
`)
})
// 3) Endpoint extra opcional: comprobación de estado
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprint(w, "OK")
})
srv := &http.Server{
Addr: ":" + port,
Handler: logRequest(mux),
}
log.Printf("Servidor escuchando en http://localhost:%s", port)
log.Fatal(srv.ListenAndServe())
}
// Middleware simple de registro de peticiones
func logRequest(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s %s", r.RemoteAddr, r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
El Enfoque del Contenedor Eficiente (Multi-Stage Build)
Uno de los errores más comunes al empezar con Docker es generar imágenes gigantescas (de cientos de megabytes o incluso gigas) que incluyen todo el SDK, compiladores y herramientas de depuración en la imagen que finalmente irá a producción. Para solucionarlo, implementamos una estrategia profesional de Construcción Multi-Etapa (Multi-Stage Build).
La idea es simple:
1. Fase de Construcción (Builder): Utilizamos una imagen robusta de Go basada en Alpine para compilar el binario ejecutable.
2. Imagen Final (Runtime): Copiamos únicamente el ejecutable resultante a una imagen completamente limpia y ultraligera de Alpine Linux. ¡El resultado final es una imagen de apenas 5MB!
Aquí tenéis la estructura exacta y pulida de nuestro `Dockerfile` de producción:
# ETAPA 1: Compilación (aquí definimos el ejecutable)
# Versión de GO y directorio de trabajo
FROM golang:1.25 AS builder
WORKDIR /app
# Copiamos archivos de gestión de librerías
COPY go.mod ./
RUN go mod download
# Copiamos el resto del código y creamos el archivo "server" (BUILD PESADO)
# Esta imagen de “debug” no está pensada para deploy es el código en “sucio”
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server main.go
# ETAPA 2: Imagen final (el ejecutable/runtime ligero que enviamos a producción)
# Digamos que es el microservicio pensado y optimizado para deploy, minimalista
# Alpine es un “minilinux” de unos 5Mb al que le sumamos el código de nuestro servicio
FROM alpine:3.20
WORKDIR /app
# Sólo nos traemos lo necesario de la etapa anterior, librerías, dependencias y código
# mínimo, así como los archivos estáticos (fuentes, scripts, imágenes, css…)
COPY --from=builder /app/server /app/server
COPY --from=builder /app/static /app/static
# Avisamos que el contenedor usará el puerto 8081 (puerto definido para GO)
EXPOSE 8081
ENTRYPOINT ["/app/server"]
Verificación del Entorno Local
Al desplegar este microservicio en local mapeando el puerto 8081, conseguimos levantar nuestra aplicación web en cuestión de milisegundos con un consumo de recursos mínimo. La ejecución y aislamiento eran perfectos, demostrando que podíamos compilar código sin necesidad de tener el SDK instalado directamente en la máquina anfitrión.
Descargar: Producto 1 - Entorno Go + Docker
2. Producto 2: El Corazón del CI/CD (AWS = Jenkins + Minikube + GitHub Integration)
El clímax y verdadero "dolor de cabeza" de la asignatura llegó con el Producto 2. Nos despedimos del entorno local controlado y nos mudamos a la infraestructura real de la nube en Amazon Web Services (AWS) para desplegar nuestro servicio en una instancia EC2 instalando Jenkins, Minikube y todo lo necesario para empezar nuestro desarrollo en la nube.
Con la aplicación ya contenedorizada, el paso lógico era automatizar el ciclo de vida del código. En el Producto 2 nos adentramos en el verdadero flujo de integración y despliegue continuos (CI/CD) conectando Jenkins con GitHub Webhooks y orquestando el despliegue local mediante Kubernetes (Minikube).
Trabajamos estrictamente con ramas individuales (`alex-branch`, `andrey-branch`, `davidov-branch`) y prohibimos por completo los commits directos a `main`. Todo cambio debía ser auditado automáticamente por la máquina antes de que un humano (¡yo! 😉) le diera el visto bueno.
El Flujo de Validación del Pipeline
Diseñamos un archivo automatizado `Jenkinsfile` que reaccionaba de forma inmediata a cualquier `git push` en las ramas de desarrollo. El flujo seguía estos pasos rigurosos:
1. Fase de Linting (Validación): Jenkins analiza la sintaxis y la estructura del código base (HTML y Go). Para poner a prueba el sistema y comprobar que funcionaba, simulamos un error real introduciendo una etiqueta de párrafo `<p>` mal cerrada. ¡Jenkins saltó de inmediato! Marcó el commit con una X roja en la interfaz de GitHub y bloqueó automáticamente cualquier posibilidad de avanzar.
2. Creación de Pull Request: Si el código pasa el linter de forma limpia (check verde), el desarrollador sabe que su código es "bueno" y abre un *Pull Request* hacia la rama `main`.
3. Revisión y Merge: El administrador del repositorio evalúa visualmente el estado del pipeline en GitHub. Si ve el check verde de Jenkins, aprueba la fusión haciendo el *Merge*.
4. Despliegue Automático en Kubernetes: El merge dispara la fase final del pipeline en la rama principal (`main`). Jenkins compila la versión definitiva, genera la imagen Docker, le aplica el tag de `latest` y actualiza automáticamente los pods en nuestro clúster de Minikube exponiendo el servicio en el puerto 8081.

Resumen de la arquitectura del flujo de trabajo en equipo
| Rol / Quién | Acción Realizada | Entorno / Dónde ocurre |
|---|---|---|
| Desarrollador | Escribe código, hace commit y `git push` | En su rama personal (`davidov-branch`) |
| Jenkins Server | Automatiza el Linter y pruebas estáticas | Notifica con Check o X directamente en GitHub |
| Desarrollador | Solicita la fusión mediante *Pull Request* | Interfaz web de GitHub |
| Admin GitHub | Revisa los checks de automatización y hace *Merge* | Repositorio central de GitHub |
| Jenkins (Final) | Compilación global, Docker Build y Deploy | Despliegue de Pods/Services en Minikube |
Descargar: PIPELINE ADMIN GITHUB
Descargar: Producto 2 - CI/CD Jenkins + Minikube
3. Producto 3: Infraestructura en la Nube y Observabilidad Activa (AWS + Prometheus + Grafana + Loki)
Ahora toca ver que todo funciona de forma eficiente ;) vamos a monitorear...
La observabilidad es clave en producción: Las métricas puras sin visualización o las alertas sin umbrales lógicos no evitan las caídas. La combinación de Prometheus para métricas y Loki para logs consolida un diagnóstico rápido.
Recogida de Métricas con Prometheus y Node Exporter
Para saber con precisión quirúrgica qué ocurre en el servidor, instalamos Node Exporter directamente en el sistema operativo de la instancia EC2. Este agente recolecta métricas puras de hardware (uso de CPU, red, lectura de disco, memoria). Posteriormente, configuramos Prometheus para realizar el raspado (*scrape*) de esos datos en intervalos específicos.

Archivo de configuración definitivo de `prometheus.yml`:
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- "alert_rules.yml"
scrape_configs:
- job_name: "prometheus"
static_configs:
- targets: ["localhost:9091"]
- job_name: "node"
static_configs:
- targets: ["localhost:9100"] # Endpoint de escucha de Node Exporter
Gestión de Alertas Inteligentes
Tener gráficas bonitas no sirve de nada si tienes que estar mirándolas las 24 horas del día. Diseñamos un fichero de reglas de alerta robusto (`alert_rules.yml`) con umbrales críticos calculados mediante expresiones matemáticas para que el sistema nos avise si la infraestructura peligra:
groups:
- name: servidor_alertas
rules:
- alert: ServidorCaido
expr: up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Servidor caído"
description: "Prometheus no puede contactar con el objetivo de monitorización. ¡El servicio web está fuera de línea!"
- alert: CPUAlta
expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
for: 5m
labels:
severity: warning
annotations:
summary: "CPU superior al 80%"
description: "Uso elevado y sostenido del procesador detectado en la instancia cloud EC2."
- alert: MemoriaAlta
expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100 > 85
for: 5m
labels:
severity: warning
annotations:
summary: "Memoria superior al 85%"
description: "La memoria RAM disponible está alcanzando límites críticos. Peligro de OOM Killer."
El Panel de Control: Grafana y Análisis de Logs con Loki + Promtail
Con las métricas fluyendo hacia Prometheus, las conectamos como origen de datos en Grafana, diseñando un Dashboard visual unificado interactivo para ver el estado de la máquina de un solo vistazo.

Sin embargo, las métricas solo te dicen *cuándo* falla algo, pero no *por qué*. Para solucionar la otra mitad del problema de la observabilidad, implementamos Loki (el agregador de logs) junto a Promtail (el agente encargado de leer los ficheros de logs del microservicio en tiempo real). Gracias a esta integración, podíamos realizar auditorías forenses completas: si veíamos un pico anómalo de CPU en los paneles de Grafana, podíamos seleccionar ese rango de tiempo exacto y ver las líneas de error detalladas que nuestra aplicación en Go había registrado en ese mismo segundo.
Descargar: Producto 3 - Prometheus + Grafana + Loki
Conclusión y Reflexiones Finales
Esta asignatura ha sido una auténtica inmersión de realidad. DevOps no va de usar herramientas modernas porque estén de moda; va de crear puentes sólidos, automatizados, reproducibles y seguros entre el desarrollo de software y las operaciones de sistemas. He pasado de gestionar contenedores sencillos en local a entender la orquestación avanzada y cómo dormir tranquilo por las noches gracias a un stack de monitorización proactivo en la nube.
Si has llegado hasta aquí leyendo todo este "tocho técnico", te lo digo de verdad: ¡te mereces un premio! 🏆 Como me apasiona compartir conocimiento y quiero que veáis cómo defendimos todo esto ante el tribunal, os dejo aquí el acceso al vídeo completo de nuestra presentación del proyecto.
¿Tenéis alguna duda sobre cómo configurar vuestro Jenkinsfile, gestionar las alertas matemáticas de Prometheus o pelearos con las políticas de puertos de seguridad en AWS EC2? ¡Dejad un comentario abajo y hagamos un debug o un `git log` juntos!