Step FunctionsとEventBridgeでLINEのWebhook APIをノーコードで作ってみた

こちらは、[デジタルキューブ ＆ ヘプタゴン Advent Calendar 2022](https://qiita.com/advent-calendar/2022/digitalcube-heptagon) 15日目の記事です。


# はじめに

弊社は、LINE株式会社が企業のデジタルトランスフォーメーション（DX）実現を目的として提供する「LINE DX Program with AWS」のパートナー企業として、東北エリアの企業では初めての認定をされています。

LINE公式アカウントとAWSを組み合わせたサービス提供や開発を行なっていますが、今回はその連携の要となる部分・Webhook APIのお話です。


# 今回作成したもの

構成図は以下となります。
![LINE-EventBridge-StepFunctions.drawio (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116677/b17adae1-4dbf-b4e3-1e92-950a8700dbd1.png)

LINE公式アカウントに対してユーザーがメッセージを送った時、webhookで実行されるAPIを　Amazon API Gateway を用いて構築、LINEから送られてくる event の処理は AWS Step Functions を使用して Amazon EventBridge に対して送信します。

後続の処理は Rule を使用することで様々な処理と繋ぐことができますが、今回は Amazon EventBridge で設定可能な内容に限定して、ノーコードでオウム返しをするまでの処理を作成しました。

# 構築

## Step Functions API を作成

### Step Functions ワークフローの作成

API Gateway から実行される Step Functions ワークフローを作成します。

実際のフローは後で登録するので、今回は仮の内容で作成します。

1. [ステートマシンの作成] をクリック
1. [ワークフローを視覚的に作成] 、タイプは [標準] を選択して [次へ]
1. 「Pass」をD&Dで追加して、 [次へ]
1. 「生成されたコードを確認」画面はそのまま、 [次へ]
1. ステートマシン名を入力して、 [ステートマシンを作成] をクリック

![pass.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116677/b41454d4-e076-c498-ce2a-7697930a9aba.jpeg)

作成したステートマシンのARNは後で使用するのでメモしておきましょう。


### IAM Role の作成

API Gateway 用の IAM Role を作成します。

1. 信頼されたエンティティのタイプから「AWSのサービス」を選択し、「API Gateway」を選択して作成します。
1. 作成した Role にポリシーに以下を追加して、作成した Step Functions ワークフローを実行できるようにします。

```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "states:StartExecution",
            "Resource": "<作成したステートマシンのARN>"
        }
    ]
}
```

作成した IAM Role の ARN はコピーしておきましょう。


### API Gateway API を作成

1. [APIを作成] > REST API の[構築] > APIの名前を入力して、 [APIの作成] をクリックします。
1. [アクション] > [リソースの作成] の順に選択
1. 「新しい子リソース」ページで、[リソース名] に 「webhook」 と入力し、[リソースを作成] を選択
1. [アクション] > [メソッドの作成] の順に選択、リストから POST を選択し、チェックマークをクリック
1. 「/webhook - POST - 設定」ページで、以下の内容を設定して保存

|  項目名  |  内容  |
| ---- | ---- |
|  統合タイプ  |  AWSサービス  |
|  リージョン  |  ステートマシンのARNを作成したregion |
|  AWSサービス  |  Step Functions  |
|  HTTP メソッド  |  POST  |
|  アクション  |  「StartExecution」  |
|  実行ロール  |  前に作成した IAM ロールのロール ARN  |


#### APIのテスト

テストからAPIを実行して、ステートマシンが実行されるか確認します。

![test](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116677/0a3d1168-2feb-cc80-0857-33a17ecd8ebd.png)


ヘッダーに `Content-Type: application/json`、リクエスト本文に以下を入力して、[テスト]　をクリックして実行結果を確認します。

```
{"input":"{}", "stateMachineArn": "<作成したステートマシンのARN>"}
```

マネジメントコンソールでステートマシンの実行履歴を確認すると、ステートマシンに入力された内容を確認することができます。


#### デプロイしてテスト

[アクション] > [APIのデプロイ] の順に選択、新しいステージを作成して、APIをデプロイします。

作成したAPIにcurlで直接POSTリクエストを行い、ステートマシンが実行されることを確認します。

```
curl -X POST -H 'Content-Type: application/json' -d '{"input":"{}", "stateMachineArn": "<作成したステートマシンのARN>"}' <APIのURL>/webhook
```


### 統合リクエストの修正

LINEから送られてくるリクエストの内容と一緒に実行するステートマシンのARNを送りたいので、統合リクエストのマッピングテンプレートを使用してリクエストの内容を修正します。

![統合リクエスト](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116677/b885c212-1b57-1827-1bcb-9d0145916a4b.png)

![マッピングテンプレート](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116677/24cfecf9-873e-72d7-e178-669185fb842f.png)

LINEからのリクエストに対応するため、 Content-Type を `application/json` で登録します。

```
#set($input = $input.json('$'))
{
    "input": "$util.escapeJavaScript($input).replaceAll("\\'", "'")",
    "stateMachineArn": "<作成したステートマシンのARN>"
}
```

LINEからのリクエスト内容を `input` に全部入れて、 `stateMachineArn` を追加しているだけですね。

保存をしたら、再度APIのデプロイを実行します。

今度はリクエストボディにステートマシンのARNがなくても、実行できるようになりました。

```
curl -X POST -H 'Content-Type: application/json' -d '{"key": "value"}' <APIのURL>/webhook
```

### LINE公式アカウントの Webhook URL として設定する

LINE公式アカウントの管理画面から作成したAPIを使用できるように設定します。

![Messaging API](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116677/e9338cba-931d-de32-5ea0-d4b97a072bbd.png)

![応答設定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116677/c72e553d-0bf0-b859-87d7-b76743cc33f4.png)

設定後、LINE公式アカウントに対して適当なメッセージを送って、ステートマシンが実行されることを確認します。

マネジメントコンソールでステートマシンの実行履歴を確認すると、ステートマシンに入力された内容を確認することができます。

![destination](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116677/19900be4-97fa-b86e-088e-68d40dbd327c.jpeg)


この中で `input` の中に入ってくる内容がLINEから送られてきたリクエスト内容です。

さらにその中にある `destination` の値をコピーしておきます。
これはbotのユーザーIDにあたる文字列で、同じbotであることを示す値として使用するためにコピーしておきましょう。

```
{
  "input": {
    "destination": "<botのユーザーID>",
    "events": [
      {
          ...(省略）...
      }
    ]
  },
  ...(省略）...
}
```


### EventBridge の Custom EventBus を作成

LINEの webhook 専用の EventBus を作成します。
名前はなんでも構いませんが、「LINEWebhookEventBus」にしておきます。

![eventbus](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116677/ec2729fd-021e-9c2d-6187-53ecc904ef2b.jpeg)

作成画面のタイトルが「イベント**パス**を作成」となっていて、一瞬不安になるやつ(´･ω･`)


### Step Functions ワークフローの修正

あとは今回はテキストが送られてきた時だけ EventBridge にイベント発行するようにしてみます。
（本当はもっと分岐をするワークフローになるはずですが、今回は割愛します）

作成したステートマシンの画面で [編集] をクリック、[WorkFlow Studio] をクリックして編集画面に進みます。

![WorkFlow Studio](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116677/ba147e2d-004a-5942-c203-401f0791b1b0.png)

最初に設定した「Pass」ステートは消しておきましょう。

#### Map

LINEから送られてくる event はAPI実行1回につき複数送られてくる場合があるので、1件ずつ処理するようにmapを使用して処理します。

LINEからのリクエストボディの仕様で、各イベントは `events` の中に入ってくるので、項目配列へのパスを指定しています。

![map1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116677/cfb53e0f-896f-b627-31eb-a15014286c38.png)

また、各イベントと一緒に `destination` の値を渡したいので、 ItemSelector で一緒に送ってあげます。

```
{
  "event.$": "$$.Map.Item.Value",
  "destination.$": "$.destination"
}
```

![map2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116677/b194b898-a911-596e-ea04-70f027f2bea3.png)


#### PutEvents

アクションの一覧から EventBridge - PutEvents をD&DしてMapステートの中に配置します。

![PutEvents](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116677/b669f87b-61b4-cd06-60bc-ba47194008c4.png)

APIパラメータは以下の通りです。

```txt
{
  "Entries": [
    {
      "Detail": {
        "Text.$": "$.event.message.text",
        "ReplyToken.$": "$.event.replyToken"
      },
      "DetailType.$": "States.Format('{}.{}', $.event.type, $.event.message.type)",
      "EventBusName": "LINEWebhookEventBus",
      "Source.$": "$.destination"
    }
  ]
}
```

イベントソースに destination の値、送信先の EventBus は先ほど作成したものを指定しています。

イベントの `DetailType` は、LINEのリクエスト内容から `type` の情報を組み合わせて使用することにしたいので、Step Functions の [組み込み関数](https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/amazon-states-language-intrinsic-functions.html) を使って作成しています。

`Detail` の中身が実際にイベント内容として通知される内容なので、本文と返信時に使用するリプライトークンを設定しています。

「コールバックを待つ」のチェックを外すのをお忘れなく。

#### Choice

オウム返しだけなら上記だけの設定で可能ですが、Webhookイベントには様々な種類があり、きちんと場合分けをさせたいところです。

フローから `Choice` や `Pass` を選択し、条件分岐を作っていきます。

![choice](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116677/04c223bf-0724-a036-2ad6-18eb0ed948e6.png)

Rule #1 の [Add Conditions] から条件を設定します。

![condition](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116677/b0e1e707-ce93-11f9-b707-b50ddf41039d.jpeg)

最終的に以下のようなフローになりました。

![flow](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116677/ddc0a850-44cf-1b15-6259-8ff42fcad2ab.jpeg)

テキストを含むmessageオブジェクトであれば、`detail`に送られてきた文字列とリプライトークンを設定して PutEvents のステートに送っています。

![message.text](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116677/72a41b2d-0594-7d4e-edf6-9eb6aa9208eb.png)

![message.others](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116677/a82bae9a-570c-bb45-bdab-1e61bfddd318.png)

![not_supported](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116677/32971e49-e582-44ae-0ecf-d2a15be4cc9e.png)

また、各Passステートの出力タブで、ResultPathを使用して上記で生成した入力を後続のステートへと送ることが可能です。
![FireShot Capture 324 - Step Functions Management Console - ap-northeast-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116677/44830f09-df0d-8de6-a44f-1205fad10447.png)

各Passステートからの入力に `result` が入ってくるので、最終的に PutEvents へ渡されるパラメータは以下のようになりました。

![PutEvents](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116677/6a56ad2a-2ef4-d6fe-6911-7e6cf6da9cc9.png)



### ステートマシンの IAM Role にポリシーを追加

作成中のステートマシンが EventBridge に対して PutEvents するための権限を追加します。

```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "events:PutEvents"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}
```


### 動作確認

実際にLINEからメッセージを送り、 ステートマシンが実行されることを確認します。

![test](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116677/c5be0325-ca86-016b-9e31-1e8f70810fac.png)

`EventBridge PutEvents` のステップで実際に送信されるパラメータの値が適切に設定されていることが確認できます。

## イベント検知後の処理を実装

### ログを Amazon CloudWatch Logs へ送る

CloudWatch Logsにイベントの内容を全て通知して、後に確認できるように設定します。

1. 作成した EventBus を選択して、[ルールを作成]
1. 名前に「`log-<destination>`」を入力して、[次へ]　（destinationの値に置き換えてください）
1. イベントソースは[その他]を選択、イベントパターン（後述）を入力して[次へ]
1. ターゲットタイプは[AWSサービス]を選択、「ターゲットを選択」は[CloudWatch ロググループ]を選択、「ロググループ」は「`LINE/webhook/<destination>`」を入力して[次へ]
1. タグを設定ページでは、何もせず[次へ]
1. [ルールの作成]をクリック


イベントパターンは以下で登録します。
```
{
  "source": ["<destination>"]
}
```

LINEから適当なテキストを送信して、ロググループ `/aws/events/LINE/webhook/<destination>` にLINEからのリクエスト情報を含むイベントが出力されていることを確認します。

![log](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116677/150813ab-e766-dc78-ec31-128ade0f844f.png)



### Amazon EventBridge API Destinations から直接 LINE の API を叩く

API Destinations は HTTP エンドポイントや認証情報を保存して、ターゲットに設定することができます。
わざわざ Lambda で書く必要もないので、これは捗りますね。

#### API の接続先を登録

左メニューから「APIの接続先」をクリックします。

##### 接続を作成

LINEのAPIを実行する際は `channel access token` を Authorization としてリクエストヘッダーに付与する必要があるので、先に登録します。
（登録した内容は AWS Secrets Manager に保存されます）

 [APIの送信先] > [接続]タブ > [接続を作成] から、以下を入力して保存します。

|  項目名  |  内容  |
| ---- | ---- |
|  接続名  |  「LINE_API」  |
|  送信先タイプ  | その他 |
|  認証タイプ  |  APIキー  |
|  APIキー名  |  「Authorization」  |
|  値  |  Bearer {channel access token}  |

`{channel access token}` は実際の値に置き換えてください。
 

##### APIの送信先を作成

 [APIの送信先] > [APIの送信先]タブ > [API送信先を作成] から、以下を入力して保存します。

|  項目名  |  内容  |
| ---- | ---- |
|  名前  |  「LINE_reply_API」  |
|  API 送信先エンドポイント  | https://api.line.me/v2/bot/message/reply |
|  HTTP メソッド  |  POST  |
|  接続  |  既存の接続を使用 → 「LINE_API」  |


#### Rule の追加

1. 作成した EventBus を選択して、[ルールを作成]
1. 名前に「`message-text-<destination>`」を入力して、[次へ]　（destinationの値に置き換えてください）
1. イベントソースは[その他]を選択、イベントパターン（後述）を入力して[次へ]
1. ターゲット入力を設定ページの入力内容（後述）を設定して[次へ]
1. タグを設定ページでは、何もせず[次へ]
1. [ルールの作成]をクリック


イベントパターンは以下で登録します。
```
{
  "detail-type": ["message.text"],
  "source": ["<destination>"]
}
```

[ターゲット入力を設定]ページの入力内容

|  項目名  |  内容  |
| ---- | ---- |
|  ターゲットタイプ  |  EventBridge API の宛先  |
|  API送信先  | 既存のAPI送信先を使用 → 「LINE_reply_API」   |
|  ターゲット入力を設定  |  入力トランスフォーマー （[入力トランスフォーマーを設定]をクリックして、後述の設定を行ってください）  |



ターゲット入力トランスフォーマーの内容は以下の通りです。

入力パス（event内の内容から入力テンプレートに渡す内容をピックアップして名前をつけます）
```
{
  "text": "$.detail.Text",
  "token": "$.detail.ReplyToken"
}
```

入力テンプレート （LINEのAPI仕様に沿った形に成形します）
```
{
    "replyToken":<token>,
    "messages":[
        {
            "type":"text",
            "text":<text>
        }
    ]
}
```

### 動作確認

設定後、LINE公式アカウントに対して適当なメッセージを送って、送ったメッセージと同じテキストが返って来れば成功です！

![success](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116677/130d77d3-bdcc-7f87-f09c-a3f86247026a.jpeg)


# 感想

EventBridge を使用することで、Webhookイベントの受信と必要なイベントごとの処理を疎結合な形で実装することができました。

AWSの他のサービスや、別アカウントのEventBridgeにもイベントを送ることができるので、構築方法の幅が拡がりますね！
（ルールでターゲットに指定できる項目は以下をご確認ください）
https://docs.aws.amazon.com/ja_jp/eventbridge/latest/userguide/eb-targets.html

また、 Step Functions も Workflow Studio が登場したことで格段に設定しやすく、連携できるサービスは200以上もあるので、コードを書かなくてもシステム構築ができるようになっています！

GUIで繋げて作成したワークフローを直接API Gatewayに繋いでAPI化することも出来たので、普段コードを書いて作成している自分にとって新鮮な開発体験になりました。

# さいごに

(´･ω･`)oO(果たしてこれはノーコードと言っていいのだろうか)

(´･ω･`)oO(...たぶん大丈夫)

# 参考
[API Gateway を使用した Step Functions API の作成](https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/tutorial-api-gateway.html)