🕵️‍♀️

AWS SAMでBacklogとNotionの連携

2024/04/05に公開

概要

会社でBacklogを使用しているため、タスク管理として自身が活用しているNotionと連携してみました。

実装方針の調査

課題

  • NotionはBacklogとの統合に対応していない
  • 二つのアプリを連携させる記事が全くない

解決策
実装方針は、BacklogのWebhookNotion APIを用いて連携させる方針としました。
また、APIは、AWS SAM(Serverless Application Model)を用いて構築します。

使用技術の参考場所

構成図

リポジトリURL

今回の構成図は、写真の通りです。
構成図

1.SAMの環境構築

AWS SAMはAWS上でサーバーレスアプリケーションを迅速に構築するためのツールです。
今回は、API GatewayとLambdaの構築に使用します。

  1. SAM CLIのインストール
    こちらのインストール手順に従ってください

  2. SAMの初期構築

sam init
% sam init

You can preselect a particular runtime or package type when using the `sam init` experience.
Call `sam init --help` to learn more.

Which template source would you like to use?
        1 - AWS Quick Start Templates
        2 - Custom Template Location
Choice: 1

Choose an AWS Quick Start application template
        1 - Hello World Example
        2 - Data processing
        3 - Hello World Example with Powertools for AWS Lambda
        4 - Multi-step workflow
        5 - Scheduled task
        6 - Standalone function
        7 - Serverless API
        8 - Infrastructure event management
        9 - Lambda Response Streaming
        10 - Serverless Connector Hello World Example
        11 - Multi-step workflow with Connectors
        12 - GraphQLApi Hello World Example
        13 - Full Stack
        14 - Lambda EFS example
        15 - Hello World Example With Powertools for AWS Lambda
        16 - DynamoDB Example
        17 - Machine Learning
Template: 1

Use the most popular runtime and package type? (Python and zip) [y/N]: y

Would you like to enable X-Ray tracing on the function(s) in your application?  [y/N]: 

Would you like to enable monitoring using CloudWatch Application Insights?
For more info, please view https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch-application-insights.html [y/N]: 

Would you like to set Structured Logging in JSON format on your Lambda functions?  [y/N]: 

Project name [sam-app]: 

    -----------------------
    Generating application:
    -----------------------
    Name: sam-app
    Runtime: python3.9
    Architectures: x86_64
    Dependency Manager: pip
    Application Template: hello-world
    Output Directory: .
    Configuration file: sam-app/samconfig.toml
    
    Next steps can be found in the README file at sam-app/README.md
        

Commands you can use next
=========================
[*] Create pipeline: cd sam-app && sam pipeline init --bootstrap
[*] Validate SAM template: cd sam-app && sam validate
[*] Test Function in the Cloud: cd sam-app && sam sync --stack-name {stack-name} --watch
  1. 初期構築のデプロイ
sam deploy
% cd {プロジェクト名}
% sam deploy

                Managed S3 bucket: 生成バケット名
                A different default S3 bucket can be set in samconfig.toml
                Or by specifying --s3-bucket explicitly.
        Uploading to c6ce8fa8b5a97dd022ecd006536eb5a4  847 / 847  (100.00%)

        Deploying with following values
        ===============================
        Stack name                   : sam-app
        Region                       : ap-northeast-1
        Confirm changeset            : True
        Disable rollback             : False
        Deployment s3 bucket         : 生成バケット名
        Capabilities                 : ["CAPABILITY_IAM"]
        Parameter overrides          : {}
        Signing Profiles             : {}

Initiating deployment
=====================

        Uploading to cdb3e4a740923d8a859b465974347896.template  1191 / 1191  (100.00%)


Waiting for changeset to be created..

CloudFormation stack changeset
-----------------------------------------------------------------------------------------------------------------------------------------------------
Operation                             LogicalResourceId                     ResourceType                          Replacement                         
-----------------------------------------------------------------------------------------------------------------------------------------------------
+ Add                                 HelloWorldFunctionHelloWorldPermiss   AWS::Lambda::Permission               N/A                                 
                                      ionProd                                                                                                         
+ Add                                 HelloWorldFunctionRole                AWS::IAM::Role                        N/A                                 
+ Add                                 HelloWorldFunction                    AWS::Lambda::Function                 N/A                                 
+ Add                                 ServerlessRestApiDeployment47fc2d5f   AWS::ApiGateway::Deployment           N/A                                 
                                      9d                                                                                                              
+ Add                                 ServerlessRestApiProdStage            AWS::ApiGateway::Stage                N/A                                 
+ Add                                 ServerlessRestApi                     AWS::ApiGateway::RestApi              N/A                                 
-----------------------------------------------------------------------------------------------------------------------------------------------------


Changeset created successfully. arn:aws:cloudformation:ap-northeast-1:008176221809:changeSet/samcli-deploy1712275627/c40f9997-9a0e-4991-87dd-ddfe5080cbd1


Previewing CloudFormation changeset before deployment
======================================================
Deploy this changeset? [y/N]: y

2024-04-05 09:07:17 - Waiting for stack create/update to complete

CloudFormation events from stack operations (refresh every 5.0 seconds)
-----------------------------------------------------------------------------------------------------------------------------------------------------
ResourceStatus                        ResourceType                          LogicalResourceId                     ResourceStatusReason                
-----------------------------------------------------------------------------------------------------------------------------------------------------
CREATE_IN_PROGRESS                    AWS::CloudFormation::Stack            sam-app                               User Initiated                      
CREATE_IN_PROGRESS                    AWS::IAM::Role                        HelloWorldFunctionRole                -                                   
CREATE_IN_PROGRESS                    AWS::IAM::Role                        HelloWorldFunctionRole                Resource creation Initiated         
CREATE_COMPLETE                       AWS::IAM::Role                        HelloWorldFunctionRole                -                                   
CREATE_IN_PROGRESS                    AWS::Lambda::Function                 HelloWorldFunction                    -                                   
CREATE_IN_PROGRESS                    AWS::Lambda::Function                 HelloWorldFunction                    Resource creation Initiated         
CREATE_IN_PROGRESS                    AWS::Lambda::Function                 HelloWorldFunction                    Eventual consistency check          
                                                                                                                  initiated                           
CREATE_IN_PROGRESS                    AWS::ApiGateway::RestApi              ServerlessRestApi                     -                                   
CREATE_IN_PROGRESS                    AWS::ApiGateway::RestApi              ServerlessRestApi                     Resource creation Initiated         
CREATE_COMPLETE                       AWS::ApiGateway::RestApi              ServerlessRestApi                     -                                   
CREATE_IN_PROGRESS                    AWS::ApiGateway::Deployment           ServerlessRestApiDeployment47fc2d5f   -                                   
                                                                            9d                                                                        
CREATE_IN_PROGRESS                    AWS::Lambda::Permission               HelloWorldFunctionHelloWorldPermiss   -                                   
                                                                            ionProd                                                                   
CREATE_COMPLETE                       AWS::Lambda::Function                 HelloWorldFunction                    -                                   
CREATE_IN_PROGRESS                    AWS::Lambda::Permission               HelloWorldFunctionHelloWorldPermiss   Resource creation Initiated         
                                                                            ionProd                                                                   
CREATE_COMPLETE                       AWS::Lambda::Permission               HelloWorldFunctionHelloWorldPermiss   -                                   
                                                                            ionProd                                                                   
CREATE_IN_PROGRESS                    AWS::ApiGateway::Deployment           ServerlessRestApiDeployment47fc2d5f   Resource creation Initiated         
                                                                            9d                                                                        
CREATE_COMPLETE                       AWS::ApiGateway::Deployment           ServerlessRestApiDeployment47fc2d5f   -                                   
                                                                            9d                                                                        
CREATE_IN_PROGRESS                    AWS::ApiGateway::Stage                ServerlessRestApiProdStage            -                                   
CREATE_IN_PROGRESS                    AWS::ApiGateway::Stage                ServerlessRestApiProdStage            Resource creation Initiated         
CREATE_COMPLETE                       AWS::ApiGateway::Stage                ServerlessRestApiProdStage            -                                   
CREATE_COMPLETE                       AWS::CloudFormation::Stack            sam-app                               -                                   
-----------------------------------------------------------------------------------------------------------------------------------------------------

CloudFormation outputs from deployed stack
--------------------------------------------------------------------------------------------------------------------------------------------------------
Outputs                                                                                                                                                
--------------------------------------------------------------------------------------------------------------------------------------------------------
Key                 HelloWorldFunctionIamRole                                                                                                          
Description         Implicit IAM Role created for Hello World function                                                                                 
Value               arn:aws:iam::008176221809:role/sam-app-HelloWorldFunctionRole-L1JzW0JuQFHm                                                         

Key                 HelloWorldApi                                                                                                                      
Description         API Gateway endpoint URL for Prod stage for Hello World function                                                                   
Value               apiGatewayのPath名                                                            

Key                 HelloWorldFunction                                                                                                                 
Description         Hello World Lambda Function ARN                                                                                                    
Value               lambda名                                        
--------------------------------------------------------------------------------------------------------------------------------------------------------


Successfully created/updated stack - sam-app in ap-northeast-1

4.今回触るファイルについて

現状のファイル構成
% tree
.
├── README.md
├── __init__.py
├── events
│   └── event.json
├── hello_world
│   ├── __init__.py
│   ├── app.py
│   └── requirements.txt
├── samconfig.toml
├── template.yaml
└── tests
    ├── __init__.py
    ├── integration
    │   ├── __init__.py
    │   └── test_api_gateway.py
    ├── requirements.txt
    └── unit
        ├── __init__.py
        └── test_handler.py

6 directories, 14 files

下記の3点を使用

  • hello_worldディレクトリ
    • APIのロジックが記載されているファイル
  • samconfig.toml
    • デプロイメントに関する設定が入っている。今回は、環境変数のセットに使用
  • template.yaml
    • サーバーレスアプリケーションの構成やリソースを定義するためのテンプレートファイル。今回は、API構成の設定に使用。
  1. APIのエンドポイントの確認
sam list endpoints --output json
% sam list endpoints --output json
[
  {
    "LogicalResourceId": "HelloWorldFunction",
    "PhysicalResourceId": "xxxxx",
    "CloudEndpoint": "-",
    "Methods": "-"
  },
  {
    "LogicalResourceId": "ServerlessRestApi",
    "PhysicalResourceId": "xxxx",
    "CloudEndpoint": [
      "https://XXXX/Prod",
      "https://XXXX/Stage"
    ],
    "Methods": [
      "/hello['get']"
    ]
  }
]

今回使用するのは、https://XXXX/Prod/{API_PATH}になります。
下記のように「hello world」が取得できれば、SAMの環境構築は完了となります。

$ curl https://XXXX/Prod/hello
{"message": "hello world"}

2.Notionの環境構築

ここでは、Notion APIを使用する準備と、Backlogのデータが保管されるデータベースをNotionに作成します。

Notion APIを使用する為の準備

  1. こちらから新しいインテグレーションを作成してください。
  2. 画像のシークレットトークンを表示コピーして、一旦メモしておいてください
    シークレットトークンの取得

Notionのデータベース設定

  1. Notionで下記のデータベースを作成してください。
プロパティの種類 カラムの名前
タイトル タイトル
ステータス 状態
未着手: 未対応
進行中: 処理中
完了: 完了/処理済み
セレクト タグ
セレクト マイルストーン
セレクト 優先順位
テキスト 担当者
テキスト 期限
テキスト 作成者
作成日時 作成日時
日付 日付
  1. テーブルIDを取得します。テーブルIDは一旦メモしておいてください。
    画像の「リンクをコピー」からURLを取得します。
    テーブルID

おそらく下記のフォーマットのURLが取得できます。その中の「abcde」部分がテーブルIDとなります。
https://www.notion.so/abcde?v=a&pvs=1

  1. 作成したテーブルにAPIからの参照を許可します。
    データベースを作成したページの設定から「接続先 → 作成したシークレットトークンの名前」を選択してください。

APIの接続許可

3.Backlogの環境構築

BacklogのWebhook機能を用いて、APIにリクエストを送る設定を行います。

  1. webhookの設定ページへ遷移
    プロジェクト設定 → インテグレーション → Webhookの「設定」を選択してください
    webhookの設定

  2. webhookの設定
    webhookの追加 → 画像のように情報を入力してください。「WebHook URL」は先ほど取得したAPIのパスになります。

    • API PathはAWS SAMが作成したデフォルトのhelloを使用します。
      webhookの設定詳細
  3. 下に実行テストがありますので、プルダウンから「課題の追加」を選択し、テストを実行してください。「実行完了」とメッセージがでれば設定は成功ですので、「Webhookを追加する」を押下してください。

4.課題追加APIの作成

それぞれの環境構築が完了した為、次は課題追加用のAPIを作っていきます。

AWS SAMを用いて、課題追加用のAPIを作成していきます。
変更するのは、下記3つのファイルになります。

.
├── hello_world
│   ├── app.py
├── samconfig.toml
├── template.yaml
  1. 環境変数の設定
    APIからNotionに書き込むためには、途中でメモをしたNotionのテーブルIDとシークレットトークンが必要になります。この二つのIDは公開するとセキュリティ上のリスクがある為、APIの環境変数として設定を行います。

samconfig.tomlに下記を追記してください。
※git管理されている方はファイルを.gitignoreの対象にすることをお勧めします。

[default]
[default.global.parameters]
~
parameter_overrides = "apiKey='Notionのシークレットキー' tableId='テーブルID'"

template.yamlのGlobal以降を下記に上書き

template.yaml Global以降
Globals:
  Function:
    Timeout: 3
    MemorySize: 128

Parameters:
  apiKey:
    Type: String
  tableId:
    Type: String

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.9
      Architectures:
        - x86_64
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: post
      Environment:
        Variables:
          API_KEY: !Ref apiKey
          TABLE_ID: !Ref tableId
  1. APIの作成とデプロイ
    app.pyに下記のapiをコピペ
app.py
import json
import requests
import os
import time


def lambda_handler(event, context):
    API_KEY = os.getenv('API_KEY')
    TABLE_ID = os.getenv('TABLE_ID')

    # event["body"] をJSONオブジェクトにデシリアライズ
    print(API_KEY, TABLE_ID, event["body"])
    body = json.loads(event["body"]) if event["body"] else {}

    # Notion APIへのリクエストデータ
    data = {
        "parent": {"type": "database_id", "database_id": TABLE_ID},
        "properties": {
            "タグ": {
                "select": {
                    "name": body["content"]["issueType"].get("name", ""),
                }
            },
            "状態": {
                "type": "status",
                "status": {
                    "name": body["content"]["status"].get("name", ""),
                }
            },
            "タイトル": {
                "title": [{"text": {"content": body["project"].get("projectKey", "") + "-" + str(body["content"].get("key_id", ""))+ "\n" + body["content"].get("summary", "")}}]
            },
            "担当者": {
                "rich_text": [] if body["content"]["assignee"] is None else [{
                    "type": "text",
                    "text": {
                        "content": body["content"]["assignee"].get("name", "")
                    }
                }]
            },
            "優先順位": {
                "select": {
                    "name": body["content"]["priority"].get("name", "")
                }
            },
            "期限": {
                "rich_text": [{
                    "type": "text",
                    "text": {
                        "content": "" if body["content"]["dueDate"] is None else body["createdUser"].get("name", "")
                    }
                }],
            },
            "作成者": {
                "rich_text": [{
                    "type": "text",
                    "text": {
                        "content": "" if body.get("createdUser") is None else body["createdUser"].get("name", "")
                    }
                }],
            },
        },
        "children": [
            {
                "object": "block",
                "type": "paragraph",
                "paragraph": {
                "rich_text": [{
                    "type": "text",
                    "text": {
                        "content": body.get("content", {}).get("description", "")
                    }
                }
            ]}}
        ]
    }
    # "マイルストーン"プロパティの追加(空のリストでない場合のみ)
    if body["content"].get("milestone") and body["content"]["milestone"]:
        data["properties"]["マイルストーン"] = {
            "select": {
                "name": body["content"]["milestone"][0].get("name", "")
            }
        }

    # Notion APIへのリクエストヘッダー
    headers = {
        'Authorization': 'Bearer ' + API_KEY,
        'Content-Type': 'application/json',
        'Notion-Version': '2022-06-28'
    }

    # Notion APIにデータをPOST
    response = requests.post('https://api.notion.com/v1/pages', headers=headers, json=data)
    print(response.json())

    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": "Successfully created a new page in Notion",
        }),
    }

  1. sam build
    • 変更内容の読み込みを行います。
  2. sam deploy
    • 変更内容の反映をします

5.動作確認

  1. Backlogで課題を追加します。
    backlog 課題追加

  2. Notionのテーブルに追加されていれば成功です。
    notion 課題追加

もし追加されなかった場合

手段1 ログからエラー調査

SAMで実施した場合、HelloWorldFunctionLmabdaが作成されています。
Lambdaのコンソール画面に入り、写真の「CloudWatchログを表示」ボタンを押下すると、Lambdaのログを見ることができます。そこからエラー原因を調査してみてください。
cloud watch

手段2 Notionのテーブルを見直す

APIが想定している通りにNotionのテーブルを生成する必要があります。その為、「Notionのデータベース設定」のセクションに記載している通り下記2点を再度確認してください。

  • 「プロパティの種類」と「カラムの名前」が正しいか再確認
  • 状態のカラムについては、下記のプロパティ設定にする

まとめ

今回はNotionのAPIとBacklogのWebhookを用いて「課題追加の反映」のみを行いました。
この方法だとBacklogのアクション毎にAPIを追加する必要があります。その為、Backlogの全てのアクションを対応するとかなり手間になるのが難点です。
一方でカスタム制はかなり高いので、Backlogの内容をカスタマイズして自身のnoteにまとめるには適していると感じました。
今後BacklogとNotionの統合機能がリリースされれば良いと思いますが、それまでは一旦このやり方に落ち着きそうです。

Discussion