Módulo 6: Rollback And Safety Nets

3. Manos a la obra: el pipeline de `git revert`

Descripción

Esta es la lección donde el mecanismo de la lección 2 deja de ser un diagrama y se vuelve algo que corriste con tus propias manos: vas a revertir de verdad el commit que agregó el tag Compliance al bucket andes-cargo-shipment-docs (Módulo 3, lección 8), correr ci.yml con act sobre ese revert, confirmar en la salida literal que el tag desapareció del plan, y correr apply.yml para ver el mismo intento real —con el mismo fallo honesto de siempre— aplicándolo.

Conexión con el módulo

La lección 2 te dio el mecanismo completo, en teoría. Esta lección lo ejecuta sobre el mismo repositorio que vienes construyendo desde el Módulo 1: mismo ci.yml, mismo apply.yml, ningún archivo nuevo salvo el evento de PR que simula el revert. La lección 4 cambia de tema —branch protection—, pero el hilo de commits que esta lección deja en andes-cargo-infra/ sigue siendo, para siempre, parte del historial real del proyecto.


Punto de partida: el commit que vas a revertir

Recuerda el cierre del Módulo 3: el proyecto agregó un tag Compliance = "manifest-retention-required" al bucket de manifiestos, con el commit a10c9b7 Add Compliance tag to the shipment-docs bucket. Imagina que, semanas después, el equipo de cumplimiento de Andes Cargo avisa que esa política de retención todavía no está aprobada formalmente — el tag se agregó antes de tiempo, y hay que quitarlo hasta que el proceso de aprobación termine. Es exactamente el tipo de "error de negocio" que la lección 1 nombró: nada técnico salió mal, el apply (si hubiera corrido contra una cuenta real) habría funcionado sin errores — el problema es que el cambio nunca debió aprobarse todavía.

cd andes-cargo-infra
git log --oneline | grep -i compliance

Qué esperar (literal, el commit exacto que agregó el tag en el Módulo 3):

a10c9b7 Add Compliance tag to the shipment-docs bucket

Paso 1 — git revert, de verdad

git revert --no-edit a10c9b7

--no-edit acepta el mensaje de commit que Git genera automáticamente, sin abrir un editor de texto — perfectamente aceptable aquí, porque el mensaje generado ya describe con precisión lo que pasó.

Qué esperar (literal en la estructura, variable en el hash — el tuyo va a ser distinto del de esta ejecución, porque depende del momento exacto del commit):

[main <hash>] Revert "Add Compliance tag to the shipment-docs bucket"
 Date: <fecha-de-tu-máquina>
 1 file changed, 1 insertion(+), 3 deletions(-)
git log --oneline -3

Qué esperar (literal en la estructura y en los mensajes, <hash> variable):

<hash> Revert "Add Compliance tag to the shipment-docs bucket"
8d4df2c drift.yml: schedule-based drift detection with terraform_wrapper: false
24fe98f apply.yml: needs, artifact download, and the real terraform apply attempt

Fíjate en algo que la lección 2 ya adelantó: a10c9b7 sigue en tu historial, más abajo — no desapareció. Confírmalo:

git log --oneline | grep -i compliance

Qué esperar (literal en la estructura, <hash> variable en la primera línea):

<hash> Revert "Add Compliance tag to the shipment-docs bucket"
a10c9b7 Add Compliance tag to the shipment-docs bucket

Dos commits, ambos visibles, para siempre: el que agregó el tag y el que lo revirtió. Exactamente el historial auditable que la lección 2 prometió.

Confirma el diff exacto que produjo el revert:

git show --stat HEAD
cat s3.tf

Qué esperar (literal — s3.tf de vuelta al estado anterior al Módulo 3, lección 8, con tags = local.common_tags en vez del merge(...) que agregaba Compliance):

module "shipment_docs_bucket" {
  source = "./modules/s3-bucket"

  bucket_name        = var.bucket_name
  enable_versioning  = true
  bucket_policy_json = data.aws_iam_policy_document.require_https.json
  tags               = local.common_tags
}

Paso 2 — ci.yml sobre el revert: el mismo plan, un cambio distinto

Simula el Pull Request que traería este revert a revisión. Escribe un evento nuevo, con un número de PR y un nombre de rama que describan honestamente lo que es —un revert, no una nueva funcionalidad—:

.github/act-events/pr-event-revert.json:

{
  "action": "opened",
  "number": 45,
  "pull_request": {
    "number": 45,
    "title": "Revert: Add Compliance tag to the shipment-docs bucket",
    "head": {
      "ref": "revert-shipment-tags",
      "sha": "b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1"
    },
    "base": {
      "ref": "main",
      "sha": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b"
    },
    "html_url": "https://github.com/andes-cargo/andes-cargo-infra/pull/45",
    "user": {
      "login": "andes-cargo-dev"
    }
  },
  "repository": {
    "name": "andes-cargo-infra",
    "full_name": "andes-cargo/andes-cargo-infra",
    "default_branch": "main"
  }
}

sha en head/base son valores inventados a mano, exactamente como el pr-event.json original del Módulo 2 — no representan ningún commit real de GitHub, solo necesitan tener el formato correcto de un SHA de Git para que el payload sea válido.

Corre el mismo ci.yml de siempre, sin cambiar una sola línea de ese archivo:

export ARTIFACT_ADDR=$(ipconfig getifaddr en0)   # Linux: hostname -I | awk '{print $1}'
rm -rf .artifacts && mkdir -p .artifacts

act pull_request -e .github/act-events/pr-event-revert.json -j terraform-checks \
  --artifact-server-path ./.artifacts \
  --artifact-server-addr "$ARTIFACT_ADDR"

Qué esperar (salida literal, ejecutada para escribir esta lección):

[ci/terraform-checks] ⭐ Run Set up job
[ci/terraform-checks]   ✅  Success - Set up job
[ci/terraform-checks] ⭐ Run Main Check out andes-cargo-infra
[ci/terraform-checks]   ✅  Success - Main Check out andes-cargo-infra [50.523167ms]
[ci/terraform-checks] ⭐ Run Main Set up Terraform
[ci/terraform-checks]   ✅  Success - Main Set up Terraform [2.533139042s]
[ci/terraform-checks] ⭐ Run Main Terraform format check
[ci/terraform-checks]   ✅  Success - Main Terraform format check [139.099541ms]
[ci/terraform-checks] ⭐ Run Main Terraform init
[ci/terraform-checks]   ✅  Success - Main Terraform init [14.141802916s]
[ci/terraform-checks] ⭐ Run Main Terraform validate
[ci/terraform-checks]   ✅  Success - Main Terraform validate [2.080303708s]
[ci/terraform-checks] ⭐ Run Main Install awslocal
[ci/terraform-checks]   ✅  Success - Main Install awslocal [8.526287s]
[ci/terraform-checks] ⭐ Run Main Confirm the runner can reach LocalStack on the host
[ci/terraform-checks]   | Could not connect to the endpoint URL: "http://host.docker.internal:4566/"
[ci/terraform-checks] Failed but continue next step
[ci/terraform-checks]   ❌  Failure - Main Confirm the runner can reach LocalStack on the host [13.07124875s]
[ci/terraform-checks] ⭐ Run Main Install tflocal
[ci/terraform-checks]   ✅  Success - Main Install tflocal [2.301586834s]
[ci/terraform-checks] ⭐ Run Main Terraform plan
[ci/terraform-checks]   | Plan: 12 to add, 0 to change, 0 to destroy.
[ci/terraform-checks]   ✅  Success - Main Terraform plan [4.722843583s]
[ci/terraform-checks] ⭐ Run Main Publish the plan to the job summary
[ci/terraform-checks]   ✅  Success - Main Publish the plan to the job summary [73.929ms]
[ci/terraform-checks]   ⚙  Summary - ## Terraform plan — andes-cargo-infra
[ci/terraform-checks] ⭐ Run Main Upload the plan for apply.yml to use later
[ci/terraform-checks]   | Artifact terraform-plan has been successfully uploaded! Final size is 12428 bytes.
[ci/terraform-checks]   ✅  Success - Main Upload the plan for apply.yml to use later [909.815333ms]
[ci/terraform-checks] ⭐ Run Complete job
[ci/terraform-checks]   ✅  Success - Complete job
[ci/terraform-checks] 🏁  Job succeeded

Leyendo el plan del revert: por qué sigue diciendo "12 to add"

Antes de celebrar que el revert "funcionó", lee este número con el mismo cuidado que ya aprendiste en el Módulo 5 (lección 7). Plan: 12 to add, 0 to change, 0 to destroy. es idéntico al de cualquier otra corrida de este proyecto en esta máquina — no dice 1 to change, como esperarías de un revert que solo quita un tag de un recurso ya existente. La razón es la misma de siempre: el state de este proyecto, sin un apply real completado contra LocalStack, sigue completamente vacío. Un plan sobre un state vacío siempre calcula una creación completa de los 12 recursos, sin importar qué tan pequeño sea el cambio de HCL que lo disparó — el revert es real, el cambio en el HCL es real, pero el plan no tiene ningún recurso existente contra el cual comparar ese cambio como una simple actualización.

Lo que puedes confirmar, con la misma disciplina que el Módulo 3 (lección 8) ya te enseñó, es que el contenido del plan refleja el revert:

grep -c "Compliance" plan-output.txt

Qué esperar (literal): 0 — ninguna aparición, a diferencia de las dos apariciones que viste en el Módulo 3 (lección 8) antes del revert.

Confírmalo mirando el bloque completo de tags del bucket dentro del plan:

  # module.shipment_docs_bucket.aws_s3_bucket.this will be created
  + resource "aws_s3_bucket" "this" {
      ...
      + tags                        = {
          + "Environment" = "dev"
          + "ManagedBy"   = "terraform"
          + "Project"     = "andes-cargo"
        }

Tres tags, no cuatro — Compliance desapareció. Esta es la evidencia real de que el revert deshizo el cambio, aunque el número de la última línea (12 to add) no lo diga por sí solo. La lección de fondo, otra vez: lee el contenido del plan, no solo su resumen, cuando el resumen por sí solo no puede distinguir dos escenarios distintos.


Paso 3 — apply.yml: el mismo intento real, el mismo fallo honesto

En un repositorio real, alguien revisaría este plan —viendo que el tag Compliance desaparece, exactamente lo que el título del PR #45 prometía— y aprobaría la fusión. Simúlalo corriendo apply.yml directamente, exactamente como en el Módulo 5 (lección 4):

act push -W .github/workflows/apply.yml \
  --artifact-server-path ./.artifacts \
  --artifact-server-addr "$ARTIFACT_ADDR"

Qué esperar (salida literal, ejecutada para escribir esta lección — el Stage 0 con éxito, el SHA256 coincidiendo con el que ci.yml subió):

[apply/fetch-reviewed-plan] ⭐ Run Main Download the plan reviewed in the pull request
[apply/fetch-reviewed-plan]   | SHA256 digest of downloaded artifact is 54e5f9fcdc6181197394e0f187aaa717e06d0423843dea623878c9962f9cbd50
[apply/fetch-reviewed-plan]   ✅  Success - Main Download the plan reviewed in the pull request [422.849417ms]
[apply/fetch-reviewed-plan] ⭐ Run Main Confirm the plan file arrived intact
[apply/fetch-reviewed-plan]   ✅  Success - Main Confirm the plan file arrived intact [67.458375ms]
[apply/fetch-reviewed-plan] 🏁  Job succeeded
[apply/terraform-apply    ] ⭐ Run Main Check out andes-cargo-infra
[apply/terraform-apply    ]   ✅  Success - Main Check out andes-cargo-infra [40.789375ms]
[apply/terraform-apply    ] ⭐ Run Main Set up Terraform
[apply/terraform-apply    ]   ✅  Success - Main Set up Terraform [2.383532167s]
[apply/terraform-apply    ] ⭐ Run Main Download the plan reviewed in the pull request
[apply/terraform-apply    ]   | SHA256 digest of downloaded artifact is 54e5f9fcdc6181197394e0f187aaa717e06d0423843dea623878c9962f9cbd50
[apply/terraform-apply    ]   ✅  Success - Main Download the plan reviewed in the pull request [424.188333ms]
[apply/terraform-apply    ] ⭐ Run Main Terraform init
[apply/terraform-apply    ]   ✅  Success - Main Terraform init [15.040384667s]
[apply/terraform-apply    ] ⭐ Run Main Install tflocal
[apply/terraform-apply    ]   ✅  Success - Main Install tflocal [5.5976365s]
[apply/terraform-apply    ] ⭐ Run Main Terraform apply
[apply/terraform-apply    ]   | aws_dynamodb_table.shipments: Creating...
[apply/terraform-apply    ]   | module.app_server_role.aws_iam_role.this: Creating...
[apply/terraform-apply    ]   | module.lambda_manifest_processor_role.aws_iam_role.this: Creating...
[apply/terraform-apply    ]   | module.shipment_docs_bucket.aws_s3_bucket.this: Creating...
[apply/terraform-apply    ]   | module.shipment_docs_bucket.aws_s3_bucket.this: Still creating... [00m40s elapsed]
[apply/terraform-apply    ]   | ╷
[apply/terraform-apply    ]   | │ Error: creating S3 Bucket (andes-cargo-shipment-docs): operation error S3: CreateBucket, exceeded maximum number of attempts, 9, ... connect: connection refused
[apply/terraform-apply    ]   | ╵
[apply/terraform-apply    ]   ❗  ::error::Terraform exited with code 1.
[apply/terraform-apply    ]   ❌  Failure - Main Terraform apply [53.744650083s]
[apply/terraform-apply    ] 🏁  Job failed
Error: Job 'terraform-apply' failed

El SHA256 coincide en ambos jobs (54e5f9fcdc6181197394e0f187aaa717e06d0423843dea623878c9962f9cbd50), la misma prueba de integridad del Módulo 5: lo que terraform-apply intentó aplicar es exactamente el archivo que ci.yml calculó, sin ninguna reimpresión en el medio. El fallo final es, otra vez, el mismo de siempre —connection refused tras 9 reintentos del proveedor de AWS, porque esta máquina no tiene LocalStack corriendo con un token válido—: el mecanismo del pipeline es real de punta a punta; lo único representativo, como en cada módulo anterior, es el resultado final de escribir en una infraestructura que aquí no existe.

Con un LOCALSTACK_AUTH_TOKEN válido y la infraestructura de Andes Cargo ya aplicada, este mismo apply habría terminado en Apply complete!, con el bucket andes-cargo-shipment-docs de vuelta a solo tres tags — el rollback completo, de punta a punta, sin que nadie haya tocado la consola de AWS ni corrido terraform apply a mano.


Errores comunes

Esperar 1 to change en el plan del revert (de expectativa, el error central de esta lección). Qué pasa: alguien corre el revert, ve Plan: 12 to add, y concluye que el revert no funcionó. Cómo detectarlo: comparas el número con lo que esperabas de la lección 2 (una actualización, no una creación) y no coincide. Cómo corregirlo: como ya explicó esta lección, el número de la última línea depende del state, que sigue vacío en esta máquina — el revert sí funcionó, y la evidencia correcta es el contenido del plan (el grep -c "Compliance" en 0), no el resumen de la última línea.

Olvidar que a10c9b7 sigue en el historial después del revert (conceptual, revisita la lección 2). Qué pasa: alguien busca el commit original con git log y, al no encontrarlo en las primeras líneas, asume que desapareció. Cómo corregirlo: git log --oneline sin límite (o git log --oneline | grep -i compliance) muestra ambos commits — el original sigue exactamente donde estaba, el revert simplemente se agregó después, nunca lo reemplazó.

Confundir el fallo de apply.yml con un problema del revert (de diagnóstico, revisita el Módulo 5). Qué pasa: alguien ve Job failed en rojo después del revert y sospecha que algo salió mal específicamente con este cambio. Cómo detectarlo: el mensaje —connection refused tras reintentos reales, sobre los cuatro recursos raíz— es idéntico, carácter por carácter, al que ya viste en el Módulo 5 (lección 4) sobre un cambio completamente distinto. Cómo corregirlo: docker ps -a --filter name=localstack_main confirma, otra vez, que la causa es la misma de siempre —LocalStack no está corriendo—, sin ninguna relación con el contenido específico de este revert.


Ejercicios

Ejercicio 1 — Reconstruye el hilo completo de commits de memoria. Sin mirar esta lección, describe en orden los dos commits que existen ahora sobre el tag Compliance, y explica qué comando de Git produjo cada uno.

Ver solución

Primero, a10c9b7 Add Compliance tag to the shipment-docs bucket (Módulo 3, lección 8) — un commit normal, escrito a mano, que agregó el tag vía merge(local.common_tags, { Compliance = "..." }). Después, un commit nuevo (hash variable) con el mensaje Revert "Add Compliance tag to the shipment-docs bucket" — producido por git revert --no-edit a10c9b7, cuyo contenido es exactamente el diff opuesto: quita el merge(...) y deja tags = local.common_tags. Ambos commits coexisten, en ese orden, en el historial de main.

Ejercicio 2 — Explica por qué el plan del revert no dice 1 to change, a un colega que espera verlo. Usa el vocabulario exacto de esta lección y del Módulo 5.

Ver solución

Una respuesta completa suena, más o menos, así: "~ update in-place (el símbolo que produciría 1 to change) solo aparece cuando el state de Terraform ya tiene el recurso registrado como existente, y el HCL propone modificar uno de sus atributos. En esta máquina, sin un LOCALSTACK_AUTH_TOKEN válido, nunca completamos un apply real, así que el state sigue vacío — Terraform no tiene ningún bucket 'existente' contra el cual comparar el HCL revertido, así que calcula, otra vez, una creación completa de los 12 recursos. El revert es real y está reflejado en el contenido del plan —el tag Compliance ya no aparece en ningún lado—, solo que el resumen de la última línea no puede distinguir 'creación desde cero, sin el tag' de 'actualización que quita el tag', porque ambos casos parten del mismo HCL final."

Ejercicio 3 — Diseña el commit que revertiría este revert. Si Andes Cargo decidiera, la semana siguiente, que el tag Compliance sí estaba aprobado después de todo y había que volver a agregarlo, ¿qué comando de Git usarías, y qué le pasaría al historial?

Ver solución

git revert --no-edit <hash-del-commit-de-revert> — revertir el revert, con el mismo mecanismo exacto de esta lección, apuntando ahora al hash del commit "Revert...". El resultado sería un tercer commit nuevo, cuyo contenido vuelve a agregar el tag Compliance — el historial completo tendría entonces tres commits relacionados con este tag, todos visibles, todos con su propio autor y fecha: el original, el revert, y el revert del revert. Ninguno de los tres desaparece nunca; el historial simplemente crece, cada vez más preciso sobre qué pasó y cuándo.


Resumen y siguiente paso

En esta lección ejecutaste el mecanismo completo de rollback de infraestructura, de punta a punta: git revert --no-edit a10c9b7 creó un commit nuevo sin borrar el original, ci.yml corrió el mismo plan de siempre sobre ese revert (con Plan: 12 to add sin cambiar, pero con el tag Compliance genuinamente ausente del contenido), y apply.yml intentó aplicarlo de verdad, con el mismo SHA256 coincidiendo entre jobs y el mismo fallo honesto de conexión que ya conoces desde el Módulo 5.

Antes de avanzar deberías poder: correr git revert --no-edit sobre cualquier commit de este proyecto; explicar por qué el plan de un revert, en esta máquina específica, no cambia el número de recursos "to add"; y confirmar el contenido real de un revert con grep, sin depender solo del resumen de la última línea de un plan.

La lección 4 cambia de capa por completo: en vez de un mecanismo que corre dentro de un workflow, vas a ver el control que vive en la configuración del repositorio mismo —branch protection— y por qué ningún act puede ejecutarlo, aunque su efecto sobre main sea completamente real.

Recursos

  1. Git Docs — git revert — referencia oficial, incluido --no-edit.
  2. nektosact.com — User Guide — referencia de act pull_request -e y act push -W, usadas de punta a punta en esta lección.
  3. Módulo 3 de esta guía (08-project-andes-cargos-ci-workflow.md) — el commit original del tag Compliance que esta lección revierte.
  4. Módulo 5 de esta guía (04-hands-on-building-apply-yml.md) — el mecanismo de verificación por SHA256 y el patrón de fallo honesto de terraform apply, reutilizados aquí sin cambios.