Saltar al contenido principal

Clase 08 — APIs en DevOps

APIs en cada etapa del ciclo DevOps

Plan → Code → Build → Test → Release → Deploy → Operate → Monitor
│ │ │ │ │ │ │ │
Jira GitHub Docker Pytest GitHub K8s Docker Prometheus
API API API API Releases API API API

CI/CD: Orquestar pipelines con APIs

GitHub Actions — Trigger y Monitoreo

#!/bin/bash
GH="https://api.github.com"
AUTH=(-H "Authorization: token $GITHUB_TOKEN" -H "Accept: application/vnd.github.v3+json")
REPO="mi-org/pokemon-api"

# Trigger workflow manualmente
trigger_deploy() {
local env="${1:-staging}" version="${2:-latest}"
curl -s -X POST "${AUTH[@]}" \
"$GH/repos/$REPO/actions/workflows/deploy.yml/dispatches" \
-d "$(jq -n --arg ref "main" --arg env "$env" --arg ver "$version" \
'{ref: $ref, inputs: {environment: $env, version: $ver}}')"
echo "✅ Deploy triggereado: $env$version"
}

# Esperar resultado del workflow
wait_for_workflow() {
local max_wait=300 elapsed=0
echo "Esperando resultado del workflow..."
while [[ $elapsed -lt $max_wait ]]; do
local run
run=$(curl -s "${AUTH[@]}" "$GH/repos/$REPO/actions/runs?per_page=1" | jq '.workflow_runs[0]')
local status=$(echo "$run" | jq -r '.status')
local conclusion=$(echo "$run" | jq -r '.conclusion')

if [[ "$status" == "completed" ]]; then
[[ "$conclusion" == "success" ]] && { echo "✅ Workflow completado"; return 0; }
echo "❌ Workflow falló: $conclusion"; return 1
fi
printf " ⏳ [%ds] Estado: %s\r" "$elapsed" "$status"
sleep 10; elapsed=$((elapsed + 10))
done
echo "❌ Timeout"; return 1
}

# Ver estado de workflows
workflow_status() {
curl -s "${AUTH[@]}" "$GH/repos/$REPO/actions/runs?per_page=5" | \
jq -r '.workflow_runs[] | "\(.id)\t\(.name)\t\(.status)\t\(.conclusion // "running")"' | column -t
}

case "${1:-status}" in
trigger) trigger_deploy "$2" "$3" ;;
wait) wait_for_workflow ;;
status) workflow_status ;;
*) echo "Uso: $0 {trigger|wait|status}" ;;
esac

Container Management — Docker API

#!/bin/bash
SOCK="--unix-socket /var/run/docker.sock"
API="http://localhost"

docker_ps() {
curl -s $SOCK "$API/containers/json?all=${1:-false}" | \
jq '.[] | {
id: .Id[0:12], name: .Names[0][1:],
image: .Image, status: .Status, state: .State
}'
}

docker_stats() {
local container="$1"
curl -s $SOCK "$API/containers/$container/stats?stream=false" | jq '{
cpu_percent: ((.cpu_stats.cpu_usage.total_usage - .precpu_stats.cpu_usage.total_usage) /
(.cpu_stats.system_cpu_usage - .precpu_stats.system_cpu_usage) * 100 | floor),
memory_mb: (.memory_stats.usage / 1048576 | floor),
memory_limit_mb: (.memory_stats.limit / 1048576 | floor)
}'
}

docker_deploy() {
local image="$1" name="$2" port="$3"
echo "Pulling $image..."
curl -s -X POST $SOCK "$API/images/create?fromImage=$image" > /dev/null
curl -s -X POST $SOCK "$API/containers/$name/stop" > /dev/null 2>&1
curl -s -X DELETE $SOCK "$API/containers/$name?force=true" > /dev/null 2>&1
curl -s -X POST $SOCK "$API/containers/create?name=$name" \
-H "Content-Type: application/json" \
-d "{\"Image\":\"$image\",\"ExposedPorts\":{\"$port/tcp\":{}},\"HostConfig\":{\"PortBindings\":{\"$port/tcp\":[{\"HostPort\":\"$port\"}]}}}" | jq '.Id[0:12]'
curl -s -X POST $SOCK "$API/containers/$name/start"
echo "✅ Deployed: $name ($image)"
}

case "$1" in
ps) docker_ps "$2" ;;
stats) docker_stats "$2" ;;
deploy) docker_deploy "$2" "$3" "$4" ;;
*) echo "Uso: $0 {ps|stats|deploy} [args]" ;;
esac

Infrastructure as Code — Terraform Cloud API

#!/bin/bash
TF="https://app.terraform.io/api/v2"
TF_AUTH=(-H "Authorization: Bearer $TFC_TOKEN" -H "Content-Type: application/vnd.api+json")
ORG="mi-org"

tf_workspaces() {
curl -s "${TF_AUTH[@]}" "$TF/organizations/$ORG/workspaces" | \
jq '.data[] | {
name: .attributes.name,
terraform_version: .attributes."terraform-version",
resource_count: .attributes."resource-count"
}'
}

tf_plan() {
local ws_id="$1" message="${2:-Triggered via API}"
curl -s -X POST "${TF_AUTH[@]}" "$TF/runs" \
-d "$(jq -n --arg msg "$message" --arg ws "$ws_id" \
'{data: {type: "runs", attributes: {message: $msg}, relationships: {workspace: {data: {type: "workspaces", id: $ws}}}}}'
)" | jq '{run_id: .data.id, status: .data.attributes.status}'
}

tf_apply() {
local run_id="$1"
curl -s -X POST "${TF_AUTH[@]}" "$TF/runs/$run_id/actions/apply" \
-d '{"comment": "Approved via API"}' -w "HTTP %{http_code}\n"
}

Monitoreo con APIs

Prometheus

# Query instantánea
curl -s "http://prometheus:9090/api/v1/query?query=up" | jq '.data.result[] | {
job: .metric.job, instance: .metric.instance,
status: (if .value[1] == "1" then "UP" else "DOWN" end)
}'

# Alertas activas
curl -s "http://prometheus:9090/api/v1/alerts" | \
jq '.data.alerts[] | {alertname: .labels.alertname, state: .state}'

Grafana — Anotaciones de deploy

# Marcar deploy en gráficas
curl -s -X POST -H "Authorization: Bearer $GRAFANA_TOKEN" \
"http://grafana:3000/api/annotations" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg text "Deploy v2.0 - pokemon-api" \
--argjson time "$(date +%s)000" \
'{time: $time, text: $text, tags: ["deploy","production"]}'
)"

Notificaciones y Alertas

#!/bin/bash
# notify.sh - Sistema de notificaciones multi-canal

notify_slack() {
local msg="$1" color="${2:-#36a64f}"
curl -s -X POST "$SLACK_WEBHOOK" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg text "$msg" --arg color "$color" \
'{attachments: [{color: $color, text: $text, footer: "DevOps Bot"}]}')"
}

notify_discord() {
local msg="$1" color="${2:-3066993}"
curl -s -X POST "$DISCORD_WEBHOOK" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg desc "$msg" --argjson color "$color" \
'{embeds: [{description: $desc, color: $color}]}')"
}

notify_pagerduty() {
local summary="$1" severity="${2:-critical}"
curl -s -X POST "https://events.pagerduty.com/v2/enqueue" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg key "$PAGERDUTY_ROUTING_KEY" --arg summary "$summary" --arg severity "$severity" \
'{routing_key: $key, event_action: "trigger", payload: {summary: $summary, severity: $severity, source: "api-monitor"}}')"
}

# Uso integrado
alert() {
local message="$1" severity="${2:-info}"
case "$severity" in
critical) notify_slack "🚨 $message" "#dc3545"; notify_pagerduty "$message" "critical" ;;
warning) notify_slack "⚠️ $message" "#ffc107" ;;
info) notify_slack "ℹ️ $message" "#2196F3" ;;
esac
}

alert "API pokemon-api no responde" "critical"
alert "Deploy v2.0 completado" "info"

Ejemplo: Pipeline de deploy completo

#!/bin/bash
set -euo pipefail

APP="pokemon-api"
VERSION="${1:?Uso: $0 <version>}"
ENV="${2:-staging}"

log() { echo "[$(date '+%H:%M:%S')] $*"; }
fail() { log "❌ $*"; notify_slack "❌ Deploy $APP v$VERSION falló: $*" "#dc3545"; exit 1; }

source notify.sh

# 1. Pre-checks
log "Verificando imagen..."
IMAGE_CHECK=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $REGISTRY_TOKEN" \
"https://ghcr.io/v2/$ORG/$APP/manifests/$VERSION")
[[ "$IMAGE_CHECK" != "200" ]] && fail "Imagen no encontrada"

# 2. Notificar + marcar en Grafana
alert "🚀 Iniciando deploy de $APP v$VERSION en $ENV" "info"

# 3. Deploy
log "Desplegando..."
kubectl set image "deployment/$APP" "$APP=ghcr.io/$ORG/$APP:$VERSION" -n "$ENV"

# 4. Health check
log "Verificando salud..."
for i in {1..30}; do
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 \
"https://$APP.$ENV.ejemplo.com/health" 2>/dev/null || echo "000")
[[ "$code" == "200" ]] && { log "✅ Servicio saludable"; break; }
[[ $i -eq 30 ]] && fail "Timeout"
sleep 10
done

# 5. Smoke tests + notificar
log "✅ Deploy completado: $APP v$VERSION$ENV"
alert "✅ Deploy $APP v$VERSION completado en $ENV" "info"

Ejercicios

  1. Creá un script que dispare un GitHub Actions workflow y espere su resultado
  2. Usá la Docker API (socket) para: listar contenedores, ver stats y recrear uno
  3. Implementá notificaciones a Slack y Discord en un solo script
  4. Creá un pipeline de deploy que: verifique la imagen, despliegue, health check, smoke test, notifique
  5. Integrá una anotación en Grafana cada vez que hagas un deploy