【Lambda】めんどくさいGitHubのIssue作成はBotにやらせよう！

## はじめに
Mizushima Keita([@0xb5951](https://twitter.com/0xb5951))です。
Slackからメンションを飛ばすとGitHubのIssueを作成してくれるBotを作成したので、経緯を含めて記事にしたいと思います。

具体的な導入方法はGitHubに書いておいたので、ここでは設計や導入の背景について書いていきます。

## なにができるのか
KPTや週報など定期的に作成するIssueの情報をDynamoDBに保存しておき、SlackからBotにメンションを飛ばすことでGitHubのIssueを作成させることができます。

![create_issue.gif](https://qiita-image-store.s3.amazonaws.com/0/285875/d7a4fa25-7f9d-7065-a5eb-1c45e4c956e1.gif)


作成したIssueのURLを返答してくれるのが地味に便利だったりします。

**構成の全体像**
![Create_github_issue.png](https://qiita-image-store.s3.amazonaws.com/0/285875/f01b7d75-2ce6-924a-bd56-7eb80e500a95.png)


具体的な導入方法と使い方はGitHubのほうに書きました。

**GitHubリポジトリ**
https://github.com/odrum428/create_github_issue_from_lambda

## 使用技術
- Python 3.6
- AWS Lambda
- AWS API GateWay
- AWS DynamoDB
- Slack API
- GitHub API

## 背景
定期的にGitHubのIssueを作成する必要があり、毎回毎回手動で作成するのは面倒なのでBotに作成させるために実装しました。

そもそも手動で作成は、度たび作成し忘れることがあったり、そもそもIssueを作成するのが面倒という問題があるので、定期的に作成するIssueを自動化する需要はそこそこあるのではないかと思います。

## 設計
今回の実装で、役に立ちそうな部分だけを書き出しておきます。

### Lambda
**メリット**
Lambdaは月100万回の関数呼び出し無料なので、コストをかけず、気軽に関数を実装できるのが良さ。

しかも、Serverlessなので面倒な設定やサーバー管理などを考えなくてもいいので、関数の設計だけを行えばいいという良さもあります。

個人的には一日に一回実行するだけ等、頻繁に実装するわけではなく、そこまで負荷のかからない処理ならLambdaを使って実装するのがベストなのではないかなーと思います（何か他にこんなのいいよ的なものがあれば教えてください）。

**デメリットとその対処**
また、Lambdaは正常に動作した場合でも2回以上呼び出されることがあるという問題があり、これに対応する必要がある。Issueの作成に関しては冪等性が担保されないので、何らかの対応をとる必要があった。

フラグで管理する方法なども考えたが、今回は同時実行数を1にすることで対応した。方法としてはあまりイケてないと思うが、同時にIssueを作成するタイミングはあまりないと思ったので、この実装にした。

### DynamoDB
DynamoDBの導入が少し悩んだ部分。
Issueを作成するのに必要な情報は以下の情報になる。

- Issueのtitle
- 作成対象のowner
- 作成対象のRepositoty
- Issueの内容


これを毎回毎回打ち込むのは面倒なので、どこかに保存させておきたい欲があった。

最初の方は思い付きでLambdaの中にテキストファイルを用意してそこに保存させるなどという奇行に走っていた。

そもそもLambdaはステートレスだし、テキストファイルでデータを管理するなど今思えば自分でもなんでこんな訳の分からない実装をしたのかと思う。

最終的にはDynamoDBにIssueを作成するのに必要な情報を保存して、作成の際に呼び出す設計にした。
その際、PrimaryKeyをIssueのタイトル、SortedKeyを作成先のOwnerにすることでタイトルが重複した場合でも大丈夫な設計にしてある。

もう少しいい設計があれば取り入れて改良したい。

### GitHub API
GitHubのAPIを使ってIssueを作成させることができます。
Documentは[ここで確認することができます](https://developer.github.com/v3/)。


Issueを作成するには以下のURLにPOSTすればOK.
`"https://api.github.com/repos/[owner]/[repo]/issues`

Ownerは作成したい先のユーザ名、repoは作成したい先のリポジトリ名を入力する。

この時GitHubのPersonal Access Tokenを渡す必要がある。これを考慮したURLが以下。
`https://api.github.com/repos/[owner]/[repo]/issues?access_token=[Access token]`

作成したIssueのURLを取得させ、Botが教えてくれた方が便利なので実装する。
IssueのURLはResponseに入っているので（GitHubに感謝）それを使用した。

```
    ret = requests.post(post_url, params)
    ret_json = ret.json()

    if ret.status_code == 201:
        msg = "issueを作成したよ！\n作成先のURL : " + ret_json['html_url']
```

### Slack
[Slack API](https://api.slack.com/apps/)を使用して、トリガーとなるイベントを設定しました。

Webhookを利用して実装する方法もありますが、この方式だとトリガーイベントの設計などがやりやすいのでAPIでの実装を採用しています（言っちゃえば個人的にAPIの方が全体像がすっきりする感じがして好きという理由が大きいです）。

Botを追加して、Eventを設計してワークスペースに追加するだけで追加するだけで作成できるのでいつも使ってます。

トリガーイベントは`app_mention`のみを設定しました。
初めはIssueの作成部屋を作成し、特定のワードがあったときには起動する設計にしていたのですが、Lambdaの起動が多くなる点や意図しないタイミングでの起動を考慮し、ボットへのメンションをトリガーにした。


またEventの設定時にRequest URLの認証でエラーがでることがあります。原因は対象のURLに対してPOSTされるChallengeパラメータがかえってきていないことが考えられます。 

Request URLを登録するときにURLの動作確認として、以下のパラメータがURLに対してPOSTされます。

```
POST
"body": {
	 "type": "url_verification",
	 "token": "XXXXXXXXXXXXXXXXXXXXXXXXXXX",
	 "challenge": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXxx"
}
```
Slack APIは認証時にchallengeパラメータが正しくかえってきているかの確認を行っており、challengeをそのまま返す必要がある。 もし、このエラーが出た場合はindex内にreturn eventを追記してやるとVerifyになると思う。

## まとめ
今回の実装は全体で8時間ほどでした、夜のちょこちょこ設計にしては、そこそこのペースで開発できたと思います。

まだまだ改良する部分が多いと思うので、使用していく中で改良していきたいと思います。

興味をもって頂いたら、使ってみてもらえると嬉しいです。
https://github.com/odrum428/create_github_issue_from_lambda

ご質問等あれば、気軽にコメントしていただけると嬉しいです！


## 参考
- [Botkitを利用してSlack上の発言からGitHubのIssueを作成するBotを作る](https://qiita.com/potato4d/items/81e9e8aef6cd57c234af)
- [【AWS】Lambdaでpipしたいと思ったときにすべきこと](https://qiita.com/Hironsan/items/0eb5578f3321c72637b4)
- [Boto3 から DynamoDB の各種操作メモ（テーブル一覧取得、データ追加、データ取得、データ更新）](https://qiita.com/inokappa/items/e38445881311211c87e7)
- [GitHub Developer](https://developer.github.com/v3/)
- [GitHub「Personal access tokens」の設定方法](https://qiita.com/kz800/items/497ec70bff3e555dacd0)