🦁

Notionの利用情報をSlackに定期送信したいんだ!

2023/12/09に公開

今回初めて Qiita で技術記事を書いてみます!!
皆様、お手柔らかに頂けますと幸いです。

はじめに

本記事は プロもくチャット Adevent Calendar 2023 の 10 日目です!!

https://qiita.com/advent-calendar/2023/puromoku

全体像

最終的なシステムの全貌は以下の通りです。
image.png

前提条件

  • 利用端末 : MacBook Air Apple M1 macOS Sonoma 14.1.1
  • 使用言語 : TypeScript
  • 実行環境 : Node.js v20.10.0
  • エディタ : Visual Studio Code
  • AWS アカウントを作成済み
  • Docker インストール済み
  • Notion でデータベースを作成済み
  • Slack でチャンネルを作成済み
  • 作成した lambda 関数のテストは AWS コンソール画面で実施

やることサマリ

  1. Slack アプリケーションの作成
  2. Notion Integration の作成
  3. (おまけ) IAM ユーザーの作成
  4. AWS CLI のインストール・設定
  5. AWS CDK のインストール・設定
  6. AWS CDK コマンドで新規プロジェクトを作成
  7. 各種 npm パッケージのインストール
  8. 各種認証情報を AWS Systems Manager Parameter Store に格納
  9. AWS リソースを定義する CDK ファイルを作成
  10. AWS Lambda で実行する TypeScript ファイルを実装
  11. AWS CDK コマンドで AWS リソースをデプロイ
  12. 利用料金等補足事項

手順詳細

1. Slack アプリケーション の作成

下記記事「Slack アプリケーションを作成する」を参照してください。
Notion で作成されたページを日時でサマリして Slack に投稿してみた

す、すみません。。サボりました。。

2. Notion Integration の作成

下記記事「Notion Integration を作成する」「Notion Integration から Notion データベースへの接続を許可」を参照
Notion で作成されたページを日時でサマリして Slack に投稿してみた

す、すみません。。サボりました。。。
書く気がないわけではないのです..!
ホントにそのまま参考になったのです....!

3. (おまけ) IAM ユーザーの作成

公式ドキュメントはこちら。

https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_users_create.html

1. ルートアカウントにて AWS IAM にアクセス後、左ペイン「ユーザー」をクリックします。
スクリーンショット 2023-11-28 21.56.57.png

2. 「ユーザーの作成」をクリックします。
スクリーンショット 2023-11-28 22.00.12.png

3. 任意のユーザー名を入力します。
スクリーンショット 2023-11-28 22.04.05.png

4. 「AWS マネジメントコンソールへのユーザーアクセスを提供する」をクリックします。
AWS Lambda へのデプロイ完了後、マネジメントコンソールにてテストを実施するためです。
クリックすると、青枠の項目が表示されます。
スクリーンショット 2023-11-28 22.05.43.png

5. 「IAM ユーザーを作成します」を選択します。
選択すると、ユーザーのログイン情報を設定する画面が表示されます。
適宜情報を入力します。

6. 下記画像の右下にある「次へ」をクリックします。
スクリーンショット 2023-11-29 20.33.25.png

7. 「ポリシーを直接アタッチする」を選択します。
スクリーンショット 2023-11-29 20.38.57.png

8. AdministratorAccess ポリシーを選択したのち、右下の「次へ」をクリックします。
スクリーンショット 2023-11-29 20.52.52.png

9. 確認画面で「ユーザーの作成」をクリックします。
スクリーンショット 2023-11-29 21.00.17.png

4. AWS CLI のインストール・設定

1. 作成したユーザーの概要画面にて「アクセスキーを作成」をクリックします。
スクリーンショット 2023-11-29 21.08.16.png

2. 「ユースケース」にて「コマンドラインインターフェイス(CLI)を選択します。
スクリーンショット 2023-11-29 21.22.53.png

3. 「上記のレコメンデーションを理解し、アクセスキーを作成します」にチェックをつけたのち、「次へ」をクリックします。

4. 適宜アクセスキーの説明を入力し、「アクセスキーの作成」をクリックします。
スクリーンショット 2023-11-29 21.31.08.png

5. 「アクセスキーの取得」画面に表示される アクセスキーシークレットキー をコピーし保存します。

6. 下記公式ドキュメントから macOS pkg ファイルをダウンロードします。
画像にあるリンクから該当ファイルをダウンロードできます。
今回は「GUI installer」を用いた方法を採用しました。
スクリーンショット 2023-11-30 20.55.43.png

7. ダウンロードした pkg ファイルを実行します。
画像のようなインストール画面が表示されると思います。
スクリーンショット 2023-11-30 21.04.54.png

8. インストーラーの指示に従い、AWS CLI をインストールします。
特別何かを変更する必要はないと思います。
(少なくとも僕は今困っていません)

9. 下記コマンドを実行し、正常にインストールできたかどうか確認します

# aws コマンドのインストール先が表示されること
$ which aws
/usr/local/bin/aws

# aws コマンドのバージョンが表示されること
$ aws --version
aws-cli/2.13.32 Python/3.11.6 Darwin/23.1.0 exe/x86_64 prompt/off

10. 下記コマンドを実行し、アクセスキーシークレットキーを設定します。

aws configure

対話型で認証情報を聞かれるので、適宜入力します。

AWS Access Key ID [None]: "先ほど控えたアクセスキー"
# Enter
AWS Secret Access Key [None]: "先ほど控えたシークレットキー"
# Enter
Default region name [None]: "任意のリージョン" # (東京リージョンが無難ですかね?)
# Enter
Default output format [None]: json # 他の形式も選択できます。公式ドキュメントには json が指定されていました。
# Enter

公式ドキュメントはこちら。
IAM ユーザー認証情報を使用して認証を行う

11. 下記コマンドを実行し、設定が正常に完了したことを確認します。

cat ~/.aws/credentials
# 出力結果
[default]
aws_access_key_id = "先ほど入力したアクセスキー"
aws_secret_access_key = "先ほど入力したシークレットキー

5. AWS CDK のインストール・設定

公式ドキュメントはこちら

https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/getting_started.html#getting_started_install

1. 下記コマンドを実行し、AWS CDK ツールキットをグローバルにインストールします。

npm i -g aws-cdk

2. 下記コマンドを実行し、インストールが正常に完了したことを確認します。

cdk --version
# cdk コマンドのバージョンが表示されること
2.110.1 (build 0d37f0d)

6. AWS CDK コマンドで新規プロジェクトを作成

インストールした AWS CDK ツールキットを利用して新規プロジェクトを作成します。

1. 新規プロジェクト用のディレクトリを作成します。

$ pwd
/Users/userName/Desktop/Lambda

$ mkdir ./qiita

2. 作成したディレクトリにて下記コマンドを実行します。

cdk init --language typescript

コマンド実行後のディレクトリは次の通りです。
スクリーンショット 2023-12-01 21.45.36.png

ふぅ〜、これで初期設定が完了しました。
あとは煮るなり焼くなり好きにできます。

7. 各種 npm パッケージのインストール

1. 下記コマンドを実行し、今回必要な各種パッケージをインストールします。

npm i @slack/web-api @notionhq/client @types/aws-lambda @aws-sdk/client-ssm
  • @slack/web-api
    Slack にデータを送信・Slack からデータをクエリするための SDK です。

https://slack.dev/node-slack-sdk/

  • @notionhq/client
    Notion にデータを送信・Notion からデータをクエリするための SDK です。

https://github.com/makenotion/notion-sdk-js

  • @types/aws-lambda
    AWS Lambda のための型定義を提供するパッケージです。

https://www.npmjs.com/package/@types/aws-lambda

  • @aws-sdk/client-ssm
    プログラムから AWS Systems Manager Parameter Store を操作するための SDK です。

8. 各種認証情報を AWS Systems Manager Parameter Store に格納

1. 下記コマンドを実行し、各種認証情報を AWS Systems Manager Parameter Store に格納します。
公式ドキュメントはこちら。

https://awscli.amazonaws.com/v2/documentation/api/latest/reference/ssm/index.html#cli-aws-ssm

https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/systems-manager-parameter-store.html#what-is-a-parameter

aws ssm put-parameter --name qiita-notionAuth --value "xxxxxxxxxx" --type "SecureString"
aws ssm put-parameter --name qiita-notionDbId --value "xxxxxxxxxx" --type "SecureString"
aws ssm put-parameter --name qiita-slackBotToken --value "xxxxxxxxxx" --type "SecureString"
aws ssm put-parameter --name qiita-channelName --value "xxxxxxxxxx" --type "String"
  • --name オプション
    パラメータの名称です。
    適宜任意のものを設定します。

  • --value オプション
    パラメータ名に対応する各種認証情報です。

  • --type オプション
    設定する認証情報文字列の種別を指定します。
    今回は下記 2 種類を利用しています。 - String : プレーンテキスト - SecureString : セキュアな方法で保存および参照する必要がある機密データ

9. AWS リソースを定義する CDK ファイルを作成

今回構築する AWS リソースを定義した CDK ファイルを作成します。
ファイルの全体は次の通りです。

qiita-stack.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";

export class QiitaStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // IAM ロールの定義
    const executionLambdaRole = new cdk.aws_iam.Role(
      this,
      "executionLambdaRole",
      {
        roleName: "qiita-executionRole",
        assumedBy: new cdk.aws_iam.ServicePrincipal("lambda.amazonaws.com"),
        managedPolicies: [
          cdk.aws_iam.ManagedPolicy.fromAwsManagedPolicyName(
            "AmazonSSMReadOnlyAccess"
          ),
          cdk.aws_iam.ManagedPolicy.fromAwsManagedPolicyName(
            "CloudWatchLogsFullAccess"
          ),
        ],
      }
    );

    // Lambda 関数の定義
    const lambda = new cdk.aws_lambda_nodejs.NodejsFunction(
      this,
      "main-handler",
      {
        runtime: cdk.aws_lambda.Runtime.NODEJS_20_X,
        entry: "lambda/handler.ts",
        role: executionLambdaRole,
        environment: {
          NOTION_AUTH: "qiita-notionAuth",
          NOTION_DB_ID: "qiita-notionDBId",
          SLACK_BOT_TOKEN: "qiita-slackBotToken",
          SLACK_CHANNEL_NAME: "qiita-channelName",
        },
        bundling: {
          sourceMap: true,
        },
        timeout: cdk.Duration.seconds(30),
      }
    );

    // EventBridge スケジュールを定義
    new cdk.aws_events.Rule(this, "Schedule", {
      schedule: cdk.aws_events.Schedule.rate(cdk.Duration.minutes(1)),
      targets: [new cdk.aws_events_targets.LambdaFunction(lambda)],
    });
  }
}

コードにて定義しているリソース・各種説明は次の通りです。

  • IAM ロール
    公式ドキュメントはこちら
    class Role (construct)

    const executionLambdaRole = new cdk.aws_iam.Role(
      this,
      "executionLambdaRole",
      {
        roleName: "qiita-executionRole",
        // AWS Lambda を信頼ポリシーに設定します。
        assumedBy: new cdk.aws_iam.ServicePrincipal("lambda.amazonaws.com"),
        // ロールに含めるポリシーを指定します。
        // 今回は AWS から提供されている管理ポリシーを指定しました。
        managedPolicies: [
          // AWS SSM への読み取り権限
          cdk.aws_iam.ManagedPolicy.fromAwsManagedPolicyName(
            "AmazonSSMReadOnlyAccess"
          ),
          // AWS CloudWatch Logs へのフルアクセス権限
          cdk.aws_iam.ManagedPolicy.fromAwsManagedPolicyName(
            "CloudWatchLogsFullAccess"
          ),
        ],
      }
    );
    
  • AWS Lambda
    公式ドキュメントはこちら
    class NodejsFunction (construct)

    const lambda = new cdk.aws_lambda_nodejs.NodejsFunction(
      this,
      "main-handler",
      {
        // lambda 関数の実行環境を指定します。
        runtime: cdk.aws_lambda.Runtime.NODEJS_20_X,
        // lambda 関数のパスを指定します。
        // 今回は「qiita」ディレクトリ配下に「lambda」ディレクトリを作成しました。
        entry: "lambda/handler.ts",
        // 先ほど定義した IAM ロールです。
        role: executionLambdaRole,
        // lambda 関数に環境変数を設定します。
        // 以下に補足事項ありです。
        environment: {
          NOTION_AUTH: "qiita-notionAuth",
          NOTION_DB_ID: "qiita-notionDBId",
          SLACK_BOT_TOKEN: "qiita-slackBotToken",
          SLACK_CHANNEL_NAME: "qiita-channelName",
        },
        // lambda 関数の最大実行時間を設定します。
        // 今回は 30 秒を指定しました。(フィーリング)
        timeout: cdk.Duration.seconds(30),
      }
    );
    
  • AWS EventBridge
    公式ドキュメントはこちら
    class Role (construct)

    new cdk.aws_events.Rule(this, "Schedule", {
      // 本イベントの発火ルールを指定します。
      // 今回は 1 分に一回実行するように指定しました。(迷惑🙄)
      schedule: cdk.aws_events.Schedule.rate(cdk.Duration.minutes(1)),
      // 当該イベントの発火時に実行される lambda 関数を指定します。
      targets: [new cdk.aws_events_targets.LambdaFunction(lambda)],
    });
    

10. AWS Lambda で実行する TypeScript ファイルを実装

実際に実行される処理を実装します。
ファイル全体は次の通りです。

全部ベタ打ちで実装しているので、コードがかなり汚いです。。お手柔らかにお願いします。。
また、エラーハンドリング処理が雑いと思われます。
僕自身勉強中ゆえ、ご指摘等頂けますと幸いです。

各種公式ドキュメントはこちら。

lambda/handler.ts
import {
  GetParameterCommand,
  GetParametersCommand,
  SSMClient,
} from "@aws-sdk/client-ssm";
import { Client } from "@notionhq/client";
import { WebClient } from "@slack/web-api";
import { Context, ScheduledEvent } from "aws-lambda";

const NOTION_AUTH_KEY = process.env["NOTION_AUTH"]!;
const NOTION_DB_ID_KEY = process.env["NOTION_DB_ID"]!;
const SLACK_BOT_TOKEN_KEY = process.env["SLACK_BOT_TOKEN"]!;
const SLACK_CHANNEL_NAME_KEY = process.env["SLACK_CHANNEL_NAME"]!;

// リージョンを指定して SSMClient をインスタンス化
const ssm = new SSMClient({ region: "ap-northeast-1" });

const getParametersFromSSM = async () => {
  // AWS SSM に対して認証情報(type: SecureString)を問い合わせ
  const secureStrResponse = await ssm.send(
    new GetParametersCommand({
      Names: [NOTION_AUTH_KEY, NOTION_DB_ID_KEY, SLACK_BOT_TOKEN_KEY],
      WithDecryption: true,
    })
  );

  // AWS SSM に対して slack のチャンネル名(type: String)を問い合わせ
  const strResponse = await ssm.send(
    new GetParameterCommand({
      Name: SLACK_CHANNEL_NAME_KEY,
      WithDecryption: false,
    })
  );

  return {
    notionAuth: secureStrResponse.Parameters?.find(
      (p) => p.Name === NOTION_AUTH_KEY
    )?.Value,

    notionDBId: secureStrResponse.Parameters?.find(
      (p) => p.Name === NOTION_DB_ID_KEY
    )?.Value,

    slackBotToken: secureStrResponse.Parameters?.find(
      (p) => p.Name === SLACK_BOT_TOKEN_KEY
    )?.Value,

    slackChannelName: strResponse.Parameter?.Value,
  };
};

export const handler = async (event: ScheduledEvent, context: Context) => {
  try {
    const { notionAuth, notionDBId, slackBotToken, slackChannelName } =
      await getParametersFromSSM();

    if (!notionAuth || !notionDBId || !slackBotToken || !slackChannelName) {
      throw new Error("必要な情報を全て取得できませんでした");
    }

    const notionClient = new Client({
      auth: notionAuth,
    });
    const slackClient = new WebClient(slackBotToken);

    // notionにクエリを実行します。
    // 今回はページプロパティが「期限:有」かつ「未実施」のものを検索するクエリを実行します。
    // 詳しい方法は上記公式ドキュメント参照です。
    const queryResult = await notionClient.databases.query({
      database_id: notionDBId,
      filter: {
        and: [
          {
            property: "期限",
            status: {
              equals: "有",
            },
          },
          {
            property: "done",
            checkbox: {
              equals: false,
            },
          },
        ],
      },
    });

    // クエリ結果からページ数を取得します。
    const pageCount = queryResult.results.length.toString();

    // 取得したページ数を slack に投稿します。
    // 今回はページ数のみ投稿しています。
    // 詳しい利用方法は上記公式ドキュメント参照です。
    await slackClient.chat.postMessage({
      text: pageCount,
      channel: slackChannelName!,
    });
  } catch (error: unknown) {
    if (error instanceof Error) {
      console.error("エラーが発生しました", error);
    }
  }
};

11. AWS CDK コマンドで AWS リソースをデプロイ

1. Docker を起動します。

2. 「qiita」ディレクトリにて、下記コマンドを実行します。
公式ドキュメントはこちら。
まだ一度も lambda 関数をデプロイしたことがない場合にのみ実行します。
AWS 環境のブートストラップ

$ cdk bootstrap

3. 「qiita」ディレクトリにて、下記コマンドを実行します。
公式ドキュメントはこちら。
以降、lambda 関数を修正した際は下記コマンドを実行し、修正を反映させます。
スタックのデプロイ

$ cdk deploy

ソースコード等を変更した際は、再度上記コマンドを実行します。

これで手順は終了です!!!!
うまくいけば、指定したチャンネルに 1 分間隔でメッセージが投稿されるはずです。(迷惑 🙄)

あ、テストの方法は、、書いてないです。。。
サボりました。。。
本当は AWS SAM を使ってローカルで lambda 関数をテストできるようにしたいので、それができたらまた記事にしようかな

12. 利用料金等補足事項

AWS には無期限の無料利用枠というものがあり、この範囲内の利用なら課金されません。
詳細はこちら。
AWS 無料利用枠 (常に無料)

僕は現在、「6 時間に1回」・「1 週間に 2 回」起動する lambda 関数をそれぞれ運用しておりますが、この枠内で収まっています。

まとめ

長々とお付き合いいただきありがとうございました。
どうせ今後また同じことをやりたくなるだろうと思い、手順書作成の意味を込めて記事にしてみました。
もしこの記事が誰かの役に立てますと幸いです。

Discussion