Azure Functionsを用いたサーバーレスなTwitter botを開発してみた（その３、ローカル開発環境でツィートする）

# これまでのお話

Azure FunctionsのTimerTriggerの実行・開発環境を作りました。いよいよコードを書きます。

https://qiita.com/ataraken/items/58ead74b62d30aceb854

# どんなTwitter botを作るのか

開発する場合、まずは全体を設計してから詳細の設計をし、次いで実装するものですが、今回は技術調査もかねて作りながら考えていこうと思います。当方はAzure初心者ですし。Azureのことを理解しつつ全体像を考えることにします。

# まずはツィートしてみる

まずはツィートしてみましょう。そのほうが楽しいでしょうし。

## 認証のための情報を保存する

Twitter APIを使用するための認証情報を保存します。保存する場所はAzure Functionsが提供してくれます。

### デバッグ実行の場合

`local.settings.json`にTwitter APIへの認証情報を保存しましょう。`json`のキーは当方で適当に決めています。

```json
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": <Azure Storage Accountの接続文字列>,
    "FUNCTIONS_WORKER_RUNTIME": "python",
    "TWITTER_ACCESS_TOKEN": <アクセストークン>,
    "TWITTER_TOKEN_SECRET": <アクセストークンのシークレット>,
    "TWITTER_API_KEY": <APIキー>,
    "TWITTER_API_SECRET_KEY": <APIキーのシークレット>
  }
}
```

余談ですが、`.gitignore`で誤ってGitリポジトリにコミットされないように記載があります。

```
# Azure Functions artifacts
bin
obj
appsettings.json
local.settings.json
.python_packages
```

### 本番環境の場合

Azure Portalで該当Azure Functionsを選択します。ナビゲーションバーから`構成`を選択してください。次のような表示がされると思います。

![2021-06-24-000.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1278980/a732f8c4-5c42-1dd0-0b35-4ebc5a801369.png)

`新しいアプリケーション設定`を押下して４つのキーと値を入力してください。その後、`保存`ボタンを押下するとAzure Functionsが再起動して環境変数として扱えるようになります。

## Twitter APIの呼び出しを共通化する

一般的に言って、共通化は必要になったときに共通にしたほうが良いと思います。今回は別途開発していたTwitter APIのHelper関数を持っているのでそれを使いたいというのと、`OAuth`による認証は共通化したくなることが見えているので先にやってしまします。（単純に興味があるというのが本音です。好奇心には勝てない。遊びですしね。笑）

共通化のやり方はこちらのページに書いてあるのでそれに従います。Azure Functionsの流儀に従い共通化は関数で行い、１ファイル１関数にするのがよさそうです。

https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-reference-python

まずはルートに`shared_code`というフォルダを追加します。

`shared_code/ フォルダーには、絶対インポート構文を使用するときに Python パッケージとしてマークするための __init__.py ファイルが含まれている必要があります。`とのことです。日本語がおかしいので意味がよくわからないですが、`__init__.py`ファイルが必要のようです。上記のページに従って次の内容の`__init__.py`ファイルを作りましょう。

```python
# <project_root>/shared_code/__init__.py
# Empty __init__.py file marks shared_code folder as a Python package
```

次にTwitter APIを使用するためのOAuthセッションを生成するヘルパースクリプトを作成しました。先ほどlocal.settings.jsonやAzure Functionsの構成で設定した環境変数から値を読み込んでいるのがポイントです。

```python
# <project_root>/shared_code/twitter_oauth_helper.py

from requests_oauthlib import OAuth1Session
from os import environ

def create_session():
  token = environ['TWITTER_ACCESS_TOKEN']
  token_secret = environ['TWITTER_TOKEN_SECRET']
  consumer_key = environ['TWITTER_API_KEY']
  consumer_secret = environ['TWITTER_API_SECRET_KEY']

  return OAuth1Session(consumer_key, consumer_secret, token, token_secret)

```

`requests_oauthlib`が見つからないと思うので`requirements.txt`に追加します。ライブラリのバージョンは読者の環境に合わせて設定してください。今回は`1.3.0`にしていますが執筆時の最新バージョンです。読者の環境に応じてバージョンをご指定ください。

```
# DO NOT include azure-functions-worker in this file
# The Python Worker is managed by Azure Functions platform
# Manually managing azure-functions-worker may cause unexpected issues

azure-functions
requests_oauthlib==1.3.0
```

次にツィートするヘルパースクリプトを作成します。Twitter APIのドキュメントは公式ページをご覧ください。

```python
# <project_root>/shared_code/twitter_update_helper.py

import json
from shared_code import twitter_oauth_helper

class Param:
  def __init__(self, status):
    self._status= status

  def get_status(self):
    return self._status

def request(param):
  endpoint_url = 'https://api.twitter.com/1.1/statuses/update.json'

  client = twitter_oauth_helper.create_session()

  params = {}
  params['status'] = param.get_status()

  res = client.post(endpoint_url, params=params)

  if res.status_code == 200:
    res = json.loads(res.text)
  else:
    res = json.loads(res.text)

  return res
```

## 共通化した関数を呼び出してみる

`TimerTriggerTweet`関数から共通化した関数を呼び出してみます。

```python
# <project_root>/TimerTriggerTweet/__init__.py

import datetime
import logging

import azure.functions as func

from shared_code import twitter_update_helper

def main(mytimer: func.TimerRequest) -> None:

  utc_timestamp = datetime.datetime.utcnow().replace(
    tzinfo=datetime.timezone.utc).isoformat()

  if mytimer.past_due:
    logging.info('The timer is past due!')

  logging.info('Python timer trigger function ran at %s', utc_timestamp)

  param = twitter_update_helper.Param('ほげ')
  twitter_update_helper.request(param)

  logging.info('Python timer trigger function exit at %s', utc_timestamp)
```

## デバッグ実行したらModuleNotFoundError発生

F5を押下してデバッグ実行したところ、`ModuleNotFoundError`エラーが発生しました。`requests_oauthlib`が見つからないようです。

```powershell
> Executing task: .venv\Scripts\python -m pip install -r requirements.txt <

Requirement already satisfied: azure-functions in simple-twitter-bot\.venv\lib\site-packages (from -r requirements.txt (line 5)) (1.7.0)
Collecting requests-oauthlib
  Downloading requests_oauthlib-1.3.0-py2.py3-none-any.whl (23 kB)
Collecting requests>=2.0.0
  Downloading requests-2.25.1-py2.py3-none-any.whl (61 kB)
     |████████████████████████████████| 61 kB 2.0 MB/s
Collecting oauthlib>=3.0.0
  Downloading oauthlib-3.1.1-py2.py3-none-any.whl (146 kB)
     |████████████████████████████████| 146 kB 6.4 MB/s
Collecting certifi>=2017.4.17
  Downloading certifi-2021.5.30-py2.py3-none-any.whl (145 kB)
     |████████████████████████████████| 145 kB 3.2 MB/s
Collecting idna<3,>=2.5
  Downloading idna-2.10-py2.py3-none-any.whl (58 kB)
     |████████████████████████████████| 58 kB 2.9 MB/s
Collecting chardet<5,>=3.0.2
  Downloading chardet-4.0.0-py2.py3-none-any.whl (178 kB)
     |████████████████████████████████| 178 kB 3.3 MB/s
Collecting urllib3<1.27,>=1.21.1
  Downloading urllib3-1.26.5-py2.py3-none-any.whl (138 kB)
     |████████████████████████████████| 138 kB 3.3 MB/s
Installing collected packages: certifi, idna, chardet, urllib3, requests, oauthlib, requests-oauthlib
Successfully installed certifi-2021.5.30 chardet-4.0.0 idna-2.10 oauthlib-3.1.1 requests-2.25.1 requests-oauthlib-1.3.0 urllib3-1.26.5
WARNING: You are using pip version 20.1.1; however, version 21.1.2 is available.
You should consider upgrading via the 'simple-twitter-bot\.venv\Scripts\python.exe -m pip install --upgrade pip' command.

Terminal will be reused by tasks, press any key to close it.

> Executing task: .venv\Scripts\activate ; func host start <

Found Python version 3.7.9 (python3).

Azure Functions Core Tools
Core Tools Version:       3.0.3477 Commit hash: 5fbb9a76fc00e4168f2cc90d6ff0afe5373afc6d  (64-bit)
Function Runtime Version: 3.0.15584.0


Functions:

        TimerTriggerTweet: timerTrigger

For detailed output, run func with --verbose flag.
[2021-06-24T13:22:07.244Z] Executing 'Functions.TimerTriggerTweet' (Reason='Timer fired at 2021-06-24T22:22:07.2118593+09:00', Id=e80707da-4241-46e3-9fb4-62ab4d927465)
[2021-06-24T13:22:07.247Z] Trigger Details: UnscheduledInvocationReason: IsPastDue, OriginalSchedule: 2021-06-24T22:00:00.0000000+09:00
[2021-06-24T13:22:08.091Z] Worker process started and initialized.
[2021-06-24T13:22:08.454Z] The timer is past due!
[2021-06-24T13:22:08.459Z] Python timer trigger function ran at 2021-06-24T13:22:08.451326+00:00
[2021-06-24T13:22:08.533Z] Executed 'Functions.TimerTriggerTweet' (Failed, Id=e80707da-4241-46e3-9fb4-62ab4d927465, Duration=1308ms)
[2021-06-24T13:22:08.535Z] System.Private.CoreLib: Exception while executing function: Functions.TimerTriggerTweet. System.Private.CoreLib: Result: Failure
Exception: ModuleNotFoundError: No module named 'requests_oauthlib'
Stack:   File "C:\Program Files\Microsoft\Azure Functions Core Tools\workers\python\3.7/WINDOWS/X64\azure_functions_worker\dispatcher.py", line 402, in _handle__invocation_request
    invocation_id, fi_context, fi.func, args)
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.7_3.7.2544.0_x64__qbz5n2kfra8p0\lib\concurrent\futures\thread.py", line 57, in run
    result = self.fn(*self.args, **self.kwargs)
  File "C:\Program Files\Microsoft\Azure Functions Core Tools\workers\python\3.7/WINDOWS/X64\azure_functions_worker\dispatcher.py", line 604, in _run_sync_func
    func)(params)
  File "C:\Program Files\Microsoft\Azure Functions Core Tools\workers\python\3.7/WINDOWS/X64\azure_functions_worker\extension.py", line 215, in _raw_invocation_wrapper        
    result = function(**args)
  File "simple-twitter-bot\TimerTriggerTweet\__init__.py", line 19, in main
    twitter_update_helper.request(param)
  File "simple-twitter-bot\shared_code\twitter_update_helper.py", line 16, in request
    client = twitter_oauth_helper.create_session()
  File "simple-twitter-bot\shared_code\twitter_oauth_helper.py", line 5, in create_session
    from requests_oauthlib import OAuth1Session
```

Microsoftがトラブル・シューティングのページを用意してくれていました。しかしながら、今回の問題の解決には至りませんでした。

https://docs.microsoft.com/ja-jp/azure/azure-functions/recover-python-functions?tabs=vscode#troubleshoot-modulenotfounderror

### 問題の切り分け、venv環境で実行

Power Shellからvenv環境を立ち上げ、Pythonインタプリタから`requests_oauthlib`をインクルードしてオブジェクトを作ったところ、生成できました。venv環境ではライブラリ`requests_oauthlib`は見えているようです。

```powershell
PS simple-twitter-bot> .\.venv\Scripts\Activate.ps1
(.venv) PS simple-twitter-bot> python
Python 3.7.9 (tags/v3.7.9:13c94747c7, Aug 17 2020, 16:30:00) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import requests_oauthlib
>>> requests_oauthlib.OAuth1Session('', '', '', '')
<requests_oauthlib.oauth1_session.OAuth1Session object at 0x0000020F7D41A248>
>>>
```

### 問題の切り分け、Power ShellからAzure Core Toolsを直接実行

Power Shellからvenvを起動したのちに、`func host start`コマンドでAzure Functions Core Toolsを直接たたいてみたところ同じエラーが発生しました。Azure Core Toolsが `pip` でインストールしたライブラリを見つけられていないようです。

```powershell
PS simple-twitter-bot> .\.venv\Scripts\Activate.ps1
(.venv) PS simple-twitter-bot> func host start
Found Python version 3.7.9 (python3).

Azure Functions Core Tools
Core Tools Version:       3.0.3477 Commit hash: 5fbb9a76fc00e4168f2cc90d6ff0afe5373afc6d  (64-bit)
Function Runtime Version: 3.0.15584.0


Functions:

        TimerTriggerTweet: timerTrigger

For detailed output, run func with --verbose flag.
[2021-06-27T15:49:36.349Z] Worker process started and initialized.
[2021-06-27T15:49:41.812Z] Host lock lease acquired by instance ID '000000000000000000000000919C9F3E'.
[2021-06-27T16:00:00.039Z] Executing 'Functions.TimerTriggerTweet' (Reason='Timer fired at 2021-06-28T01:00:00.0200726+09:00', Id=5b23d7b2-8578-422c-9a76-39369e14c7bc)
[2021-06-27T16:00:00.071Z] Python timer trigger function ran at 2021-06-27T16:00:00.070155+00:00
[2021-06-27T16:00:00.129Z] Executed 'Functions.TimerTriggerTweet' (Failed, Id=5b23d7b2-8578-422c-9a76-39369e14c7bc, Duration=99ms)
[2021-06-27T16:00:00.132Z] System.Private.CoreLib: Exception while executing function: Functions.TimerTriggerTweet. System.Private.CoreLib: Result: Failure
Exception: ModuleNotFoundError: No module named 'requests_oauthlib'
Stack:   File "C:\Program Files\Microsoft\Azure Functions Core Tools\workers\python\3.7\WINDOWS\X64\azure_functions_worker\dispatcher.py", line 402, in _handle__invocation_request
    invocation_id, fi_context, fi.func, args)
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.7_3.7.2544.0_x64__qbz5n2kfra8p0\lib\concurrent\futures\thread.py", line 57, in run
    result = self.fn(*self.args, **self.kwargs)
  File "C:\Program Files\Microsoft\Azure Functions Core Tools\workers\python\3.7\WINDOWS\X64\azure_functions_worker\dispatcher.py", line 604, in _run_sync_func
    func)(params)
  File "C:\Program Files\Microsoft\Azure Functions Core Tools\workers\python\3.7\WINDOWS\X64\azure_functions_worker\extension.py", line 215, in _raw_invocation_wrapper
    result = function(**args)
  File "simple-twitter-bot\TimerTriggerTweet\__init__.py", line 19, in main
    twitter_update_helper.request(param)
  File "simple-twitter-bot\shared_code\twitter_update_helper.py", line 16, in request
    client = twitter_oauth_helper.create_session()
  File "simple-twitter-bot\shared_code\twitter_oauth_helper.py", line 5, in create_session
    from requests_oauthlib import OAuth1Session
.
```

Azure Functions Core Toolsの使い方はこちらをご覧ください。

https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-run-local?tabs=windows%2Ccsharp%2Ccmd

エラーをよく見ると `.venv` のPythonではなく、当方の開発PCにインストールしているPythonを参照しているように見えます。 `.venv` 環境には `requests_oauthlib` をインストールしていないので見つからないのは当然ですが、 `.venv` 環境を作成して `Activate.ps1` スクリプトで仮想環境を有効にしているのになぜにホストPCの環境を参照するのかはよくわかりません。

https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-reference-python?tabs=azurecli-linux%2Capplication-level#package-management

公式の開発者ガイドの `パッケージの管理` には `Azure Functions Core Tools または Visual Studio Code を使用してローカルで開発を行う場合は、必要なパッケージの名前とバージョンを requirements.txt ファイルに追加し、pip を使用してそれらをインストールしてください。` と記載があるのでそういうもののようです。

次のコマンドで必要なライブラリをインストールします。

```powershell
PS simple-twitter-bot> pip install -r .\requirements.txt
Collecting azure-functions==1.7.0
  Using cached azure_functions-1.7.0-py3-none-any.whl (135 kB)
Collecting requests-oauthlib==1.3.0
  Using cached requests_oauthlib-1.3.0-py2.py3-none-any.whl (23 kB)
Collecting requests>=2.0.0
  Using cached requests-2.27.0-py2.py3-none-any.whl (63 kB)
Collecting oauthlib>=3.0.0
  Using cached oauthlib-3.1.1-py2.py3-none-any.whl (146 kB)
Collecting charset-normalizer~=2.0.0
  Using cached charset_normalizer-2.0.10-py3-none-any.whl (39 kB)
Collecting certifi>=2017.4.17
  Downloading certifi-2021.10.8-py2.py3-none-any.whl (149 kB)
     |████████████████████████████████| 149 kB 3.3 MB/s
Collecting idna<4,>=2.5
  Downloading idna-3.3-py3-none-any.whl (61 kB)
     |████████████████████████████████| 61 kB 4.0 MB/s
Collecting urllib3<1.27,>=1.21.1
  Downloading urllib3-1.26.7-py2.py3-none-any.whl (138 kB)
     |████████████████████████████████| 138 kB ...
Installing collected packages: urllib3, idna, charset-normalizer, certifi, requests, oauthlib, requests-oauthlib, azure-functions
  WARNING: The script normalizer.exe is installed in 'C:\Users\XXX\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.7_qbz5n2kfra8p0\LocalCache\local-packages\Python37\Scripts' which is not on PATH.
  Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.
Successfully installed azure-functions-1.7.0 certifi-2021.10.8 charset-normalizer-2.0.10 idna-3.3 oauthlib-3.1.1 requests-2.27.0 requests-oauthlib-1.3.0 urllib3-1.26.7
WARNING: You are using pip version 21.1.2; however, version 21.3.1 is available.
You should consider upgrading via the 'C:\Users\XXX\AppData\Local\Microsoft\WindowsApps\PythonSoftwareFoundation.Python.3.7_qbz5n2kfra8p0\python.exe -m pip install --upgrade pip' command.
PS simple-twitter-bot>
```

```powershell
> Executing task: .venv\Scripts\python -m pip install -r requirements.txt <

Requirement already satisfied: azure-functions==1.7.0 in simple-twitter-bot\.venv\lib\site-packages (from -r requirements.txt (line 5)) (1.7.0)
Requirement already satisfied: requests-oauthlib==1.3.0 in simple-twitter-bot\.venv\lib\site-packages (from -r requirements.txt (line 6)) (1.3.0)
Requirement already satisfied: requests>=2.0.0 in simple-twitter-bot\.venv\lib\site-packages (from requests-oauthlib==1.3.0->-r requirements.txt (line 6)) (2.27.0)
Requirement already satisfied: oauthlib>=3.0.0 in simple-twitter-bot\.venv\lib\site-packages (from requests-oauthlib==1.3.0->-r requirements.txt (line 6)) (3.1.1)
Requirement already satisfied: urllib3<1.27,>=1.21.1 in simple-twitter-bot\.venv\lib\site-packages (from requests>=2.0.0->requests-oauthlib==1.3.0->-r requirements.txt (line 6)) (1.26.5)
Requirement already satisfied: charset-normalizer~=2.0.0; python_version >= "3" in simple-twitter-bot\.venv\lib\site-packages (from requests>=2.0.0->requests-oauthlib==1.3.0->-r requirements.txt (line 6)) (2.0.10)
Requirement already satisfied: idna<4,>=2.5; python_version >= "3" in simple-twitter-bot\.venv\lib\site-packages (from requests>=2.0.0->requests-oauthlib==1.3.0->-r requirements.txt (line 6)) (2.10)
Requirement already satisfied: certifi>=2017.4.17 in simple-twitter-bot\.venv\lib\site-packages (from requests>=2.0.0->requests-oauthlib==1.3.0->-r requirements.txt (line 6)) (2021.5.30)
WARNING: You are using pip version 20.1.1; however, version 21.3.1 is available.
You should consider upgrading via the 'simple-twitter-bot\.venv\Scripts\python.exe -m pip install --upgrade pip' command.

Terminal will be reused by tasks, press any key to close it.

> Executing task: .venv\Scripts\activate ; func host start <

Found Python version 3.7.9 (python3).

Azure Functions Core Tools
Core Tools Version:       4.0.3971 Commit hash: d0775d487c93ebd49e9c1166d5c3c01f3c76eaaf  (64-bit)
Function Runtime Version: 4.0.1.16815


Functions:

        TimerTriggerTweet: timerTrigger

For detailed output, run func with --verbose flag.
[2022-01-05T12:44:22.529Z] Executing 'Functions.TimerTriggerTweet' (Reason='Timer fired at 2022-01-05T21:44:22.4760677+09:00', Id=57b2b437-f64b-4e81-843f-a6657456c2d5)
[2022-01-05T12:44:22.534Z] Trigger Details: UnscheduledInvocationReason: RunOnStartup
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/2 POST http://127.0.0.1:56818/AzureFunctionsRpcMessages.FunctionRpc/EventStream application/grpc -
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint 'gRPC - /AzureFunctionsRpcMessages.FunctionRpc/EventStream'
[2022-01-05T12:44:23.825Z] Worker process started and initialized.
[2022-01-05T12:44:24.526Z] Python timer trigger function ran at 2022-01-05T12:44:24.497660+00:00
[2022-01-05T12:44:24.830Z] Python timer trigger function exit at 2022-01-05T12:44:24.497660+00:00
[2022-01-05T12:44:24.880Z] Executed 'Functions.TimerTriggerTweet' (Succeeded, Id=57b2b437-f64b-4e81-843f-a6657456c2d5, Duration=2381ms)
[2022-01-05T12:44:26.706Z] Host lock lease acquired by instance ID '000000000000000000000000919C9F3E'.
```

無事ツィートが成功しました。

しかし、実行ログを見ると冒頭 `.venv` 環境で `requirements.txt`のライブラリをインストールしているように見えますが、あれはいったい何なのでしょうか？開設できる方がいらっしゃいましたらぜひともご教授ください。

# まとめ

かなり納得がいかないですが、何とかローカル環境でツィートできるようになりました。次回はいよいよAzure Functionsにデプロイしてみようと思います。

# おまけ

## .python_packagesの作り方

`.gitignore`ファイルに`.python_packages`の記載があったので、`.python_packages`を作ればうまくいくかと思い試しましたが、ダメでした。忘備録もかねてどんなことをしたかを残します。こんな感じのコマンドで作成できるようです。

```powershell
PS simple-twitter-bot> pip install --target=".python_packages/lib/site-packages"  -r requirements.txt
ERROR: Can not combine '--user' and '--target'

PS C:\Users\Yusuk\Development\simple-twitter-bot> pip install --target=".python_packages/lib/site-packages"  -r requirements.txt --no-user
```

## TimerTriggerのデバッグTips

タイマートリガーはその時間にならないと動作しません。TimerTriggerの関数が実装されているフォルダに保存されている `function.json` に次のキーと値を記述すると起動直後に動作してくれます。誤ってコミットしないようにご注意ください。

```json
      "runOnStartup": true
```