今回初めてQiitaで技術記事を書いてみます!!
皆様、お手柔らかに頂けますと幸いです。
はじめに
本記事は プロもくチャット Adevent Calendar 2023
の 10 日目です!!
この記事のゴール
- ローカル環境にて
AWS CLI
コマンドを実行できるようになります - ローカル環境にて
AWS CDK
コマンドを実行できるようになります -
AWS CDK
コマンドによって AWS 環境に各種リソースを作成できるようになります -
AWS SDK
を用いて AWS のリソースを操作できるようになります - Notion の特定データベースに対して実行したクエリ結果を Slack に投稿できるようになります
全体像
前提条件
- 利用端末 :
MacBook Air
Apple M1
macOS Sonoma 14.1.1
- 使用言語 :
TypeScript
- 実行環境 :
Node.js v20.10.0
- エディタ :
Visual Studio Code
- AWS アカウントを作成済み
-
Docker Desktop
インストール済み - Notion でデータベースを作成済み
- Slack でチャンネルを作成済み
- 作成した lambda 関数のテストは AWS コンソール画面で実施
やることサマリ
- Slack アプリケーションの作成
- Notion Integration の作成
- (おまけ) IAM ユーザーの作成
-
AWS CLI
のインストール・設定 -
AWS CDK
のインストール・設定 -
AWS CDK
コマンドで新規プロジェクトを作成 - 各種
npm
パッケージのインストール - 各種認証情報を
AWS Systems Manager Parameter Store
に格納 - AWS リソースを定義する CDK ファイルを作成
-
AWS Lambda
で実行する TypeScript ファイルを実装 -
AWS CDK
コマンドで AWS リソースをデプロイ - 利用料金等補足事項
手順詳細
1. Slack アプリケーション の作成
下記記事「Slack アプリケーションを作成する」を参照してください。
Notion で作成されたページを日時でサマリして Slack に投稿してみた
す、すみません。。サボりました。。
2. Notion Integration の作成
下記記事「Notion Integration を作成する」「Notion Integration から Notion データベースへの接続を許可」を参照
Notion で作成されたページを日時でサマリして Slack に投稿してみた
Notion Integration の Secrets とデータベースID を控えます。
す、すみません。。サボりました。。。
書く気がないわけではないのです..!
ホントにそのまま参考になったのです....!
3. (おまけ) IAM ユーザーの作成
公式ドキュメントはこちら。
「4. AWS CLI
のインストール・設定」にてIAMユーザーが必要になります。
AWS アカウントはあるけど IAM ユーザーは持っていない、という方はぜひご一読ください。
IAM におけるベストプラクティスは、IDプロバイダーとのフェデレーションを使用して一時的な認証情報によって AWS にアクセスする方法とのことです。(上記リンク参照)
今回は簡単のため、IAM ユーザーを作成する手段を採用します。
1. ルートアカウントにて AWS IAM
にアクセス後、左ペイン「ユーザー」をクリックします。
4. 「AWS マネジメントコンソールへのユーザーアクセスを提供する」をクリックします。
AWS Lambda
へのデプロイ完了後、マネジメントコンソールにてテストを実施するためです。
クリックすると、青枠の項目が表示されます。
5. 「IAM ユーザーを作成します」を選択します。
選択すると、ユーザーのログイン情報を設定する画面が表示されます。
適宜情報を入力します。
「許可のオプション」におけるベストプラクティスは、目的のポリシーをアタッチしたユーザーグループに対して今回作成したユーザーを追加することのようです。(画面に表示されている通り)
今回は簡単のため、ポリシーを直接アタッチする手段を採用します。
8. AdministratorAccess
ポリシーを選択したのち、右下の「次へ」をクリックします。
当然、管理者権限を付与することは最小権限の原則から逸脱した行為ですが、今回はトラブルシューティング簡素化(という言い訳)のため当該ポリシーを採用しました。
4. AWS CLI
のインストール・設定
1. 作成したユーザーの概要画面にて「アクセスキーを作成」をクリックします。
2. 「ユースケース」にて「コマンドラインインターフェイス(CLI)を選択します。
画面には、推奨される代替案が2つ表示されています。
ただ、本記事では言及しません。僕の勉強不足ゆえ。。
公式ドキュメントをご参照ください。
IAM ユーザーのアクセスキーの管理
3. 「上記のレコメンデーションを理解し、アクセスキーを作成します」にチェックをつけたのち、「次へ」をクリックします。
4. 適宜アクセスキーの説明を入力し、「アクセスキーの作成」をクリックします。
5. 「アクセスキーの取得」画面に表示される アクセスキー と シークレットキー をコピーし保存します。
6. 下記公式ドキュメントから macOS pkg
ファイルをダウンロードします。
画像にあるリンクから該当ファイルをダウンロードできます。
今回は「GUI installer」を用いた方法を採用しました。
7. ダウンロードした pkg
ファイルを実行します。
画像のようなインストール画面が表示されると思います。
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 ユーザー認証情報を使用して認証を行う
ベストプラクティスは、AWS IAM Identity Center などの ID プロバイダーとのフェデレーションを使用することのようです。(上記ドキュメント参照)
僕の勉強不足ゆえ、今回は IAM ユーザーの認証情報を利用します。
11. 下記コマンドを実行し、設定が正常に完了したことを確認します。
cat ~/.aws/credentials
# 出力結果
[default]
aws_access_key_id = "先ほど入力したアクセスキー"
aws_secret_access_key = "先ほど入力したシークレットキー
5. AWS CDK
のインストール・設定
公式ドキュメントはこちら
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
ふぅ〜、これで初期設定が完了しました。
あとは煮るなり焼くなり好きにできます。
7. 各種 npm
パッケージのインストール
1. 下記コマンドを実行し、今回必要な各種パッケージをインストールします。
npm i @slack/web-api @notionhq/client @types/aws-lambda @aws-sdk/client-ssm
- @slack/web-api
Slack にデータを送信・Slack からデータをクエリするための SDK です。
- @notionhq/client
Notion にデータを送信・Notion からデータをクエリするための SDK です。
- @types/aws-lambda
AWS Lambda
のための型定義を提供するパッケージです。
- @aws-sdk/client-ssm
プログラムからAWS Systems Manager Parameter Store
を操作するための SDK です。
8. 各種認証情報を AWS Systems Manager Parameter Store
に格納
1. 下記コマンドを実行し、各種認証情報を AWS Systems Manager Parameter Store
に格納します。
公式ドキュメントはこちら。
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 ファイルを作成します。
ファイルの全体は次の通りです。
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), } );
lambda 関数の環境変数には
AWS Systems Manager Parameter Store
へ登録した認証情報に対応するパラメータ名称を指定しています。
実際の認証情報は lambda 関数内で取得するようにしています。(後述)
- 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 ファイルを実装
実際に実行される処理を実装します。
ファイル全体は次の通りです。
全部ベタ打ちで実装しているので、コードがかなり汚いです。。お手柔らかにお願いします。。
また、エラーハンドリング処理が雑いと思われます。
僕自身勉強中ゆえ、ご指摘等頂けますと幸いです。
12/08 更新:ちょっとだけリファクタリングしました
各種公式ドキュメントはこちら。
- GetParameterCommand (aws-sdk/client-ssm)
- GetParametersCommand (aws-sdk/client-ssm)
- Query a database (notion sdk)
- Filter database entries (notion sdk)
- Web API (slack api)
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 関数をそれぞれ運用しておりますが、この枠内で収まっています。
まとめ
長々とお付き合いいただきありがとうございました。
どうせ今後また同じことをやりたくなるだろうと思い、手順書作成の意味を込めて記事にしてみました。
もしこの記事が誰かの役に立てますと幸いです。