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
- Desde el proyecto de apis se genera una Cloud Task.
- Cloud Tasks envía una solicitud HTTP a la Cloud Function.
- La función valida que la solicitud proviene de Cloud Tasks.
- Se valida el formato de los datos de auditoría.
- Los datos son transformados y enviados a BigQuery.
- 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
- Se verifica que la tabla existe (
checkTableExists) - Se transforma la estructura de datos (
prepareAuditRecord) - Se inserta la información en BigQuery (
insertAuditRecord)
3. services.js (Validaciones y Seguridad)
validateAuditData: Valida la estructura del evento de auditoríaisRequestFromCloudTask: 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
| Stage | Descripción | Job(s) |
|---|---|---|
| test | Ejecuta unit tests con cobertura Jest. Si fallan, notifica a Discord. | test |
| sonarqube | Análisis estático; distingue MR vs rama directa. | sonarqube |
| build | Compila TypeScript → dist/. Guarda artefacto 30 min. | build:dev |
| deploy | Despliega jobs-bq-insert-auditorias a Cloud Functions (Node 20). | deploy:dev |
| notify | Enví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 enpackage-lock.json.npm run build: construye el proyecto, generandodist/.- 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
devo cuando el MR apunta adev.
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.--memoryy--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: ¬ify_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.yamlcon las variables de entorno de la función.
Cómo Usarla
En el proyecto APIs
- Armar el payload de auditorías
Por ejemplo, en027‑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
};
- Crear la tarea en Cloud Tasks
Usa la funcióncreateAuditoriaTask(url, payload)definida entask-utils.js. La URL del servicio Cloud Run se pasa desde la variable de entornoCLOUD_RUN_FUNCTIONS_AUDITORIAS:
await createAuditoriaTask(CLOUD_RUN_FUNCTIONS_AUDITORIAS, auditoria);
- 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ódigo | Cuándo sucede | Body JSON que recibiría Cloud Tasks |
|---|---|---|
| 202 Accepted | Payload válido, insertado en BigQuery | { "status": "ok" } |
| 400 Bad Request | Falta/valor inválido en el payload | { "status": "error", "message": "<detalle>" } |
| 403 Forbidden | La llamada no proviene de Cloud Tasks | { "status": "forbidden" } |
| 405 Method Not Allowed | Se usó GET/PUT/… en vez de POST | { "status": "error", "message": "Method not allowed" } |
| 5xx | Falla 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.
- Valor Propuesto:
-
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.
- Valor Propuesto:
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).
- Valor Propuesto:
-
Duración máxima del reintento (
maxRetryDuration):- Valor Propuesto:
3600segundos (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.
- Valor Propuesto:
-
Retirada mínima (
minBackoff):- Valor Propuesto:
1.0segundos - Justificación: Esperar al menos 1 segundo antes de reintentar evita saturar el sistema inmediatamente después de un fallo breve.
- Valor Propuesto:
-
Tiempo de retirada máximo (
maxBackoff):- Valor Propuesto:
600segundos (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.
- Valor Propuesto:
-
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.
- Valor Propuesto:
Resumen de Valores Propuestos:
- Límites de Frecuencia:
maxDispatchesPerSecond: 10maxConcurrentDispatches: 25
- Configuración de Reintentos:
maxAttempts: 10maxRetryDuration: 3600s (1 hora)minBackoff: 1.0smaxBackoff: 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.