This commit is contained in:
kosukesuenaga 2025-11-17 14:21:29 +09:00
commit 922fa0e77a
62 changed files with 2586 additions and 0 deletions

14
.gitignore vendored Executable file
View file

@ -0,0 +1,14 @@
handle-company-webhook/
terraform.*
.terraform*
IAM/
test/
venv/
__pycache__/
*.csv
request.json

80
README.md Executable file
View file

@ -0,0 +1,80 @@
# Sales Tool プロジェクト
## 概要
このリポジトリは、Sales Tool と呼ばれるシステムのバックエンドおよびインフラ構成を管理しています。主に以下のコンポーネントで構成されています。
- API ゲートウェイOpenAPI 定義)
- Google Cloud Functions各種ファンクション
- ワークフロー定義Cloud Workflow
- Terraform を使ったインフラ構成
- テスト用スクリプト・データ
---
## フォルダ構成
```
.
├── requirements.txt
├── api-gateway
│ ├── openapi_dev.yaml # 開発環境用 OpenAPI 定義
│ └── openapi.yaml # 本番環境用 OpenAPI 定義
├── functions # Cloud Functions 実装
│ ├── add-company
│ │ ├── _scripts
│ │ │ └── deploy_dev.sh # デプロイスクリプト(開発環境)
│ │ └── source
│ │ ├── main.py
│ │ └── requirements.txt
│ ├── create-meeting-log-to-hubspot
│ │ └── source
│ │ └── main.py
│ ├── create-minutes
│ │ └── source
│ │ ├── main.py
│ │ └── requirements.txt
│ └── trigger-minutes-workflow-from-miitel
│ └── source
│ ├── main.py
│ └── requirements.txt
├── test # テスト用スクリプトとサンプルデータ
│ ├── main.py
│ ├── mst_company.csv
│ ├── request.json
│ ├── requirements.txt
│ └── __pycache__
│ └── main.cpython-38.pyc
├── terraform # インフラTerraform
│ └── dev
│ ├── main.tf
│ ├── terraform.tfstate
│ └── terraform.tfstate.backup
└── workflows # Cloud Workflow 定義
└── workflow-create-minutes
└── main.yaml
```
---
## ディレクトリ詳細
- **api-gateway**: OpenAPI 仕様ファイルを管理し、API ゲートウェイで使用。
- **functions**: 各種 Google Cloud Functions のソースコードと依存定義。
- **test**: ローカルでの動作確認やユニットテストに使用するスクリプト・データ。
- **terraform**: Google Cloud 上のリソースStorage バケットなど)の構築をコード化。
- **workflows**: Cloud Workflow の実行定義ファイル(`.yaml`)。
---
## ローカル実行方法(概要)
1. Python の依存インストール: `pip install -r requirements.txt`
2. Functions のデプロイ: 各フォルダ内の `deploy_dev.sh` を実行
3. Terraform 初期化・適用:
```bash
cd terraform/dev && terraform init && terraform apply -auto-approve
```
4. ワークフロー実行: Cloud Console または `gcloud workflows` CLI
詳細は各ディレクトリの README やコメントを参照してください。

16
_test/test_dev.sh Executable file
View file

@ -0,0 +1,16 @@
#!/bin/bash
# APIエンドポイントURL
API_URL="https://sales-tool-gw-dev-ex1cujb.an.gateway.dev/trigger-minutes-workflow-from-miitel"
# APIキーヘッダーに付与する場合
API_KEY="AIzaSyBVJOtvJTB4noAfUGEyMhCRqsF5yfypENc"
# リクエストボディ
JSON_FILE="request.json"
# curlコマンド実行
curl -X POST "$API_URL" \
-H "Content-Type: application/json" \
-H "x-api-key: $API_KEY" \
-d @"$JSON_FILE"

17
api-gateway/create_api_dev.sh Executable file
View file

@ -0,0 +1,17 @@
#!/bin/bash
# プロジェクトIDを設定
PROJECT_ID="datacom-poc"
# 使用するAPI名
API_NAME="sales-tool-api"
# プロジェクトを設定
gcloud auth application-default set-quota-project dmiru-dev
gcloud config set project $PROJECT_ID
# API Gatewayを作成
gcloud api-gateway apis create sales-tool-api \
echo "API Gateway '${API_NAME}' が作成され、有効化されました。"

31
api-gateway/deploy_dev.sh Executable file
View file

@ -0,0 +1,31 @@
#!/bin/bash
# 環境変数
API_NAME="sales-tool-api"
API_CONFIG_NAME="sales-tool-gw-dev-conf-20250619"
GATEWAY_NAME="sales-tool-gw-dev"
OPENAPI_SPEC="openapi_dev.yaml"
PROJECT_ID="datacom-poc"
SERVICE_ACCOUNT="api-gateway-mpos@datacom-poc.iam.gserviceaccount.com"
LOCATION="asia-northeast1"
gcloud auth application-default set-quota-project $PROJECT_ID
gcloud config set project $PROJECT_ID
# API GatewayのAPI Configを作成
echo "Creating API Config..."
gcloud api-gateway api-configs create $API_CONFIG_NAME \
--api=$API_NAME \
--openapi-spec=$OPENAPI_SPEC \
--project=$PROJECT_ID \
--backend-auth-service-account=$SERVICE_ACCOUNT
# API GatewayのGatewayを作成
echo "Creating Gateway..."
gcloud api-gateway gateways create $GATEWAY_NAME \
--api=$API_NAME \
--api-config=$API_CONFIG_NAME \
--location=$LOCATION \
--project=$PROJECT_ID
echo "API Gateway deployment completed successfully."

77
api-gateway/openapi.yaml Executable file
View file

@ -0,0 +1,77 @@
swagger: '2.0'
info:
title: crate-minutes-api
description: 'Meeting Minutes Generator Web-API'
version: '1.0.0'
schemes:
- 'https'
host: 'crate-minutes-gw-a8slsa47.an.gateway.dev'
x-google-endpoints:
- name: 'crate-minutes-gw-a8slsa47.an.gateway.dev'
allowCors: True
paths:
/create-minutes:
post:
description: '商談レポート作成'
operationId: 'create-minutes'
x-google-backend:
address: https://asia-northeast1-rational-timing-443808-u0.cloudfunctions.net/create-minutes
path_translation: CONSTANT_ADDRESS
consumes:
- application/json
produces:
- application/json
parameters:
- in: body
name: body
description: JSON payload
required: false
schema:
type: object
additionalProperties: true
responses:
200:
description: 'OK'
schema:
type: object
additionalProperties: true
401:
description: 'Auth Error'
schema:
type: object
properties:
error:
type: string
500:
description: 'Error'
schema:
type: object
properties:
error:
type: string
security:
- APIKeyHeader: []
options:
summary: 'CORS support'
operationId: 'create-minutes-options'
x-google-backend:
address: https://asia-northeast1-rational-timing-443808-u0.cloudfunctions.net/create-minutes
path_translation: CONSTANT_ADDRESS
responses:
204:
description: 'CORS preflight'
headers:
Access-Control-Allow-Origin:
type: string
default: '*'
Access-Control-Allow-Methods:
type: string
default: 'GET, POST, OPTIONS'
Access-Control-Allow-Headers:
type: string
default: 'Content-Type, x-api-key'
securityDefinitions:
APIKeyHeader:
type: apiKey
name: x-api-key
in: header

73
api-gateway/openapi_dev.yaml Executable file
View file

@ -0,0 +1,73 @@
swagger: '2.0'
info:
title: crate-minutes-api
description: 'Meeting Minutes Generator Web-API'
version: '1.0.0'
schemes:
- 'https'
paths:
/trigger-minutes-workflow-from-miitel:
post:
description: 'ワークフロー呼び出し処理'
operationId: 'trigger-minutes-workflow-from-miitel'
x-google-backend:
address: https://asia-northeast1-datacom-poc.cloudfunctions.net/mrt-trigger-minutes-workflow-from-miitel
path_translation: CONSTANT_ADDRESS
consumes:
- application/json
produces:
- application/json
parameters:
- in: body
name: body
description: JSON payload
required: false
schema:
type: object
additionalProperties: true
responses:
200:
description: 'OK'
schema:
type: object
additionalProperties: true
401:
description: 'Auth Error'
schema:
type: object
properties:
error:
type: string
500:
description: 'Error'
schema:
type: object
properties:
error:
type: string
security:
- APIKeyHeader: []
options:
summary: 'CORS support'
operationId: 'trigger-minutes-workflow-from-miitel-options'
x-google-backend:
address: https://asia-northeast1-datacom-poc.cloudfunctions.net/mrttrigger-minutes-workflow-from-miitel
path_translation: CONSTANT_ADDRESS
responses:
204:
description: 'CORS preflight'
headers:
Access-Control-Allow-Origin:
type: string
default: '*'
Access-Control-Allow-Methods:
type: string
default: 'GET, POST, OPTIONS'
Access-Control-Allow-Headers:
type: string
default: 'Content-Type, x-api-key'
securityDefinitions:
APIKeyHeader:
type: apiKey
name: x-api-key
in: header

196
cloudbuild_dev.yaml Executable file
View file

@ -0,0 +1,196 @@
substitutions:
_ENV: 'dev'
_CF_SERVICE_ACCOUNT: 'mrt-cloudfunctions-sa-devtest'
_CW_SERVICE_ACCOUNT: 'mrt-cloudworkflows-sa-devtest'
options:
logging: CLOUD_LOGGING_ONLY
steps:
# 会社一覧取得
- id: 'gcloud functions deploy mrt-export-companies-to-gcs'
name: gcr.io/cloud-builders/gcloud
dir: 'functions/export-companies-to-gcs'
args: [
'functions',
'deploy',
'mrt-export-companies-to-gcs',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_dev',
'--project=$PROJECT_ID',
'--quiet',
]
waitFor: ['-']
# 担当者一覧取得
- name: gcr.io/cloud-builders/gcloud
dir: 'functions/export-owners-to-gcs'
args: [
'functions',
'deploy',
'mrt-export-owners-to-gcs',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--no-allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_dev',
'--project=$PROJECT_ID',
'--quiet',
]
waitFor: ['-']
# スプレッドシート作成
- name: gcr.io/cloud-builders/gcloud
dir: 'functions/create-log-sheet'
args: [
'functions',
'deploy',
'mrt-create-log-sheet',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--no-allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_dev',
'--project=$PROJECT_ID',
'--quiet',
]
waitFor: ['-']
# ワークフロー呼び出し関数
- name: gcr.io/cloud-builders/gcloud
dir: 'functions/trigger-minutes-workflow-from-miitel'
args: [
'functions',
'deploy',
'mrt-trigger-minutes-workflow-from-miitel',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--no-allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_dev',
'--project=$PROJECT_ID',
'--quiet',
]
waitFor: ['-']
# 議事録作成関数
- name: gcr.io/cloud-builders/gcloud
dir: 'functions/generate-meeting-minutes'
args: [
'functions',
'deploy',
'mrt-generate-meeting-minutes',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--cpu=0.5',
'--memory=1Gi',
'--no-allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_dev',
'--project=$PROJECT_ID',
'--timeout=10m',
'--quiet',
]
waitFor: ['-']
# 議事録をドライブへアップロードする関数
- name: gcr.io/cloud-builders/gcloud
dir: 'functions/upload-minutes-to-drive'
args: [
'functions',
'deploy',
'mrt-upload-minutes-to-drive',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--cpu=0.5',
'--memory=1Gi',
'--no-allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_dev',
'--project=$PROJECT_ID',
'--quiet',
]
waitFor: ['-']
# Hubspot連携関数
- name: gcr.io/cloud-builders/gcloud
dir: 'functions/create-hubspot-meeting-log'
args: [
'functions',
'deploy',
'mrt-create-hubspot-meeting-log',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--no-allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_dev',
'--project=$PROJECT_ID',
'--quiet',
]
waitFor: ['-']
# スプレッドシートへ記録
- name: gcr.io/cloud-builders/gcloud
dir: 'functions/append-log-to-sheet'
args: [
'functions',
'deploy',
'mrt-append-log-to-sheet',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--no-allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_dev',
'--project=$PROJECT_ID',
'--quiet',
]
waitFor: ['-']
# ワークフロー
- name: gcr.io/cloud-builders/gcloud
dir: 'workflows/workflow-create-minutes'
args:
[
'workflows',
'deploy',
'mrt-workflow-create-minutes',
'--location=asia-northeast1',
'--source=main.yaml',
'--service-account=$_CW_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--quiet',
]

193
cloudbuild_prod.yaml Executable file
View file

@ -0,0 +1,193 @@
substitutions:
_ENV: 'prod'
_CF_SERVICE_ACCOUNT: 'mrt-cloudfunctions-sa'
_CW_SERVICE_ACCOUNT: 'mrt-cloudworkflows-sa'
steps:
# 会社一覧取得
- id: 'gcloud functions deploy mrt-export-companies-to-gcs'
name: gcr.io/cloud-builders/gcloud
dir: 'functions/export-companies-to-gcs'
args: [
'functions',
'deploy',
'mrt-export-companies-to-gcs',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_prod',
'--project=$PROJECT_ID',
'--quiet',
]
waitFor: ['-']
# 担当者一覧取得
- name: gcr.io/cloud-builders/gcloud
dir: 'functions/export-owners-to-gcs'
args: [
'functions',
'deploy',
'mrt-export-owners-to-gcs',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--no-allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_prod',
'--project=$PROJECT_ID',
'--quiet',
]
waitFor: ['-']
# スプレッドシート作成
- name: gcr.io/cloud-builders/gcloud
dir: 'functions/create-log-sheet'
args: [
'functions',
'deploy',
'mrt-create-log-sheet',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--no-allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_prod',
'--project=$PROJECT_ID',
'--quiet',
]
waitFor: ['-']
# ワークフロー呼び出し関数
- name: gcr.io/cloud-builders/gcloud
dir: 'functions/trigger-minutes-workflow-from-miitel'
args: [
'functions',
'deploy',
'mrt-trigger-minutes-workflow-from-miitel',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--no-allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_prod',
'--project=$PROJECT_ID',
'--quiet',
]
waitFor: ['-']
# 議事録作成関数
- name: gcr.io/cloud-builders/gcloud
dir: 'functions/generate-meeting-minutes'
args: [
'functions',
'deploy',
'mrt-generate-meeting-minutes',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--cpu=0.5',
'--memory=1Gi',
'--no-allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_prod',
'--project=$PROJECT_ID',
'--timeout=10m',
'--quiet',
]
waitFor: ['-']
# 議事録をドライブへアップロードする関数
- name: gcr.io/cloud-builders/gcloud
dir: 'functions/upload-minutes-to-drive'
args: [
'functions',
'deploy',
'mrt-upload-minutes-to-drive',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--cpu=0.5',
'--memory=1Gi',
'--no-allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_prod',
'--project=$PROJECT_ID',
'--quiet',
]
waitFor: ['-']
# Hubspot連携関数
- name: gcr.io/cloud-builders/gcloud
dir: 'functions/create-hubspot-meeting-log'
args: [
'functions',
'deploy',
'mrt-create-hubspot-meeting-log',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--no-allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_prod',
'--project=$PROJECT_ID',
'--quiet',
]
waitFor: ['-']
# スプレッドシートへ記録
- name: gcr.io/cloud-builders/gcloud
dir: 'functions/append-log-to-sheet'
args: [
'functions',
'deploy',
'mrt-append-log-to-sheet',
'--gen2',
'--runtime=python312',
'--region=asia-northeast1',
'--source=./source', # dir で切り替えているので「.」
'--entry-point=handle_request', # 変更する場合はここ
'--trigger-http',
'--no-allow-unauthenticated',
'--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--env-vars-file=.env_prod',
'--project=$PROJECT_ID',
'--quiet',
]
waitFor: ['-']
# ワークフロー
- name: gcr.io/cloud-builders/gcloud
dir: 'workflows/workflow-create-minutes'
args:
[
'workflows',
'deploy',
'mrt-workflow-create-minutes',
'--location=asia-northeast1',
'--source=main.yaml',
'--service-account=$_CW_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com',
'--quiet',
]

View file

@ -0,0 +1,5 @@
KEY_PATH=projects/32472615575/secrets/sa-access-google-drive-key
LOG_FOLDER_ID=1IZToaM9K9OJXrgV05aLO5k2ZCXpdlJzX
MEETING_FOLDER_ID=1cCDJKusfrlDrJe2yHCR8pCHJXRqX-4Hw
HUBSPOT_COMPANY_URL=https://app-na2.hubspot.com/contacts/242960467/record/0-2
MODE=dev

View file

@ -0,0 +1,5 @@
KEY_PATH: projects/32472615575/secrets/sa-access-google-drive-key
LOG_FOLDER_ID: 1IZToaM9K9OJXrgV05aLO5k2ZCXpdlJzX
MEETING_FOLDER_ID: 1cCDJKusfrlDrJe2yHCR8pCHJXRqX-4Hw
HUBSPOT_COMPANY_URL: https://app-na2.hubspot.com/contacts/242960467/record/0-2
MODE: dev

View file

@ -0,0 +1,5 @@
KEY_PATH: projects/570987459910/secrets/sa-create-minutes-key
LOG_FOLDER_ID: 1arL6AxpvA7N6Umg4wdrdAcRWBdKc-Jfb
MEETING_FOLDER_ID: 0AGT_1dSq66qYUk9PVA
HUBSPOT_COMPANY_URL: https://app.hubspot.com/contacts/22400567/record/0-2
MODE: production

View file

@ -0,0 +1,33 @@
#!/bin/bash
# プロジェクトIDを設定
PROJECT_ID="datacom-poc"
# デプロイする関数名
FUNCTION_NAME="mrt-append-log-to-sheet"
# 関数のエントリポイント
ENTRY_POINT="handle_request"
# ランタイム
RUNTIME="python312"
# リージョン
REGION="asia-northeast1"
# 環境変数ファイル
ENV_VARS_FILE=".env_dev"
gcloud auth application-default set-quota-project $PROJECT_ID
gcloud config set project $PROJECT_ID
# デプロイコマンド
gcloud functions deploy $FUNCTION_NAME \
--gen2 \
--region $REGION \
--runtime $RUNTIME \
--source=./source \
--trigger-http \
--no-allow-unauthenticated \
--entry-point $ENTRY_POINT \
--env-vars-file $ENV_VARS_FILE

View file

@ -0,0 +1,267 @@
import functions_framework
from google.cloud import secretmanager
from google.oauth2 import service_account
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
import json
import os
from datetime import datetime, timezone, timedelta
sm_client = secretmanager.SecretManagerServiceClient()
SCOPES = ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/drive.file"]
HEADER_VALUES = ["タイムスタンプ","商談日", "タイトル", "登録先企業","担当者", "ミーティングURL", "議事録URL", "HubSpot会社概要URL"]
@functions_framework.http
def handle_request(request):
# POSTリクエストの処理
if request.method != 'POST':
return ('', 405, {'Allow': 'POST', 'Content-Type': 'application/json'}) # メソッドがPOSTでない場合は405エラーを返す
"""Shows basic usage of the Drive Activity API.
Prints information about the last 10 events that occured the user's Drive.
"""
try:
request_json = request.get_json()
print(request_json)
title = request_json['title'] # 会議タイトル
document_id = request_json['document_id'] # 議事録ファイルのID
matched_company_id = request_json['matched_company_id'] # マッチした会社ID
matched_company_name = request_json['matched_company_name'] # マッチした会社名
host_name = request_json['host_name'] # ホストユーザー名
video_url = request_json['video_url'] # 会議履歴URL
starts_at = request_json['starts_at'] # 開始日時
log_folder_id = os.getenv("LOG_FOLDER_ID") # 共有ドライブID
meeting_folder_id = os.getenv("MEETING_FOLDER_ID") # ミーティングフォルダID
hubspot_company_url = os.getenv("HUBSPOT_COMPANY_URL") # HubSpotの会社情報URL
mode = os.getenv("MODE") # モードdevまたはprod
service_account_info = get_service_account_info()
# 認証
credentials = get_credentials(service_account_info)
# APIクライアントの構築
drive_service = build("drive", "v3", credentials=credentials)
sheet_service = build("sheets", "v4", credentials=credentials)
# 現在日時をJSTに変換
jst_now = datetime.now(timezone.utc).astimezone(timezone(timedelta(hours=9)))
# JSTの現在日時を文字列に変換
ym_str = jst_now.strftime("%Y%m")
y_str = jst_now.strftime("%Y")
# 年別のフォルダを検索
target_folder = get_directory_files_dev(drive_service, log_folder_id, y_str) if mode == "dev" else get_directory_files_prod(drive_service, meeting_folder_id, log_folder_id, y_str)
print("target_folder", target_folder)
year_folder_id = None
if not target_folder:
# フォルダが存在しない場合は新規作成
year_folder_id = create_new_folder(drive_service, log_folder_id, y_str)
else:
# フォルダが存在する場合はそのIDを使用
year_folder_id = target_folder[0]['id']
print("年別のフォルダID:", year_folder_id)
# スプレッドシートを検索
target_files = get_directory_files_dev(drive_service, year_folder_id, ym_str) if mode == "dev" else get_directory_files_prod(drive_service, meeting_folder_id, year_folder_id, ym_str)
print("スプレッドシート", target_files)
if not target_files:
print('not found')
# スプレッドシートを作成
spreadsheet_id = create_new_spreadsheet(drive_service, year_folder_id, ym_str)
print("スプレッドシートID:", spreadsheet_id)
# 注意事項追加
append_log_to_sheet(sheet_service, spreadsheet_id, ["※シート名変更厳禁"])
# ヘッダーを追加
append_log_to_sheet(sheet_service, spreadsheet_id, HEADER_VALUES)
else:
print('found')
# ファイルIDを取得
spreadsheet_id = target_files[0]['id']
documnet_url = f"https://docs.google.com/document/d/{document_id}/edit" if document_id else ""
hubspot_url = f"{hubspot_company_url}/{matched_company_id}" if matched_company_id else ""
# テストログを追加
row_data = [jst_now.strftime("%Y-%m-%d %H:%M:%S"),
convert_to_jst_ymd(starts_at),
title,
matched_company_name,
host_name,
video_url,
documnet_url,
hubspot_url
]
append_log_to_sheet(sheet_service, spreadsheet_id, row_data)
print("ログを追加しました:", row_data)
return (json.dumps({"status": "success"}, ensure_ascii=False), 200, {"Content-Type": "application/json"})
except HttpError as error:
# TODO(developer) - Handleerrors from drive activity API.
print(f"An error occurred: {error}")
#
# SecretManagerから秘密鍵を取得
#
def get_service_account_info():
key_path = os.getenv('KEY_PATH') + "/versions/1"
# 秘密鍵取得
response = sm_client.access_secret_version(name=key_path)
# 秘密鍵の値をデコード
secret_key = response.payload.data.decode("UTF-8")
return json.loads(secret_key)
# Google Drive認証
def get_credentials(service_account_info):
credentials = service_account.Credentials.from_service_account_info(
service_account_info,
scopes=SCOPES
)
return credentials
# 開発用マイドライブからのファイルを取得
def get_directory_files_dev(service,shared_folder_id, filename):
"""
対象のディレクトリ配下からファイル名で検索した結果を配列で返す
:param filename: ファイル名
:param directory_id: ディレクトリID
:param pages_max: 最大ページ探索数
:return: ファイルリスト
"""
items = []
page = 0
pages_max = 10 # 最大ページ数
while True:
page += 1
if page == pages_max:
break
results = service.files().list(
corpora="user",
includeItemsFromAllDrives=True,
includeTeamDriveItems=True,
q=f"'{shared_folder_id}' in parents and name = '{filename}' and trashed = false",
supportsAllDrives=True,
pageSize=10,
fields="nextPageToken, files(id, name)").execute()
items += results.get("files", [])
page_token = results.get('nextPageToken', None)
if page_token is None:
break
return items
# 本番用共有ドライブからのファイルを取得
def get_directory_files_prod(service,shared_folder_id,sub_folder_id,filename):
"""
対象のディレクトリ配下からファイル名で検索した結果を配列で返す
:param filename: ファイル名
:param directory_id: ディレクトリID
:param pages_max: 最大ページ探索数
:return: ファイルリスト
"""
items = []
page = 0
pages_max = 10 # 最大ページ数
while True:
page += 1
if page == pages_max:
break
results = service.files().list(
corpora="drive",
driveId=shared_folder_id,
includeItemsFromAllDrives=True,
includeTeamDriveItems=True,
q=f"'{sub_folder_id}' in parents and name = '{filename}' and trashed = false",
supportsAllDrives=True,
pageSize=10,
fields="nextPageToken, files(id, name, parents)").execute()
items += results.get("files", [])
page_token = results.get('nextPageToken', None)
if page_token is None:
break
return items
def create_new_folder(service, sub_folder_id, title):
"""
Google Drive APIを使用して新しいフォルダを作成する
:param service: Google Drive APIのサービスオブジェクト
:param title: フォルダのタイトル
:return: 作成したフォルダのID
"""
file_metadata = {
"name": title,
"parents": [sub_folder_id], # 共有ドライブのIDを指定
"mimeType": "application/vnd.google-apps.folder",
}
result = service.files().create(body=file_metadata, fields="id", supportsAllDrives=True).execute()
return result.get('id')
def create_new_spreadsheet(service,folder_id,title):
"""
Google Sheets APIを使用して新しいスプレッドシートを作成する
:param service: Google Sheets APIのサービスオブジェクト
:param title: スプレッドシートのタイトル
:return: 作成したスプレッドシートのID
"""
file_metadata = {
'name': title,
'parents': [folder_id], # 作成したフォルダのIDを指定
'mimeType': 'application/vnd.google-apps.spreadsheet',
}
result = (
service.files()
.create(body=file_metadata, fields="id", supportsAllDrives=True)
.execute()
)
return result.get("id")
def append_log_to_sheet(service, spreadsheet_id, row_data):
"""
Google Sheets APIを使用してスプレッドシートにログを追加する
:param service: Google Sheets APIのサービスオブジェクト
:param spreadsheet_id: スプレッドシートのID
:param row_data: 追加するログデータリスト形式
"""
body = {
'values': [row_data]
}
# スプレッドシートにログを追加
result = service.spreadsheets().values().append(
spreadsheetId=spreadsheet_id,
range='Sheet1',
valueInputOption="USER_ENTERED",
insertDataOption='INSERT_ROWS',
body=body,
).execute()
print(f"{result.get('updates').get('updatedCells')} cells appended.")
def convert_to_jst_ymd(starts_at):
"""
開始日時をYYYY年MM月DD日形式に変換する
:param starts_at: 開始日時の文字列
:return: YYYY年MM月DD日形式の文字列
"""
# 開始日時をUTCからJSTに変換
dt = datetime.fromisoformat(starts_at.replace("Z", "+00:00")).astimezone(timezone(timedelta(hours=9)))
# YYYY年MM月DD日形式に変換
return dt.strftime("%Y年%m月%d")

View file

@ -0,0 +1,5 @@
functions-framework==3.*
google-cloud-secret-manager
google-api-python-client
google-auth-httplib2
google-auth-oauthlib

View file

@ -0,0 +1,5 @@
PROJECT_ID=datacom-poc
LOCATION=asia-northeast1
BUCKET=meeting-report-data
KEY_PATH=projects/32472615575/secrets/mrt-hubspot-accesstoken
MODE=dev

View file

@ -0,0 +1,5 @@
PROJECT_ID: datacom-poc
LOCATION: asia-northeast1
BUCKET: meeting-report-data
KEY_PATH: projects/32472615575/secrets/mrt-hubspot-accesstoken
MODE: dev

View file

@ -0,0 +1,5 @@
PROJECT_ID: rational-timing-443808-u0
LOCATION: asia-northeast1
BUCKET: meeting-data
KEY_PATH: projects/570987459910/secrets/mrt-hubspot-accesstoken
MODE: prod

View file

@ -0,0 +1,33 @@
#!/bin/bash
# プロジェクトIDを設定
PROJECT_ID="datacom-poc"
# デプロイする関数名
FUNCTION_NAME="mrt-create-hubspot-meeting-log"
# 関数のエントリポイント
ENTRY_POINT="handle_request"
# ランタイム
RUNTIME="python312"
# リージョン
REGION="asia-northeast1"
# 環境変数ファイル
ENV_VARS_FILE=".env_dev"
gcloud auth application-default set-quota-project $PROJECT_ID
gcloud config set project $PROJECT_ID
# デプロイコマンド
gcloud functions deploy $FUNCTION_NAME \
--gen2 \
--region $REGION \
--runtime $RUNTIME \
--source=./source \
--trigger-http \
--no-allow-unauthenticated \
--entry-point $ENTRY_POINT \
--env-vars-file $ENV_VARS_FILE

View file

@ -0,0 +1,200 @@
import functions_framework
from google.cloud import storage, secretmanager
import os
import hubspot
from hubspot.crm.objects.meetings import SimplePublicObjectInputForCreate, ApiException
import requests
import csv
import io
import re
import jaconv
from rapidfuzz import process, fuzz
import json
CUTOFF = 80 # Fuzzy 閾値 (0-100)
LEGAL_SUFFIX = r'(株式会社|(株)|\(株\)|有限会社|合同会社|Inc\.?|Corp\.?|Co\.?Ltd\.?)'
cs_client = storage.Client(project=os.getenv("PROJECT_ID"))
sm_client = secretmanager.SecretManagerServiceClient()
@functions_framework.http
def handle_request(request):
try:
request_json = request.get_json()
print(request_json)
mode = os.getenv("MODE") # モードdevまたはprod
title = request_json['title']
host_id = request_json['host_id'] if mode == 'prod' else 'ksuenaga@datacom.jp' # ホストユーザーID(開発環境では固定値を使用)
starts_at = request_json['starts_at']
ends_at = request_json['ends_at']
minutes = request_json['minutes']
# タイトルから【】を削除
title = title.replace("", "").replace("", "")
# タイトルから企業名を抽出
company_name = title.split("")[0].strip() # "様" で分割して企業名を取得
print("抽出した企業名:", company_name)
# 会社名から会社IDを取得
matched_company_id, matched_company_name = search_company(company_name)
# マッチしたときだけ処理を行う
if matched_company_id:
# ユーザーIDを取得
by_email = load_owners()
user_id = None
if host_id in by_email:
user_id = by_email[host_id]['id']
print("取得したユーザーID:", user_id)
# 改行コードを <br> タグに変換
minutes_html = minutes.replace("\n", "<br>")
# ミーティングログを作成
create_meeting_log(matched_company_id, title, user_id, starts_at, ends_at, minutes_html)
response_data = {
"matched_company_id": matched_company_id, # マッチした会社ID
"matched_company_name": matched_company_name, # マッチした会社名
}
return (json.dumps(response_data, ensure_ascii=False), 200, {"Content-Type": "application/json"})
except ApiException as e:
print("Exception when calling basic_api->create: %s\n" % e)
def normalize(name: str) -> str:
"""表記ゆれ吸収用の正規化"""
n = jaconv.z2h(name, kana=False, digit=True, ascii=True).lower()
n = re.sub(LEGAL_SUFFIX, '', n)
return re.sub(r'[\s\-・・,,、\.]', '', n)
# GCSから会社一覧取得
def load_componies():
"""
毎回 Cloud Storage から CSV を読み込む
*応答速度を気にしない* 前提なのでキャッシュしなくても OK
"""
blob = cs_client.bucket(os.getenv("BUCKET")).blob('master/mst_company.csv')
raw = blob.download_as_bytes() # bytes
recs, by_norm = [], {}
with io.StringIO(raw.decode("utf-8")) as f:
reader = csv.DictReader(f)
for row in reader:
row["norm_name"] = normalize(row["company_name"])
recs.append(row)
by_norm[row["norm_name"]] = row # 完全一致用ハッシュ
return recs, by_norm # (list[dict], dict)
# GCSから担当者一覧取得
def load_owners():
"""
GCS から担当者一覧 CSV を読み込み
email -> row 辞書 のマッピングを返す
"""
blob = cs_client.bucket(os.getenv("BUCKET")).blob('master/mst_owner.csv')
raw = blob.download_as_bytes() # bytes
by_email = {}
with io.StringIO(raw.decode("utf-8")) as f:
reader = csv.DictReader(f)
for row in reader:
# row に "email" と "user_id" フィールドがある前提
email = row["email"].strip().lower()
by_email[email] = row
return by_email
def fuzzy_candidates(norm: str, recs):
"""
norm : 正規化済み検索語
recs : 会社レコード list[dict] (norm_name 含む)
戻り値 : list[(score:int, idx:int)]
"""
top = 2 # 上位 2 件を取得
matches = process.extract(
norm,
[r["norm_name"] for r in recs],
scorer=fuzz.WRatio,
score_cutoff=CUTOFF,
limit=top
)
print("ファジーマッチ結果:", matches)
if len(matches) == 0:
return None # マッチなしの場合は None を返す
elif len(matches) == 1:
return recs[matches[0][2]] # 上位 1 件のみの場合はそのレコードを返す
else:
if(matches[0][1] == matches[1][1]):
return None # 上位 2 件のスコアが同じ場合は None を返す
return recs[matches[0][2]] # 上位 1 件のみの場合はそのレコードを返す
def search_company(company_name):
# -------------------- マスタ読み込み --------------------
recs, by_norm = load_componies()
norm_company_name = normalize(company_name)
print("正規化した企業名:", norm_company_name)
matched_company_id = None
matched_company_name = None
# -------------------- 完全一致 --------------------
if norm_company_name in by_norm:
matched_company_id = by_norm[norm_company_name]["company_id"]
matched_company_name = by_norm[norm_company_name]["company_name"]
# -------------------- ファジーマッチ複数 --------------------
else :
result = fuzzy_candidates(norm_company_name, recs)
if result:
matched_company_id = result["company_id"]
matched_company_name = result["company_name"]
print("マッチした会社ID:", matched_company_id)
print("マッチした会社名:", matched_company_name)
return matched_company_id, matched_company_name
def create_meeting_log(company_id ,title, user_id, starts_at, ends_at, minutes):
"""
HubSpot API を使ってミーティングログを作成する
"""
access_key = get_access_key() # Secret Manager からアクセストークンを取得
hs_client = hubspot.Client.create(access_token=access_key)
properties = {
"hs_timestamp": starts_at,
"hs_meeting_title": title,
"hubspot_owner_id": user_id,
"hs_meeting_body": minutes,
"hs_meeting_start_time": starts_at,
"hs_meeting_end_time": ends_at,
}
simple_public_object_input_for_create = SimplePublicObjectInputForCreate(
associations=[{"types":[{"associationCategory":"HUBSPOT_DEFINED","associationTypeId":188}],"to":{"id":company_id}}],
properties=properties
)
api_response = hs_client.crm.objects.meetings.basic_api.create(simple_public_object_input_for_create=simple_public_object_input_for_create)
print(api_response)
#
# SecretManagerからアクセストークンを取得
#
def get_access_key():
key_path = os.getenv('KEY_PATH') + "/versions/1"
# アクセストークン取得
response = sm_client.access_secret_version(name=key_path)
# アクセストークンをデコード
access_token = response.payload.data.decode("UTF-8")
return access_token

View file

@ -0,0 +1,8 @@
functions-framework==3.*
Flask
google-cloud-storage
google-cloud-workflows
google-cloud-secret-manager
hubspot-api-client
rapidfuzz
jaconv

View file

@ -0,0 +1,5 @@
KEY_PATH=projects/32472615575/secrets/sa-access-google-drive-key
LOG_FOLDER_ID=1IZToaM9K9OJXrgV05aLO5k2ZCXpdlJzX
MEETING_FOLDER_ID=1cCDJKusfrlDrJe2yHCR8pCHJXRqX-4Hw
HUBSPOT_COMPANY_URL=https://app-na2.hubspot.com/contacts/242960467/record/0-2
MODE=dev

View file

@ -0,0 +1,5 @@
KEY_PATH: projects/32472615575/secrets/sa-access-google-drive-key
LOG_FOLDER_ID: 1IZToaM9K9OJXrgV05aLO5k2ZCXpdlJzX
MEETING_FOLDER_ID: 1cCDJKusfrlDrJe2yHCR8pCHJXRqX-4Hw
HUBSPOT_COMPANY_URL: https://app-na2.hubspot.com/contacts/242960467/record/0-2
MODE: dev

View file

@ -0,0 +1,5 @@
KEY_PATH: projects/570987459910/secrets/sa-create-minutes-key
LOG_FOLDER_ID: 1arL6AxpvA7N6Umg4wdrdAcRWBdKc-Jfb
MEETING_FOLDER_ID: 0AGT_1dSq66qYUk9PVA
HUBSPOT_COMPANY_URL: https://app.hubspot.com/contacts/22400567/record/0-2
MODE: production

View file

@ -0,0 +1,33 @@
#!/bin/bash
# プロジェクトIDを設定
PROJECT_ID="datacom-poc"
# デプロイする関数名
FUNCTION_NAME="mrt-create-log-sheet"
# 関数のエントリポイント
ENTRY_POINT="handle_request"
# ランタイム
RUNTIME="python312"
# リージョン
REGION="asia-northeast1"
# 環境変数ファイル
ENV_VARS_FILE=".env_dev"
gcloud auth application-default set-quota-project $PROJECT_ID
gcloud config set project $PROJECT_ID
# デプロイコマンド
gcloud functions deploy $FUNCTION_NAME \
--gen2 \
--region $REGION \
--runtime $RUNTIME \
--source=./source \
--trigger-http \
--no-allow-unauthenticated \
--entry-point $ENTRY_POINT \
--env-vars-file $ENV_VARS_FILE

View file

@ -0,0 +1,218 @@
import functions_framework
from google.cloud import secretmanager
from google.oauth2 import service_account
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
import json
import os
from datetime import datetime, timezone, timedelta
sm_client = secretmanager.SecretManagerServiceClient()
SCOPES = ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/drive.file"]
HEADER_VALUES = ["タイムスタンプ","商談日", "タイトル", "登録先企業","担当者", "ミーティングURL", "議事録URL", "HubSpot会社概要URL"]
@functions_framework.http
def handle_request(request):
# POSTリクエストの処理
if request.method != 'POST':
return ('', 405, {'Allow': 'POST', 'Content-Type': 'application/json'}) # メソッドがPOSTでない場合は405エラーを返す
"""Shows basic usage of the Drive Activity API.
Prints information about the last 10 events that occured the user's Drive.
"""
try:
log_folder_id = os.getenv("LOG_FOLDER_ID") # 共有ドライブID
meeting_folder_id = os.getenv("MEETING_FOLDER_ID") # ミーティングフォルダID
mode = os.getenv("MODE") # モードdevまたはprod
service_account_info = get_service_account_info()
# 認証
credentials = get_credentials(service_account_info)
# APIクライアントの構築
drive_service = build("drive", "v3", credentials=credentials)
sheet_service = build("sheets", "v4", credentials=credentials)
# 現在日時をJSTに変換
jst_now = datetime.now(timezone.utc).astimezone(timezone(timedelta(hours=9)))
# JSTの現在日時を文字列に変換
ym_str = jst_now.strftime("%Y%m")
y_str = jst_now.strftime("%Y")
# 年別のフォルダを検索
target_folder = get_directory_files_dev(drive_service, log_folder_id, y_str) if mode == "dev" else get_directory_files_prod(drive_service, meeting_folder_id, log_folder_id, y_str)
print("target_folder", target_folder)
year_folder_id = None
if not target_folder:
# フォルダが存在しない場合は新規作成
year_folder_id = create_new_folder(drive_service, log_folder_id, y_str)
else:
# フォルダが存在する場合はそのIDを使用
year_folder_id = target_folder[0]['id']
print("年別のフォルダID:", year_folder_id)
# スプレッドシートを作成
spreadsheet_id = create_new_spreadsheet(drive_service, year_folder_id, ym_str)
print("スプレッドシートID:", spreadsheet_id)
# 注意事項追加
append_log_to_sheet(sheet_service, spreadsheet_id, ["※シート名変更厳禁"])
# ヘッダーを追加
append_log_to_sheet(sheet_service, spreadsheet_id, HEADER_VALUES)
return (json.dumps({"status": "success"}, ensure_ascii=False), 200, {"Content-Type": "application/json"})
except HttpError as error:
# TODO(developer) - Handleerrors from drive activity API.
print(f"An error occurred: {error}")
#
# SecretManagerから秘密鍵を取得
#
def get_service_account_info():
key_path = os.getenv('KEY_PATH') + "/versions/1"
# 秘密鍵取得
response = sm_client.access_secret_version(name=key_path)
# 秘密鍵の値をデコード
secret_key = response.payload.data.decode("UTF-8")
return json.loads(secret_key)
# Google Drive認証
def get_credentials(service_account_info):
credentials = service_account.Credentials.from_service_account_info(
service_account_info,
scopes=SCOPES
)
return credentials
# 開発用マイドライブからのファイルを取得
def get_directory_files_dev(service,shared_folder_id, filename):
"""
対象のディレクトリ配下からファイル名で検索した結果を配列で返す
:param filename: ファイル名
:param directory_id: ディレクトリID
:param pages_max: 最大ページ探索数
:return: ファイルリスト
"""
items = []
page = 0
pages_max = 10 # 最大ページ数
while True:
page += 1
if page == pages_max:
break
results = service.files().list(
corpora="user",
includeItemsFromAllDrives=True,
includeTeamDriveItems=True,
q=f"'{shared_folder_id}' in parents and name = '{filename}' and trashed = false",
supportsAllDrives=True,
pageSize=10,
fields="nextPageToken, files(id, name)").execute()
items += results.get("files", [])
page_token = results.get('nextPageToken', None)
if page_token is None:
break
return items
# 本番用共有ドライブからのファイルを取得
def get_directory_files_prod(service,shared_folder_id,sub_folder_id,filename):
"""
対象のディレクトリ配下からファイル名で検索した結果を配列で返す
:param filename: ファイル名
:param directory_id: ディレクトリID
:param pages_max: 最大ページ探索数
:return: ファイルリスト
"""
items = []
page = 0
pages_max = 10 # 最大ページ数
while True:
page += 1
if page == pages_max:
break
results = service.files().list(
corpora="drive",
driveId=shared_folder_id,
includeItemsFromAllDrives=True,
includeTeamDriveItems=True,
q=f"'{sub_folder_id}' in parents and name = '{filename}' and trashed = false",
supportsAllDrives=True,
pageSize=10,
fields="nextPageToken, files(id, name, parents)").execute()
items += results.get("files", [])
page_token = results.get('nextPageToken', None)
if page_token is None:
break
return items
def create_new_folder(service, sub_folder_id, title):
"""
Google Drive APIを使用して新しいフォルダを作成する
:param service: Google Drive APIのサービスオブジェクト
:param title: フォルダのタイトル
:return: 作成したフォルダのID
"""
file_metadata = {
"name": title,
"parents": [sub_folder_id], # 共有ドライブのIDを指定
"mimeType": "application/vnd.google-apps.folder",
}
result = service.files().create(body=file_metadata, fields="id", supportsAllDrives=True).execute()
return result.get('id')
def create_new_spreadsheet(service,folder_id,title):
"""
Google Sheets APIを使用して新しいスプレッドシートを作成する
:param service: Google Sheets APIのサービスオブジェクト
:param title: スプレッドシートのタイトル
:return: 作成したスプレッドシートのID
"""
file_metadata = {
'name': title,
'parents': [folder_id], # 作成したフォルダのIDを指定
'mimeType': 'application/vnd.google-apps.spreadsheet',
}
result = (
service.files()
.create(body=file_metadata, fields="id", supportsAllDrives=True)
.execute()
)
return result.get("id")
def append_log_to_sheet(service, spreadsheet_id, row_data):
"""
Google Sheets APIを使用してスプレッドシートにログを追加する
:param service: Google Sheets APIのサービスオブジェクト
:param spreadsheet_id: スプレッドシートのID
:param row_data: 追加するログデータリスト形式
"""
body = {
'values': [row_data]
}
# スプレッドシートにログを追加
result = service.spreadsheets().values().append(
spreadsheetId=spreadsheet_id,
range='Sheet1',
valueInputOption="USER_ENTERED",
insertDataOption='INSERT_ROWS',
body=body,
).execute()
print(f"{result.get('updates').get('updatedCells')} cells appended.")

View file

@ -0,0 +1,5 @@
functions-framework==3.*
google-cloud-secret-manager
google-api-python-client
google-auth-httplib2
google-auth-oauthlib

View file

@ -0,0 +1,5 @@
PROJECT_ID=datacom-poc
LOCATION=asia-northeast1
BUCKET=meeting-report-data
OBJECT=master/mst_company.csv
KEY_PATH=projects/32472615575/secrets/mrt-hubspot-accesstoken

View file

@ -0,0 +1,5 @@
PROJECT_ID: datacom-poc
LOCATION: asia-northeast1
BUCKET: meeting-report-data
OBJECT: master/mst_company.csv
KEY_PATH: projects/32472615575/secrets/mrt-hubspot-accesstoken

View file

@ -0,0 +1,5 @@
PROJECT_ID: rational-timing-443808-u0
LOCATION: asia-northeast1
BUCKET: meeting-data
OBJECT: master/mst_company.csv
KEY_PATH: projects/570987459910/secrets/mrt-hubspot-accesstoken

View file

@ -0,0 +1,87 @@
import functions_framework
from google.cloud import storage, secretmanager
import os
import hubspot
from hubspot.crm.objects.meetings import ApiException
import csv
import io
import json
cs_client = storage.Client()
sm_client = secretmanager.SecretManagerServiceClient()
@functions_framework.http
def handle_request(request):
try:
# 会社一覧取得
companies = fetch_all_companies()
# メモリ上で CSV を生成
csv_buffer = io.StringIO()
writer = csv.writer(csv_buffer)
# ヘッダー行
writer.writerow(["company_id", "company_name"])
# 各行を書き込み
for row in companies:
company_id = row['properties']['hs_object_id']
company_name = row['properties']['name']
writer.writerow([company_id, company_name])
# Cloud Storage にアップロード
upload_to_gcs(csv_buffer)
return 'success', 200
except ApiException as e:
print("Exception when calling basic_api->create: %s\n" % e)
return (json.dumps("", ensure_ascii=False), 500, {"Content-Type": "application/json"})
def fetch_all_companies():
"""
Companies API get_page をページネーション付きで呼び出し
全オブジェクトをリストで返す
"""
access_key = get_access_key() # Secret Manager からアクセストークンを取得
hs_client = hubspot.Client.create(access_token=access_key)
all_companies = []
after = None
limit = 100 # 1 回あたりの取得件数(最大 100
while True:
# get_page の基本呼び出し
response = hs_client.crm.companies.basic_api.get_page(
limit=limit,
archived=False,
after=after
)
# レスポンスから companies の配列を追加
if response.results:
all_companies.extend([c.to_dict() for c in response.results])
# 次ページがない場合はループ終了
paging = response.paging
if not paging or not paging.next or not paging.next.after:
break
# next.after をセットして次ループへ
after = paging.next.after
return all_companies
def upload_to_gcs(data):
"""
メモリ上の CSV データを Cloud Storage にアップロード
"""
bucket = cs_client.bucket(os.getenv("BUCKET"))
blob = bucket.blob(os.getenv("OBJECT"))
blob.upload_from_string(data.getvalue(), content_type='text/csv')
#
# SecretManagerからアクセストークンを取得
#
def get_access_key():
key_path = os.getenv('KEY_PATH') + "/versions/1"
# アクセストークン取得
response = sm_client.access_secret_version(name=key_path)
# アクセストークンをデコード
access_token = response.payload.data.decode("UTF-8")
return access_token

View file

@ -0,0 +1,5 @@
functions-framework==3.*
Flask
google-cloud-storage
google-cloud-secret-manager
hubspot-api-client

View file

@ -0,0 +1,5 @@
PROJECT_ID=datacom-poc
LOCATION=asia-northeast1
BUCKET=meeting-report-data
OBJECT=master/mst_owner.csv
KEY_PATH=projects/32472615575/secrets/mrt-hubspot-accesstoken

View file

@ -0,0 +1,5 @@
PROJECT_ID: datacom-poc
LOCATION: asia-northeast1
BUCKET: meeting-report-data
OBJECT: master/mst_owner.csv
KEY_PATH: projects/32472615575/secrets/mrt-hubspot-accesstoken

View file

@ -0,0 +1,5 @@
PROJECT_ID: rational-timing-443808-u0
LOCATION: asia-northeast1
BUCKET: meeting-data
OBJECT: master/mst_owner.csv
KEY_PATH: projects/570987459910/secrets/mrt-hubspot-accesstoken

View file

@ -0,0 +1,90 @@
import functions_framework
from google.cloud import storage, secretmanager
import os
import hubspot
from hubspot.crm.objects.meetings import ApiException
import csv
import io
import json
cs_client = storage.Client()
sm_client = secretmanager.SecretManagerServiceClient()
@functions_framework.http
def handle_request(request):
try:
# 会社一覧取得
owners = fetch_all_owners()
# メモリ上で CSV を生成
csv_buffer = io.StringIO()
writer = csv.writer(csv_buffer)
# ヘッダー行
writer.writerow(["id", "email"])
# 各行を書き込み
for row in owners:
user_id = row['id']
email = row['email']
writer.writerow([user_id, email])
# Cloud Storage にアップロード
upload_to_gcs(csv_buffer)
return (json.dumps('', ensure_ascii=False), 200, {"Content-Type": "application/json"})
except ApiException as e:
print("Exception when calling basic_api->create: %s\n" % e)
return (json.dumps("", ensure_ascii=False), 200, {"Content-Type": "application/json"})
def fetch_all_owners():
"""
Companies API get_page をページネーション付きで呼び出し
全オブジェクトをリストで返す
"""
access_key = get_access_key() # Secret Manager からアクセストークンを取得
hs_client = hubspot.Client.create(access_token=access_key)
all_owners = []
after = None
limit = 100 # 1 回あたりの取得件数(最大 100
while True:
# get_page の基本呼び出し
response = hs_client.crm.owners.owners_api.get_page(
limit=limit,
archived=False,
after=after
)
# レスポンスから companies の配列を追加
if response.results:
all_owners.extend([c.to_dict() for c in response.results])
# 次ページがない場合はループ終了
paging = response.paging
if not paging or not paging.next or not paging.next.after:
break
# next.after をセットして次ループへ
after = paging.next.after
return all_owners
def upload_to_gcs(data):
"""
メモリ上の CSV データを Cloud Storage にアップロード
"""
bucket = cs_client.bucket(os.getenv("BUCKET"))
blob = bucket.blob(os.getenv("OBJECT"))
blob.upload_from_string(data.getvalue(), content_type='text/csv')
#
# SecretManagerからアクセストークンを取得
#
def get_access_key():
key_path = os.getenv('KEY_PATH') + "/versions/1"
# アクセストークン取得
response = sm_client.access_secret_version(name=key_path)
# アクセストークンをデコード
access_token = response.payload.data.decode("UTF-8")
return access_token

View file

@ -0,0 +1,5 @@
functions-framework==3.*
Flask
google-cloud-storage
google-cloud-secret-manager
hubspot-api-client

View file

@ -0,0 +1,3 @@
MIITEL_URL=https://datacom.miitel.jp/
PROJECT_ID=datacom-poc
MODEL_ID=gemini-2.5-flash

View file

@ -0,0 +1,3 @@
MIITEL_URL: https://datacom.miitel.jp/
PROJECT_ID: datacom-poc
MODEL_ID: gemini-2.5-flash

View file

@ -0,0 +1,3 @@
MIITEL_URL: https://datacom.miitel.jp/
PROJECT_ID: rational-timing-443808-u0
MODEL_ID: gemini-2.5-flash

View file

@ -0,0 +1,35 @@
#!/bin/bash
# プロジェクトIDを設定
PROJECT_ID="datacom-poc"
# デプロイする関数名
FUNCTION_NAME="mrt-generate-meeting-minutes"
# 関数のエントリポイント
ENTRY_POINT="handle_request"
# ランタイム
RUNTIME="python312"
# リージョン
REGION="asia-northeast1"
# 環境変数ファイル
ENV_VARS_FILE=".env_dev"
gcloud auth application-default set-quota-project $PROJECT_ID
gcloud config set project $PROJECT_ID
# デプロイコマンド
gcloud functions deploy $FUNCTION_NAME \
--gen2 \
--region $REGION \
--runtime $RUNTIME \
--source=./source \
--trigger-http \
--cpu=0.5 \
--memory=1Gi \
--no-allow-unauthenticated \
--entry-point $ENTRY_POINT \
--env-vars-file $ENV_VARS_FILE

View file

@ -0,0 +1,132 @@
import functions_framework
import vertexai
from vertexai.generative_models import GenerativeModel, ChatSession
from google.cloud import storage
from google.cloud import secretmanager
import json
import requests
import os
from datetime import datetime, timezone, timedelta
import gzip
# Storage クライアントを作成
storage_client = storage.Client()
sm_client = secretmanager.SecretManagerServiceClient()
@functions_framework.http
def handle_request(request):
# POSTリクエストの処理
if request.method != 'POST':
return ({'error': 'Method not allowed'}, 405, {'Content-Type': 'application/json'})
try:
request_json = request.get_json()
print(request_json)
project_id = os.getenv("PROJECT_ID")
miitel_url = os.getenv("MIITEL_URL")
video_info = request_json["video"]
access_permission = video_info["access_permission"]
video_id = video_info["id"] # 会議履歴ID
host_name = video_info["host"]["user_name"] # ホストユーザー名
host_id = video_info["host"]["login_id"] # ホストユーザーID
starts_at = video_info["starts_at"] # 開始日時
ends_at = video_info["ends_at"] # 終了日時
video_url = miitel_url + "app/video/" + video_id # 会議履歴URL
title = video_info["title"] # 会議タイトル
print("会議タイトル",title)
# 閲覧制限のない会議のみ生成
if access_permission != "EVERYONE":
return (json.dumps({"status": "end"}, ensure_ascii=False), 200, {"Content-Type": "application/json"})
# 社外ミーティングのみ議事録作成
if "" not in title or "社内" in title:
return (json.dumps({"status": "end"}, ensure_ascii=False), 200, {"Content-Type": "application/json"})
# 議事録ファイル名
jst_date_str = generate_jst_date(starts_at) # 開始日時をJSTに変換
file_name = f"{jst_date_str} {title} {host_name}"
print(file_name)
# 議事録作成
speech_recognition = video_info["speech_recognition"]["raw"] # 文字起こしデータ
minutes_text = create_minutes(project_id,speech_recognition)
print("議事録作成完了")
# テキスト内容をセット
minutes = f"会議履歴URL{video_url}\n"
minutes += f"担当者:{host_name}\n\n"
minutes += minutes_text
response_data = {
"status": "next", # ステータス
"title": title, # 会議タイトル
"host_id": host_id, # ホストユーザーID
"host_name": host_name, # ホストユーザー名
"video_url": video_url, # 会議履歴URL
"starts_at": starts_at, # 開始日時
"ends_at": ends_at, # 終了日時
"file_name": file_name, # 議事録ファイル名
"minutes": minutes, # 議事録内容
}
return (json.dumps(response_data, ensure_ascii=False), 200, {"Content-Type": "application/json"})
except Exception as e:
# エラー
error_response = {
"error": str(e) #エラー内容
}
print(str(e))
return json.dumps(error_response), 500, {'Content-Type': 'application/json'} #エラー
def generate_jst_date(starts_at):
# UTCの文字列をdatetimeオブジェクトに変換
utc_datetime = datetime.fromisoformat(starts_at)
# JSTへの変換
jst_timezone = timezone(timedelta(hours=9)) # JSTはUTC+9
jst_datetime = utc_datetime.astimezone(jst_timezone)
# yyyy-MM-dd形式にフォーマット
jst_date_str = jst_datetime.strftime("%Y年%m月%d")
return jst_date_str
def create_minutes(project_id,speech_recognition):
location = "us-central1"
model_id = os.getenv("MODEL_ID")
# print("モデルID:", model_id)
vertexai.init(project=project_id, location=location)
model = GenerativeModel(model_id)
# print("モデル初期化完了")
prompt = f"""
あなたは議事録作成のプロフェッショナルです以下の文字起こし結果は営業マンが録音した商談の文字起こしです以下の制約条件に従い最高の商談報告の議事録を作成してください
制約条件:
1. 文字起こし結果にはAIによる書き起こしミスがある可能性を考慮してください
2. 冒頭に主要な決定事項アクションアイテムをまとめてください
3. 議論のポイントを議題ごとに要約してください
4. 見出しや箇条書きを用いて情報が探しやすい構造で簡潔かつ明瞭に記述してください
5. 要約は500文字以内に収めてください
6. 箇条書き形式で簡潔にまとめてください
7. マークダウン記法は使わず各項目を等を使って見やすくしてください
文字起こし結果
{speech_recognition}
"""
# print("-------------プロンプト-------------")
# print(prompt[:1000])
# print("-------------議事録作成-------------")
response = model.generate_content(prompt)
# print(response.text)
return response.text

View file

@ -0,0 +1,5 @@
functions-framework==3.*
google-cloud-storage
google-cloud-aiplatform
google-cloud-secret-manager
pydrive2

View file

@ -0,0 +1,4 @@
PROJECT_ID=datacom-poc
LOCATION=asia-northeast1
BUCKET=meeting-report-data
WORKFLOW=mrt-workflow-create-minutes

View file

@ -0,0 +1,4 @@
PROJECT_ID: datacom-poc
LOCATION: asia-northeast1
BUCKET: meeting-report-data
WORKFLOW: mrt-workflow-create-minutes

View file

@ -0,0 +1,4 @@
PROJECT_ID: rational-timing-443808-u0
LOCATION: asia-northeast1
BUCKET: meeting-data
WORKFLOW: mrt-workflow-create-minutes

View file

@ -0,0 +1,33 @@
#!/bin/bash
# プロジェクトIDを設定
PROJECT_ID="datacom-poc"
# デプロイする関数名
FUNCTION_NAME="mrt-trigger-minutes-workflow-from-miitel"
# 関数のエントリポイント
ENTRY_POINT="handle_request"
# ランタイム
RUNTIME="python312"
# リージョン
REGION="asia-northeast1"
# 環境変数ファイル
ENV_VARS_FILE=".env_dev"
gcloud auth application-default set-quota-project $PROJECT_ID
gcloud config set project $PROJECT_ID
# デプロイコマンド
gcloud functions deploy $FUNCTION_NAME \
--gen2 \
--region $REGION \
--runtime $RUNTIME \
--source=./source \
--trigger-http \
--no-allow-unauthenticated \
--entry-point $ENTRY_POINT \
--env-vars-file $ENV_VARS_FILE

View file

@ -0,0 +1,75 @@
import functions_framework
from google.cloud import storage
from google.cloud.workflows import executions_v1
from google.cloud.workflows.executions_v1.types import Execution
import json
import os
import gzip
# Storage クライアントを作成
cs_client = storage.Client()
wf_client = executions_v1.ExecutionsClient()
@functions_framework.http
def handle_request(request):
# POSTリクエストの処理
if request.method != 'POST':
# 他のメソッドに対するエラーレスポンス
return ({'error': 'Method not allowed'}, 405)
try:
request_json = request.get_json()
print(request_json)
if "challenge" in request_json:
# MiiTelのチャレンジリクエストに対する応答
return (request_json["challenge"], 200, {'Content-Type':'text/plain'})
project_id = os.getenv("PROJECT_ID")
bucket_name = os.getenv("BUCKET") # 共有ドライブID
location = os.getenv("LOCATION") # ワークフローのロケーション
workflow = os.getenv("WORKFLOW") # ワークフロー名
# デバッグ用に保存
save_to_gcs(bucket_name,request_json)
# ワークフロー呼び出し
argument = json.dumps({"video": request_json["video"]})
execution = Execution(argument=argument)
parent = f"projects/{project_id}/locations/{location}/workflows/{workflow}"
print(parent)
response = wf_client.create_execution(request={"parent": parent, "execution": execution})
print(f"Workflow execution started: {response.name}")
return (json.dumps({}), 200, {'Content-Type': 'application/json'})
except Exception as e:
# エラー
error_response = {
"error": str(e) #エラー内容
}
print(str(e))
return json.dumps(error_response), 500, {'Content-Type': 'application/json'} #エラー
def save_to_gcs(bucket_name,request_json):
file_name = request_json["video"]["id"] + ".json.gz"
bucket = cs_client.bucket(bucket_name)
# GCS バケットのブロブを取得
blob = bucket.blob(f"request_log/{file_name}")
# JSONを文字列に変換
json_string = json.dumps(request_json)
# Gzip圧縮
compressed_data = gzip.compress(json_string.encode('utf-8'))
# 圧縮されたデータをアップロード
blob.upload_from_string(compressed_data, content_type='application/gzip')

View file

@ -0,0 +1,4 @@
functions-framework==3.*
Flask
google-cloud-storage
google-cloud-workflows

View file

@ -0,0 +1,2 @@
KEY_PATH=projects/570987459910/secrets/sa-create-minutes-key
FOLDER_ID=0AGT_1dSq66qYUk9PVA

View file

@ -0,0 +1,2 @@
KEY_PATH: projects/32472615575/secrets/sa-access-google-drive-key
FOLDER_ID: 1cCDJKusfrlDrJe2yHCR8pCHJXRqX-4Hw

View file

@ -0,0 +1,2 @@
KEY_PATH: projects/570987459910/secrets/sa-create-minutes-key
FOLDER_ID: 0AGT_1dSq66qYUk9PVA

View file

@ -0,0 +1,33 @@
#!/bin/bash
# プロジェクトIDを設定
PROJECT_ID="datacom-poc"
# デプロイする関数名
FUNCTION_NAME="mrt-create-minutes"
# 関数のエントリポイント
ENTRY_POINT="handle_request"
# ランタイム
RUNTIME="python312"
# リージョン
REGION="asia-northeast1"
# 環境変数ファイル
ENV_VARS_FILE=".env_dev"
gcloud auth application-default set-quota-project $PROJECT_ID
gcloud config set project $PROJECT_ID
# デプロイコマンド
gcloud functions deploy $FUNCTION_NAME \
--gen2 \
--region $REGION \
--runtime $RUNTIME \
--source=./source \
--trigger-http \
--no-allow-unauthenticated \
--entry-point $ENTRY_POINT \
--env-vars-file $ENV_VARS_FILE

View file

@ -0,0 +1,128 @@
import functions_framework
from google.cloud import secretmanager
from google.oauth2 import service_account
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
import json
import os
SCOPES = ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/drive.file"]
sm_client = secretmanager.SecretManagerServiceClient()
@functions_framework.http
def handle_request(request):
# POSTリクエストの処理
if request.method != 'POST':
# 他のメソッドに対するエラーレスポンス
return ({'error': 'Method not allowed'}, 405)
try:
request_json = request.get_json()
print(request_json)
folder_id = os.getenv("FOLDER_ID") # 共有ドライブID
file_name = request_json["file_name"] # 会議タイトル
minutes = request_json["minutes"] # 議事録
# Secret Manager からサービスアカウントJSON文字列を取得
service_account_info = get_service_account_info()
# 認証
credentials = get_credentials(service_account_info)
# APIクライアントの構築
drive_service = build("drive", "v3", credentials=credentials)
docs_service = build("docs", "v1", credentials=credentials)
# ファイル作成
document_id = create_new_document(drive_service, folder_id, file_name)
print(f"Created document with ID: {document_id}")
# テキスト内容をセット
append_minutes_to_doc(docs_service, document_id, minutes)
response_data = {
"document_id": document_id, # 作成したドキュメントのID
}
return json.dumps(response_data) , 200, {"Content-Type": "application/json"}
except Exception as e:
# エラー
error_response = {
"error": str(e) #エラー内容
}
print(str(e))
return json.dumps(error_response), 500, {'Content-Type': 'application/json'} #エラー
#
# SecretManagerから秘密鍵を取得
#
def get_service_account_info():
key_path = os.getenv('KEY_PATH') + "/versions/1"
# 秘密鍵取得
response = sm_client.access_secret_version(name=key_path)
# 秘密鍵の値をデコード
secret_key = response.payload.data.decode("UTF-8")
return json.loads(secret_key)
# Google Drive認証
def get_credentials(service_account_info):
credentials = service_account.Credentials.from_service_account_info(
service_account_info,
scopes=SCOPES
)
return credentials
def create_new_document(service,folder_id,title):
"""
Google Sheets APIを使用して新しいスプレッドシートを作成する
:param service: Google Sheets APIのサービスオブジェクト
:param title: スプレッドシートのタイトル
:return: 作成したスプレッドシートのID
"""
file_metadata = {
'name': title,
'parents': [folder_id], # 作成したフォルダのIDを指定
'mimeType': 'application/vnd.google-apps.document',
}
result = (
service.files()
.create(body=file_metadata, fields="id", supportsAllDrives=True)
.execute()
)
return result.get("id")
def append_minutes_to_doc(service, document_id, minutes):
"""
Google Sheets APIを使用してスプレッドシートにログを追加する
:param service: Google Sheets APIのサービスオブジェクト
:param spreadsheet_id: スプレッドシートのID
:param row_data: 追加するログデータリスト形式
"""
requests = [
{
'insertText': {
'location': {
'index': 1,
},
'text': minutes
}
},
]
body = {
'requests': requests
}
# スプレッドシートにログを追加
result = service.documents().batchUpdate(
documentId=document_id,
body=body,
).execute()
return result

View file

@ -0,0 +1,5 @@
functions-framework==3.*
google-cloud-secret-manager
google-api-python-client
google-auth-httplib2
google-auth-oauthlib

5
requirements.txt Executable file
View file

@ -0,0 +1,5 @@
functions-framework==3.*
requests==2.26.0
google-cloud-storage
google-cloud-bigquery
hubspot

78
terraform/dev/initial/main.tf Executable file
View file

@ -0,0 +1,78 @@
variable "project_id" {
type = string
default = "datacom-poc"
}
variable "project_number" {
type = string
default = "32472615575"
}
variable "region" {
type = string
default = "asia-northeast1"
}
# Cloud Functionsサービスアカウント
resource "google_service_account" "cf_sa" {
project = var.project_id
account_id = "mrt-cloudfunctions-sa-devtest"
display_name = "Cloud Functions SA"
}
# SAに付与
resource "google_project_iam_member" "cf_sa_role" {
for_each = toset(["roles/storage.objectAdmin","roles/workflows.invoker", "roles/secretmanager.secretAccessor", "roles/aiplatform.user"])
project = var.project_id
role = each.value
member = "serviceAccount:${google_service_account.cf_sa.email}"
}
# Cloud Workflows用サービスアカウント
resource "google_service_account" "workflows_sa" {
project = var.project_id
account_id = "mrt-cloudworkflows-sa-devtest"
display_name = "Cloud Workflows SA"
}
# SA
resource "google_project_iam_member" "wf_cf_role" {
for_each = toset(["roles/cloudfunctions.invoker","roles/run.invoker"])
project = var.project_id
role = each.value
member = "serviceAccount:${google_service_account.workflows_sa.email}"
}
# API Gateway用サービスアカウント
resource "google_service_account" "gateway_sa" {
project = var.project_id
account_id = "mrt-apigateway-sa-devtest"
display_name = "Cloud Functions 起動用サービスアカウント"
}
# SA
resource "google_project_iam_member" "gateway_role" {
for_each = toset(["roles/cloudfunctions.invoker","roles/run.invoker"])
project = var.project_id
role = each.value
member = "serviceAccount:${google_service_account.gateway_sa.email}"
}
# cloud build用サービスアカウント
resource "google_service_account" "cloudbuild_sa" {
project = var.project_id
account_id = "mrt-cloudbuild-sa-devtest"
display_name = "Cloud Build 用サービスアカウント"
}
# SA
resource "google_project_iam_member" "cloudbuild_role" {
for_each = toset(["roles/cloudbuild.builds.builder","roles/storage.objectAdmin", "roles/artifactregistry.writer", "roles/developerconnect.readTokenAccessor", "roles/cloudfunctions.developer","roles/workflows.admin", "roles/iam.serviceAccountUser"])
project = var.project_id
role = each.value
member = "serviceAccount:${google_service_account.cloudbuild_sa.email}"
}

51
terraform/dev/scheduler/main.tf Executable file
View file

@ -0,0 +1,51 @@
variable "project_id" {
type = string
default = "datacom-poc"
}
variable "region" {
type = string
default = "asia-northeast1"
}
variable "function_name" {
type = string
default = "mrt-create-log-sheet"
}
# Scheduler実行用サービスアカウント
resource "google_service_account" "cf_scheduler_sa" {
project = var.project_id
account_id = "mrt-scheduler-sa-devtest"
display_name = "Cloud Functions 起動用サービスアカウント"
}
# SA
resource "google_project_iam_member" "scheduler_role" {
for_each = toset(["roles/cloudfunctions.invoker","roles/run.invoker"])
project = var.project_id
role = each.value
member = "serviceAccount:${google_service_account.cf_scheduler_sa.email}"
}
# 10 Function Scheduler
resource "google_cloud_scheduler_job" "monthly_cf_trigger" {
project = var.project_id
name = "monthly-cf-trigger"
description = "Invoke Cloud Function on the 1st of each month at 00:00"
region = var.region
schedule = "0 0 1 * *"
time_zone = "Asia/Tokyo"
http_target {
uri = "https://${var.region}-${var.project_id}.cloudfunctions.net/${var.function_name}"
http_method = "POST"
oidc_token {
service_account_email = google_service_account.cf_scheduler_sa.email
audience = "https://${var.region}-${var.project_id}.cloudfunctions.net/${var.function_name}"
}
}
}

78
terraform/prod/initial/main.tf Executable file
View file

@ -0,0 +1,78 @@
variable "project_id" {
type = string
default = "rational-timing-443808-u0"
}
variable "project_number" {
type = string
default = "32472615575"
}
variable "region" {
type = string
default = "asia-northeast1"
}
# Cloud Functionsサービスアカウント
resource "google_service_account" "cf_sa" {
project = var.project_id
account_id = "mrt-cloudfunctions-sa"
display_name = "Cloud Functions SA"
}
# SAに付与
resource "google_project_iam_member" "cf_sa_role" {
for_each = toset(["roles/storage.objectAdmin","roles/workflows.invoker", "roles/secretmanager.secretAccessor", "roles/aiplatform.user"])
project = var.project_id
role = each.value
member = "serviceAccount:${google_service_account.cf_sa.email}"
}
# Cloud Workflows用サービスアカウント
resource "google_service_account" "workflows_sa" {
project = var.project_id
account_id = "mrt-cloudworkflows-sa"
display_name = "Cloud Workflows SA"
}
# SA
resource "google_project_iam_member" "wf_cf_role" {
for_each = toset(["roles/cloudfunctions.invoker","roles/run.invoker"])
project = var.project_id
role = each.value
member = "serviceAccount:${google_service_account.workflows_sa.email}"
}
# API Gateway用サービスアカウント
resource "google_service_account" "gateway_sa" {
project = var.project_id
account_id = "mrt-apigateway-sa"
display_name = "Cloud Functions 起動用サービスアカウント"
}
# SA
resource "google_project_iam_member" "gateway_role" {
for_each = toset(["roles/cloudfunctions.invoker","roles/run.invoker"])
project = var.project_id
role = each.value
member = "serviceAccount:${google_service_account.gateway_sa.email}"
}
# cloud build用サービスアカウント
resource "google_service_account" "cloudbuild_sa" {
project = var.project_id
account_id = "mrt-cloudbuild-sa"
display_name = "Cloud Build 用サービスアカウント"
}
# SA
resource "google_project_iam_member" "cloudbuild_role" {
for_each = toset(["roles/cloudbuild.builds.builder","roles/storage.objectAdmin", "roles/artifactregistry.writer", "roles/developerconnect.readTokenAccessor", "roles/cloudfunctions.developer","roles/workflows.admin", "roles/iam.serviceAccountUser"])
project = var.project_id
role = each.value
member = "serviceAccount:${google_service_account.cloudbuild_sa.email}"
}

View file

@ -0,0 +1,51 @@
variable "project_id" {
type = string
default = "rational-timing-443808-u0"
}
variable "region" {
type = string
default = "asia-northeast1"
}
variable "function_name" {
type = string
default = "mrt-create-log-sheet"
}
# Scheduler実行用サービスアカウント
resource "google_service_account" "cf_scheduler_sa" {
project = var.project_id
account_id = "mrt-scheduler-sa"
display_name = "Cloud Functions 起動用サービスアカウント"
}
# SA
resource "google_project_iam_member" "scheduler_role" {
for_each = toset(["roles/cloudfunctions.invoker","roles/run.invoker"])
project = var.project_id
role = each.value
member = "serviceAccount:${google_service_account.cf_scheduler_sa.email}"
}
# 10 Function Scheduler
resource "google_cloud_scheduler_job" "monthly_cf_trigger" {
project = var.project_id
name = "monthly-cf-trigger"
description = "Invoke Cloud Function on the 1st of each month at 00:00"
region = var.region
schedule = "0 0 1 * *"
time_zone = "Asia/Tokyo"
http_target {
uri = "https://${var.region}-${var.project_id}.cloudfunctions.net/${var.function_name}"
http_method = "POST"
oidc_token {
service_account_email = google_service_account.cf_scheduler_sa.email
audience = "https://${var.region}-${var.project_id}.cloudfunctions.net/${var.function_name}"
}
}
}

View file

@ -0,0 +1,14 @@
#!/bin/bash
# 環境変数
PROJECT_ID="datacom-poc"
WORKFLOW_NAME="mrt-workflow-create-minutes"
gcloud auth application-default set-quota-project $PROJECT_ID
gcloud config set project $PROJECT_ID
gcloud workflows deploy $WORKFLOW_NAME \
--source=main.yaml \
--location=asia-northeast1

View file

@ -0,0 +1,71 @@
main:
params: [input]
steps:
- initialize:
assign:
- project_id: ${sys.get_env("GOOGLE_CLOUD_PROJECT_ID")}
- create_hubspot_meeting_log_result: {}
- upload_minutes_to_drive_result: {}
- generate_meeting_minutes:
call: http.post
args:
url: ${"https://asia-northeast1-" + project_id + ".cloudfunctions.net/mrt-generate-meeting-minutes"}
body:
video: ${input.video}
auth:
type: OIDC
result: generate_meeting_minutes_result
- conditinal_switch:
switch:
- condition: ${generate_meeting_minutes_result.body.status != "end"}
steps:
- parallel_execute:
parallel:
shared:
[
create_hubspot_meeting_log_result,
upload_minutes_to_drive_result,
]
branches:
- create_hubspot_meeting_log_branch:
steps:
- create_hubspot_meeting_log:
call: http.post
args:
url: ${"https://asia-northeast1-" + project_id + ".cloudfunctions.net/mrt-create-hubspot-meeting-log"}
body:
title: ${generate_meeting_minutes_result.body.title}
host_id: ${generate_meeting_minutes_result.body.host_id}
starts_at: ${generate_meeting_minutes_result.body.starts_at}
ends_at: ${generate_meeting_minutes_result.body.ends_at}
minutes: ${generate_meeting_minutes_result.body.minutes}
auth:
type: OIDC
result: create_hubspot_meeting_log_result
- upload_minutes_to_drive_branch:
steps:
- upload-minutes-to-drive:
call: http.post
args:
url: ${"https://asia-northeast1-" + project_id + ".cloudfunctions.net/mrt-upload-minutes-to-drive"}
body:
file_name: ${generate_meeting_minutes_result.body.file_name}
minutes: ${generate_meeting_minutes_result.body.minutes}
auth:
type: OIDC
result: upload_minutes_to_drive_result
- append_log_to_sheet:
call: http.post
args:
url: ${"https://asia-northeast1-" + project_id + ".cloudfunctions.net/mrt-append-log-to-sheet"}
body:
title: ${generate_meeting_minutes_result.body.title}
host_name: ${generate_meeting_minutes_result.body.host_name}
video_url: ${generate_meeting_minutes_result.body.video_url}
starts_at: ${generate_meeting_minutes_result.body.starts_at}
matched_company_id: ${create_hubspot_meeting_log_result.body.matched_company_id}
matched_company_name: ${create_hubspot_meeting_log_result.body.matched_company_name}
document_id: ${upload_minutes_to_drive_result.body.document_id}
auth:
type: OIDC
result: append_log_to_sheet_result