Zenn

LINE上からEventBridge Schedulerを設定してリマインド通知する方法

2025/03/23に公開

概要

LINE Bot の開発であらかじめ決められた定期通知ではなく、ユーザー自身が入力した特定の時間に合わせてリマインド通知が届くように実装する方法をご紹介します。
line
ユーザーが時間を設定

今回は AWS の EventBridge Scheduler でリマインド通知をやっていこうと思います。
下記画像のようにサクッと実装できるような構成になっています。
aws

対象読者

  • LINE 上から AWS の EventBridge Scheduler を用いて、リマインド通知がしたいと思っている方。
  • Hono を使って LINE Bot 開発をしてみたい方。

今回のサンプルコードは以下です。
https://github.com/reki204/line-scheduler-sample

必要な技術と準備

使用する技術の概要

  • AWS CDK
  • Hono
  • TypeScript
  • AWS サービス
    • API Gateway
    • Lambda
    • EventBridge Scheduler
    • SSM Parameter Store
  • LINE Messaging API

環境セットアップの基本手順

https://hono.dev/docs/getting-started/aws-lambda

Hono のドキュメントを参考にして、セットアップしていきましょう。

mkdir my-app
cd my-app
cdk init app -l typescript
bun add hono
mkdir lambda
touch lambda/index.ts

LINE Messaging API の設定

https://developers.line.biz/ja/
LINE Developers からコンソールへログインし、使用する Messaging API のチャネルアクセストークンチャネルシークレットを取得しましょう。

取得したチャネルアクセストークンとチャネルシークレットは.env ファイルに記載しておきましょう。

.env
CHANNEL_ACCESS_TOKEN=XXX
CHANNEL_SECRET=XXX

Webhook の基本設定

LINE Developers からチャネルアクセストークンが記載してあった Messaging API 設定へいくと Bot 情報や Webhook 設定などがあります。ここのWebhook URLに今回だと API Gateway の URL を設定することになります。CDK をデプロイしてから設定しましょう。
WebhookImage

プロダクトコード

続いてプロダクトコードの方ですが、エンドポイントは/webhookにしておきます。先ほどのチャネルアクセストークンを使用することで Messaging API Client クラスを使えるようになります。ここではユーザーに返信する用のリプライメッセージを使います。

app.ts
import { Hono } from "hono";
import { env } from "hono/adapter";
import { messagingApi, type WebhookEvent } from "@line/bot-sdk";

const app = new Hono();

app.post("/webhook", async (c) => {
  const { CHANNEL_ACCESS_TOKEN } = env<{ CHANNEL_ACCESS_TOKEN: string }>(c);
  const lineClient = new messagingApi.MessagingApiClient({
    channelAccessToken: CHANNEL_ACCESS_TOKEN,
  });

  const events: WebhookEvent[] = (await c.req.json()).events;
  for (const event of events) {
    if (event.type === "message" && event.message.type === "text") {
      const replyToken = event.replyToken;
      const userInputMessage = event.message.text;

      await lineClient.replyMessage({
        replyToken,
        messages: [{ type: "text", text: `${userInputMessage}に設定しました。` }],
      });
    }
  }

  return c.json({ message: "Webhook received" });
});

次にリクエストを検証する必要があるため、ミドルウェアを設定していきましょう。

署名を検証する Middleware の設定

https://developers.line.biz/ja/reference/messaging-api/#signature-validation

引用文リクエストが LINE プラットフォームから送られたことを確認するために、ボットサーバーでリクエストヘッダーの x-line-signature に含まれる署名を検証します。
検証の手順は以下のとおりです。

  1. リクエストボディのダイジェストを計算します。チャネルシークレットを秘密鍵として HMAC-SHA256 アルゴリズムを使用します。
  2. ダイジェストを Base64 エンコードし、リクエストヘッダーの x-line-signature に含まれる署名と一致するかどうかを確認します。

Messaging API のリファレンスにあるように、リクエストヘッダーにあるx-line-signatureを検証する必要があります。そのため、ミドルウェアを使いましょう。

Hono のカスタムミドルウェアを使っていきますが、実際の検証手順は line-sdk から validateSignature 関数が用意されているのでそれを使いましょう。

lineWebhookMiddleware.ts
import { createMiddleware } from "hono/factory";
import { HTTPException } from "hono/http-exception";
import { env } from "hono/adapter";
import { validateSignature } from "@line/bot-sdk";

export const lineWebhookMiddleware = createMiddleware(async (c, next) => {
  const signature = c.req.header("x-line-signature");
  const body = await c.req.text();
  const { CHANNEL_SECRET } = env<{ CHANNEL_SECRET: string }>(c);

  if (!signature || !CHANNEL_SECRET)
    throw new HTTPException(400, {
      message: "署名またはCHANNEL_SECRETが見つかりません",
    });
  if (!validateSignature(body, CHANNEL_SECRET, signature))
    throw new HTTPException(403, { message: "無効な署名です" });

  await next();
});
app.ts
app.use("/webhook", lineWebhookMiddleware);

ここまでで、Hono(Lambda)の実装が終わりました。次は実際のリマインド通知を担当する EventBridge Scheduler の設定をして、LINE Messaging API からスケジュール時間を取得できるようにしていきます。

AWS Scheduler(EventBridge Scheduler)の設定

リマインダー用スケジュールの作成方法

EventBridge Scheduler を操作できる SDK をインストールしましょう。

bun install @aws-sdk/client-scheduler
reminderService.ts
import {
  SchedulerClient,
  CreateScheduleCommand,
  CreateScheduleCommandInput,
} from "@aws-sdk/client-scheduler";

const schedulerClient = new SchedulerClient({
  region: "ap-northeast-1",
});

const LAMBDA_ARN = process.env.REMINDER_HANDLER_ARN || "";
const SCHEDULER_ROLE_ARN = process.env.SCHEDULER_ROLE_ARN || "";

/**
 * EventBridge Schedulerを登録する
 *
 * @param scheduleTime スケジュール時間
 */
export const setScheduleReminders = async (scheduleTime: string) => {
  const [hour, minute] = scheduleTime.split(":");
  const cronExpression = `cron(${minute} ${hour} * * ? *)`;
  const scheduleName = `sample-reminder-${hour}${minute}`;

  const inputPayload = JSON.stringify({
    type: "SAMPLE_REMINDER",
    scheduledTime: scheduleTime,
  });

  const createScheduleParams: CreateScheduleCommandInput = {
    Name: scheduleName,
    ScheduleExpression: cronExpression,
    ScheduleExpressionTimezone: "Asia/Tokyo",
    FlexibleTimeWindow: { Mode: "OFF" },
    Target: {
      Arn: LAMBDA_ARN,
      RoleArn: SCHEDULER_ROLE_ARN,
      Input: inputPayload,
    },
  };

  try {
    await schedulerClient.send(new CreateScheduleCommand(createScheduleParams));
    console.log("Created schedule:", scheduleName);
  } catch (error) {
    console.error("Error creating schedule:", error);
  }
};

ここでは、スケジュール名と時間、Lambda へ送るペイロードを設定しています。時間は cron 形式にします。

CreateScheduleCommand でスケジュールを登録していきます。EventBrige だと時間が UTC 基準でしたが、Scheduler だとタイムゾーンを設定できるのがいいですね。

Lambda を API 用とリマインド通知用に分割

今回は LINE からの Webhook 専用とリマインド通知を分けて実装していくため、Lambda のハンドラーを分離していきましょう。

api-handler/index.ts
import { handle } from "hono/aws-lambda";
import app from "../src/app";

/**
 * API Lambdaのハンドラー
 * APIリクエスト処理(LINE Webhook)のみを担当
 */
export const handler = handle(app);

API 用のハンドラーはこれだけで十分です。Hono をラップしているのでスッキリになりました。

次にリマインド通知用のハンドラーの設定です。
LINE Developers のコンソールから Messaging API を選択し、チャネル基本設定にいくとユーザー ID があるのでこれを今回は使いましょう。

UserIdの場所

今回は自分用という前提なので固定のユーザー ID を使っていきます。環境変数に指定しておきましょう。

.env
USERID=XXX

通知する LINE の宛先をこれで設定できます。Reminder Lambda は下記です。スケジューラーを登録する時に設定した Lambda へ渡すペイロードからイベントタイプとスケジュール時間を受け取ります。

reminder-handler/index.ts
import { messagingApi } from "@line/bot-sdk";

/**
 * Reminder Lambdaのハンドラー
 * EventBridge Schedulerからのイベント処理のみを担当
 */
export const handler = async (event: any) => {
  if (event.type === "SAMPLE_REMINDER") {
    await sendMedicationReminder(event.scheduledTime);
    return { statusCode: 200, body: "リマインダー通知が成功しました。" };
  }

  return { statusCode: 400, body: "リマインダー通知に失敗しました。" };
};

/**
 * リマインダー通知をLINEに送信する
 *
 * @param scheduledTime スケジュール時間
 */
const sendMedicationReminder = async (scheduledTime: string) => {
  const lineClient = new messagingApi.MessagingApiClient({
    channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN!,
  });

  const userId = process.env.USERID!;
  // リマインドメッセージ
  const message = `予定時間: ${scheduledTime}にお知らせしています。`;

  // メッセージを送信
  await lineClient.pushMessage({
    to: userId,
    messages: [
      {
        type: "text",
        text: message,
      },
    ],
  });
};

これで EventBridge Scheduler からのイベントを受け取り、LINE ユーザーへ通知が送れます。
Messaging API Client の pushMessage で宛先とメッセージをテキストベースで送ります。

Lambda 関数との連携

CDK で AWS 構成をコードで記載していきます。コードとして定義されることで、デプロイすることで一貫した環境が構築できるため、管理や再利用がしやすくなります。
具体的には、先ほど分けた 2 つの Lambda、API Gateway、IAM ロール、そして EventBridge Scheduler との連携をコードで定義しています。

Lambda 関数の定義

  • Reminder Handler Lambda
    この Lambda 関数は、EventBridge Scheduler からトリガーされ、実際にリマインダー通知を LINE に送信する役割を担います。
// Reminder Handler Lambda (EventBridge Schedulerからの呼び出し専用)
const reminderHandlerLambda = new NodejsFunction(this, "sampleReminderHandler", {
  entry: "lambda/reminder-handler/index.ts",
  handler: "handler",
  runtime: lambda.Runtime.NODEJS_20_X,
  environment: {
    CHANNEL_ACCESS_TOKEN: channelAccessToken,
    ENV: "production",
  },
  timeout: cdk.Duration.seconds(30),
});

また、この Lambda にはScheduler からの呼び出しを許可するための権限を追加します。後で設定するSchedulerRole にも、この Lambda 関数を Invoke できるようにポリシーを設定していきます。

  • API Handler Lambda
    LINE からの Webhook リクエストのみを受け取り、スケジュールの登録処理などを行う API 用の Lambda 関数です。
// API Handler Lambda (APIリクエスト処理用)
const apiHandlerLambda = new NodejsFunction(this, "sampleApiHandler", {
  entry: "lambda/api-handler/index.ts",
  handler: "handler",
  runtime: lambda.Runtime.NODEJS_20_X,
  role: apiHandlerLambdaRole,
  environment: {
    CHANNEL_ACCESS_TOKEN: channelAccessToken,
    CHANNEL_SECRET: channelSecret,
    ENV: "production",
    REMINDER_HANDLER_ARN: reminderHandlerLambda.functionArn,
    SCHEDULER_ROLE_ARN: schedulerRole.roleArn,
  },
  timeout: cdk.Duration.seconds(30),
});

この関数では、ユーザーが LINE 上で指定した時間を元に、EventBridge Scheduler でリマインダーを設定する処理を行います。

IAM ロールと権限の作成

次に、2 種類の IAM ロールを作成しています。

  • SchedulerRole
    EventBridge Scheduler がリマインダー用の Lambda 関数を呼び出すためのロールです。初期設定では Lambda の Invoke 権限を付与しておき、後から対象の Lambda 関数の ARN を指定しています。
// EventBridge SchedulerからReminder Lambdaを呼び出すためのIAMロール
const schedulerRole = new iam.Role(this, "SchedulerRole", {
  assumedBy: new iam.ServicePrincipal("scheduler.amazonaws.com"),
  description: "Role for EventBridge Scheduler to invoke Lambda",
});
// Reminder LambdaのARNに対してのみInvoke権限を付与
schedulerRole.addToPrincipalPolicy(
  new iam.PolicyStatement({
    actions: ["lambda:InvokeFunction"],
    resources: [],
  })
);
  • ApiHandlerLambdaRole
    API リクエスト(LINE の Webhook など)を処理する Lambda 関数用の実行ロールです。このロールには、EventBridge Scheduler でスケジュールを作成する権限や、SchedulerRole を他のサービスに渡すための PassRole 権限がを付与しています。
// API Handler Lambda用のIAMロール
const apiHandlerLambdaRole = new iam.Role(this, "ApiHandlerLambdaRole", {
  assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
  description: "API Handler Lambda execution role",
});
// API Handler LambdaがEventBridge Schedulerに対してスケジュールを作成できる権限
apiHandlerLambdaRole.addToPrincipalPolicy(
  new iam.PolicyStatement({
    actions: ["scheduler:CreateSchedule"],
    resources: [`arn:aws:scheduler:${this.region}:${this.account}:schedule/default/sample-reminder-*`],
  })
);
// API Handler LambdaがschedulerRoleをPassRoleできるようにする
apiHandlerLambdaRole.addToPrincipalPolicy(
  new iam.PolicyStatement({
    effect: iam.Effect.ALLOW,
    actions: ["iam:PassRole"],
    resources: [schedulerRole.roleArn],
  })
);

SSM パラメータの登録と取得

まずは、LINE Messaging API で必要な「チャネルアクセストークン」と「チャネルシークレット」を、SSM パラメータストアから取得しています。

秘匿情報は SSM パラメータストアに格納して、lambda がこれを参照できるようにしましょう。

aws ssm put-parameter --name "channel-access-token" --type "String" --value "チャンネルアクセストークンの値"
aws ssm put-parameter --name "channel-secret" --type "String" --value "チャンネルシークレットの値"
aws ssm put-parameter --name "user-id" --type "String" --value "ユーザーID"
const channelAccessToken = ssm.StringParameter.valueForStringParameter(
  this,
  "channel-access-token"
);
const channelSecret = ssm.StringParameter.valueForStringParameter(
  this,
  "channel-secret"
);
const userId = ssm.StringParameter.valueForStringParameter(
  this,
  "user-id"
);

API Gateway の設定

最後に、API Gateway を作成し、API Handler Lambda と連携することで、外部からの HTTP リクエスト(例:LINE の Webhook リクエスト)を受け付けるエンドポイントを構築しています。

new apigw.LambdaRestApi(this, "LineSchedulerSample", {
  handler: apiHandlerLambda,
});

この設定により、ユーザーからのメッセージや通知リクエストが API Gateway を経由して Lambda に届き、システム全体の処理が開始されます。

コードの全体像
lib/line-scheduler-sample-stack.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as apigw from "aws-cdk-lib/aws-apigateway";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
import * as ssm from "aws-cdk-lib/aws-ssm";
import * as iam from "aws-cdk-lib/aws-iam";

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

    const channelAccessToken = ssm.StringParameter.valueForStringParameter(
      this,
      "channel-access-token"
    );
    const channelSecret = ssm.StringParameter.valueForStringParameter(
      this,
      "channel-secret"
    );
    const userId = ssm.StringParameter.valueForStringParameter(
      this,
      "user-id"
    );

    // EventBridge SchedulerからReminder Lambdaを呼び出すためのIAMロール
    const schedulerRole = new iam.Role(this, "SchedulerRole", {
      assumedBy: new iam.ServicePrincipal("scheduler.amazonaws.com"),
      description: "Role for EventBridge Scheduler to invoke Lambda",
    });
    // Reminder LambdaのARNに対してのみInvoke権限を付与
    schedulerRole.addToPrincipalPolicy(
      new iam.PolicyStatement({
        actions: ["lambda:InvokeFunction"],
        resources: [],
      })
    );

    // API Handler Lambda用のIAMロール
    const apiHandlerLambdaRole = new iam.Role(this, "ApiHandlerLambdaRole", {
      assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
      description: "API Handler Lambda execution role",
    });
    // API Handler LambdaがEventBridge Schedulerに対してスケジュールを作成できる権限
    apiHandlerLambdaRole.addToPrincipalPolicy(
      new iam.PolicyStatement({
        actions: ["scheduler:CreateSchedule"],
        resources: [`arn:aws:scheduler:${this.region}:${this.account}:schedule/default/sample-reminder-*`],
      })
    );
    // API Handler LambdaがschedulerRoleをPassRoleできるようにする
    apiHandlerLambdaRole.addToPrincipalPolicy(
      new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        actions: ["iam:PassRole"],
        resources: [schedulerRole.roleArn],
      })
    );

    // Reminder Handler Lambda (EventBridge Schedulerからの呼び出し専用)
    const reminderHandlerLambda = new NodejsFunction(this, "sampleReminderHandler", {
      entry: "lambda/reminder-handler/index.ts",
      handler: "handler",
      runtime: lambda.Runtime.NODEJS_20_X,
      environment: {
        CHANNEL_ACCESS_TOKEN: channelAccessToken,
        USER_ID: userId,
        ENV: "production",
      },
      timeout: cdk.Duration.seconds(30),
    });
    // SchedulerからのInvoke許可を付与
    reminderHandlerLambda.addPermission("AllowSchedulerInvoke", {
      principal: new iam.ServicePrincipal("scheduler.amazonaws.com"),
      action: "lambda:InvokeFunction",
      sourceArn: `arn:aws:scheduler:${this.region}:${this.account}:schedule/*`,
    });
    // schedulerRoleにReminder LambdaのInvoke対象のARNを設定
    schedulerRole.addToPrincipalPolicy(
      new iam.PolicyStatement({
        actions: ["lambda:InvokeFunction"],
        resources: [reminderHandlerLambda.functionArn],
      })
    );

    // API Handler Lambda (APIリクエスト処理用)
    const apiHandlerLambda = new NodejsFunction(this, "sampleApiHandler", {
      entry: "lambda/api-handler/index.ts",
      handler: "handler",
      runtime: lambda.Runtime.NODEJS_20_X,
      role: apiHandlerLambdaRole,
      environment: {
        CHANNEL_ACCESS_TOKEN: channelAccessToken,
        CHANNEL_SECRET: channelSecret,
        ENV: "production",
        REMINDER_HANDLER_ARN: reminderHandlerLambda.functionArn,
        SCHEDULER_ROLE_ARN: schedulerRole.roleArn,
      },
      timeout: cdk.Duration.seconds(30),
    });

    // API Gatewayの作成(Webhook用エンドポイント)
    new apigw.LambdaRestApi(this, "LineSchedulerSample", {
      handler: apiHandlerLambda,
    });
  }
}

お疲れ様でした。プロダクトコードは以上です。次からは実際にローカルで動作確認をして、AWS へデプロイしていきましょう。

動作確認

下記の手順でローカルサーバーを立てて、動作確認してみましょう。

CloudFormationテンプレートを出力
cdk synth --no-staging > template.yml

ローカルサーバーを立ち上げる
sam local start-api

ローカルサーバーが立ち上がると思います。しかし、localhost のままだと LINE 上からメッセージを送ることができないので、ngrok のサービスを使い、一時的に外部公開にしましょう。
https://ngrok.com/

ngrok http 3000

ngrok

ngrok から発行された https 化した URL を LINE Developers の Webhook 設定に/webhookを加えて設定したら OK です。

ローカルで動作確認が終わったらいよいよデプロイしてみましょう。

cdk deploy

デプロイに成功すると Outputs に API Gateway の URL が表示されます。

LINE Developers の Webhook に、この URL のエンドポイントに/webhook を追加したものを設定しましょう。

webhook設定

実際に LINE で時刻を入力して、その時間に通知が来れば完成です。
AWS コンソールで EventBridge Scheduler を検索し、スケジュールに追加されていると思います。

まとめ

以上が LINE 上からの入力で EventBridge Scheduler にスケジュール設定するやり方でした。
ご参考になれば幸いです。

Discussion

ログインするとコメントできます