Netlify FunctionsでLINE BOTを作ってみる


[Netlify Functions](https://www.netlify.com/products/functions/)はAWS Lambda上で動作するNode.js環境です。最近の[now](http://now.sh)とかに似てる雰囲気ですね。

**ある程度無料でNode.jsがホスティングできる環境**っぽい雰囲気で覚えておくと良いかもしれません。

> ちなみに今日は[東京ハッカソン@ヒーローズ・リーグ 2019 by MA](https://mashupawards.connpass.com/event/145604/)のハッカソン会場からです。 #ヒーローズリーグ

## Netlify Functionsでのハローワールド

[【入門】Netlify Functionsコトハジメ](https://qiita.com/Sr_Bangs/items/7867853f5e71bd4ada56)などの記事を見るとハローワールドが出来ます。

### プロジェクトルートにnetlify.tomlを追加

まずは、通常のNode.jsプロジェクトを作ります。

```bash
$ mkdir mylinebot
$ cd mylinebot
$ npm init -y
```

`package.json`が作成されます。
このプロジェクトのルートに`netlify.toml`を作成します。

```netlify.toml
[build]
  command = "npm run build"
  functions = "dist/api"
```

### 必要なモジュールを追加

```bash
$ npm i express @line/bot-sdk serverless-http netlify-lambda
```

ミニマムでハローワールドを試すなら`netlify-lambda`だけで問題ないですが、後の手順のためにLINE BOTのSDKやexpress、serverless-httpも入れておきます。

### 任意のフォルダにJSファイルを設置

今回は`src`フォルダを作成して、その中に`server.js`を設置します。

`package.json`の`scripts`の箇所に起動スクリプトを記述します。

```
  "scripts": {
    "dev": "netlify-lambda serve ./src",
    "build": "netlify-lambda build ./src"
  },
```

今回はsrcフォルダですが`"netlify-lambda serve <任意フォルダ>"`という形になります。

### hello.js

srcフォルダにhello.jsを作成します。

```src/hello.js
exports.handler = function(event, context, callback) {
    callback(null, {
        statusCode: 200,
        body: `Hello, World`
    });
}
```

### ローカル起動

```bash
$ npm run dev
```

これで`http://localhost:9000/.netlify/functions/hello`にアクセスすると`Hello, World`が表示されます。

`/.netlify/functions/<ファイル名>`というアクセス方法になるらしく、今回はhello.jsとしたのでこのようなURLでのアクセスになりました。

> `http://localhost:9000/hello`みたいなアクセス方法って出来ないのかな......? 

### デプロイして確認

GitLabやGitHubにデプロイして、Netlifyとリポジトリを連携すればOKです。

`https://xxxxxxxxxxxxxxxxx.netlify.com/.netlify/functions/hello`にアセクスすると`Hello, World`が表示されるはずです。

## LINE BOTを作成する

ここからLINE BOT作成ですが、基本は[1時間でLINE BOTを作るハンズオン](https://qiita.com/n0bisuke/items/ceaa09ef8898bee8369d)のコードをベースにして作っていきます。

* LINE BOTの基礎
* ngrokの使い方

を事前に把握しておきましょう。

### Netlify Functionsでexpressを扱うサンプル

通常のExpressのコードをそのまま利用することが出来ないため、[元のソースコード](https://qiita.com/n0bisuke/items/ceaa09ef8898bee8369d#%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E4%BD%9C%E6%88%90%E3%81%A8%E3%83%8F%E3%83%AD%E3%83%BC%E3%83%AF%E3%83%BC%E3%83%AB%E3%83%89)を改変します。

[neverendingqs/netlify-express](https://github.com/neverendingqs/netlify-express/blob/0780127cd575704e2a2a00a1a648ba5a5a66c388/express/server.js)が参考になりました。

### 依存モジュール

↑でインストールしていれば問題ないです。

```bash
$ npm i express @line/bot-sdk serverless-http netlify-lambda
```

### おうむ返しLINE BOTのコード

↑で作成したsrcフォルダにserver.jsを作成します。

```src/server.js
'use strict'

const express = require('express');
const line = require('@line/bot-sdk');
const serverless = require('serverless-http'); //追加
const app = express();

const config = {
    channelSecret: 'チャンネルシークレット',
    channelAccessToken: 'チャンネルアクセストークン'
};

const router = express.Router(); //ルーティング用に追加
router.get('/', (req, res) => res.send('Hello LINE BOT!(GET)')); //ブラウザ確認用(無くても問題ない)
router.post('/webhook', line.middleware(config), (req, res) => {
    console.log(req.body.events);

    //ここのif分はdeveloper consoleの"接続確認"用なので削除して問題ないです。
    if(req.body.events[0].replyToken === '00000000000000000000000000000000' && req.body.events[1].replyToken === 'ffffffffffffffffffffffffffffffff'){
        res.send('Hello LINE BOT!(POST)');
        console.log('疎通確認用');
        return; 
    }

    Promise
      .all(req.body.events.map(handleEvent))
      .then((result) => res.json(result));
});

const client = new line.Client(config);
function handleEvent(event) {
    if (event.type !== 'message' || event.message.type !== 'text') {
      return Promise.resolve(null);
    }

    return client.replyMessage(event.replyToken,{
      type: 'text',
      text: event.message.text
    });
}

app.use('/.netlify/functions/server', router); //ルーティング追加
module.exports = app; //追加
module.exports.handler = serverless(app); //追加
```

### ローカルで試す

```bash
$ npm run dev
```

これでlocalhostの9000番ポートで起動します。

```bash
$ ngrok http 9000
```

これでngrok側で発行されたURLをLINE Developers側に登録して動作確認してみましょう。

### LINE BOTの設定

今回ngrokのアドレスが`7db932fe.ngrok.io`で生成されたので以下のようなパスをLINE DevelopersのBOTのWebhookの設定に書いてあげます。

> ![スクリーンショット 2019-09-14 19.05.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35387/a8be00c5-e4ef-594e-e644-8536651bb1b9.png "スクリーンショット 2019-09-14 19.05.36.png")

ローカルでの確認が終わったら、デプロイして`https://xxxxxxxxxxxxxxxx.netlify.com/.netlify/functions/server/webhook`などのデプロイ後のURLを登録し直しましょう。

これでBOTが永続かできます。

### 試してみる

ちゃんとおうむ返しをしてくれているので問題なく動いてそうです。

> ![スクリーンショット 2019-09-14 19.08.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35387/0202c549-218b-15bc-4eee-a7367c1cb330.png "スクリーンショット 2019-09-14 19.08.41.png")

## GitHubにあげておきました。

完成版っぽいサンプルです。

最終的にはdotenv使ってます。

https://github.com/n0bisuke/netlify-functions-linebot-example


## まとめ

まだまだドキュメントやサンプルが少ない印象だけど、無料で結構使える模様なので、個人制作のサービスや、ハッカソンなどで作ったものをお金書けずに置いておくとかの場合に使えそうです。

課金すればもっと色々やれるっぽいですが、とりあえずは無料FaaSの選択肢の一個として頭に入れておくと良いかもしれません。

## おまけ

Netlify Functionsで謎のデプロイの失敗がありました。

Deployのログを見ると`failed during stage 'deploying site': Failed to execute deploy~~`みたいな感じで、調べると[Netlifyでfunctionsのデプロイがなぜか失敗する時の対処法](https://crieit.net/posts/Netlify-functions)などの記事が。

```
待とう。何かNetlifyの機嫌が悪いだけかもしれない。もしくはアカウントやプロジェクトを作ったばかりの時はLambda側の連携の準備ができていないのかもしれない。

僕の場合は何時間かたって再度Retryしてみたらデプロイができた。

ただ、元々成功していたプロジェクトのデプロイが急にできなくなったのであれば、違う原因かもしれないので注意。エラーメッセージを要確認。
```

ということで、原因はよくわからないけど時間が解決したという話があったので、一回Netlify上でアプリを削除して再度デプロイしたら特に問題なくデプロイできるようになりました。

なんだったのか。

## デプロイについて: 2020年9月17追記

久々に自分の記事を見たらデプロイについて特に記載がなかったので追記。

ローカルで作ったプロジェクトをGitHubやGitLabなどにあげて、NetlifyでGitリポジトリと連携させてアプリケーション作成し、pushすればOKです。

Netlifyの管理画面に表示される`https://xxxxxxxxx.netlify.app`というドメインの後ろに`/.netlify/functions/server/webhook`というパスをつけたURLが最終的なURLになります。

デプロイ後はこちらのURLをLINE BOTと紐付けましょう。

> ![](https://i.gyazo.com/e57a61b2c16fee4e64f9ac9a70b77357.png)

## 参考記事

* [Netlify Functions](https://functions.netlify.com/)
* [How to run Express.js apps with Netlify Functions](https://www.netlify.com/blog/2018/09/13/how-to-run-express.js-apps-with-netlify-functions/)