FastAPIでLINEBotを作成しTerraformでGCPのCloud Runに爆速デプロイする

# 概要

最近、ハッカソンなどでLINEBotの作成することが増えてきました。

そこで、開発を円滑に進めるために、Pythonで作成したLINE BotのサーバをGoogle Cloud Platform (GCP) にTerraformを使用してデプロイするテンプレートを作成しました。

https://github.com/sey323/python-gcp-linebot

この記事では、Pythonで作成した「おうむ返し」機能を持つLINEBotをGCPのCloud Runにデプロイし、LINE Botが実際に動作するまでの手順を説明します。

さらに、GithubとGCPを連携させてCI/CD環境を構築します。これにより、Githubの`main`ブランチが更新されると、Cloud Runに自動で最新版がデプロイされるようなCloud Build Triggerを設定します。

本手順実行後の最終的な構成は以下の通りです。

![スクリーンショット 2024-02-12 22.08.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163680/52355704-df8b-6c91-7a4f-03eaf6a1fef6.png)

## この記事で実施すること

- Fast APIを使用してLINE Botを構築
- Terraformを使用してCloud Runにデプロイ

# 事前準備

本記事は、以下のリポジトリをベースに構築していく。

https://github.com/sey323/python-gcp-linebot

ローカルにリポジトリをクローンしておく。

## 開発用のLINEBotの作成とトークンの取得

開発用のLINEBotを作成するために、まずはLINE Developerコンソールにログインする。

https://developers.line.biz/ja/

ログイン後、下記のドキュメントに従い「チャンネルアクセストークンを発行する」まで進める。

https://developers.line.biz/ja/docs/messaging-api/getting-started/

チャンネルアクセストークンは「長期のチャンネルアクセストークン」を選択し発行する。

https://developers.line.biz/ja/docs/basics/channel-access-token/#why-use-channel-access-token

発行した２つのトークンは、後の工程で利用するので控えておく。
- チャネルシークレット 
- チャネルアクセストークン（長期）

### デフォルトの「あいさつメッセージ」と「応答メッセージ」を無効

デフォルトの場合、友達追加時の「挨拶メッセージ」と、メッセージを入力時の「応答メッセージ」が有効になっているため、これらを無効にする

「チャンネルアクセストークン」を取得した画面と同じ画面にある「LINE公式アカウント機能」の「編集」ボタンから「応答設定」画面に遷移し、これら2つの機能を無効化する。

![スクリーンショット 2024-02-12 20.42.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163680/32e98b84-8905-e6d2-eed2-8ce161cc07c8.png)

以下の状態になっていれば、無効化が完了している。「Webhook」は今回作成するLINEBotが利用するtあめ、有効にしておく必要がある。

![スクリーンショット 2024-02-12 20.42.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163680/37d0dba9-c89f-31e4-882a-15cecf646f23.png)

## Githubのシークレットトークンの取得

CI/CDの設定のためにGithubのトークンが必要なため、以下のリンクからトークンを取得する。

https://github.com/settings/tokens

## `.env`の作成

クローンしたソースコードの`.env.sample`を参考に、`.env`ファイルを作成する。先ほど取得したLINEとGithubのシークレットを、`.env`の各環境変数に設定していく。

```shell:.env
# 下記のURLにアクセスし、GITHUBのトークンを取得してください
# https://github.com/settings/tokens
GITHUB_TOKEN=

# 下記のURLにアクセスし、Messaging APIの利用を開始してください
# https://developers.line.biz/console/
LINE_CHANNEL_SECRET=
LINE_CHANNEL_ACCESS_TOKEN=
```


# 実装

Fast APIサーバのローカルの動作確認から、Cloud Runへのデプロイまでを実施する。

## Fast APIでLINE Botサーバの構築

まず、poetryを利用して仮想環境と必要なライブラリをインストールする。


```sh:
poetry install
```

`poetry`がインストールされていない場合は、以下のリンクを参考にインストールする。

https://qiita.com/ksato9700/items/b893cf1db83605898d8a

次に、環境変数を更新する。

```sh:
source .env
```

そして、Fast APIサーバを起動する。

```sh:
poetry run uvicorn app.main:app --host 0.0.0.0 --port 8080
```

以下のURLにアクアセスしSwaggerのAPIドキュメントが表示されれば、正常に起動できている。

- http://0.0.0.0:8080/docs

`app/routes/line_bot_callback_route.py`にはおうむ返しの処理が記述してある。自分が作りたい機能がある場合は、このファイルを修正する。

```python:app/routes/line_bot_callback_route.py
from fastapi import APIRouter, BackgroundTasks, Header, Request
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import MessageEvent, TextMessage
from starlette.exceptions import HTTPException

from app import config


line_bot_callback_router = APIRouter(prefix="", tags=["line_bot"])
line_bot_api = LineBotApi(config.LINE_CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(config.LINE_CHANNEL_SECRET)


@line_bot_callback_router.post("/callback")
async def callback(
    request: Request,
    background_tasks: BackgroundTasks,
    x_line_signature=Header(None),
):
    """Line Bot用のエンドポイント"""
    body = await request.body()

    try:
        background_tasks.add_task(
            handler.handle, body.decode("utf-8"), x_line_signature
        )
    except InvalidSignatureError:
        raise HTTPException(status_code=400, detail="Invalid signature")

    return "ok"


@handler.add(MessageEvent)
def handle_message(event):
    if event.type != "message" or event.message.type != "text":
        return
    # 受け取ったメッセージをそのまま返す
    # 処理をカスタムする場合は、ここを編集する。
    message = TextMessage(text=event.message.text)
    line_bot_api.reply_message(event.reply_token, message)
```


## GCP にデプロイ

Fast APIで作成したおうむ返しするLINE BotをGCPに構築する。利用するTerraformはリポジトリの`terraform`以下にある。

### 必要パッケージのインストール

Terraformとその前処理を実行するために、`terraform`(今回は`tfenv`)と`google-cloud-sdk`をインストールする。

```sh:
brew install tfenv
brew install --cask google-cloud-sdk
```

`tfenv`は複数のバージョンのTerraformを利用するときに、Terraformのバージョン管理を簡単に行ってくれるツール。

https://qiita.com/kamatama_41/items/ba59a070d8389aab7694

https://github.com/tfutils/tfenv

### GCPの設定

まず、GCP上に新規でプロジェクトを作成し、プロジェクトIDを取得する。その後、作成したプロジェクトに課金アカウントの紐付けを行う。

課金アカウントの紐付けは、公式ドキュメントを参照。

https://cloud.google.com/billing/docs/how-to/modify-project?hl=ja#how-to-enable-billing

### Terraform実行アカウントをGCPに作成する

プロジェクトの作成と課金アカウントの紐付けが完了したら、Terraformの実行アカウントとサービスアカウントのキーを発行する。

```sh:
cd terraform
bash - ./init.sh ${プロジェクトID}
```

実行後、下記のような`OAuth`ログイン画面が表示される。GCPのプロジェクトIDと紐づいているアカウントを選択し、ログインを行う。

![スクリーンショット 2023-05-21 11.06.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163680/c0595d3e-6ef3-4569-bd37-dd0b823f25d8.png)

`OAuth`ログインが完了しコマンド実行後、`terraform/key`の配下に`terraform-key.json`が作成される。

このキーがTerraformを実行する際の認証キーとなる。

<details><summary>`init.sh`の詳細はこちら</summary>

`init.sh`はterraformのデプロイユーザを作成し、その認証キーを発行しローカルに保存するスクリプト。

```bash:terraform/init.sh
PROJECT_ID=$1

gcloud auth application-default login --disable-quota-project

echo ${PROJECT_ID}
gcloud config set project ${PROJECT_ID}
gcloud iam service-accounts create terraform-account \
  --display-name "Used by Terraform on the local machine"
gcloud projects add-iam-policy-binding ${PROJECT_ID} \
  --member serviceAccount:terraform-account@${PROJECT_ID}.iam.gserviceaccount.com \
  --role roles/admin

# Save Service Account json File
gcloud iam service-accounts keys create key/terraform-key.json \
  --iam-account terraform-account@${PROJECT_ID}.iam.gserviceaccount.com

```

</details>


### TerraformでGCPのデプロイ

Terraformでデプロイされるサービスは以下の通り。

| サービス名 | 名称 | 概要 | 
| -- | - | - |
| Cloud Run | `${Gitのリポジトリ名}` | FastAPIサーバがデプロイされるサーバ |
| Cloud Build Trigger | `${Gitのリポジトリ名}-trigger` | Githubのmainブランチの更新を検知し、Build/Cloud Runへのデプロイを行う | 
| Artifact Registry | `${Gitのリポジトリ名}/${Gitのリポジトリ名}` | Cloud Runで起動するアプリケーションのDockerイメージ | 
| Secret Manager | `LINE_CHANNEL_SECRET`,`LINE_CHANNEL_ACCESS_TOKEN` | LINEBotで利用する`LINE_CHANNEL_SECRET`,`LINE_CHANNEL_ACCESS_TOKEN`。認証情報なので環境変数ではなく、Secret Managerとして保存する |


初回実行時はTerraformの初期化処理を行う

```sh:
terraform init
```

その後、リソースをデプロイする。実行後プロジェクトIdの入力が必要になるため、前の手順で取得したプロジェクトIDを入力する

```sh:
$ terraform apply
var.project_id
  gcp project id

  Enter a value: ${プロジェクトID}
```

10分以上必要なので、待機しておく。

実行完了後Cloud Runのコンソールに遷移し、サーバが起動していることを確認する。コンソールの詳細ページからURLを取得し、URLの末尾に`/docs`を付与してSwaggerにアクセスする

![スクリーンショット 2024-02-12 19.00.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163680/111f13e4-e416-4e0b-0157-7e56df2d37c2.png)

ローカルで確認した時と同様に、Swaggerの画面が表示されたら、サーバの起動はOK

![スクリーンショット 2024-02-12 20.12.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163680/83469723-62c4-02b7-738e-1de6fc6e20e2.png)

## LINEのWebhook URL設定

LINE Developerコンソールに戻り、「事前準備」で作成したLINEBotのWebhook URLに、Cloud Runのエンドポイントを設定する。

エンドポイントは、Cloud Runに表示されていたURLに`/callback`を付与したものとなる。

![スクリーンショット 2024-02-12 20.19.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163680/f5166fa2-b37e-d27a-08bc-17c947019c8a.png)

設定が完了したら、「検証」ボタンをクリックして接続を確認する。「成功」が表示されれば、LINE BotとCloud RunのAPIは正しく接続されている。

![スクリーンショット 2024-02-12 20.19.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163680/2023ed3e-4c34-7e35-67e9-08d3701ad6ea.png)

# 動作確認

LINE DeveloperコンソールのQRコードをスキャンして友達追加する。その後、トーク画面で任意のメッセージを送信する。

![スクリーンショット 2024-02-12 20.46.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163680/cead3abd-c1fb-dce9-2055-80fa504e8287.png)

送信したメッセージと同じ内容が返ってきたら、正常にLINE Botが構築されている。

# おわりに

利用したソースコードは以下の Github で公開しています

https://github.com/sey323/python-gcp-linebot