Muchos clientes de Looker quieren permitir que sus usuarios vayan más allá de la generación de informes sobre los datos de su almacén de datos y que puedan escribir en ese almacén de datos y actualizarlo.
A través de su API de Action, Looker admite este caso de uso para cualquier almacén de datos o destino. En esta página de documentación, se guía a los clientes que usan Google Cloud infraestructura para implementar una solución en Cloud Run Functions para escribir en BigQuery. En esta página, se abordan los siguientes temas:
Consideraciones sobre la solución
Usa esta lista de consideraciones para validar que esta solución se alinee con tus necesidades.
- Cloud Run Functions
- ¿Por qué usar Cloud Run Functions? Como oferta "sin servidores" de Google, Cloud Run Functions es una excelente opción por su facilidad de operación y mantenimiento. Una consideración que debes tener en cuenta es que la latencia, en particular para las invocaciones en frío, puede ser más larga que con una solución que se basa en un servidor dedicado.
- Lenguaje y entorno de ejecución Cloud Run Functions admite varios lenguajes y entornos de ejecución. Esta página de documentación se centrará en un ejemplo en JavaScript y Node.js. Sin embargo, los conceptos se pueden traducir directamente a los otros lenguajes y entornos de ejecución admitidos.
- BigQuery
- ¿Por qué BigQuery? Aunque en esta página de documentación se supone que ya usas BigQuery, BigQuery es una excelente opción para un almacén de datos en general. Ten en cuenta las siguientes consideraciones:
- API de BigQuery Storage Write: BigQuery ofrece varias interfaces para actualizar datos en tu almacén de datos, incluidas, por ejemplo, las instrucciones del lenguaje de manipulación de datos (DML) en trabajos basados en SQL. Sin embargo, la mejor opción para las escrituras de gran volumen es la API de BigQuery Storage Write.
- Agregar en lugar de actualizar: Aunque esta solución solo agregará filas, no las actualizará, siempre puedes derivar tablas de "estado actual" en el momento de la consulta desde un registro de solo anexos, lo que simula las actualizaciones.
- ¿Por qué BigQuery? Aunque en esta página de documentación se supone que ya usas BigQuery, BigQuery es una excelente opción para un almacén de datos en general. Ten en cuenta las siguientes consideraciones:
- Servicios de asistencia
- Secret Manager: Secret Manager contiene valores secretos para asegurarse de que no se almacenen en lugares demasiado accesibles, como directamente en la configuración de la función.
- Identity and Access Management (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 deseada.
- Cloud Build: Aunque Cloud Build no se analizará en detalle en esta página, Cloud Run Functions lo usa en segundo plano, y puedes usar Cloud Build para automatizar las actualizaciones implementadas de forma continua en tus funciones a partir de los cambios en tu código fuente en 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 la integración con los recursos y activos de origen de tu organización es autenticar las solicitudes como provenientes de tu instancia de Looker con el mecanismo de autenticación basado en tokens de la API de Action de Looker y, luego, autorizar la función para actualizar datos en BigQuery con una cuenta de servicio.
- OAuth: Otra opción, que no se aborda en esta página, sería usar la función OAuth de la API de Action de Looker. 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 con IAM, en lugar de usar su acceso en Looker o la lógica ad hoc dentro del código de tu función.
Explicación del código de demostración
Tenemos un solo archivo que contiene la totalidad de la lógica de nuestra acción de demostración disponible en GitHub. En esta sección, analizaremos 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 escribirá la acción. En la sección Guía de implementación más adelante en esta página, se te indicará que reemplaces el ID del proyecto por el tuyo, que será la única modificación necesaria para 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 y se inicializan algunas dependencias de código que usará tu acción. Proporcionamos un ejemplo que accede a Secret Manager "en el código" con el módulo de Node.js de Secret Manager. Sin embargo, también puedes eliminar esta dependencia de código con la función integrada de Cloud Run Functions 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 @google-cloud a las que se hace referencia también se declaran en nuestro archivo package.json para permitir que las dependencias se carguen previamente y estén disponibles para nuestro entorno de ejecución de Node.js. crypto es un módulo integrado de Node.js y no se declara en package.json.
Control y enrutamiento de solicitudes HTTP
La interfaz principal que tu código expone al entorno de ejecución de Cloud Run Functions es una función de JavaScript exportada que sigue las convenciones del servidor web Express de Node.js. En particular, tu función recibe dos argumentos: el primero representa la solicitud HTTP, desde la que puedes leer varios parámetros y valores de solicitud, y el segundo representa un objeto de respuesta, al que emites tus datos de respuesta. Aunque el nombre de la función puede ser el que desees, deberás proporcionarle el nombre a Cloud Run Functions más adelante, como se detalla en la sección Guía de implementación.
/*** Entry-point for requests ***/
exports.httpHandler = async function httpHandler(req,res) {
En la primera sección de la función httpHandler, se declaran las distintas rutas que reconocerá nuestra acción, lo que refleja de cerca los extremos obligatorios de la API de Action para una sola acción y las funciones que controlarán cada ruta, definidas más adelante en el archivo.
Si bien algunos ejemplos de acciones + Cloud Run Functions implementan una función independiente para cada ruta de este tipo para alinearse uno a uno con el enrutamiento predeterminado de Cloud Run Functions, las funciones pueden aplicar un "subenrutamiento" adicional dentro de su código, como se muestra aquí. En última instancia, esto es una cuestión de preferencia, pero realizar este enrutamiento adicional en el código minimiza la cantidad de funciones que debemos implementar y nos ayuda a mantener un solo estado de código coherente en todos los extremos 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 de controlador HTTP implementa el control de la solicitud HTTP en función de las declaraciones de ruta anteriores y conecta los valores que muestran 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.")
}
}
Con el controlador HTTP y las declaraciones de ruta fuera del camino, profundizaremos en los tres extremos de acción principales que debemos implementar:
Extremo de la lista de acciones
Cuando un administrador de Looker conecta por primera vez una instancia de Looker a un servidor de Action, Looker llamará a la URL proporcionada, denominada "extremo de la lista de acciones", para obtener información sobre las acciones que están disponibles a través del servidor.
En nuestras declaraciones de ruta que mostramos anteriormente, hicimos que este extremo esté disponible en la ruta raíz (/) en la URL de nuestra función y se indicó que la función hubListing lo controlaría.
Como puedes ver en la siguiente definición de función, no hay demasiado "código" en absoluto, solo muestra los mismos datos JSON cada vez. Una cosa que debes tener en cuenta es 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}
]
}
]
}
}
Para fines de demostración, nuestro código no requirió autenticación para recuperar esta lista. Sin embargo, si consideras que los metadatos de tu acción 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 controlar varias acciones, lo que explica nuestra convención de ruta de /action-X/.... Sin embargo, nuestra función de Cloud Run de demostración implementará solo una acción.
Extremo del formulario de acción
Aunque no todos los casos de uso requerirán un formulario, tener uno se adapta bien al caso de uso de las escrituras de bases de datos, ya que los usuarios pueden inspeccionar los datos en Looker y, luego, proporcionar valores para insertarlos en la base de datos. Dado que nuestra lista de acciones proporcionó un parámetro form_url, Looker invocará este extremo del formulario de acción cuando un usuario comience a interactuar con tu acción para determinar qué datos adicionales capturar del usuario.
En nuestras declaraciones de ruta, hicimos que este extremo esté disponible en la ruta /action-0/form y asociamos dos controladores con él: requireInstanceAuth y action0Form.
Configuramos nuestras declaraciones de ruta para permitir varios controladores como este porque parte de la lógica se puede volver a usar para varios extremos.
Por ejemplo, podemos ver que requireInstanceAuth se usa para varias rutas. Usamos este controlador donde queremos requerir que una solicitud debe provenir de nuestra instancia de Looker. El controlador recupera el valor de token secreto esperado de Secret Manager y rechaza cualquier solicitud que no tenga ese valor de token esperado.
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 verificación de igualdad estándar (==), para evitar la filtración de información de sincronización de canales laterales que permitiría a un atacante averiguar rápidamente el valor de nuestro secreto.
Suponiendo que una solicitud pasa la verificación de autenticación de la instancia, el controlador action0Form la controla.
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 para ciertos casos de uso. Por ejemplo, según la selección de un usuario en un menú desplegable inicial, se pueden mostrar diferentes campos.
Extremo de ejecución de acción
El extremo de ejecución de acción es donde reside la mayor parte de la lógica de cualquier acción y donde analizaremos la lógica específica del caso de uso de inserción de BigQuery.
En nuestras declaraciones de ruta, hicimos que este extremo esté disponible en la ruta /action-0/execute y asociamos tres controladores con él: requireInstanceAuth, processRequestBody y action0Execute.
Ya abordamos requireInstanceAuth, y el controlador processRequestBody proporciona un preprocesamiento en su mayoría poco interesante para convertir ciertos campos inconvenientes en el cuerpo de la solicitud de Looker en un formato más conveniente, pero puedes consultarlo en el archivo de código completo.
La función action0Execute comienza mostrando ejemplos de extracción de 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 diferentes campos, según lo que declares en tus extremos de lista y formulario.
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,
}
Luego, el código pasa a un código estándar de BigQuery para insertar los datos. Ten en cuenta que las APIs de BigQuery Storage Write ofrecen otras variaciones más complejas que son más adecuadas para una conexión de transmisión persistente o inserciones masivas de muchos registros. Sin embargo, para responder a las interacciones individuales de los usuarios 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 extremo de "estado" para solucionar problemas, pero este extremo no es obligatorio para la integración de la API de Action.
Guía de Deployment
Por último, proporcionaremos una guía paso a paso para implementar la demostración por tu cuenta, que abarca los requisitos previos, la implementación de la función de Cloud Run, la configuración de BigQuery y la configuración de Looker.
Requisitos previos del proyecto y del servicio
Antes de comenzar a configurar cualquier detalle, revisa esta lista para comprender 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 BigQuery y Cloud Run Functions por primera vez en la IU de la consola de Cloud, se te solicitará que habilites las APIs requeridas para los servicios necesarios, incluidos BigQuery, Artifact Registry, Cloud Build, Cloud Functions, Cloud Logging, Pub/Sub, Cloud Run Admin y Secret Manager.
- Política para invocaciones no autenticadas: Este caso de uso requiere que implementemos Cloud Run Functions que "permitan el acceso público", ya que controlaremos la autenticación de las solicitudes entrantes en nuestro código según la API de Action, en lugar de usar IAM. Si bien esto se permite de forma predeterminada, la política de la organización suele restringir este uso. En particular, la política
constraints/iam.allowedPolicyMemberDomainsrestringe quién puede recibir permisos de IAM, y es posible que debas ajustarla para permitir la principalallUserspara el acceso no autenticado. Consulta esta guía, Cómo crear servicios públicos de Cloud Run cuando se aplique el uso compartido restringido del dominio, para obtener más información si no puedes permitir el acceso público. - Otras políticas: Ten en cuenta que otras Google Cloud restricciones de la política de la organización también pueden impedir la implementación de servicios que, de lo contrario, se permiten de forma predeterminada.
Implementa la función de Cloud Run
Una vez que hayas creado un proyecto nuevo, sigue estos pasos para implementar la función de Cloud Run.
- En Cloud Run Functions, haz clic en Crear función.
- Elige cualquier nombre para tu función (por ejemplo, "demo-bq-insert-action").
- En la configuración del activador , haz lo siguiente:
- El tipo de activador ya debería ser "HTTPS".
- Configura la autenticación como Permitir invocaciones no autenticadas.
- Copia el valor de la URL al portapapeles.
- En la configuración de Entorno de ejecución > Variables de entorno de ejecución , haz lo siguiente:
- Haz clic en Agregar variable.
- Configura el nombre de la variable como
CALLBACK_URL_PREFIX. - Pega la URL del paso anterior como valor.
- Haz clic en Siguiente.
- Haz clic en el archivo
package.jsony pega el contenido. - Haz clic en el archivo
index.jsy pega el contenido. - Asigna la variable
projectIden la parte superior del archivo a tu propio ID del proyecto. - Establece el punto de entrada en
httpHandler. - Haz clic en Implementar.
- Otorga los permisos solicitados (si corresponde) a la cuenta de servicio de compilación.
- Espera a que se complete la implementación.
- Si, en algún paso futuro, recibes 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. La usaremos en pasos posteriores para asegurarnos de que la función tenga los permisos que necesita.
- Para probar la implementación de tu función directamente en el navegador, visita la URL. Deberías ver una respuesta JSON que contenga tu lista de integración.
- Si recibes un error 403, es posible que tu intento de configurar Permitir invocaciones no autenticadas haya fallado de forma silenciosa como resultado de una política de la organización. Verifica si tu función permite invocaciones no autenticadas, revisa la configuración de la política de la organización, y trata de actualizar la configuración.
Acceso a la tabla de destino de BigQuery
En la práctica, la tabla de destino en la que se insertará puede residir en un proyecto diferente Google Cloud . Sin embargo, para fines de demostración, crearemos una tabla de destino nueva en nuestro mismo proyecto. En cualquier caso, deberás asegurarte de que la cuenta de servicio de tu función de Cloud Run tenga permisos para escribir en la tabla.
- Navega a la consola de BigQuery.
Crea la tabla de demostración:
- En la barra Explorador, usa el menú de puntos suspensivos junto a tu proyecto y selecciona Crear conjunto de datos.
- Asigna a tu conjunto de datos el ID
demo_datasety haz clic en Crear conjunto de datos. - Usa el menú de puntos suspensivos en el conjunto de datos que acabas de crear y selecciona Crear tabla.
- Asigna a tu tabla el nombre
demo_table. En Esquema, selecciona Editar como texto, usa el siguiente esquema y, luego, haz 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"} ]
Asignar permisos:
- En la barra Explorador, haz clic en tu conjunto de datos.
- En la página conjunto de datos, haz clic en Uso compartido > Permisos.
- Haz clic en Agregar principal.
- Establece el principal nuevo en la cuenta de servicio de tu función, que se mencionó anteriormente en esta página.
- Asigna el rol de Editor de datos de BigQuery.
- Haz clic en Guardar.
Conéctate a Looker
Ahora que tu función está implementada, conectaremos Looker a ella.
- Necesitaremos un secreto compartido para que tu acción autentique que las solicitudes provienen de tu instancia de Looker. Genera una cadena aleatoria larga y mantenla segura. La usaremos en pasos posteriores como nuestro valor de secreto de Looker.
- En la consola de Cloud, navega a Secret Manager.
- Haz clic en Crear secreto.
- Ingresa
LOOKER_SECRETen Nombre. (Esto está codificado en el código de esta demostración, pero puedes elegir cualquier nombre cuando trabajes con tu propio código). - Establece el valor secreto en el valor secreto que generaste.
- Haz clic en Crear secreto.
- En la página Secreto, haz clic en la pestaña Permisos.
- Haz clic en Otorgar acceso.
- Establece Principales nuevos en la cuenta de servicio de tu función, que se mencionó anteriormente.
- Asigna el rol de Descriptor de acceso a los Secrets de Secret Manager.
- Haz clic en Guardar.
- Para confirmar que tu función accede correctamente al secreto, visita la ruta
/statusagregada a la URL de tu función.
- En tu instancia de Looker, haz lo siguiente:
- Navega a Administrador > Plataforma > Acciones.
- Ve a la parte inferior de la página para hacer clic en Agregar Action Hub.
- 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 Agregar Action Hub.
- Ahora deberías ver una nueva entrada de Action Hub con una acción llamada Demo BigQuery Insert.
- En la entrada de Action Hub, haz clic en Configurar autorización.
- Ingresa 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, haz clic en Habilitar.
- Mueve el interruptor Habilitado a la posición Activado.
- Debería ejecutarse automáticamente una prueba de la acción, lo que confirma que tu función acepta la solicitud de Looker y responde correctamente al extremo del formulario.
- Haz clic en Guardar.
Prueba de extremo a extremo
Ahora deberíamos poder usar nuestra nueva acción. Esta acción está configurada para funcionar con cualquier consulta, por lo que puedes elegir cualquier exploración (por ejemplo, una exploración de actividad del sistema integrada), agregar algunos campos a una consulta nueva, ejecutarla y, luego, elegir Enviar en el menú de ajustes. Deberías ver la acción como uno de los destinos disponibles y se te solicitarán algunas entradas de campo:

Cuando presiones Enviar, deberías tener una fila nueva insertada en tu tabla de BigQuery (y el correo electrónico de tu cuenta de usuario de Looker identificado en la columna invoked_by).