許多 Looker 客戶希望使用者除了製作資料倉儲資料的報表,還能實際寫入資料倉儲並更新資料。
透過 Action API,Looker 可支援任何資料倉儲或目的地的這類用途。本說明文件頁面將引導使用 Google Cloud 基礎架構的客戶,在 Cloud Run 函式上部署解決方案,以便將資料寫回 BigQuery。本頁面涵蓋以下主題:
解決方案考量事項
請參考下列考量重點,確認這個解決方案是否符合您的需求。
- Cloud Run 函式
- 為什麼要使用 Cloud Run 函式?作為 Google 的「無伺服器」服務,Cloud Run 函式是簡化營運和維護作業的絕佳選擇。請注意,延遲時間 (特別是冷啟動) 可能會比使用專屬伺服器的解決方案還要長。
- 語言和執行階段:Cloud Run 函式支援多種語言和執行階段。本說明文件頁面將著重於 JavaScript 和 Node.js 中的範例。不過,這些概念可直接轉譯為其他支援的語言和執行階段。
- BigQuery
- 為何選用 BigQuery?雖然本說明文件頁面假設您已在使用 BigQuery,但 BigQuery 通常是資料倉儲的絕佳選擇。請注意下列事項:
- BigQuery Storage Write API:BigQuery 提供多種介面,可用於更新資料倉儲中的資料,例如,在以 SQL 為基礎的工作中使用資料操縱語言 (DML) 陳述式。不過,如果要大量寫入資料,建議使用 BigQuery Storage Write API。
- 附加而非更新:雖然這個解決方案只會附加資料列,而不會更新資料列,但您還是可以從只附加記錄中,在查詢時衍生出「目前狀態」表格,進而模擬更新。
- 為何選用 BigQuery?雖然本說明文件頁面假設您已在使用 BigQuery,但 BigQuery 通常是資料倉儲的絕佳選擇。請注意下列事項:
- 支援服務
- Secret Manager: Secret Manager 會保留密鑰值,確保這些值不會儲存在容易存取的位置,例如直接儲存在函式設定中。
- 身分與存取權管理 (IAM): IAM 會授權函式在執行階段存取必要的機密金鑰,並寫入指定的 BigQuery 資料表。
- Cloud Build:雖然本頁不會深入探討 Cloud Build,但 Cloud Run 會在背景使用 Cloud Build,您可以使用 Cloud Build 自動化持續部署更新,以便在 Git 存放區中變更原始碼。
- 動作和使用者驗證
- Cloud Run 服務帳戶:使用 Looker 動作整合貴機構的專屬第一方資產和資源,最簡單的方法就是使用 Looker Action API 的權杖驗證機制,驗證來自 Looker 執行個體的要求,然後授權該函式使用服務帳戶更新 BigQuery 中的資料。
- OAuth:另一個未在本頁提及的選項,是使用 Looker Action API 的 OAuth 功能。這種方法較為複雜,通常不建議使用,但如果您需要使用 IAM 定義終端使用者寫入資料表的存取權,而非在函式程式碼中使用 Looker 或 ad hoc 邏輯,則可以使用這種方法。
示範程式碼逐步操作說明
我們有一個包含完整示範動作邏輯的單一檔案,可在 GitHub 上取得。在本節中,我們將逐步講解程式碼的主要元素。
設定代碼
第一個部分包含一些示範常數,可識別動作要寫入的資料表。在本頁稍後的部署指南中,您會看到如何將專案 ID 替換為自己的 ID,這是程式碼唯一需要修改的地方。
/*** Demo constants */
const projectId = "your-project-id"
const datasetId = "demo_dataset"
const tableId = "demo_table"
下一節會宣告及初始化動作會使用的幾個程式碼依附元件。我們提供一個範例,說明如何使用 Secret Manager Node.js 模組存取「程式碼內」的 Secret Manager;不過,您也可以使用 Cloud Run 函式的內建功能,在初始化期間擷取密鑰,藉此消除此程式碼依附性。
/*** 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
請注意,我們也會在 package.json
檔案中宣告參照的 @google-cloud
依附元件,讓依附元件預先載入,並提供給 Node.js 執行階段。crypto
是內建的 Node.js 模組,並未在 package.json
中宣告。
HTTP 要求處理和轉送
程式碼向 Cloud Run 函式執行階段公開的主要介面,是遵循 Node.js Express 網路伺服器慣例的匯出 JavaScript 函式。具體來說,函式會收到兩個引數:第一個代表 HTTP 要求,您可以從中讀取各種要求參數和值;第二個代表回應物件,您可以向其中發出回應資料。雖然您可以將任何名稱用於函式,但日後必須將名稱提供給 Cloud Run 函式,詳情請參閱「部署指南」一節。
/*** Entry-point for requests ***/
exports.httpHandler = async function httpHandler(req,res) {
httpHandler
函式的前一個部分會宣告動作可辨識的各種路徑,並密切反映 Action API 針對單一動作所需的端點,以及稍後在檔案中定義的處理每個路徑的函式。
雖然某些動作 + Cloud Run 函式範例會為每個路徑部署個別函式,以便與 Cloud Run 函式的預設路由一對一對應,但函式可以在程式碼中套用額外的「子路由」,如下所示。這最終取決於您的偏好設定,但在程式碼中執行這項額外路由可盡量減少要部署的函式數量,並有助於在所有動作端點中維持單一一致的程式碼狀態。
const routes = {
"/": [hubListing],
"/status": [hubStatus], // Debugging endpoint. Not required.
"/action-0/form": [
requireInstanceAuth,
action0Form
],
"/action-0/execute": [
requireInstanceAuth,
processRequestBody,
action0Execute
]
}
HTTP 處理常式函式的其餘部分會根據先前的路徑宣告,實作 HTTP 要求的處理作業,並將這些處理常式的傳回值連結至回應物件。
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.")
}
}
在 HTTP 處理常式和路徑宣告完成後,我們將深入探討必須實作的三個主要動作端點:
動作清單端點
Looker 管理員首次將 Looker 例項連結至 Actions 伺服器時,Looker 會呼叫提供的網址 (稱為「動作清單端點」),取得透過伺服器提供的動作資訊。
在先前顯示的路由宣告中,我們已在函式的網址底下的根路徑 (/
) 中提供此端點,並指出該端點會由 hubListing
函式處理。
如您在下列函式定義中看到的,這裡沒有太多「程式碼」,只會每次傳回相同的 JSON 資料。值得注意的是,Looker 會動態將「自己的」網址加入部分欄位,讓 Looker 例項將後續要求傳回至相同的函式。
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}
]
}
]
}
}
為了示範,我們的程式碼不需要驗證即可擷取這項資訊。不過,如果您認為動作中繼資料屬於機密資料,也可以為這個路徑要求驗證,如下一節所示。
請注意,我們的 Cloud Run 函式可以公開及處理多個動作,這就是我們使用 /action-X/...
做為路由慣例的原因。不過,我們的 Cloud Run 函式範例只會實作一個動作。
動作表單端點
雖然並非所有用途都需要表單,但在資料庫回寫用途中,表單非常實用,因為使用者可以在 Looker 中檢查資料,然後提供要插入資料庫的值。由於「動作清單」提供 form_url
參數,因此當使用者開始與動作互動時,Looker 就會叫用這個動作表單端點,以決定要從使用者擷取哪些額外資料。
在路由宣告中,我們讓這個端點在 /action-0/form
路徑下可用,並與兩個處理常式建立關聯:requireInstanceAuth
和 action0Form
。
我們設定路徑宣告,允許多個處理程序像這樣運作,因為某些邏輯可重複用於多個端點。
舉例來說,我們可以看到 requireInstanceAuth
用於多個路徑。我們會在要求必須來自 Looker 執行個體的情況下使用這個處理常式。這個處理程序會從 Secret Manager 擷取預期的密鑰符記號值,並拒絕任何沒有該預期符記號值的要求。
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;
}
}
請注意,我們使用 timingSafeEqual
實作項目,而非標準相等性檢查 (==
),以免洩漏側邊通道時間資訊,讓攻擊者快速找出密鑰的值。
假設要求通過例項驗證檢查,系統就會由 action0Form
處理常式處理要求。
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"}
]
}
雖然我們的示範範例非常靜態,但表單程式碼可在特定用途下提供更互動的體驗。舉例來說,系統會根據使用者在初始下拉式選單中的選項,顯示不同的欄位。
動作執行端點
Action Execute 端點是任何動作邏輯的主要所在,也是我們將介紹 BigQuery 插入用途專屬邏輯的地方。
在路徑宣告中,我們讓這個端點在 /action-0/execute
路徑下可用,並與三個處理常式建立關聯:requireInstanceAuth
、processRequestBody
和 action0Execute
。
我們已介紹 requireInstanceAuth
,而 processRequestBody
處理常見的預處理作業,將 Looker 要求主體中某些不便使用的欄位轉換為更方便的格式,但您也可以在完整程式碼檔案中參照該處理常見的預處理作業。
action0Execute
函式會先顯示從動作要求的幾個部分擷取資訊的範例,這些資訊可能很實用。實際上,請注意,程式碼中稱為 formParams
和 actionParams
的要求元素可能包含不同的欄位,這取決於您在資訊清單和表單端點中宣告的內容。
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,
}
程式碼接著會轉換成一些標準 BigQuery 程式碼,實際插入資料。請注意,BigQuery Storage Write API 提供其他更複雜的變化版本,更適合用於持續串流連線或大量插入多個記錄;但如果要在 Cloud Run 函式內容中回應個別使用者互動,這項變化版本是最直接的做法。
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()}
}
}
試用版程式碼也包含「status」端點,可用於疑難排解,但 Action API 整合不必使用這個端點。
部署指南
最後,我們會提供逐步指南,協助您自行部署示範,其中涵蓋必要條件、Cloud Run 函式部署、BigQuery 設定和 Looker 設定。
專案和服務先決條件
開始設定任何具體項目前,請先查看這份清單,瞭解解決方案需要哪些服務和政策:
- 新專案:您需要建立新專案,用於容納範例中的資源。
- 服務:首次在 Cloud 控制台使用者介面中使用 BigQuery 和 Cloud Run 函式時,系統會提示您為必要服務啟用必要 API,包括 BigQuery、Artifact Registry、Cloud Build、Cloud Functions、Cloud Logging、Pub/Sub、Cloud Run 管理員和 Secret Manager。
- 未經驗證的叫用政策:這個用途需要部署「允許未經驗證的叫用」Cloud Run 函式,因為我們會根據 Action API 處理程式碼中傳入要求的驗證,而非使用 IAM。雖然預設情況下允許這項操作,但機構政策通常會限制這類用途。具體來說,
constraints/iam.allowedPolicyMemberDomains
政策會限制可授予 IAM 權限的使用者,因此您可能需要調整政策,允許allUsers
主體存取未經認證的資源。如果您無法允許未經驗證的叫用作業,請參閱這份指南:如何在強制執行「網域限定共用」政策時建立公開 Cloud Run 服務,瞭解詳情。 - 其他政策:請注意,其他Google Cloud 機構政策限制也可能會禁止部署預設允許的服務。
部署 Cloud Run 函式
建立新專案後,請按照下列步驟部署 Cloud Run 函式
- 在「Cloud Run 函式」中,按一下「建立函式」。
- 請為函式選擇任何名稱 (例如「demo-bq-insert-action」)。
- 在「觸發條件」設定下方:
- 觸發條件類型應已設為「HTTPS」。
- 將「驗證」設為「允許未經驗證的叫用」。
- 將 網址 值複製到剪貼簿。
- 在「Runtime」>「Runtime environment variables」設定下方:
- 按一下「新增變數」。
- 將變數名稱設為
CALLBACK_URL_PREFIX
。 - 將上一步的網址貼上做為值。
- 點按「Next」。
- 按一下
package.json
檔案,然後貼上內容。 - 按一下
index.js
檔案,然後貼上內容。 - 將檔案頂端的
projectId
變數指派給您自己的專案 ID。 - 將「Entry Point」(進入點) 設為
httpHandler
。 - 按一下 [Deploy] (部署)。
- 將要求的權限 (如有) 授予建構服務帳戶。
- 等待部署作業完成。
- 如果在後續步驟中收到指示您查看 Google Cloud 記錄的錯誤訊息,請注意,您可以透過本頁的「記錄」分頁存取此函式的記錄。
- 離開 Cloud Run 函式頁面前,請先在「Details」分頁中找出函式的「Service Account」,並記下相關資訊。我們會在後續步驟中使用這個值,確保函式擁有所需的權限。
- 請前往網址,直接在瀏覽器中測試函式部署作業。您應該會看到含有整合項目清單的 JSON 回應。
- 如果您收到 403 錯誤,表示貴機構的政策可能會導致您設定「Allow unauthenticated invocations」時失敗,檢查函式是否允許未經驗證的叫用,查看貴機構的政策設定,然後嘗試更新設定。
擁有 BigQuery 目的地資料表的存取權
實際上,要插入的目的地資料表可以位於不同的 Google Cloud 專案中;但為了示範,我們會在相同專案中建立新的目的地資料表。無論是哪種情況,您都必須確保 Cloud Run 函式的服務帳戶具有寫入資料表的權限。
- 前往 BigQuery 主控台。
建立示範資料表:
- 在「Explorer」列中,使用專案旁的「…」選單,然後選取「建立資料集」。
- 為資料集指定 ID
demo_dataset
,然後按一下「建立資料集」。 - 在剛建立的資料集中使用「…」選單,然後選取「建立資料表」。
- 將資料表命名為
demo_table
。 在「Schema」下方,選取「Edit as text」,使用下列結構定義,然後點選「Create table」。
[ {"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"} ]
指派權限:
- 在「Explorer」列中,按一下資料集。
- 在「資料集」頁面上,依序按一下「共用」>「權限」。
- 按一下「新增主體」。
- 將「新主體」設為函式的服務帳戶 (如本頁稍早所述)。
- 指派 BigQuery 資料編輯者角色。
- 按一下 [儲存]。
連線至 Looker
函式已部署完成,我們會將 Looker 連結至該函式。
- 我們需要你的操作共用密鑰,才能驗證來自 Looker 例項的請求。產生長的隨機字串並妥善保管。我們會在後續步驟中將其用作 Looker 密鑰值。
- 在 Cloud 控制台中前往「Secret Manager」。
- 按一下「Create Secret」。
- 將「Name」設為
LOOKER_SECRET
。(這在本範例的程式碼中為硬式編碼,但您在使用自己的程式碼時,可以選擇任何名稱。) - 將「Secret Value」設為您產生的密鑰值。
- 按一下「Create Secret」。
- 在「Secret」頁面上,按一下「Permissions」分頁標籤。
- 點選「授予存取權」。
- 將「新增主體」設為函式的服務帳戶 (如先前所述)。
- 指派 Secret Manager 密鑰存取者角色。
- 按一下 [儲存]。
- 您可以前往函式網址附加的
/status
路徑,確認函式是否成功存取機密。
- 在 Looker 執行個體中:
- 依序前往「管理」>「平台」>「動作」。
- 前往頁面底部,然後點選「新增 Action Hub」。
- 提供函式的網址 (例如 https://your-region-your-project.cloudfunctions.net/demo-bq-insert-action),然後按一下「Add Action Hub」確認。
- 您現在應該會看到新的行動中心項目,其中包含一個名為「Demo BigQuery 插入」的動作。
- 在 Action Hub 項目中,按一下「設定授權」。
- 將產生的 Looker 密鑰輸入「授權權杖」欄位,然後按一下「更新權杖」。
- 在「Demo BigQuery Insert」動作上,按一下「Enable」。
- 將「Enabled」(啟用)切換鈕切換為開啟。
- 系統應會自動執行動作測試,確認函式是否接受 Looker 的要求,並正確回應表單端點。
- 按一下 [儲存]。
端對端測試
我們現在應該可以實際使用新動作了。這個動作已設定為可搭配任何查詢運作,因此請選擇任何探索 (例如內建的「系統活動」探索),在新的查詢中新增一些欄位並執行,然後從齒輪選單中選擇「傳送」。您應該會看到這項動作是可用的目的地之一,並且系統會提示您輸入一些欄位:
按下「Send」後,BigQuery 資料表應會插入一列資料 (並在 invoked_by
欄中標示 Looker 使用者帳戶的電子郵件地址)!