Cloud Run 함수에서 Looker 작업을 사용하여 BigQuery 리드백

많은 Looker 고객은 사용자가 데이터 웨어하우스의 데이터를 보고하는 것 이상으로 데이터 웨어하우스에 다시 작성하고 업데이트할 수 있도록 지원하고자 합니다.

Looker는 작업 API를 통해 모든 데이터 웨어하우스 또는 대상에 대해 이 사용 사례를 지원합니다. 이 문서 페이지에서는 Google Cloud 인프라를 사용하는 고객이 BigQuery에 다시 작성하기 위해 Cloud Run 함수에 솔루션을 배포하는 방법을 안내합니다. 이 페이지에서는 다음 주제에 대해 설명합니다.

솔루션 고려사항

이 고려사항 목록을 사용하여 이 솔루션이 요구사항에 적합한지 확인합니다.

  • Cloud Run 함수
    • Cloud Run Functions를 사용해야 하는 이유 Google의 '서버리스' 서비스인 Cloud Run 함수는 간편한 운영 및 유지보수를 위해 적합한 선택입니다. 한 가지 고려할 점은 특히 콜드 호출의 경우 지연 시간이 전용 서버를 사용하는 솔루션보다 길 수 있다는 점입니다.
    • 언어 및 런타임 Cloud Run 함수는 여러 언어와 런타임을 지원합니다. 이 문서 페이지에서는 JavaScript 및 Node.js의 예시를 중점적으로 다룹니다. 그러나 개념은 지원되는 다른 언어 및 런타임으로 직접 번역할 수 있습니다.
  • BigQuery
    • BigQuery를 사용해야 하는 이유 이 문서 페이지에서는 이미 BigQuery를 사용하고 있다고 가정하지만, BigQuery는 일반적으로 데이터 웨어하우스에 적합합니다. 다음 사항에 유의하세요.
      • BigQuery Storage Write API: BigQuery는 SQL 기반 작업의 데이터 조작 언어 (DML) 문 등 데이터 웨어하우스의 데이터를 업데이트하기 위한 여러 인터페이스를 제공합니다. 그러나 대량 쓰기에 가장 적합한 옵션은 BigQuery Storage Write API입니다.
      • 업데이트 대신 추가: 이 솔루션은 행을 업데이트하는 것이 아니라 추가하는 것만 하지만, 항상 추가 전용 로그에서 쿼리 시 '현재 상태' 테이블을 파생하여 업데이트를 시뮬레이션할 수 있습니다.
  • 지원 서비스
    • Secret Manager: Secret Manager는 보안 비밀 값을 보관하여 함수 구성에 직접 저장되는 등 과도하게 액세스 가능한 위치에 저장되지 않도록 합니다.
    • Identity and Access Management (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 기능을 사용하는 것입니다. 이 접근 방식은 더 복잡하며 일반적으로 필요하지는 않지만, Looker의 액세스 권한을 사용하거나 함수 코드 내에서 임시 로직을 사용하는 대신 IAM을 사용하여 테이블에 쓰기 위한 최종 사용자의 액세스 권한을 정의해야 하는 경우에 사용할 수 있습니다.

데모 코드 둘러보기

데모 작업 로직 전체가 포함된 단일 파일은 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

참조된 @google-cloud 종속 항목은 종속 항목을 미리 로드하고 Node.js 런타임에서 사용할 수 있도록 package.json 파일에서도 선언됩니다. 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 인스턴스를 작업 서버에 처음 연결하면 Looker는 '작업 목록 엔드포인트'라고 하는 제공된 URL을 호출하여 서버를 통해 사용할 수 있는 작업에 관한 정보를 가져옵니다.

이전에 보여준 경로 선언에서 이 엔드포인트를 함수의 URL 아래 루트 경로 (/)에서 사용할 수 있도록 했으며 hubListing 함수에서 처리할 것이라고 표시했습니다.

다음 함수 정의에서 볼 수 있듯이 '코드'가 많지 않습니다. 매번 동일한 JSON 데이터를 반환하기만 합니다. 한 가지 유의할 점은 일부 필드에 '자체' URL이 동적으로 포함되므로 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 경로에서 사용할 수 있도록 했으며 requireInstanceAuthaction0Form라는 두 핸들러를 연결했습니다.

일부 로직은 여러 엔드포인트에 재사용할 수 있으므로 이와 같이 여러 핸들러를 허용하도록 경로 선언을 설정했습니다.

예를 들어 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 함수는 작업 요청의 여러 부분에서 유용할 수 있는 정보를 추출하는 예시를 보여주는 것으로 시작합니다. 실제로 코드에서 formParamsactionParams로 참조하는 요청 요소는 등록정보 및 양식 엔드포인트에서 선언한 내용에 따라 다른 필드를 포함할 수 있습니다.

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 구성을 다루는 데모 배포를 위한 단계별 가이드를 제공합니다.

프로젝트 및 서비스 기본 요건

세부사항을 구성하기 전에 이 목록을 검토하여 솔루션에 필요한 서비스와 정책을 파악하세요.

  1. 새 프로젝트: 예시의 리소스를 보관할 새 프로젝트가 필요합니다.
  2. 서비스: Cloud 콘솔 UI에서 BigQuery 및 Cloud Run 함수를 처음 사용할 때 BigQuery, Artifact Registry, Cloud Build, Cloud Functions, Cloud Logging, Pub/Sub, Cloud Run Admin, Secret Manager 등 필요한 서비스에 필요한 API를 사용 설정하라는 메시지가 표시됩니다.
  3. 인증되지 않은 호출에 대한 정책: 이 사용 사례에서는 IAM을 사용하는 대신 Action API에 따라 코드에서 수신 요청의 인증을 처리하므로 '인증되지 않은 호출을 허용'하는 Cloud Run 함수를 배포해야 합니다. 기본적으로 이러한 작업은 허용되지만 조직 정책에 따라 이러한 사용이 제한되는 경우가 많습니다. 특히 constraints/iam.allowedPolicyMemberDomains 정책은 IAM 권한을 부여받을 수 있는 사용자를 제한하며, allUsers 주 구성원의 인증되지 않은 액세스를 허용하도록 조정해야 할 수 있습니다. 인증되지 않은 호출을 허용할 수 없는 경우 이 가이드인 도메인 제한 공유가 적용될 때 공개 Cloud Run 서비스를 만드는 방법을 참고하세요.
  4. 기타 정책: 다른 Google Cloud 조직 정책 제약조건으로 인해 기본적으로 허용되는 서비스의 배포가 차단될 수도 있습니다.

Cloud Run 함수 배포

새 프로젝트를 만든 후 다음 단계에 따라 Cloud Run 함수를 배포합니다.

  1. Cloud Run 함수에서 함수 만들기를 클릭합니다.
  2. 함수의 이름을 선택합니다 (예: 'demo-bq-insert-action').
  3. 트리거 설정에서 다음을 수행합니다.
    1. 트리거 유형은 이미 'HTTPS'여야 합니다.
    2. 인증인증되지 않은 호출 허용으로 설정합니다.
    3. URL 값을 클립보드에 복사합니다.
  4. 런타임 > 런타임 환경 변수 설정에서 다음을 수행합니다.
    1. 변수 추가를 클릭합니다.
    2. 변수 이름을 CALLBACK_URL_PREFIX로 설정합니다.
    3. 이전 단계의 URL을 값으로 붙여넣습니다.
  5. 다음을 클릭합니다.
  6. package.json 파일을 클릭하고 콘텐츠를 붙여넣습니다.
  7. index.js 파일을 클릭하고 콘텐츠를 붙여넣습니다.
  8. 파일 상단의 projectId 변수를 자체 프로젝트 ID에 할당합니다.
  9. 진입점httpHandler로 설정합니다.
  10. 배포를 클릭합니다.
  11. 빌드 서비스 계정에 요청된 권한 (있는 경우)을 부여합니다.
  12. 배포가 완료될 때까지 기다립니다.
  13. 이후 단계에서 Google Cloud 로그를 검토하라는 오류가 표시되면 이 페이지의 로그 탭에서 이 함수의 로그에 액세스할 수 있습니다.
  14. Cloud Run 함수 페이지에서 나가기 전에 세부정보 탭에서 함수의 서비스 계정을 찾아 기록합니다. 이 값은 이후 단계에서 함수에 필요한 권한이 있는지 확인하는 데 사용합니다.
  15. URL을 방문하여 브라우저에서 직접 함수 배포를 테스트합니다. 통합 등록정보가 포함된 JSON 응답이 표시됩니다.
  16. 403 오류가 발생하면 조직 정책으로 인해 인증되지 않은 호출 허용을 설정하려는 시도가 자동으로 실패했을 수 있습니다. 함수가 인증되지 않은 호출을 허용하는지 확인하고 조직 정책 설정을 검토한 후 설정을 업데이트해 봅니다.

BigQuery 대상 테이블 액세스

실제로 삽입할 대상 테이블은 다른 Google Cloud 프로젝트에 있을 수 있지만, 데모를 위해 동일한 프로젝트에 새 대상 테이블을 만들겠습니다. 두 경우 모두 Cloud Run 함수의 서비스 계정에 테이블에 쓸 권한이 있는지 확인해야 합니다.

  1. BigQuery 콘솔로 이동합니다.
  2. 데모 테이블을 만듭니다.

    1. 탐색기 표시줄에서 프로젝트 옆에 있는 말줄임표 메뉴를 사용하고 데이터 세트 만들기를 선택합니다.
    2. 데이터 세트의 ID를 demo_dataset로 지정하고 데이터 세트 만들기를 클릭합니다.
    3. 새로 만든 데이터 세트에서 점 3개 메뉴를 사용하고 테이블 만들기를 선택합니다.
    4. 테이블 이름을 demo_table로 지정합니다.
    5. 스키마에서 텍스트로 수정을 선택하고 다음 스키마를 사용한 후 테이블 만들기를 클릭합니다.

      [
       {"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"}
      ]
      
  3. 권한 할당:

    1. 탐색기 막대에서 데이터 세트를 클릭합니다.
    2. 데이터 세트 페이지에서 공유 > 권한을 클릭합니다.
    3. 주 구성원 추가를 클릭합니다.
    4. 새 주 구성원을 이 페이지 앞부분에 설명된 함수의 서비스 계정으로 설정합니다.
    5. BigQuery 데이터 편집자 역할을 할당합니다.
    6. 저장을 클릭합니다.

Looker에 연결

함수가 배포되었으므로 이제 Looker를 함수에 연결하겠습니다.

  1. Looker 인스턴스에서 요청이 발생했음을 인증하려면 작업에 공유 비밀번호가 필요합니다. 긴 임의의 문자열을 생성하고 안전하게 보관합니다. 이 값은 후속 단계에서 Looker 비밀 값으로 사용됩니다.
  2. Cloud 콘솔에서 Secret Manager로 이동합니다.
    1. 보안 비밀 만들기를 클릭합니다.
    2. 이름LOOKER_SECRET로 설정합니다. 이 이름은 이 데모의 코드에 하드코딩되어 있지만 자체 코드로 작업할 때는 어떤 이름이든 선택할 수 있습니다.
    3. Secret Value(비밀번호 값)를 생성한 비밀번호 값으로 설정합니다.
    4. 보안 비밀 만들기를 클릭합니다.
    5. 보안 비밀 페이지에서 권한 탭을 클릭합니다.
    6. 액세스 권한 부여를 클릭합니다.
    7. 새 주 구성원을 앞에서 확인한 함수의 서비스 계정으로 설정합니다.
    8. Secret Manager 보안 비밀 접근자 역할을 할당합니다.
    9. 저장을 클릭합니다.
    10. 함수 URL에 추가된 /status 경로를 방문하여 함수가 보안 비밀에 성공적으로 액세스하는지 확인할 수 있습니다.
  3. Looker 인스턴스에서 다음 단계를 따르세요.
    1. 관리 > 플랫폼 > 작업으로 이동합니다.
    2. 페이지 하단으로 이동하여 작업 허브 추가를 클릭합니다.
    3. 함수의 URL (예: https://your-region-your-project.cloudfunctions.net/demo-bq-insert-action)을 입력하고 작업 허브 추가를 클릭하여 확인합니다.
    4. 이제 Demo BigQuery Insert라는 작업이 하나 포함된 새 작업 허브 항목이 표시됩니다.
    5. 작업 허브 항목에서 승인 구성을 클릭합니다.
    6. 생성된 Looker Secret승인 토큰 입력란에 입력하고 토큰 업데이트를 클릭합니다.
    7. BigQuery 삽입 데모 작업에서 사용 설정을 클릭합니다.
    8. 사용 설정됨 스위치를 사용으로 전환합니다.
    9. 작업 테스트가 자동으로 실행되어 함수가 Looker의 요청을 수락하고 양식 엔드포인트에 올바르게 응답하는지 확인합니다.
    10. 저장을 클릭합니다.

엔드 투 엔드 테스트

이제 새 작업을 실제로 사용할 수 있습니다. 이 작업은 모든 쿼리에서 작동하도록 구성되어 있으므로 Explore (예: 내장된 시스템 활동 Explore)를 선택하고 새 쿼리에 필드를 추가한 후 실행한 다음 톱니바퀴 메뉴에서 보내기를 선택합니다. 작업이 사용 가능한 대상 중 하나로 표시되고 일부 입력란을 입력하라는 메시지가 표시됩니다.

새 작업이 선택된 Looker 'Send'(보내기) 모달의 스크린샷

Send를 누르면 BigQuery 테이블에 새 행이 삽입되고 invoked_by 열에 Looker 사용자 계정의 이메일이 표시됩니다.