Saltar al contenido principal

Cloud Function: jobs-bq-insert-auditorias

En este documento veremos cómo funciona el registro de auditorías en Keeper, qué modelos se ven afectados y cómo se procesan los datos para su almacenamiento en BigQuery.

Descripción

La Cloud Function jobs-bq-insert-auditorias tiene la funcion processAuditoriaTask recibe eventos de auditoría desde una Cloud Task y los inserta en una tabla de BigQuery. Esta función valida los datos entrantes, verifica que la solicitud provenga de Cloud Tasks y maneja errores en el proceso de almacenamiento.

Arquitectura

  1. Desde el proyecto de apis se genera una Cloud Task.
  2. Cloud Tasks envía una solicitud HTTP a la Cloud Function.
  3. La función valida que la solicitud proviene de Cloud Tasks.
  4. Se valida el formato de los datos de auditoría.
  5. Los datos son transformados y enviados a BigQuery.
  6. Se retorna una respuesta HTTP según el resultado del proceso.

Archivos Principales

1. index.js (Punto de entrada de la función)

  • Expone la función processAuditoriaTask
  • Inicializa la conexión con BigQuery en entornos no test
  • Maneja la lógica de validación, procesamiento y respuesta

2. bigquery.js (Manejo de BigQuery)

  • Inicializa la conexión con BigQuery (initBigQuery)
  • Verifica si la tabla existe (checkTableExists)
  • Prepara los datos antes de insertarlos (prepareAuditRecord)
  • Inserta los datos en la tabla de auditoría (insertAuditRecord)
Proceso de Inserción en BigQuery
  1. Se verifica que la tabla existe (checkTableExists)
  2. Se transforma la estructura de datos (prepareAuditRecord)
  3. Se inserta la información en BigQuery (insertAuditRecord)

3. services.js (Validaciones y Seguridad)

  • validateAuditData: Valida la estructura del evento de auditoría
  • isRequestFromCloudTask: Verifica si la solicitud proviene de Cloud Tasks revisando el User-Agent

Configuración y Despliegue

Variables de Entorno Necesarias

Antes de desplegar, define estas variables de entorno:

BIGQUERY_DATASETID="tu_dataset"
BIGQUERY_TABLEID="tu_tabla"

CI/CD Pipeline - Build & Deploy to Google Cloud Functions (dev)

Este pipeline automatiza el proceso de construcción y despliegue de una función en Google Cloud Functions, apuntando al entorno de desarrollo (dev).

Imagen Base

image: node:20-alpine
  • Se utiliza una imagen ligera de Node.js 20 para optimizar tiempos de ejecución y recursos.

Etapas del Pipeline

StageDescripciónJob(s)
testEjecuta unit tests con cobertura Jest. Si fallan, notifica a Discord.test
sonarqubeAnálisis estático; distingue MR vs rama directa.sonarqube
buildCompila TypeScript → dist/. Guarda artefacto 30 min.build:dev
deployDespliega jobs-bq-insert-auditorias a Cloud Functions (Node 20).deploy:dev
notifyEnvía mensaje de éxito a Discord (por stage).notify:*

Configuración global

image: node:20-alpine

default:
before_script:
- apk add --no-cache curl # Necesario para notify_discord.sh
- npm ci # Instalación limpia
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
- .npm/

1. test

Objetivo

Ejecutar las pruebas unitarias con Jest, generar métricas de cobertura y publicar el reporte JUnit para su visualización en GitLab CI.

Configuración
.test_template: &test_template
stage: test
script:
- mkdir -p output/coverage/jest
- |
if npm run test -- --coverage --coverageDirectory=output/coverage/jest; then
echo "✅ Unit tests passed"
else
./scripts/notify_discord.sh "failed" "Test stage"
exit 1
fi
artifacts:
reports:
junit: output/coverage/jest/junit.xml
paths:
- output/coverage/jest/lcov.info
expire_in: 1 hour
rules:
# Ejecuta solo en la rama dev o en un MR cuyo target sea dev
- if: ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "dev" || $CI_COMMIT_BRANCH == "dev")
changes: ['**/*']
when: always
# Cambios en el README no disparan el job
- changes: ['README.md']
when: never

test:
<<: *test_template
Detalles
  • npm run test -- --coverage: corre Jest con generación de cobertura.
  • Cobertura guardada en output/coverage/jest/; el reporte junit.xml permite ver resultados de tests en GitLab.
  • Artefactos disponibles 1 hora para etapas posteriores (SonarQube).
  • Notificación a Discord en caso de fallo.
  • Se dispara únicamente para cambios que afecten dev (push directo o MR dirigido a dev).

2. sonarqube

Objetivo

Analizar estáticamente el código con SonarQube; cuando se trata de un Merge Request, registrar el análisis como Pull Request en SonarQube para mostrar issues y Quality Gate en la MR.

Configuración
sonarqube:
stage: sonarqube
needs:
- job: test
artifacts: true
script: |
if [[ -n "$CI_MERGE_REQUEST_IID" && "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" == "dev" ]]; then
echo "🔍 Ejecutando análisis como Merge Request..."
ANALYSIS_CMD="npx sonar-scanner \
-Dsonar.token=$SONAR_TOKEN \
-Dsonar.pullrequest.key=$CI_MERGE_REQUEST_IID \
-Dsonar.pullrequest.branch=$CI_COMMIT_REF_NAME \
-Dsonar.pullrequest.base=$CI_MERGE_REQUEST_TARGET_BRANCH_NAME \
-Dsonar.javascript.lcov.reportPaths=output/coverage/jest/lcov.info"
else
echo "📦 Ejecutando análisis como rama normal..."
ANALYSIS_CMD="npx sonar-scanner \
-Dsonar.token=$SONAR_TOKEN \
-Dsonar.branch.name=$CI_COMMIT_REF_NAME \
-Dsonar.javascript.lcov.reportPaths=output/coverage/jest/lcov.info"
fi

if eval $ANALYSIS_CMD; then
echo "✅ SonarQube analysis successful"
else
./scripts/notify_discord.sh "failed" "SonarQube stage"
exit 1
fi
variables:
SONAR_TOKEN: $SONAR_TOKEN
rules:
# Ejecuta solo en la rama dev o en un MR cuyo target sea dev
- if: ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "dev" || $CI_COMMIT_BRANCH == "dev")
changes: ['**/*']
when: always
# Cambios en el README no disparan el job
- changes: ['README.md']
when: never
Detalles
  • Dependencia de artefactos: recupera lcov.info generado en la etapa de test.
  • Detecta automáticamente si la ejecución proviene de un MR (CI_MERGE_REQUEST_IID); en tal caso, configura parámetros sonar.pullrequest.* para vincular el análisis con la MR.
  • Para ejecuciones en la rama dev (push directo) realiza análisis tradicional por rama (sonar.branch.name).
  • En caso de fallo, se notifica a Discord y el pipeline se detiene.
  • Requiere la variable protegida SONAR_TOKEN con permisos de ejecución de análisis.

3. build:dev

Objetivo

Compilar el código fuente y generar la carpeta dist/ con los artefactos listos para desplegar.

Configuración
build:dev:
stage: build
script:
- npm ci
- npm run build
artifacts:
paths:
- dist
expire_in: 30 mins
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
- .npm/
rules:
- if: ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "dev" || $CI_COMMIT_BRANCH == "dev")
Detalles
  • npm ci: instalación limpia basada en package-lock.json.
  • npm run build: construye el proyecto, generando dist/.
  • Los artefactos se mantienen por 30 minutos para ser usados por la etapa de despliegue.
  • Uso de cache para acelerar builds reutilizando dependencias.
  • Solo se ejecuta en la rama dev o cuando el MR apunta a dev.

4. deploy:dev

Objetivo

Desplegar la función jobs-bq-insert-auditorias en Google Cloud Functions usando el artefacto generado en dist/.

Configuración
deploy:dev:
stage: deploy
image: google/cloud-sdk:482.0.0-alpine
only:
- dev
dependencies:
- build:dev
script:
- gcloud auth activate-service-account --key-file="$SERVICE_ACCOUNT_DEV"
- gcloud config set project "$PROJECT_ID"
- |
gcloud functions deploy jobs-bq-insert-auditorias \
--entry-point processAuditoriaTask \
--timeout 10s \
--memory 128MB \
--runtime nodejs18 \
--trigger-http \
--project "$PROJECT_ID" \
--vpc-connector "projects/$GCP_PROJECT/locations/$GCP_ZONE/connectors/$VPC_CONNECTOR" \
--env-vars-file ${ENV_FILE} \
--source=dist/
environment: dev
Detalles
  • Usa la imagen oficial de GCP para acceder a la CLI (gcloud).
  • Se autentica con una cuenta de servicio mediante la variable SERVICE_ACCOUNT_DEV.
  • Establece el proyecto activo con gcloud config set project.
  • Despliega la función HTTP con configuración personalizada:
    • --entry-point: nombre del handler (función) principal.
    • --runtime: entorno Node.js 18.
    • --memory y --timeout: recursos asignados.
    • --vpc-connector: conexión privada a recursos en la VPC.
    • --env-vars-file: archivo con variables de entorno.
    • --source=dist/: código fuente compilado en la etapa anterior.
  • Se ejecuta solo en la rama dev.

5. notify

Objetivo

Notificar al canal de Discord —mediante el script notify_discord.sh— que la etapa correspondiente finalizó correctamente.

Configuración
.notify_template: &notify_template
stage: notify
image: node:20-alpine
before_script:
- apk add --no-cache curl
script:
- ./scripts/notify_discord.sh "success" "$STAGE_NAME"

notify:success-deploy:
<<: *notify_template
variables:
STAGE_NAME: 'Deploy'
needs:
- job: deploy:dev
artifacts: false
rules:
- if: '$CI_COMMIT_BRANCH == "dev"'

notify:success-build:
<<: *notify_template
variables:
STAGE_NAME: 'Build'
needs:
- job: test
artifacts: false
- job: build:dev
artifacts: false
rules:
- if: '$CI_MERGE_REQUEST_ID && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "dev"'
Detalles
  • Plantilla única (notify_template): centraliza la lógica común para ambas notificaciones.

    • Usa la misma imagen Node 20 y añade curl (requerido por el webhook).
    • Ejecuta notify_discord.sh "success" "$STAGE_NAME" donde STAGE_NAME se inyecta por variable.
  • notify:success-deploy

    • Depende del job deploy:dev (needs) y se ejecuta solo cuando el commit está en la rama dev.
  • notify:success-build

    • Se dispara para un Merge Request cuyo destino sea dev y tras los jobs test y build:dev.
    • No requiere artefactos, pues solo manda la notificación.

Script notify_discord.sh

#!/bin/sh

STATUS=$1
STAGE=$2
WEBHOOK_URL="${DISCORD_WEBHOOK_URL}"
BRANCH="${CI_COMMIT_BRANCH:-sin-rama}"
AUTHOR="${GITLAB_USER_NAME:-autor-desconocido}"
PROJECT="${CI_PROJECT_NAME:-proyecto-desconocido}"
URL="${CI_PIPELINE_URL:-https://gitlab.com}"
JOB="${CI_JOB_NAME:-job}"

case "$STATUS" in
success)
COLOR="3066993"
TITLE="✅ Pipeline exitoso"
;;
failed)
COLOR="15158332"
TITLE="❌ Pipeline falló"
;;
fixed)
COLOR="10181046"
TITLE="🔧 Pipeline recuperado"
;;
*)
COLOR="0"
TITLE="ℹ️ Estado desconocido"
;;
esac

# Escapar comillas y backslashes
safe() {
echo "$1" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g'
}

TITLE_ESCAPED=$(safe "$TITLE")
PROJECT_ESCAPED=$(safe "$PROJECT")
STAGE_ESCAPED=$(safe "$STAGE")
BRANCH_ESCAPED=$(safe "$BRANCH")
AUTHOR_ESCAPED=$(safe "$AUTHOR")

PAYLOAD=$(cat <<EOF
{
"embeds": [{
"title": "$TITLE_ESCAPED",
"description": "**Proyecto:** $PROJECT_ESCAPED\\n**Stage:** $STAGE_ESCAPED\\n**Rama:** $BRANCH_ESCAPED\\n**Autor:** $AUTHOR_ESCAPED\\n[Ver pipeline]($URL)",
"color": $COLOR
}]
}
EOF
)

echo "Payload generado:"
echo "$PAYLOAD"

curl -s -H "Content-Type: application/json" -X POST -d "$PAYLOAD" "$WEBHOOK_URL"

Variables de Entorno Esperadas

Asegurate de definir las siguientes variables en GitLab CI/CD:

  • SERVICE_ACCOUNT_DEV: credenciales en JSON de la cuenta de servicio.
  • PROJECT_ID: ID del proyecto de GCP.
  • GCP_PROJECT: ID del proyecto (reutilizado en la ruta del VPC connector).
  • GCP_ZONE: zona donde está ubicado el VPC connector.
  • VPC_CONNECTOR: nombre del conector VPC.
  • ENV_FILE: ruta al archivo .env.yaml con las variables de entorno de la función.

Cómo Usarla

En el proyecto APIs

  1. Armar el payload de auditorías
    Por ejemplo, en 027‑auditorias.js:
const auditoria = {
usuarioId: 'abc123', // string |null
recurso: 'vehiculos', // string (modelo lógico)
recursoId: 45, // string | number | null
accion: 'update', // 'new' | 'update' | 'delete'
oldData: { patente: 'AAA111' }, // objeto ({} si no aplica)
data: { patente: 'BBB222' }, // objeto ({} si no aplica)
timestamp: '2024-07-05T18:23:09Z', // ISO‑8601 UTC
tenantId: null, // opcional
};
  1. Crear la tarea en Cloud Tasks
    Usa la función createAuditoriaTask(url, payload) definida en task-utils.js. La URL del servicio Cloud Run se pasa desde la variable de entorno CLOUD_RUN_FUNCTIONS_AUDITORIAS:
await createAuditoriaTask(CLOUD_RUN_FUNCTIONS_AUDITORIAS, auditoria);
  1. Reutilizar en otras funciones
    Siempre que respetes el payload anterior y llames a createAuditoriaTask() con la URL apropiada, cualquier función puede registrar auditorías del mismo modo.

Manejo de Errores

CódigoCuándo sucedeBody JSON que recibiría Cloud Tasks
202 AcceptedPayload válido, insertado en BigQuery{ "status": "ok" }
400 Bad RequestFalta/valor inválido en el payload{ "status": "error", "message": "<detalle>" }
403 ForbiddenLa llamada no proviene de Cloud Tasks{ "status": "forbidden" }
405 Method Not AllowedSe usó GET/PUT/… en vez de POST{ "status": "error", "message": "Method not allowed" }
5xxFalla BigQuery o excepción interna{ "status": "error", "message": "Internal server error" }

Cómo Probar Localmente

Nota Importante

Advertencia: Las modificaciones y configuraciones descritas a continuación son ÚNICAMENTE para pruebas locales y NO deben ser desplegadas en producción.

Pasos de Configuración

1. Modificaciones en Librerías

Reemplazar las librerías de Google Cloud Task e instalar @grpc/grpc-js

npm install @google-cloud/tasks@2.6.0 @grpc/grpc-js@1.6.12

2. Modificaciones en el Código en APIs

Archivo: 027-auditorias.js

Modifica la función registerAuditoria:

const CLOUD_RUN_FUNCTIONS_AUDITORIAS = 'http://localhost:3000';

Archivo: task-utils.js

Agregar la siguiente función:

async function createHttpTaskLocal(url, payload, client, parent) {
const convertedPayload = JSON.stringify(payload);
const body = Buffer.from(convertedPayload).toString('base64');
const task = {
httpRequest: {
httpMethod: 'POST',
url,
headers: {
'Content-Type': 'application/json',
},
body,
},
};

try {
const [response] = await client.createTask({ parent, task });
logger.debug(`Created task ${response.name}`);
return response.name;
} catch (error) {
logger.error(`TASKS_ERROR: ${error.message}`);
}
}

Modificar la función initAuditoriaClient:

const initAuditoriaClient = () => {
// const queue = process.env.QUEUE_NAME_AUDITORIAS;
// const project = process.env.PROJECT_ID;
// const location = process.env.QUEUE_LOCATION;
const queue = 'anotherq';
const project = 'dev';
const location = 'here';
const client = new CloudTasksClient({
port: 8124,
servicePath: 'localhost',
// credentials: credentials.createInsecure(),
sslCreds: ChannelCredentials.createInsecure(),
});
const parent = client.queuePath(project, location, queue);
return { client, parent };
};

Reemplazar createHttpTask por createHttpTaskLocal en createAuditoriaTask:

const createAuditoriaTask = async (url, payload) => {
const client = taskClientAuditoria.client;
const parent = taskClientAuditoria.parent;
// await createHttpTask(url, payload, client, parent);
await createHttpTaskLocal(url, payload, client, parent);
};

3. Levantar Cloud Task Emulator

  • Repositorio: Cloud Task Emulator
  • Comando: go run ./ -host localhost -port 8124 -queue projects/dev/locations/here/queues/anotherq
  • Puerto: 8124

4. Levantar Cloud Function Local

  • en el package.json agregar type: module
  • Agregar esta variable de entorno al .env
GOOGLE_APPLICATION_CREDENTIALS="/ruta/a/tu/credencial.json"
  • Comando: npm run dev
  • Puerto: 3000

5. Levantar APIs

  • Repositorio: apis
  • Ejecutar: npm install
  • Comando: npm run start:dev:debug
  • Puerto: 8000

Pasos Finales de Verificación

  • Asegúrate de que todos los servicios estén corriendo en sus respectivos puertos
  • Realiza una prueba enviando un evento de auditoría para verificar el flujo completo

Cloud Task: auditorias-queue

A continuación se presenta una configuración inicial segura y razonable para tu cola de Cloud Tasks, diseñada para evitar sobrecargas y manejar reintentos de forma controlada.

1. Límite de Frecuencia para Envío de Tareas

Estos valores protegen tu Cloud Function y BigQuery de ser inundados.

  • Cantidad máxima de envíos (maxDispatchesPerSecond):

    • Valor Propuesto: 10
    • Justificación: El valor predeterminado (500) es demasiado alto para una función que usa BigQuery. Con 10 envíos por segundo, evitamos saturar BigQuery, controlamos mejor los costos y monitoreamos el rendimiento antes de ajustar.
  • Cantidad máxima de envíos simultáneos (maxConcurrentDispatches):

    • Valor Propuesto: 25
    • Justificación: l valor por defecto (1000) crearía demasiadas instancias de la función a la vez, lo que sería innecesariamente costoso. 25 tareas simultáneas es un buen punto de inicio para manejar la carga sin riesgos, asegurando que el sistema funcione sin picos bruscos.

2. Configuración de Reintentos

Estos valores definen cómo manejar los fallos cuando tu Cloud Function devuelve un error reintentable (5xx).

  • Intentos máximos (maxAttempts):

    • Valor Propuesto: 10
    • Justificación: Suficiente para errores pasajeros (como problemas de red o carga de BigQuery), pero evita reintentar eternamente tareas con fallos permanentes (que van a la cola de fallos).
  • Duración máxima del reintento (maxRetryDuration):

    • Valor Propuesto: 3600 segundos (1 hora)
    • Justificación: Si después de 1 hora (o 10 intentos) la tarea sigue fallando, se considera un problema serio y se manda a la cola de fallos para revisión manual.
  • Retirada mínima (minBackoff):

    • Valor Propuesto: 1.0 segundos
    • Justificación: Esperar al menos 1 segundo antes de reintentar evita saturar el sistema inmediatamente después de un fallo breve.
  • Tiempo de retirada máximo (maxBackoff):

    • Valor Propuesto: 600 segundos (10 minutos)
    • Justificación: Limita el tiempo entre reintentos a un máximo de 10 minutos (en lugar de 1 hora), para que las tareas no se retrasen demasiado.
  • Duplicaciones máximas (maxDoublings):

    • Valor Propuesto: 10
    • Justificación: Controla qué tan rápido aumenta el tiempo entre reintentos (ej: 1s → 2s → 4s...). Con 10 duplicaciones, el tiempo llega cerca del máximo (10 min) sin demoras innecesarias.

Resumen de Valores Propuestos:

  • Límites de Frecuencia:
    • maxDispatchesPerSecond: 10
    • maxConcurrentDispatches: 25
  • Configuración de Reintentos:
    • maxAttempts: 10
    • maxRetryDuration: 3600s (1 hora)
    • minBackoff: 1.0s
    • maxBackoff: 600s (10 minutos)
    • maxDoublings: 10

Nota: Estos son valores iniciales recomendados. Es crucial monitorizar el comportamiento de la cola y la función para ajustarlos según las necesidades y el rendimiento observado.