Saltar al contenido principal

Módulo 18 — Automatización y DevOps

Introducción

Un administrador de sistemas eficiente no repite tareas: las automatiza. DevOps no es una tecnología ni un cargo; es una cultura que acorta el tiempo entre "el código está listo" y "está en producción" eliminando los cuellos de botella manuales y los errores por repetición.

Este módulo recoge el Shell Scripting del Módulo 10, la seguridad con GPG del Módulo 14, los contenedores del Módulo 16 y los servicios del Módulo 17 y los lleva al siguiente nivel: control de versiones con Git, gestión de configuración con Ansible, infraestructura reproducible con Terraform/OpenTofu, pipelines CI/CD y copias de seguridad que en realidad funcionan.

Objetivos de aprendizaje

Al finalizar este módulo, serás capaz de:

  • ✅ Usar Git con fluidez: branching, rebase interactivo, bisect, cherry-pick y firmar commits con GPG
  • ✅ Escribir playbooks Ansible con roles, plantillas Jinja2 y secretos con ansible-vault
  • ✅ Describir infraestructura con Terraform/OpenTofu y entender el ciclo plan/apply/destroy
  • ✅ Construir pipelines CI/CD con GitHub Actions y GitLab CI (lint, test, deploy)
  • ✅ Diseñar una estrategia de backups 3-2-1 e implementarla con restic y borgbackup
  • ✅ Ubicarte en el mapa del ecosistema DevOps/SRE y entender cómo encaja Linux en la nube

18.1 — Git esencial para administradores

Git no es solo para desarrolladores. Versionar /etc con etckeeper, mantener scripts de administración en un repositorio y firmar los cambios de configuración son prácticas que han salvado servidores en producción.

Modelo mental de Git

Árbol de directorios (Working Tree)
↓ git add
Área de preparación (Staging / Index)
↓ git commit
Repositorio local (.git/)
↓ git push
Repositorio remoto (GitHub/GitLab/Gitea)

Cada commit es una INSTANTÁNEA completa, no un diff.
Git almacena el árbol completo de archivos en cada commit,
pero de forma eficiente (los archivos sin cambios se comparten).
El historial es una cadena de commits; cada uno apunta al anterior (padre).

Operaciones fundamentales

# Configuración inicial (solo una vez por máquina)
git config --global user.name "Tu Nombre"
git config --global user.email "tu@email.com"
git config --global core.editor "vim" # Editor para commits
git config --global init.defaultBranch main # Rama por defecto
git config --global pull.rebase true # pull = fetch + rebase (no merge)

# Iniciar un repositorio
git init /ruta/mi-proyecto # Nuevo repositorio
cd mi-proyecto

# El flujo básico
git status # Estado del árbol de trabajo
git add archivo.conf # Preparar un archivo
git add -p # Preparar interactivamente (hunk a hunk)
git commit -m "Descripción breve en imperativo"
git log --oneline --graph # Historial compacto con grafo de ramas
git diff # Cambios sin preparar
git diff --staged # Cambios preparados (van en el próximo commit)

# Deshacer cambios (con cuidado)
git restore archivo.conf # Descartar cambios en el working tree (IRREVERSIBLE)
git restore --staged archivo # Sacar del staging (sin perder los cambios)
git revert HEAD # Crear un commit que deshace el último (seguro, preserva historia)

Ramas y fusiones

# Crear y cambiar de rama
git branch feature/nueva-config # Crear rama
git switch feature/nueva-config # Cambiar a esa rama (nuevo en Git 2.23)
git switch -c fix/bug-critico # Crear y cambiar en un paso

# Fusionar
git switch main
git merge feature/nueva-config # Merge (crea un commit de merge si hay divergencia)
git merge --ff-only feature/limpia # Solo si es fast-forward (sin commit de merge)

# Ver el grafo de ramas
git log --oneline --graph --all

# Eliminar ramas ya fusionadas
git branch -d feature/nueva-config # -d: solo si ya fue fusionada
git branch -D feature/borrar-forzado # -D: forzar borrado (con cuidado)

# Resolución de conflictos
git merge feature-con-conflicto
# Git marca los conflictos en los archivos:
# <<<<<<< HEAD
# versión de main
# =======
# versión de la rama
# >>>>>>> feature-con-conflicto
#
# Editar el archivo, dejar la versión correcta, luego:
git add archivo-en-conflicto
git commit # Finalizar el merge

Trabajo con remotos

# Añadir un remoto
git remote add origin https://github.com/usuario/repo.git
git remote -v # Ver remotos configurados

# Flujo de trabajo con remoto
git fetch origin # Descargar sin fusionar
git pull # Fetch + rebase (según pull.rebase = true)
git push origin main # Publicar la rama main
git push -u origin feature/nueva # -u: rastrear el remoto (luego solo 'git push')
git push --tags # Publicar etiquetas

# Clonar un repositorio existente
git clone https://github.com/usuario/repo.git
git clone --depth 1 https://github.com/usuario/repo.git # Solo el último commit (más rápido)

# Etiquetas para versiones
git tag v1.0.0 # Etiqueta ligera
git tag -a v1.0.0 -m "Versión 1.0.0 estable" # Etiqueta anotada (recomendada)
git push origin v1.0.0

# .gitignore: no versionar lo que no debe versionarse
cat > .gitignore << 'EOF'
*.log
*.tmp
.env
.env.*
__pycache__/
node_modules/
*.pyc
/secrets/
EOF

Versionar /etc con etckeeper

# etckeeper: convierte /etc en un repositorio git automáticamente
sudo apt install etckeeper

# etckeeper se integra con apt: hace commit automático antes/después de instalar paquetes
# Ver el historial de cambios en /etc
cd /etc && sudo git log --oneline | head -20
sudo git diff HEAD~1 HEAD -- passwd # Ver qué cambió en /etc/passwd

# Commit manual cuando se edita un archivo de configuración
sudo vim /etc/nginx/sites-available/mi-sitio
sudo etckeeper commit "nginx: añadir sitio mi-sitio.ejemplo.com"

# Enviar el historial de /etc a un repositorio privado
cd /etc && sudo git remote add origin https://gitea.interno/admin/etc-servidor01.git
sudo git push -u origin master

18.2 — Git intermedio: flujos de trabajo avanzados

Rebase: historia lineal y limpia

# Rebase: reaplicar commits sobre otra base
# Útil para: mantener historia lineal, actualizar una rama con main

# Actualizar una feature branch con los cambios de main
git switch feature/mi-cambio
git rebase main # Reaplicar los commits de feature sobre main actualizado
# Resultado: feature aparece como si hubiera salido de main actual (historia lineal)

# Rebase interactivo: reescribir la historia local (NUNCA commits ya publicados)
git rebase -i HEAD~3 # Editar los últimos 3 commits
# En el editor se abren los commits con operaciones:
# pick → conservar tal cual
# reword → cambiar el mensaje
# edit → pausar para modificar el commit
# squash → combinar con el commit anterior (mantiene ambos mensajes)
# fixup → como squash pero descarta el mensaje de este commit
# drop → eliminar el commit

# Caso práctico: limpiar 5 commits de "WIP" antes de hacer PR
git rebase -i HEAD~5
# Cambiar "pick" a "fixup" en los commits intermedios
# → Un solo commit limpio en lugar de 5 commits de trabajo

stash, cherry-pick y bisect

# git stash: guardar cambios temporalmente sin hacer commit
git stash # Guardar cambios no commiteados
git stash push -m "fix a medias" # Con mensaje descriptivo
git stash list # Ver los stashes guardados
git stash pop # Aplicar el último stash y eliminarlo
git stash apply stash@{2} # Aplicar uno específico sin eliminarlo

# git cherry-pick: aplicar un commit específico en otra rama
# Caso: hay un fix urgente en main que necesitas en la rama de producción
git log --oneline main | head -5 # Buscar el hash del commit
git switch produccion
git cherry-pick abc1234 # Aplicar ese commit aquí
# Útil también para "rescatar" un commit que se hizo en la rama equivocada

# git bisect: encontrar el commit que introdujo un bug (búsqueda binaria)
git bisect start
git bisect bad HEAD # El estado actual está roto
git bisect good v1.2.0 # Esta versión funcionaba bien
# Git hace checkout de commits intermedios
# Probar si el bug está presente y decirle a Git:
git bisect good # Este commit está bien
git bisect bad # Este commit tiene el bug
# Git reduce el rango a la mitad cada vez
# Al final: "abc1234 is the first bad commit"
git bisect reset # Volver al estado original

# Automatizar bisect con un script de prueba
git bisect run ./tests/test-funcionalidad.sh

Firmar commits con GPG

# Firmar commits con GPG (enlaza con Módulo 14 — seguridad)
# Demuestras que TÚ hiciste el commit (no se puede suplantar)

# Ver las claves GPG disponibles (Módulo 14)
gpg --list-secret-keys --keyid-format LONG

# Configurar Git para firmar
git config --global user.signingkey TU_KEYID_LARGO
git config --global commit.gpgsign true # Firmar siempre
git config --global tag.gpgsign true # Firmar etiquetas también

# Hacer un commit firmado
git commit -S -m "feat: nueva funcionalidad (firmado)" # -S explícito
# Con commit.gpgsign = true: todos los commits se firman automáticamente

# Verificar la firma de un commit
git log --show-signature -1
git verify-commit HEAD

# En GitHub/GitLab: añadir tu clave pública GPG para que aparezca "Verified"
# GitHub: Settings → SSH and GPG keys → New GPG key
gpg --armor --export TU_KEYID | xclip # Copiar la clave pública

18.3 — Ansible: gestión de configuración

Por qué gestión de configuración

Problema: 50 servidores. Hay que instalar nginx con la misma configuración en todos.

Solución manual: SSH a cada servidor, ejecutar los mismos comandos 50 veces.
Error humano garantizado en al menos uno.

Solución con script: Un script bash que hace SSH a los 50 servidores.
¿Y si el servidor 23 falla a mitad? ¿Es el script idempotente?
¿Cómo sabes qué versión del script se ejecutó en cada servidor?

Solución con Ansible:
• Describe el ESTADO DESEADO (idempotente: si ya está instalado, no reinstala)
• Ejecuta en todos los servidores en paralelo (SSH, sin agente)
• El resultado es siempre predecible y reproducible
• La "documentación" ES el código (YAML legible por humanos)

Instalación y arquitectura

# Ansible no necesita nada instalado en los nodos gestionados (solo SSH + Python)
# Solo se instala en el nodo de control (tu máquina o un servidor de CI)

sudo apt install ansible
# o con pip (versión más reciente):
pip3 install ansible

ansible --version
# ansible [core 2.16.x]

# Arquitectura:
# Nodo de control (tú) → SSH → Nodos gestionados (los servidores)
# No hay agentes en los nodos, solo SSH y Python 3

Inventarios

# /etc/ansible/hosts o inventario.ini

# Hosts individuales
web01.ejemplo.com
web02.ejemplo.com

# Grupos de hosts
[webservers]
web01.ejemplo.com
web02.ejemplo.com
web03.ejemplo.com ansible_port=2222 # Variables por host

[databases]
db01.ejemplo.com ansible_user=ubuntu ansible_become=yes
db02.ejemplo.com

[production:children] # Grupo que contiene otros grupos
webservers
databases

# Inventario en YAML (más moderno)
# inventario.yaml:
# all:
# children:
# webservers:
# hosts:
# web01.ejemplo.com:
# ansible_port: 22
# web02.ejemplo.com:
# Inventario dinámico: para entornos cloud donde los hosts cambian
# Ansible tiene plugins para AWS EC2, Azure, GCP, Proxmox, etc.
ansible-inventory -i inventario.ini --list --yaml # Ver el inventario parsado

# Probar la conectividad con todos los hosts
ansible all -i inventario.ini -m ping
# Resultado esperado:
# web01.ejemplo.com | SUCCESS => {"ping": "pong"}
# web02.ejemplo.com | SUCCESS => {"ping": "pong"}

Comandos ad-hoc

# Comandos ad-hoc: ejecutar un módulo en hosts sin escribir un playbook
# Útil para operaciones puntuales, no repetitivas

# Ejecutar un comando en todos los webservers
ansible webservers -i inventario.ini -m command -a "uptime"
ansible webservers -i inventario.ini -m shell -a "df -h | grep -v tmp"

# Instalar un paquete en todos los servidores
ansible webservers -i inventario.ini -m apt -a "name=nginx state=present" --become

# Copiar un archivo
ansible webservers -m copy -a "src=nginx.conf dest=/etc/nginx/nginx.conf" --become

# Reiniciar un servicio
ansible webservers -m service -a "name=nginx state=restarted" --become

# Recopilar facts (información del sistema)
ansible web01.ejemplo.com -m setup | grep ansible_distribution

# Módulos importantes (hay más de 3000):
# apt/yum/dnf: gestión de paquetes
# copy: copiar archivos
# template: copiar plantillas Jinja2
# service: gestionar servicios
# file: crear/eliminar/permisos de archivos y directorios
# user: gestionar usuarios
# cron: gestionar crontabs
# git: clonar/actualizar repositorios
# command/shell: ejecutar comandos (shell soporta pipes y redirecciones)

Playbooks

# playbooks/instalar-nginx.yaml
---
- name: Instalar y configurar Nginx
hosts: webservers # O 'all', o un host específico
become: yes # sudo
gather_facts: yes # Recopilar información del sistema

vars:
nginx_port: 80
server_name: "{{ ansible_fqdn }}" # Fact del sistema
documento_raiz: /var/www/html

tasks:
- name: Actualizar caché APT
apt:
update_cache: yes
cache_valid_time: 3600 # Solo actualizar si la caché tiene > 1 hora

- name: Instalar Nginx
apt:
name: nginx
state: present # present: instalar si no está; latest: actualizar siempre

- name: Asegurar que Nginx está activo y habilitado al arranque
service:
name: nginx
state: started
enabled: yes

- name: Crear directorio del sitio web
file:
path: "{{ documento_raiz }}"
state: directory
owner: www-data
group: www-data
mode: '0755'

- name: Desplegar la configuración de nginx
template:
src: templates/nginx-sitio.conf.j2
dest: /etc/nginx/sites-available/mi-sitio.conf
owner: root
group: root
mode: '0644'
notify: Recargar Nginx # Notificar al handler si hubo cambio

- name: Habilitar el sitio
file:
src: /etc/nginx/sites-available/mi-sitio.conf
dest: /etc/nginx/sites-enabled/mi-sitio.conf
state: link

handlers:
# Los handlers se ejecutan SOLO si fueron notificados y AL FINAL del play
- name: Recargar Nginx
service:
name: nginx
state: reloaded # reload, no restart (sin downtime)
# Ejecutar un playbook
ansible-playbook -i inventario.ini playbooks/instalar-nginx.yaml

# Opciones útiles
ansible-playbook ... --check # Dry run: simular sin ejecutar (no siempre fiable)
ansible-playbook ... --diff # Mostrar diff de archivos que cambiarían
ansible-playbook ... --limit web01 # Ejecutar solo en un host
ansible-playbook ... --tags "nginx" # Solo tareas con ese tag
ansible-playbook ... --start-at-task "Crear directorio" # Reanudar desde una tarea
ansible-playbook ... -v / -vv / -vvv # Verbosidad creciente para debug

18.4 — Ansible: nivel práctico

Plantillas Jinja2

{# templates/nginx-sitio.conf.j2 #}
server {
listen {{ nginx_port }};
listen [::]:{{ nginx_port }};
server_name {{ server_name }};

root {{ documento_raiz }};
index index.html;

{% if nginx_ssl_enabled | default(false) %}
ssl_certificate /etc/letsencrypt/live/{{ server_name }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ server_name }}/privkey.pem;
{% endif %}

access_log /var/log/nginx/{{ server_name }}.access.log;
error_log /var/log/nginx/{{ server_name }}.error.log;

{# Iterar sobre una lista de rutas bloqueadas #}
{% for ruta in rutas_bloqueadas | default([]) %}
location {{ ruta }} { deny all; }
{% endfor %}

location / {
try_files $uri $uri/ =404;
}
}

Roles: estructura reutilizable

# Un rol encapsula todo lo relacionado con un servicio: tareas, variables,
# handlers, plantillas, archivos por defecto.

# Crear la estructura de un rol
ansible-galaxy role init roles/nginx
# Genera:
# roles/nginx/
# ├── tasks/main.yaml ← tareas principales
# ├── handlers/main.yaml ← handlers
# ├── templates/ ← plantillas Jinja2
# ├── files/ ← archivos estáticos
# ├── vars/main.yaml ← variables con mayor precedencia
# ├── defaults/main.yaml ← variables con menor precedencia (sobreescribibles)
# ├── meta/main.yaml ← metadatos (dependencias, autor)
# └── README.md

# Usar el rol en un playbook
cat > site.yaml << 'EOF'
---
- name: Configurar servidores web
hosts: webservers
become: yes
roles:
- nginx # Aplica el rol roles/nginx/
- { role: certbot, when: nginx_ssl_enabled }
EOF

ansible-vault: secretos cifrados

# ansible-vault cifra archivos sensibles con AES-256
# (Enlaza con Módulo 14: cifrado y gestión de secretos)

# Crear un archivo de variables cifrado
ansible-vault create vars/secretos.yaml
# Pide contraseña, abre el editor. Escribir las variables:
# db_password: "S3cr3t0_d3_producci0n"
# api_key: "sk-abc123xyz..."

# Cifrar un archivo existente
ansible-vault encrypt vars/secretos.yaml

# Ver el contenido sin descifrar (cifrado)
cat vars/secretos.yaml # Solo texto cifrado ilegible

# Editar un archivo cifrado
ansible-vault edit vars/secretos.yaml

# Descifrar
ansible-vault decrypt vars/secretos.yaml # Deja el archivo en claro

# Usar en un playbook
cat >> site.yaml << 'EOF'
vars_files:
- vars/secretos.yaml # Ansible lo descifra automáticamente al ejecutar
EOF

# Ejecutar con la contraseña
ansible-playbook site.yaml --ask-vault-pass
# o con un archivo de contraseña (para CI/CD)
echo "mi_contraseña_vault" > ~/.vault_pass && chmod 600 ~/.vault_pass
ansible-playbook site.yaml --vault-password-file ~/.vault_pass

18.5 — Infraestructura como código

Conceptos fundamentales

Infraestructura COMO código (IaC):
Describir la infraestructura (servidores, redes, DNS, load balancers)
en archivos de texto que se pueden versionar, revisar y reutilizar.

Declarativo vs. Imperativo:
Imperativo (scripts bash): "Haz esto, luego esto, luego aquello"
Declarativo (Terraform): "Quiero que el estado sea ESTE"
→ Terraform averigua qué hacer para llegar ahí

Idempotencia:
Ejecutar el mismo código dos veces produce el mismo resultado.
Terraform aplica solo los cambios necesarios, no re-crea todo.

Drift:
Cuando la infraestructura real diverge del código que la describe
(alguien hizo un cambio manual). Terraform detecta el drift.

Terraform / OpenTofu

# Terraform (HashiCorp, BSL license) / OpenTofu (fork open source, MPL 2.0)
# OpenTofu es compatible con Terraform y es la alternativa comunitaria libre
# https://opentofu.org/

# Instalar OpenTofu
curl -fsSL https://get.opentofu.org/install-opentofu.sh | sudo bash -s -- --install-method deb

tofu --version
# main.tf — ejemplo con el proveedor de libvirt (crear VMs KVM locales)
# Para producción usar: aws, google, azurerm, hcloud (Hetzner), etc.

terraform {
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
version = "~> 0.7"
}
}
}

provider "libvirt" {
uri = "qemu:///system"
}

# Variables parametrizables
variable "vm_count" {
default = 2
description = "Número de VMs a crear"
}

variable "vm_memory" {
default = 1024
description = "RAM en MB"
}

# Imagen base (cloud image de Ubuntu)
resource "libvirt_volume" "ubuntu_base" {
name = "ubuntu-22.04.qcow2"
pool = "default"
source = "https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img"
format = "qcow2"
}

# Discos para cada VM (aprovisionados desde la imagen base)
resource "libvirt_volume" "vm_disk" {
count = var.vm_count
name = "nodo-${count.index + 1}.qcow2"
pool = "default"
base_volume_id = libvirt_volume.ubuntu_base.id
size = 21474836480 # 20 GB
}

# cloud-init para configurar los nodos
resource "libvirt_cloudinit_disk" "init" {
count = var.vm_count
name = "init-${count.index + 1}.iso"
pool = "default"
user_data = templatefile("cloud-init.yaml.tpl", {
hostname = "nodo-${count.index + 1}"
ssh_key = file("~/.ssh/id_ed25519.pub")
})
}

# Las VMs
resource "libvirt_domain" "vm" {
count = var.vm_count
name = "nodo-${count.index + 1}"
memory = var.vm_memory
vcpu = 1

disk {
volume_id = libvirt_volume.vm_disk[count.index].id
}

cloudinit = libvirt_cloudinit_disk.init[count.index].id

network_interface {
network_name = "default"
wait_for_lease = true
}
}

# Salidas: información accesible después del apply
output "vm_ips" {
value = [for vm in libvirt_domain.vm : vm.network_interface[0].addresses[0]]
}
# Ciclo de vida de Terraform/OpenTofu
tofu init # Descargar proveedores y módulos
tofu validate # Verificar sintaxis del HCL
tofu plan # Mostrar qué va a crear/modificar/destruir (sin ejecutar)
tofu apply # Aplicar los cambios (pide confirmación)
tofu apply -auto-approve # Sin confirmación (para CI/CD)
tofu destroy # Destruir toda la infraestructura gestionada

# Estado: Terraform guarda el estado de la infraestructura en terraform.tfstate
# Para trabajo en equipo: guardar el estado en un backend remoto (S3, GCS, Terraform Cloud)
tofu state list # Ver todos los recursos gestionados
tofu state show libvirt_domain.vm[0] # Ver el estado de un recurso específico

cloud-init: configuración en el primer arranque

# cloud-init.yaml.tpl — configuración inicial de VMs
#cloud-config
hostname: ${hostname}
fqdn: ${hostname}.local.lab

users:
- name: admin
groups: [sudo]
sudo: ALL=(ALL) NOPASSWD:ALL
ssh_authorized_keys:
- ${ssh_key}
shell: /bin/bash

# Paquetes a instalar en el primer arranque
packages:
- vim
- curl
- htop
- qemu-guest-agent # Para que KVM/libvirt pueda obtener la IP

package_update: true
package_upgrade: true

# Comandos a ejecutar al final
runcmd:
- systemctl enable --now qemu-guest-agent
- timedatectl set-timezone Europe/Madrid
- echo "cloud-init completado en $(date)" >> /var/log/cloud-init.custom.log

18.6 — CI/CD: integración y despliegue continuos

Conceptos

CI (Integración Continua):
Cada push al repositorio dispara automáticamente:
• Lint: ¿el código tiene errores de sintaxis/estilo?
• Tests: ¿las pruebas pasan?
• Build: ¿la imagen/binario se construye correctamente?

Objetivo: detectar errores LO ANTES POSIBLE (en el push, no en producción)

CD (Despliegue Continuo / Entrega Continua):
Si CI pasa, el código se despliega automáticamente:
• Continuous Delivery: se despliega a staging automáticamente;
producción requiere aprobación manual.
• Continuous Deployment: todo va a producción automáticamente
si los tests pasan (requiere tests muy robustos)

GitHub Actions

# .github/workflows/ci.yaml
name: CI/CD Pipeline

on:
push:
branches: [main, develop]
pull_request:
branches: [main]

env:
IMAGE_NAME: mi-app

jobs:
# JOB 1: Lint y análisis estático
lint:
name: Lint y comprobaciones
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: ShellCheck (scripts bash — Módulo 10)
uses: ludeeus/action-shellcheck@master
with:
scandir: './scripts'

- name: Lint Ansible playbooks
run: |
pip install ansible-lint
ansible-lint playbooks/*.yaml

- name: Verificar archivos YAML
run: |
pip install yamllint
yamllint .

# JOB 2: Tests
test:
name: Tests
runs-on: ubuntu-latest
needs: lint # Solo si lint pasó
steps:
- uses: actions/checkout@v4

- name: Ejecutar tests de Ansible (molecule)
run: |
pip install molecule molecule-docker ansible
cd roles/nginx && molecule test

# JOB 3: Build de imagen Docker (Módulo 16)
build:
name: Build Docker image
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- uses: actions/checkout@v4

- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login a GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build y push imagen
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.ref == 'refs/heads/main' }} # Solo en main
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max

# JOB 4: Despliegue (solo en main)
deploy:
name: Desplegar en producción
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'
environment: production # Requiere aprobación manual (si se configura)
steps:
- uses: actions/checkout@v4

- name: Desplegar con Ansible
env:
ANSIBLE_HOST_KEY_CHECKING: false
VAULT_PASS: ${{ secrets.ANSIBLE_VAULT_PASS }}
run: |
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
echo "$VAULT_PASS" > ~/.vault_pass
ansible-playbook -i inventario/produccion.ini \
--vault-password-file ~/.vault_pass \
playbooks/deploy.yaml

GitLab CI

# .gitlab-ci.yaml — equivalente para GitLab
stages:
- lint
- test
- build
- deploy

variables:
IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

# Lint en paralelo con múltiples trabajos
shellcheck:
stage: lint
image: koalaman/shellcheck-alpine
script:
- find scripts/ -name "*.sh" -exec shellcheck {} +

ansible-lint:
stage: lint
image: python:3.11
script:
- pip install ansible-lint
- ansible-lint playbooks/

test:
stage: test
image: python:3.11
script:
- pip install molecule ansible
- cd roles/nginx && molecule test
artifacts:
when: always
reports:
junit: roles/nginx/.molecule/default/junit.xml

build-image:
stage: build
image: docker:24
services:
- docker:24-dind
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $IMAGE .
- docker push $IMAGE
only:
- main

deploy-produccion:
stage: deploy
image: python:3.11
before_script:
- pip install ansible
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | ssh-add -
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
script:
- ansible-playbook -i inventario/prod.ini
--vault-password-file <(echo "$VAULT_PASS")
playbooks/deploy.yaml
environment:
name: production
url: https://mi-app.ejemplo.com
when: manual # Despliegue manual: alguien tiene que pulsar el botón
only:
- main

18.7 — Copias de seguridad en serio

La estrategia 3-2-1

Regla 3-2-1 (estándar de la industria):
3 copias de los datos
2 en soportes o ubicaciones diferentes
1 fuera del sitio (offsite)

Ejemplo práctico:
Copia 1: Los datos originales en producción
Copia 2: Backup local en un disco separado / NAS
Copia 3: Backup remoto (S3, B2, servidor de otro datacenter)

RPO (Recovery Point Objective): ¿Cuánto tiempo de datos podemos perder?
Ejemplo: "Podemos perder hasta 24 horas de pedidos" → backup diario

RTO (Recovery Time Objective): ¿En cuánto tiempo tenemos que recuperar?
Ejemplo: "Tenemos que volver a estar online en 4 horas" → prueba de restauración ≤ 4h

La pregunta crítica: ¿Cuándo fue la última vez que PROBASTE la restauración?
Un backup que no se ha restaurado nunca NO ES UN BACKUP REAL.

restic: backups cifrados modernos

# restic: herramienta de backup moderna, deduplicante y cifrada
# https://restic.net/

sudo apt install restic

# 1. Inicializar un repositorio (local o remoto)
restic init --repo /backup/repositorio # Local
restic init --repo s3:s3.amazonaws.com/mi-bucket/backups # S3

# El repositorio está siempre cifrado con una contraseña
# GUARDAR LA CONTRASEÑA: sin ella los backups son irrecuperables

# 2. Hacer un backup
restic --repo /backup/repositorio backup /var/www /etc /home
# restic deduplica automáticamente: solo guarda bloques nuevos

# Variables de entorno (para no pasar la contraseña en la línea de comandos)
export RESTIC_REPOSITORY=/backup/repositorio
export RESTIC_PASSWORD="mi_contraseña_segura"
restic backup /var/www /etc

# 3. Listar las instantáneas (snapshots)
restic snapshots
# ID Fecha Archivos Tamaño
# abc12345 2024-01-01 02:00:00 12345 234 MiB
# def67890 2024-01-02 02:00:00 12347 2.3 MiB (solo los cambios)

# 4. Comprobar el repositorio (integridad)
restic check # Verificar integridad
restic check --read-data # Verificar Y leer todos los datos (lento pero completo)

# 5. RESTAURAR (la prueba más importante)
restic restore latest --target /tmp/restauracion --include /etc/nginx
ls /tmp/restauracion/etc/nginx/ # Los archivos restaurados

# Restaurar un snapshot específico
restic restore abc12345 --target /tmp/restauracion-old

# 6. Limpieza: política de retención
restic forget \
--keep-daily 7 \ # Mantener un backup por día durante 7 días
--keep-weekly 4 \ # Un backup por semana durante 4 semanas
--keep-monthly 12 # Un backup por mes durante 12 meses

restic forget ... --prune # Eliminar físicamente los datos huérfanos (después de forget)

# Script de backup con cron (Módulo 09)
sudo tee /usr/local/bin/backup-diario.sh << 'BACKUP'
#!/bin/bash
set -euo pipefail
export RESTIC_REPOSITORY=/backup/repositorio
export RESTIC_PASSWORD_FILE=/etc/restic/password # chmod 600

restic backup /var/www /etc /home/admin \
--exclude-file /etc/restic/excludes \
--tag "$(hostname)" \
--tag "diario"

restic check --quiet

restic forget \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 6 \
--prune

echo "Backup completado: $(date)"
BACKUP
chmod 755 /usr/local/bin/backup-diario.sh

# Programar con systemd timer (mejor que cron para producción)
# /etc/systemd/system/backup.service y backup.timer (ver Módulo 09)

borgbackup: deduplicación y compresión avanzada

# BorgBackup: alternativa a restic, muy popular para backups SSH
sudo apt install borgbackup

# Inicializar repositorio (puede ser local o remoto vía SSH)
borg init --encryption=repokey-blake2 /backup/borg-repo
# O en un servidor remoto:
# borg init --encryption=repokey-blake2 usuario@backup-server:/backups/servidor01

# Backup
borg create --stats --progress \
/backup/borg-repo::"{hostname}-{now:%Y-%m-%d}" \
/var/www /etc /home \
--exclude /var/www/cache

# Listar archivos en un snapshot
borg list /backup/borg-repo
borg list /backup/borg-repo::servidor01-2024-01-01

# Restaurar
borg extract /backup/borg-repo::servidor01-2024-01-01 var/www/html

# Política de retención
borg prune \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 12 \
--list \
/backup/borg-repo

# Verificar la integridad
borg check /backup/borg-repo

Simulacro de restauración (obligatorio en producción)

#!/bin/bash
# Script de simulacro: prueba que los backups se pueden restaurar
# Ejecutar MENSUALMENTE (o más frecuente para sistemas críticos)

REPO=/backup/repositorio
TARGET=/tmp/simulacro-$(date +%Y%m%d)

echo "=== INICIO SIMULACRO DE RESTAURACIÓN $(date) ==="

# 1. Verificar integridad del repositorio
echo "[1/4] Verificando integridad del repositorio..."
restic --repo $REPO check || { echo "ERROR: Repositorio corrupto"; exit 1; }

# 2. Restaurar en un directorio temporal
echo "[2/4] Restaurando última instantánea..."
mkdir -p $TARGET
restic --repo $REPO restore latest --target $TARGET

# 3. Verificar que los archivos críticos existen
echo "[3/4] Verificando archivos críticos..."
for archivo in etc/nginx/sites-enabled etc/ssh/sshd_config var/www; do
if [ -e "$TARGET/$archivo" ]; then
echo " ✓ $archivo"
else
echo " ✗ FALTA: $archivo"
FALLO=1
fi
done

# 4. Probar la configuración restaurada (ejemplo: nginx)
echo "[4/4] Verificando configuración de nginx restaurada..."
nginx -t -c "$TARGET/etc/nginx/nginx.conf" 2>/dev/null && \
echo " ✓ Configuración nginx válida" || \
echo " ✗ Configuración nginx inválida"

# Limpiar
rm -rf $TARGET

# Resultado
if [ "${FALLO:-0}" = "1" ]; then
echo "=== SIMULACRO FALLIDO: revisar backups ==="
exit 1
else
echo "=== SIMULACRO EXITOSO: restauración verificada el $(date) ==="
fi

18.8 — El ecosistema DevOps desde Linux

El mapa del ecosistema

PLANIFICACIÓN Y SEGUIMIENTO
Jira, Linear, GitHub Issues, GitLab Issues

CÓDIGO Y CONTROL DE VERSIONES
Git → GitHub / GitLab / Gitea (autohospedado)
Gestión de configuración: etckeeper, vcsh, chezmoi (dotfiles)

CONSTRUCCIÓN Y TESTING
CI: GitHub Actions, GitLab CI, Jenkins, Forgejo Actions
Análisis estático: ShellCheck (Bash), Bandit (Python), SonarQube
Tests: pytest, Jest, Molecule (Ansible), kitchen (Chef)

CONTENEDORES Y ORQUESTACIÓN (Módulo 16)
Imágenes: Docker Build, Podman, Buildah
Registros: Docker Hub, GHCR, GitLab Registry, Harbor (privado)
Orquestación: Kubernetes, k3s, Nomad

INFRAESTRUCTURA COMO CÓDIGO
Provisionamiento: Terraform/OpenTofu, Pulumi
Configuración: Ansible, Chef, Puppet, Salt
Imágenes de SO: Packer (HashiCorp)

OBSERVABILIDAD (Módulo 15)
Métricas: Prometheus + Grafana, Datadog, Zabbix
Logs: ELK (Elasticsearch + Logstash + Kibana), Loki + Grafana
Trazas: Jaeger, OpenTelemetry
Alertas: Alertmanager, PagerDuty, OpsGenie

DESPLIEGUE Y GESTIÓN DE RELEASES
Estrategias: rolling, blue-green, canary
GitOps: ArgoCD, Flux (sincronizar k8s con el repositorio Git)

SRE: Site Reliability Engineering

SRE (Google, 2003) = Software Engineering + Sistemas
• Los "sysadmins" de Google pero con mentalidad de software

Conceptos clave de SRE:
SLI (Service Level Indicator): métrica real del servicio (latencia p99, error rate)
SLO (Service Level Objective): objetivo interno (p99 < 200ms, 99.9% disponibilidad)
SLA (Service Level Agreement): compromiso con el cliente (99.9% de uptime o penalización)

Error Budget: la cantidad de downtime/errores que "nos podemos permitir"
99.9% uptime = 8.7h downtime/año de budget
Si el error budget se agota → parar cambios y estabilizar

Toil: trabajo manual, repetitivo y automatable
→ SRE dedica al menos 50% del tiempo a reducir el toil automatizando

Blameless Postmortems:
Cuando hay un incidente: analizar QUÉ falló en el SISTEMA
(no QUIÉN cometió el error)
→ Producen mejoras del sistema; la culpa no produce nada útil

Linux en la nube

# Los grandes proveedores ofrecen instancias Linux con su CLI

# AWS CLI
aws ec2 describe-instances --query 'Reservations[].Instances[].{ID:InstanceId,IP:PublicIpAddress,State:State.Name}'
aws ec2 start-instances --instance-ids i-0123456789abcdef0
aws s3 cp /backup/bd.sql.gz s3://mi-bucket/backups/

# Google Cloud (gcloud)
gcloud compute instances list
gcloud compute ssh mi-servidor --zone=europe-west1-b

# Azure CLI
az vm list --output table
az vm start --name mi-vm --resource-group mi-grupo

# Patrones comunes de Linux en la nube:
# • Imágenes predefinidas (AMI, Compute Image, Azure Image)
# • Instancias de tipo spot/preemptible: 60-90% más baratas, pueden interrumpirse
# • Auto Scaling Groups: escalar el número de VMs según la carga
# • Load Balancer: distribuir tráfico entre instancias
# • Almacenamiento: S3/GCS/Azure Blob para objetos, EBS/Persistent Disk para bloques

18.9 — Flujo DevOps completo: de la idea a producción

Ciclo completo integrado:

1. DESARROLLAR
git checkout -b feature/nueva-funcionalidad
# Codificar, probar localmente con docker compose
git add -p && git commit -S -m "feat: ..."
git push origin feature/nueva-funcionalidad

2. REVISAR
# Pull Request / Merge Request en GitHub/GitLab
# CI dispara automáticamente: lint + tests + build
# Code review por un compañero
# Si CI pasa y hay aprobación → merge a main

3. INTEGRAR (CI automático en main)
# ShellCheck, yamllint, ansible-lint
# Tests de Ansible con Molecule
# Build de imagen Docker: docker build + trivy scan
# Push al registro: ghcr.io/usuario/mi-app:sha

4. DESPLEGAR (CD)
# Staging: automático al hacer merge a main
# Producción: manual (botón en GitLab/GitHub)
# Ansible deploy.yaml:
# → Descarga la nueva imagen del registro
# → Actualiza el contenedor (Quadlet/Compose)
# → Verifica health check
# → Notifica por correo (Postfix del Módulo 17)

5. OBSERVAR (Módulo 15)
# Prometheus scrape cada 15s
# Grafana dashboard muestra la nueva versión
# Alertmanager notifica si error rate > 1%
# journald recoge los logs del contenedor

6. RESPONDER
# Incidente: blameless postmortem
# Fix urgente: cherry-pick + hotfix branch + despliegue rápido
# Rollback: kubectl rollout undo o docker run imagen:version-anterior

Anexos

A. Flujos de trabajo Git más usados

SituaciónComando
Deshacer el último commit (sin perder cambios)git reset HEAD~1
Deshacer el último commit (y los cambios)git reset --hard HEAD~1
Recuperar un archivo de otro commitgit checkout SHA -- archivo
Ver quién escribió cada líneagit blame archivo
Buscar en la historia de todos los commitsgit log -S "término_buscado"
Ver los cambios de un commit concretogit show SHA
Limpiar archivos no rastreadosgit clean -fd

B. Interrelaciones con otros módulos

◀ Módulo 07 — Usuarios y permisos
│ Permisos en los scripts de backup, usuario de Ansible, claves SSH

◀ Módulo 09 — Procesos y systemd
│ Timers de systemd para cron de backups, unidades para apps gestionadas

◀ Módulo 10 — Shell Scripting
│ Los scripts de CI/CD y backup son Bash; ShellCheck los valida

◀ Módulo 14 — Seguridad
│ GPG para firmar commits y ansible-vault, SSH para inventario Ansible
│ Secretos: nunca en el repositorio, siempre en vault/secrets del CI

◀ Módulo 15 — Monitorización
│ El pipeline de CD puede verificar métricas tras el despliegue
│ Las alertas de Prometheus notifican fallos en el despliegue

◀ Módulo 16 — Contenedores
│ CI construye imágenes Docker; CD las despliega en producción
│ GitOps: ArgoCD sincroniza el cluster k8s con los manifiestos del repo

◀ Módulo 17 — Linux como servidor
│ Ansible gestiona los servidores del Módulo 17 de forma idempotente
│ El pipeline puede desplegar en el servidor nginx del Módulo 17

Referencias y Bibliografía

  1. Pro Git, 2ª ed. — Scott Chacon, Ben Straub
    https://git-scm.com/book/es/v2 (disponible libre online en español)

  2. Git documentation
    https://git-scm.com/docs

  3. Ansible documentation
    https://docs.ansible.com/

  4. Ansible: Up and Running, 3ª ed. — Bas Meijer, Lorin Hochstein, René Moser
    O'Reilly, 2022.

  5. Terraform: Up and Running, 3ª ed. — Yevgeniy Brikman
    O'Reilly, 2022.

  6. OpenTofu documentation
    https://opentofu.org/docs/

  7. GitHub Actions documentation
    https://docs.github.com/en/actions

  8. GitLab CI/CD documentation
    https://docs.gitlab.com/ee/ci/

  9. restic documentation
    https://restic.readthedocs.io/

  10. BorgBackup documentation
    https://borgbackup.readthedocs.io/

  11. The Site Reliability Workbook — Beyer, Murphy, et al.
    O'Reilly, 2018. https://sre.google/workbook/table-of-contents/

  12. Site Reliability Engineering — Beyer, Jones, Petoff, Murphy
    O'Reilly, 2016. https://sre.google/sre-book/table-of-contents/ (libre online)

  13. The DevOps Handbook, 2ª ed. — Kim, Humble, Debois, Willis
    IT Revolution Press, 2021.

  14. Accelerate — Forsgren, Humble, Kim
    IT Revolution Press, 2018. (Evidencia científica de las prácticas DevOps)

  15. etckeeper — Joey Hess
    https://etckeeper.branchable.com/

  16. ShellCheck documentation
    https://www.shellcheck.net/

  17. Molecule: testing framework para Ansible
    https://ansible.readthedocs.io/projects/molecule/

  18. cloud-init documentation
    https://cloud-init.readthedocs.io/

  19. Mozilla Hacks: CI/CD con GitHub Actions
    https://hacks.mozilla.org/

  20. AWS CLI documentation
    https://docs.aws.amazon.com/cli/latest/userguide/

  21. Jinja2 documentation
    https://jinja.palletsprojects.com/


Preguntas de autoevaluación

  1. ¿Cuál es la diferencia entre git merge y git rebase? ¿Cuándo es preferible cada uno? ¿Por qué no se debe hacer rebase de commits ya publicados?
  2. ¿Para qué sirve git bisect? Describe paso a paso cómo lo usarías para encontrar el commit que introdujo un bug de rendimiento.
  3. Explica el principio de idempotencia en el contexto de Ansible. ¿Por qué usar el módulo apt en lugar de command: apt install nginx?
  4. ¿Qué es un handler en Ansible y cuándo se ejecuta? ¿Por qué state: reloaded es preferible a state: restarted para un servidor web en producción?
  5. ¿Cuál es la diferencia entre vars y defaults en un rol de Ansible? ¿Cuál tiene mayor precedencia?
  6. ¿Qué almacena el archivo terraform.tfstate? ¿Por qué es crítico no borrarlo y por qué no debe commitearse en el repositorio?
  7. Explica la diferencia entre CI (Integración Continua) y CD (Entrega/Despliegue Continuos). ¿Cuándo elegiría despliegue continuo vs. entrega continua con aprobación manual?
  8. En GitHub Actions, ¿para qué sirve needs: en un job? ¿Qué pasaría si no se usa y por qué puede ser un problema de seguridad?
  9. ¿Qué significa la regla 3-2-1 de backups? Pon un ejemplo concreto con herramientas reales.
  10. ¿Cuál es la diferencia entre RPO y RTO? ¿Cómo influyen en la frecuencia de los backups y en la infraestructura necesaria para la recuperación?
  11. ¿Por qué un backup que no se ha probado restaurando no es un backup real? ¿Con qué frecuencia deberías probar la restauración en un sistema crítico?
  12. ¿Qué es el "toil" en el contexto de SRE y por qué es importante medirlo y reducirlo?

Laboratorios prácticos

Lab 18.1 — Git: flujo completo con firma GPG

# 1. Configurar Git (si no está configurado)
git config --global user.name "$(whoami)"
git config --global user.email "admin@ejemplo.com"

# 2. Crear un repositorio de prueba
mkdir /tmp/lab-git && cd /tmp/lab-git
git init && git switch -c main

# 3. Primer commit
echo "# Configuración del servidor" > README.md
echo "hostname: lab-server" > config.yaml
git add .
git commit -m "feat: configuración inicial del servidor"

# 4. Crear una rama de feature
git switch -c feature/nginx-config
echo "nginx_port: 80" >> config.yaml
git add config.yaml
git commit -m "feat(nginx): añadir configuración de puerto"

# 5. Volver a main y simular un cambio paralelo
git switch main
echo "timezone: Europe/Madrid" >> config.yaml
git add config.yaml
git commit -m "chore: configurar zona horaria"

# 6. Rebase de feature sobre main
git switch feature/nginx-config
git rebase main

# 7. Ver el historial lineal resultante
git log --oneline --graph --all

# 8. Hacer bisect para "encontrar un bug"
git switch main
echo "bug_aqui: true" >> config.yaml
git add config.yaml && git commit -m "bug: error introducido"
echo "fix: correccion" >> config.yaml
git add config.yaml && git commit -m "fix: corrección posterior"

git bisect start
git bisect bad HEAD
git bisect good HEAD~2
git bisect run grep -l "bug_aqui: true" config.yaml
git bisect reset

cd - && rm -rf /tmp/lab-git

Lab 18.2 — Ansible: ping al localhost

# Verificar que Ansible puede gestionar localhost
ansible localhost -m ping
# Si falla, puede necesitar: ansible localhost -m ping -c local

# Ver los facts de localhost
ansible localhost -m setup 2>/dev/null | grep -E "ansible_(distribution|kernel|processor_cores)"

# Crear un playbook mínimo que cree un archivo
cat > /tmp/lab-playbook.yaml << 'EOF'
---
- name: Lab Ansible básico
hosts: localhost
connection: local
gather_facts: yes
tasks:
- name: Crear archivo de prueba
copy:
content: |
Ansible funcionando en {{ ansible_hostname }}
Sistema: {{ ansible_distribution }} {{ ansible_distribution_version }}
Fecha: {{ ansible_date_time.iso8601 }}
dest: /tmp/ansible-lab.txt
mode: '0644'

- name: Mostrar el contenido creado
command: cat /tmp/ansible-lab.txt
register: contenido

- name: Imprimir resultado
debug:
var: contenido.stdout_lines
EOF

ansible-playbook /tmp/lab-playbook.yaml
cat /tmp/ansible-lab.txt
rm /tmp/ansible-lab.txt /tmp/lab-playbook.yaml

Lab 18.3 — ansible-vault: cifrar un secreto

# 1. Crear un archivo de variables con secretos
mkdir -p /tmp/lab-vault
cat > /tmp/lab-vault/secretos.yaml << 'EOF'
db_password: "contraseña_muy_segura_123"
api_key: "sk-test-abc123"
EOF

# 2. Cifrar el archivo
echo "mi_clave_vault_lab" > /tmp/lab-vault/.vault_pass
ansible-vault encrypt /tmp/lab-vault/secretos.yaml \
--vault-password-file /tmp/lab-vault/.vault_pass

echo "=== Contenido cifrado ==="
cat /tmp/lab-vault/secretos.yaml | head -3
echo ""

# 3. Usar en un playbook
cat > /tmp/lab-vault/playbook.yaml << 'EOF'
---
- name: Lab vault
hosts: localhost
connection: local
vars_files:
- secretos.yaml
tasks:
- name: Verificar que la variable existe (sin mostrar el valor)
debug:
msg: "Longitud de db_password: {{ db_password | length }}"
EOF

ansible-playbook /tmp/lab-vault/playbook.yaml \
--vault-password-file /tmp/lab-vault/.vault_pass

# 4. Descifrar para verificar
ansible-vault decrypt /tmp/lab-vault/secretos.yaml \
--vault-password-file /tmp/lab-vault/.vault_pass
cat /tmp/lab-vault/secretos.yaml

rm -rf /tmp/lab-vault

Lab 18.4 — restic: backup y restauración

# 1. Crear datos de prueba
mkdir -p /tmp/lab-datos/{web,config,logs}
echo "<html><body>Mi web</body></html>" > /tmp/lab-datos/web/index.html
echo "db_host=localhost" > /tmp/lab-datos/config/app.conf
echo "2024-01-01 ERROR: algo falló" > /tmp/lab-datos/logs/app.log

# 2. Inicializar repositorio restic
export RESTIC_REPOSITORY=/tmp/lab-restic
export RESTIC_PASSWORD="lab_password_123"
restic init

# 3. Hacer el backup
restic backup /tmp/lab-datos
restic snapshots

# 4. Simular pérdida de datos
rm -rf /tmp/lab-datos/web/

# 5. RESTAURAR
restic restore latest --target /tmp/lab-restaurado \
--include "lab-datos/web"
echo "=== Datos restaurados ==="
ls /tmp/lab-restaurado/tmp/lab-datos/web/
cat /tmp/lab-restaurado/tmp/lab-datos/web/index.html

# 6. Verificar integridad
restic check

# Limpiar
rm -rf /tmp/lab-datos /tmp/lab-restic /tmp/lab-restaurado
unset RESTIC_REPOSITORY RESTIC_PASSWORD

Lab 18.5 — GitHub Actions: pipeline básico (análisis estático)

# Crear la estructura de un repositorio con GitHub Actions
mkdir -p /tmp/lab-actions/.github/workflows

# Script bash que vamos a analizar con ShellCheck
cat > /tmp/lab-actions/deploy.sh << 'EOF'
#!/bin/bash
# ShellCheck debe detectar: comillas faltantes, variables sin declarar, etc.
SERVER=$1
echo "Desplegando en $SERVER"
if [ $SERVER = "" ]; then
echo "Error: no hay servidor"
fi
EOF

# Pipeline de GitHub Actions
cat > /tmp/lab-actions/.github/workflows/ci.yaml << 'EOF'
name: CI
on: [push, pull_request]
jobs:
shellcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: ShellCheck
run: |
sudo apt install shellcheck -y -q
shellcheck deploy.sh || true
EOF

# Probar ShellCheck localmente (simula lo que haría el CI)
if command -v shellcheck >/dev/null 2>&1; then
echo "=== Resultados de ShellCheck ==="
shellcheck /tmp/lab-actions/deploy.sh || true
echo ""
echo "ShellCheck encontró los problemas que el CI detectaría"
else
sudo apt install shellcheck -y -q
shellcheck /tmp/lab-actions/deploy.sh || true
fi

rm -rf /tmp/lab-actions

Lab 18.6 — Infraestructura como código: Terraform con null provider

# Usar el provider 'null' para practicar Terraform sin infraestructura real
command -v tofu >/dev/null || command -v terraform >/dev/null || {
echo "Instalar OpenTofu: curl -fsSL https://get.opentofu.org/install-opentofu.sh | sudo bash -s -- --install-method deb"
exit 0
}

TF_CMD=$(command -v tofu 2>/dev/null || command -v terraform)

mkdir -p /tmp/lab-tf && cd /tmp/lab-tf

cat > main.tf << 'EOF'
terraform {
required_providers {
null = { source = "hashicorp/null" }
local = { source = "hashicorp/local" }
}
}

variable "servidores" {
default = ["web01", "web02", "db01"]
}

resource "local_file" "inventario" {
for_each = toset(var.servidores)
filename = "/tmp/lab-tf/${each.key}.txt"
content = "Servidor ${each.key} gestionado por Terraform\nCreado: ${timestamp()}\n"
}

output "archivos_creados" {
value = [for k, v in local_file.inventario : v.filename]
}
EOF

$TF_CMD init -upgrade 2>/dev/null | tail -3
$TF_CMD plan
$TF_CMD apply -auto-approve
echo ""
echo "=== Archivos creados ==="
cat /tmp/lab-tf/web01.txt

$TF_CMD destroy -auto-approve 2>/dev/null
cd - && rm -rf /tmp/lab-tf

Resumen del módulo

Git esencial: modelo de objetos, staging, log/diff, ramas, merge, remotos, etckeeper
Git intermedio: rebase interactivo, stash, cherry-pick, bisect, firmar commits con GPG
Ansible fundamentos: inventarios, ad-hoc, playbooks (tasks/handlers/vars), módulos clave
Ansible práctico: plantillas Jinja2, roles (estructura galaxy), ansible-vault (AES-256)
IaC: paradigma declarativo vs imperativo, Terraform/OpenTofu (init/plan/apply/destroy), cloud-init
CI/CD: GitHub Actions y GitLab CI (stages, jobs, artifacts, environments, secrets)
Backups: regla 3-2-1, RPO/RTO, restic (deduplicación + cifrado), borgbackup, simulacro de restauración
DevOps/SRE: mapa del ecosistema, SLI/SLO/SLA, error budget, toil, blameless postmortems
Linux en la nube: AWS/GCP/Azure CLI, instancias spot, auto scaling, imágenes de máquina

Próximo paso: Apéndices y recursos. Has completado el cuerpo principal del curso. Los apéndices recogen referencias rápidas, guías de certificación y los siguientes pasos para convertir este conocimiento en experiencia real.


Última actualización: 2024-06
Versión: 1.0
Estado: ✅ Listo para enseñanza