Muchos clientes de Looker quieren dar a sus usuarios la posibilidad de ir más allá de generar informes sobre los datos de su almacén de datos y, en su lugar, escribir en ese almacén y actualizarlo.
A través de su API Action, Looker admite este caso práctico para cualquier almacén de datos o destino. En esta página de documentación se explica a los clientes que usan Google Cloud infraestructura cómo implementar una solución en Cloud Run Functions para escribir datos en BigQuery. En esta página se tratan los siguientes temas:
Consideraciones sobre la solución
Usa esta lista de consideraciones para validar que esta solución se ajusta a tus necesidades.
- Cloud Run functions
- ¿Por qué Cloud Run Functions? Como oferta "sin servidor" de Google, Cloud Run Functions es una opción excelente por su facilidad de uso y mantenimiento. Una cuestión que debes tener en cuenta es que la latencia, sobre todo en las invocaciones en frío, puede ser mayor que con una solución que se basa en un servidor dedicado.
- Idioma y tiempo de ejecución: Cloud Run functions admite varios idiomas y tiempos de ejecución. En esta página de documentación se tratará un ejemplo en JavaScript y Node.js. Sin embargo, los conceptos se pueden traducir directamente a los demás lenguajes y tiempos de ejecución admitidos.
- BigQuery
- ¿Por qué BigQuery? Aunque en esta página de documentación se da por hecho que ya usas BigQuery, este servicio es una opción excelente para un almacén de datos en general. Ten en cuenta lo siguiente:
- API Storage Write de BigQuery: BigQuery ofrece varias interfaces para actualizar los datos de tu almacén de datos, como las instrucciones del lenguaje de manipulación de datos (DML) en los trabajos basados en SQL. Sin embargo, la mejor opción para las escrituras de gran volumen es la API Storage Write de BigQuery.
- Añadir en lugar de actualizar: aunque esta solución solo añade filas, no las actualiza, siempre puedes obtener tablas de "estado actual" en el momento de la consulta a partir de un registro de solo anexión, lo que simula las actualizaciones.
- ¿Por qué BigQuery? Aunque en esta página de documentación se da por hecho que ya usas BigQuery, este servicio es una opción excelente para un almacén de datos en general. Ten en cuenta lo siguiente:
- Servicios de asistencia
- Secret Manager: Secret Manager contiene valores de secretos para asegurarse de que no se almacenan en lugares demasiado accesibles, como directamente en la configuración de la función.
- Gestión de Identidades y Accesos (IAM): IAM autoriza a la función a acceder al secreto necesario en el tiempo de ejecución y a escribir en la tabla de BigQuery correspondiente.
- Cloud Build: aunque en esta página no se tratará en profundidad, las funciones de Cloud Run lo usan en segundo plano. Puedes usar Cloud Build para automatizar las actualizaciones desplegadas de forma continua en tus funciones a partir de los cambios en el código fuente de un repositorio de Git.
- Autenticación de acciones y usuarios
- Cuenta de servicio de Cloud Run: la forma principal y más sencilla de usar las acciones de Looker para integrar los recursos y los activos propios de tu organización es autenticar las solicitudes como procedentes de tu instancia de Looker mediante el mecanismo de autenticación basado en tokens de la API Looker Action y, a continuación, autorizar la función para actualizar los datos en BigQuery mediante una cuenta de servicio.
- OAuth: otra opción, que no se trata en esta página, sería usar la función OAuth de la API Looker Action. Este enfoque es más complejo y, por lo general, no es necesario, pero se puede usar si necesitas definir el acceso de los usuarios finales para escribir en la tabla mediante IAM, en lugar de usar su acceso en Looker o una lógica específica en el código de tu función.
Guía del código de demostración
Tenemos un solo archivo que contiene toda la lógica de nuestra acción de demostración, que está disponible en GitHub. En esta sección, repasaremos los elementos clave del código.
Código de configuración
La primera sección tiene algunas constantes de demostración que identifican la tabla en la que se escribirá la acción. En la sección Guía de implementación, que se encuentra más adelante en esta página, se le indicará que sustituya el ID de proyecto por el suyo. Esta será la única modificación que deberá hacer en el código.
/*** Demo constants */
const projectId = "your-project-id"
const datasetId = "demo_dataset"
const tableId = "demo_table"
En la siguiente sección se declaran e inicializan algunas dependencias de código que usará tu acción. Proporcionamos un ejemplo que accede al administrador de secretos "en el código" mediante el módulo Node.js del administrador de secretos. Sin embargo, también puedes eliminar esta dependencia de código usando la función integrada de las funciones de Cloud Run para recuperar un secreto durante su inicialización.
/*** Code Dependencies ***/
const crypto = require("crypto")
const {SecretManagerServiceClient} = require('@google-cloud/secret-manager')
const secrets = new SecretManagerServiceClient()
const BigqueryStorage = require('@google-cloud/bigquery-storage')
const BQSManagedWriter = BigqueryStorage.managedwriter
Ten en cuenta que las dependencias de @google-cloud
a las que se hace referencia también se declaran en nuestro archivo package.json
para que se puedan precargar y estén disponibles en nuestro tiempo de ejecución de Node.js. crypto
es un módulo de Node.js integrado y no se declara en package.json
.
Gestión y enrutamiento de solicitudes HTTP
La interfaz principal que tu código expone al tiempo de ejecución de las funciones de Cloud Run es una función de JavaScript exportada que sigue las convenciones del servidor web Express de Node.js. En concreto, tu función recibe dos argumentos: el primero representa la solicitud HTTP, desde la que puedes leer varios parámetros y valores de la solicitud; y el segundo representa un objeto de respuesta, al que envías los datos de la respuesta. Aunque puedes elegir el nombre que quieras para la función, tendrás que proporcionárselo a las funciones de Cloud Run más adelante, tal como se explica en la sección Guía de despliegue.
/*** Entry-point for requests ***/
exports.httpHandler = async function httpHandler(req,res) {
La primera sección de la función httpHandler
declara las distintas rutas que reconocerá nuestra acción, que reflejan fielmente los endpoints obligatorios de la API Action para una sola acción, así como las funciones que gestionarán cada ruta, definidas más adelante en el archivo.
Aunque algunos ejemplos de acciones y funciones de Cloud Run implementan una función independiente para cada ruta de este tipo con el fin de que se correspondan uno a uno con el enrutamiento predeterminado de las funciones de Cloud Run, las funciones pueden aplicar un "subenrutamiento" adicional en su código, como se muestra aquí. En última instancia, es una cuestión de preferencias, pero hacer este enrutamiento adicional en el código minimiza el número de funciones que tenemos que implementar y nos ayuda a mantener un único estado de código coherente en todos los endpoints de las acciones.
const routes = {
"/": [hubListing],
"/status": [hubStatus], // Debugging endpoint. Not required.
"/action-0/form": [
requireInstanceAuth,
action0Form
],
"/action-0/execute": [
requireInstanceAuth,
processRequestBody,
action0Execute
]
}
El resto de la función del controlador HTTP implementa la gestión de la solicitud HTTP en las declaraciones de ruta anteriores y conecta los valores devueltos de esos controladores al objeto de respuesta.
try {
const routeHandlerSequence = routes[req.path] || [routeNotFound]
for(let handler of routeHandlerSequence) {
let handlerResponse = await handler(req)
if (!handlerResponse) continue
return res
.status(handlerResponse.status || 200)
.json(handlerResponse.body || handlerResponse)
}
}
catch(err) {
console.error(err)
res.status(500).json("Unhandled error. See logs for details.")
}
}
Ahora que ya hemos definido el controlador HTTP y las rutas, vamos a centrarnos en los tres endpoints de acción principales que tenemos que implementar:
Endpoint de lista de acciones
Cuando un administrador de Looker conecta por primera vez una instancia de Looker a un servidor de acciones, Looker llama a la URL proporcionada, denominada "endpoint de lista de acciones", para obtener información sobre las acciones disponibles a través del servidor.
En las declaraciones de ruta que hemos mostrado anteriormente, hemos hecho que este endpoint esté disponible en la ruta raíz (/
) de la URL de nuestra función y hemos indicado que lo gestionará la función hubListing
.
Como puedes ver en la siguiente definición de función, no hay mucho código, ya que solo devuelve los mismos datos JSON cada vez. Es importante tener en cuenta que incluye dinámicamente su "propia" URL en algunos de los campos, lo que permite que la instancia de Looker envíe solicitudes posteriores a la misma función.
async function hubListing(req){
return {
integrations: [
{
name: "demo-bq-insert",
label: "Demo BigQuery Insert",
supported_action_types: ["cell", "query", "dashboard"],
form_url:`${process.env.CALLBACK_URL_PREFIX}/action-0/form`,
url: `${process.env.CALLBACK_URL_PREFIX}/action-0/execute`,
icon_data_uri: "data:image/png;base64,...",
supported_formats:["inline_json"],
supported_formattings:["unformatted"],
required_fields:[
// You can use this to make your action available
// for specific queries/fields
// {tag:"user_id"}
],
params: [
// You can use this to require parameters, either
// from the Action's administrative configuration,
// or from the invoking user's user attributes.
// A common use case might be to have the Looker
// instance pass along the user's identification to
// allow you to conditionally authorize the action:
{name: "email", label: "Email", user_attribute_name: "email", required: true}
]
}
]
}
}
En esta demostración, nuestro código no ha necesitado autenticación para obtener esta ficha. Sin embargo, si consideras que los metadatos de tus acciones son sensibles, también puedes requerir autenticación para esta ruta, como se muestra en la siguiente sección.
También ten en cuenta que nuestra función de Cloud Run podría exponer y gestionar varias acciones, lo que explica nuestra convención de ruta /action-X/...
. Sin embargo, nuestra función de Cloud Run de demostración solo implementará una acción.
Endpoint de formulario de acción
Aunque no todos los casos prácticos requieren un formulario, tener uno se adapta bien al caso práctico de las escrituras de retorno de bases de datos, ya que los usuarios pueden inspeccionar los datos en Looker y, a continuación, proporcionar los valores que se insertarán en la base de datos. Como nuestra lista de acciones ha proporcionado un parámetro form_url
, Looker invocará este endpoint de formulario de acción cuando un usuario empiece a interactuar con tu acción para determinar qué datos adicionales se deben recoger del usuario.
En nuestras declaraciones de ruta, hemos hecho que este endpoint esté disponible en la ruta /action-0/form
y hemos asociado dos controladores a él: requireInstanceAuth
y action0Form
.
Hemos configurado nuestras declaraciones de ruta para permitir varios controladores de este tipo porque se puede reutilizar cierta lógica en varios endpoints.
Por ejemplo, podemos ver que requireInstanceAuth
se usa en varias rutas. Usamos este controlador siempre que queremos requerir que una solicitud proceda de nuestra instancia de Looker. El controlador obtiene el valor del token esperado del secreto de Secret Manager y rechaza las solicitudes que no tengan ese valor.
async function requireInstanceAuth(req) {
const lookerSecret = await getLookerSecret()
if(!lookerSecret){return}
const expectedAuthHeader = `Token token="${lookerSecret}"`
if(!timingSafeEqual(req.headers.authorization,expectedAuthHeader)){
return {
status:401,
body: {error: "Looker instance authentication is required"}
}
}
return
function timingSafeEqual(a, b) {
if(typeof a !== "string"){return}
if(typeof b !== "string"){return}
var aLen = Buffer.byteLength(a)
var bLen = Buffer.byteLength(b)
const bufA = Buffer.allocUnsafe(aLen)
bufA.write(a)
const bufB = Buffer.allocUnsafe(aLen) //Yes, aLen
bufB.write(b)
return crypto.timingSafeEqual(bufA, bufB) && aLen === bLen;
}
}
Ten en cuenta que usamos una implementación de timingSafeEqual
en lugar de la comprobación de igualdad estándar (==
) para evitar que se filtre información de tiempo de canal lateral que permita a un atacante averiguar rápidamente el valor de nuestro secreto.
Si una solicitud supera la comprobación de autenticación de la instancia, el controlador action0Form
la gestiona.
async function action0Form(req){
return [
{name: "choice", label: "Choose", type:"select", options:[
{name:"Yes", label:"Yes"},
{name:"No", label:"No"},
{name:"Maybe", label:"Maybe"}
]},
{name: "note", label: "Note", type: "textarea"}
]
}
Aunque nuestro ejemplo de demostración es muy estático, el código del formulario puede ser más interactivo en determinados casos prácticos. Por ejemplo, en función de lo que elija un usuario en un menú desplegable inicial, se pueden mostrar diferentes campos.
Action Execute Endpoint
El endpoint Action Execute es donde se encuentra la mayor parte de la lógica de cualquier acción y donde veremos la lógica específica del caso práctico de inserción de BigQuery.
En nuestras declaraciones de ruta, hemos puesto este endpoint a disposición en la ruta /action-0/execute
y hemos asociado tres controladores: requireInstanceAuth
, processRequestBody
y action0Execute
.
Ya hemos hablado de requireInstanceAuth
y el controlador processRequestBody
proporciona un preprocesamiento poco interesante para convertir determinados campos poco prácticos del cuerpo de la solicitud de Looker en un formato más práctico. Sin embargo, puedes consultar el archivo de código completo.
La función action0Execute
empieza mostrando ejemplos de cómo extraer información de varias partes de la solicitud de acción que podrían ser útiles. En la práctica, ten en cuenta que los elementos de solicitud a los que nuestro código hace referencia como formParams
y actionParams
pueden contener campos diferentes, en función de lo que declares en tus endpoints Listing y Form.
async function action0Execute (req){
try{
// Prepare some data that we will insert
const scheduledPlanId = req.body.scheduled_plan && req.body.scheduled_plan.scheduled_plan_id
const formParams = req.body.form_params || {}
const actionParams = req.body.data || {}
const queryData = req.body.attachment.data //If using a standard "push" action
/*In case any fields require datatype-specific preparation, check this example:
https://github.com/googleapis/nodejs-bigquery-storage/blob/main/samples/append_rows_proto2.js
*/
const newRow = {
invoked_at: new Date(),
invoked_by: actionParams.email,
scheduled_plan_id: scheduledPlanId || null,
query_result_size: queryData.length,
choice: formParams.choice,
note: formParams.note,
}
A continuación, el código pasa a un código estándar de BigQuery para insertar los datos. Ten en cuenta que las APIs Storage Write de BigQuery ofrecen otras variaciones más complejas que se adaptan mejor a una conexión de streaming persistente o a inserciones masivas de muchos registros. Sin embargo, para responder a las interacciones de usuarios individuales en el contexto de una función de Cloud Run, esta es la variación más directa.
await bigqueryConnectAndAppend(newRow)
...
async function bigqueryConnectAndAppend(row){
let writerClient
try{
const destinationTablePath = `projects/${projectId}/datasets/${datasetId}/tables/${tableId}`
const streamId = `${destinationTablePath}/streams/_default`
writerClient = new BQSManagedWriter.WriterClient({projectId})
const writeMetadata = await writerClient.getWriteStream({
streamId,
view: 'FULL',
})
const protoDescriptor = BigqueryStorage.adapt.convertStorageSchemaToProto2Descriptor(
writeMetadata.tableSchema,
'root'
)
const connection = await writerClient.createStreamConnection({
streamId,
destinationTablePath,
})
const writer = new BQSManagedWriter.JSONWriter({
streamId,
connection,
protoDescriptor,
})
let result
if(row){
// The API expects an array of rows, so wrap the single row in an array
const rowsToAppend = [row]
result = await writer.appendRows(rowsToAppend).getResult()
}
return {
streamId: connection.getStreamId(),
protoDescriptor,
result
}
}
catch (e) {throw e}
finally{
if(writerClient){writerClient.close()}
}
}
El código de demostración también incluye un endpoint "status" para solucionar problemas, pero este endpoint no es necesario para la integración de la API Action.
Guía de implementación
Por último, te proporcionaremos una guía paso a paso para desplegar la demo por tu cuenta, que incluye los requisitos previos, el despliegue de la función de Cloud Run, la configuración de BigQuery y la configuración de Looker.
Requisitos previos de proyectos y servicios
Antes de empezar a configurar los detalles, consulta esta lista para saber qué servicios y políticas necesitará la solución:
- Un proyecto nuevo: necesitarás un proyecto nuevo para alojar los recursos de nuestro ejemplo.
- Servicios: cuando uses por primera vez BigQuery y las funciones de Cloud Run en la interfaz de usuario de la consola de Cloud, se te pedirá que habilites las APIs necesarias para los servicios, como BigQuery, Artifact Registry, Cloud Build, Cloud Functions, Cloud Logging, Pub/Sub, Cloud Run Admin y Secret Manager.
- Política para invocaciones no autenticadas: en este caso práctico, debemos desplegar funciones de Cloud Run que permitan invocaciones no autenticadas, ya que gestionaremos la autenticación de las solicitudes entrantes en nuestro código de acuerdo con la API Action, en lugar de usar IAM. Aunque esta opción está habilitada de forma predeterminada, las políticas de las organizaciones suelen restringir su uso. En concreto, la política
constraints/iam.allowedPolicyMemberDomains
restringe quién puede recibir permisos de gestión de identidades y accesos, por lo que es posible que tengas que ajustarla para permitir el acceso no autenticado al principalallUsers
. Si no puedes permitir invocaciones no autenticadas, consulta esta guía sobre cómo crear servicios públicos de Cloud Run cuando se aplica el uso compartido restringido por dominio para obtener más información. - Otras políticas: ten en cuenta que otras Google Cloud restricciones de las políticas de la organización también pueden impedir la implementación de servicios que, de lo contrario, estarían permitidos de forma predeterminada.
Desplegar la función de Cloud Run
Una vez que hayas creado un proyecto, sigue estos pasos para implementar la función de Cloud Run
- En Cloud Run functions, haz clic en Crear función.
- Elige el nombre que quieras para tu función (por ejemplo, "demo-bq-insert-action").
- En la configuración de Activador:
- El tipo de activador ya debería ser "HTTPS".
- En Autenticación, selecciona Permitir las invocaciones sin autenticar.
- Copia el valor de URL en el portapapeles.
- En los ajustes de Entorno de ejecución > Variables de entorno de ejecución:
- Haga clic en Añadir variable.
- Asigna el nombre
CALLBACK_URL_PREFIX
a la variable. - Pega la URL del paso anterior como valor.
- Haz clic en Siguiente.
- Haz clic en el archivo
package.json
y pega el contenido. - Haz clic en el archivo
index.js
y pega el contenido. - Asigna la variable
projectId
en la parte superior del archivo al ID de tu proyecto. - Define Entry Point (Punto de entrada) como
httpHandler
. - Haz clic en Desplegar.
- Concede los permisos solicitados (si los hay) a la cuenta de servicio de compilación.
- Espera a que se complete la implementación.
- Si, en alguno de los pasos siguientes, se produce un error que te indica que revises los Google Cloud registros, ten en cuenta que puedes acceder a los registros de esta función desde la pestaña Registros de esta página.
- Antes de salir de la página de tu función de Cloud Run, en la pestaña Detalles, busca y anota la cuenta de servicio que tiene la función. Lo usaremos en pasos posteriores para asegurarnos de que la función tenga los permisos que necesita.
- Prueba la implementación de la función directamente en tu navegador visitando la URL. Debería ver una respuesta JSON que contenga su ficha de integración.
- Si recibes un error 403, es posible que tu intento de definir Allow unauthenticated invocations (Permitir invocaciones no autenticadas) haya fallado silenciosamente como resultado de una política de la organización. Comprueba si tu función permite invocaciones no autenticadas, revisa la configuración de la política de tu organización e intenta actualizarla.
Acceso a la tabla de destino de BigQuery
En la práctica, la tabla de destino en la que se insertarán los datos puede estar en otro proyecto. Sin embargo, para hacer una demostración, crearemos una tabla de destino en el mismo proyecto. Google Cloud En cualquier caso, debes asegurarte de que la cuenta de servicio de tu función de Cloud Run tenga permisos para escribir en la tabla.
- Ve a la consola de BigQuery.
Crea la tabla de demostración:
- En la barra Explorador, usa el menú de puntos suspensivos situado junto a tu proyecto y selecciona Crear conjunto de datos.
- Asigna el ID
demo_dataset
al conjunto de datos y haz clic en Crear conjunto de datos. - Usa el menú de puntos suspensivos del conjunto de datos que acabas de crear y selecciona Crear tabla.
- Asigna el nombre
demo_table
a la tabla. En Esquema, seleccione Editar como texto, use el siguiente esquema y, a continuación, haga clic en Crear tabla.
[ {"name":"invoked_at","type":"TIMESTAMP"}, {"name":"invoked_by","type":"STRING"}, {"name":"scheduled_plan_id","type":"STRING"}, {"name":"query_result_size","type":"INTEGER"}, {"name":"choice","type":"STRING"}, {"name":"note","type":"STRING"} ]
Asigna permisos:
- En la barra Explorador, haga clic en su conjunto de datos.
- En la página Conjunto de datos, haga clic en Compartir > Permisos.
- Haz clic en Añadir principal.
- Asigna el valor New Principal a la cuenta de servicio de tu función, que hemos indicado anteriormente en esta página.
- Asigna el rol Editor de datos de BigQuery.
- Haz clic en Guardar.
Conectando con Looker
Ahora que tu función se ha desplegado, vamos a conectar Looker a ella.
- Necesitaremos un secreto compartido para que tu acción autentique que las solicitudes proceden de tu instancia de Looker. Genera una cadena aleatoria larga y mantenla protegida. Lo usaremos en los pasos posteriores como valor de secreto de Looker.
- En la consola de Cloud, ve a Secret Manager.
- Haz clic en Crear secreto.
- Asigna el valor
LOOKER_SECRET
a Nombre. (Este valor está codificado en el código de esta demostración, pero puedes elegir el nombre que quieras cuando trabajes con tu propio código). - Asigna el Valor secreto al valor secreto que has generado.
- Haz clic en Crear secreto.
- En la página Secreto, haz clic en la pestaña Permisos.
- Haz clic en Conceder acceso.
- En Principales nuevas, asigna la cuenta de servicio de tu función, que has anotado anteriormente.
- Asigna el rol Lector de recursos de Secret Manager.
- Haz clic en Guardar.
- Para confirmar que tu función accede correctamente al secreto, ve a la ruta
/status
añadida a la URL de la función.
- En tu instancia de Looker:
- Vaya a Administrar > Plataforma > Acciones.
- Ve a la parte inferior de la página y haz clic en Añadir centro de acciones.
- Proporciona la URL de tu función (por ejemplo, https://your-region-your-project.cloudfunctions.net/demo-bq-insert-action) y confirma haciendo clic en Añadir Action Hub.
- Ahora debería ver una nueva entrada en el centro de acciones con una acción llamada Demo BigQuery Insert.
- En la entrada del centro de acciones, haga clic en Configurar autorización.
- Introduce el secreto de Looker generado en el campo Token de autorización y haz clic en Actualizar token.
- En la acción Demo BigQuery Insert (Demo de inserción de BigQuery), haz clic en Enable (Habilitar).
- Activa el interruptor Habilitado.
- Se debería ejecutar automáticamente una prueba de la acción para confirmar que tu función acepta la solicitud de Looker y responde correctamente al endpoint del formulario.
- Haz clic en Guardar.
Prueba completa
Ahora deberíamos poder usar nuestra nueva acción. Esta acción se ha configurado para que funcione con cualquier consulta, así que elige cualquier Exploración (por ejemplo, una Exploración de actividad del sistema integrada), añade algunos campos a una consulta nueva, ejecútala y, a continuación, elige Enviar en el menú de la rueda dentada. Deberías ver la acción como uno de los destinos disponibles y se te pedirá que introduzcas algunos campos:
Al pulsar Enviar, se debería insertar una nueva fila en tu tabla de BigQuery (y el correo de tu cuenta de usuario de Looker debería aparecer en la columna invoked_by
).