AWS Lambdaを使ってBacklogに繰り返しタスク登録

私のチームではそれぞれのメンバーが複数プロジェクトに関わっていますが、ヌーラボ社のBacklogにあらゆるタスクを登録して共有することで、タスクやり忘れを防止したり、稼働状況を見える化できるようにしています。 
中には定期的に繰り返して行うタスクもありますが、Backlogには繰り返し登録機能がないのでいつも手動登録するのが面倒だなって思っていました。
※例） 定期セキュリティチェック、パッチ適用、交通費精算、契約更新、定期レポート作成など

少しでも効率化を図りたかったのでLambdaで自動化を試してみました。

大まかな概要と流れはこんな感じです。

1. 定期的に発生する種類のタスクをエクセル管理表に書き出す
2. エクセル管理表を S3 にアップロード
3. １日１回 Lambdaがエクセルを読み取り、Backlog API経由でその日のタスクを登録

エクセル管理表があるならわざわざ Backlog 登録する意味あるのか？と思われる方もいると思いますが、Backlogで管理していれば毎回の作業履歴も残るし、スポットタスクも含めてBacklogで一元管理することができるので意味はあると思ってます。

<details open><summary>Lambdaサンプルコード（python）</summary>

```py
import os
import json
import boto3
from botocore.exceptions import ClientError
import sys
import ast
import requests
import openpyxl
from io import BytesIO
from datetime import datetime, timedelta, date, timezone
from dateutil.relativedelta import relativedelta

REGION_NAME = "<リージョン名>"
BUCKET_NAME = '<バケット名>'
INPUT_PREFIX = 'input'
INPUT_EXCEL_NAME = 'Backlog_Issue_list.xlsx'
INPUT_OBJECT_KEY_NAME = f'{INPUT_PREFIX}/{INPUT_EXCEL_NAME}'
INPUT_EXCEL_SHEETNAME = 'LIST'

BACKLOG_PRJ_ID="<Backlog プロジェクト ID>"
# 種別＝ルーチンタスク
BACKLOG_ISSUETYPE_ROUTINE="<ルーチン作業のBacklog種別 ID >"
# 種別＝スポットタスク
BACKLOG_ISSUETYPE_TASK="< スポットタスクのBacklog 種別 ID >"
# シークレットネーム
SECRET_NAME = "Backlog"
# BacklogApiのシークレットキーネーム
SECRET_KEY_NAME = "BacklogApiKey"

s3_client = boto3.client('s3')

def lambda_handler(event, context):

    try:
        t_delta = timedelta(hours=9)
        JST = timezone(t_delta, 'JST')
        TODAY = datetime.now(JST).strftime("%Y-%m-%d")
        DayOfWeek = datetime.now(JST).strftime('%a')
        PREFIX_SUMMARY = datetime.now(JST).strftime("[%m-%d]-")
    except Exception as e:
        print(f"Error: {e}")


    # Excelデータ取得
    try:
        s3_resp = s3_client.get_object(Bucket=BUCKET_NAME, Key=INPUT_OBJECT_KEY_NAME)
        # エクセルに入力制限があると対応していないのでWARNINGが出るが無視する。
        openpyxl.reader.excel.warnings.simplefilter('ignore')
        wb = openpyxl.load_workbook(BytesIO(s3_resp['Body'].read()),data_only=True)
        main_ws = wb[INPUT_EXCEL_SHEETNAME]
    except Exception as e:
        # エラー
        print("ERROR エクセルファイルが読み込めません")
        sys.exit()
    else:
        # BacklogのAPIキーをシークレットマネージャから読み込む
        ApiKey = get_secret(SECRET_NAME, REGION_NAME)

        #エクセルファイルを読み込む
        row_num=0
        for row in main_ws.rows:
            
            row_num +=1
            row_dic = {}

            # 1 行目の処理(ヘッダーセルとして格納)
            if row[0].row == 1:
                print('1行目はヘッダ行')
                header_cells = row

            # 2行目以降の処理
            else:
                # セルの値を「key-value」で登録
                for k, v in zip(header_cells, row):
                    row_dic[k.value] = v.value

                print("%s 行目" % row[0].row )

                # interval, NumOfDays_UntilDeadlineが未入力の行はスキップする                
                if row_dic["interval"] == None: continue
                if row_dic["NumOfDays_UntilDeadline"] == None: continue

                # 期限日を計算する
                NumOfDays_UntilDeadline = int(row_dic['NumOfDays_UntilDeadline'])
                dueDate = date.today() + relativedelta(days=NumOfDays_UntilDeadline)

                projectId = BACKLOG_PRJ_ID
                summary = PREFIX_SUMMARY + row_dic['summary']
                priorityId = "2"
                assigneeId = row_dic['assigneeId']
                description = row_dic['description']

                #### 毎月のルーチンタスク ####
                if row_dic['interval'] == "MONTHLY":
                    issueTypeId = BACKLOG_ISSUETYPE_ROUTINE
                    addDate = date.today().replace(day=int(row_dic['addTiming']))
                    # 条件が一致する時だけタスク登録する。
                    if str(addDate) != str(TODAY):
                        continue
                    else:
                        print("MONTHLY: 日付条件一致するので追加 %s" % (addDate))
                        print("期限は %s" % (dueDate))
                        print("%s %s" % (issueTypeId, summary))

                        backlog_issue_add(ApiKey, BACKLOG_PRJ_ID, summary, issueTypeId, priorityId, assigneeId, description, dueDate )

                #### 毎週のルーチンタスク ####
                elif row_dic['interval'] == "WEEKLY":
                    issueTypeId = BACKLOG_ISSUETYPE_ROUTINE

                    # 条件が一致する時だけタスク登録する。
                    if str(row_dic['addTiming']) != str(DayOfWeek):
                        continue
                    else:
                        print("WEEKLY: 日付条件一致するので追加 %s" % (DayOfWeek))
                        print("期限は %s" % (dueDate))
                        print("%s %s" % (issueTypeId, summary))

                        backlog_issue_add(ApiKey, BACKLOG_PRJ_ID, summary, issueTypeId, priorityId, assigneeId, description, dueDate )

                #### スポットのタスク ####
                elif row_dic['interval'] == "SPOT":
                    issueTypeId = BACKLOG_ISSUETYPE_TASK
                    # 条件が一致する時だけタスク登録する。
                    if str(row_dic['addTiming']) != str(TODAY):
                        continue
                    else:
                        print("SPOT: 日付条件一致するので追加 %s" % (TODAY))
                        print("期限は %s" % (dueDate))
                        print("%s %s" % (issueTypeId, summary))

                        backlog_issue_add(ApiKey, BACKLOG_PRJ_ID, summary, issueTypeId, priorityId, assigneeId, description, dueDate )


def backlog_issue_add(ApiKey, projectId, summary, issueTypeId, priorityId, assigneeId, description,dueDate): 
        url = 'https://xxxx.backlog.com/api/v2/issues?apiKey=' + ApiKey
        data = {
                'projectId': projectId,
                'summary': summary,
                'issueTypeId': issueTypeId,
                'priorityId': priorityId,
                'assigneeId': assigneeId,
                'description': description,
                'dueDate': dueDate
            }   
        headers = {
                'Content-Type': 'application/x-www-form-urlencoded'
        }
        r = requests.post(url, data = data, headers = headers)
        print(r.status_code)
        print(r.json())


def get_secret(secret_name, region_name):

    # Create a Secrets Manager client
    session = boto3.session.Session()
    client = session.client(
        service_name='secretsmanager',
        region_name=region_name
    )

    try:
        get_secret_value_response = client.get_secret_value(
            SecretId=SECRET_NAME
        )
    except ClientError as e:
        # For a list of exceptions thrown, see
        # https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html
        raise e

    secret = get_secret_value_response['SecretString']
    secret_list = ast.literal_eval(secret)

    return secret_list[SECRET_KEY_NAME]

```
</details>
※エクセルの読み書き用に openpyxl レイヤーを使ってます。<br>
※Backlog API とREST通信するように requestレイヤーを使てってます。



# 1. 定期的に発生する種類のタスクをエクセル管理表に書き出す

定期的に発生するタイミングとして、毎月／毎週／都度(３か月に一回とか)などがあるかなと想定して、エクセル管理表のフォーマットをいったん以下のように考えてみました。これを「 Backlog_Issue_list.xlsx 」というファイル名で作成します。
一行目はヘッダ行で以下のような形式になります。
![BacklogIssueListSample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49353/1383b5e3-28a2-4eaa-a6f6-9beece80ce47.png)


**A列： interval**
Backlog登録するのが、毎月なのか、毎週なのか、都度なのかを入力する列です。
| 値 | 説明 |
|:-:|:-:|
| MONTHLY | 毎月決まった日付にタスクをBacklog登録する |
| WEEKLY | 毎週決まった曜日にタスクをBacklog登録する  |
| SPOT | 指定した特定に日にタスクをBacklog登録する |

**B列： addTiming**
タスクを登録するタイミング(日付や曜日)を入力する列です。interval の形式によって入力方法が異なります。
| interva形式 | 入力する内容 |
|:-:|---|
| MONTHLYの場合| 毎月何日に登録したいのか日付を入力します<br>例）毎月１日であれば「1」、毎月15日であれば「15」 |
| WEEKLY | 毎週何曜日に登録したいのかアルファベット３文字で入力します。<br> 例) 毎週月曜であれば「Mon」、毎週木曜であれば「Thu」|
| SPOT | チケット登録したい特定の年月日を「YYYY-MM-DD」で入力します。<br>例）2025年3月31日であれば「2025-03-31」 |

**C列,D列： assigneeName, assigneeId**
Backlog上の担当者として割り当てる人の名前(assigneeName)をリスト形式で選択する列です。Backlog API で登録する時にはassigneeNameではなく assigneeIdが必要ですが、assigneeNameをリスト形式で選択すれば自動的にassigneeIdがD列に入るようにしてます。

**E列： summary**
Backlogに登録する件名を入力する列です。

**F列： description**
Backlogに登録するタスクの概要を入力する列です。

**G列： NumOfDays_UntilDeadline**
タスク登録した日から何日後を期限とするかを入力する列です。


# 2. エクセル管理表を S3 にアップロード
作成したエクセル管理表を Lambda で設定したパスに合わせてS3バケットにアップロードします。


# 3. １日１回 Lambdaがエクセルを読み取り、Backlog API経由でその日のタスクを登録

Sampleソースコードで必要な箇所を修正してLambda関数を登録します。

LambdaにアタッチするIAMロールに必要なポリシーは以下です。
+ AWSLambdaVPCAccessExecutionRole
+ SecretsManagerReadWrite
+ AmazonS3ReadOnlyAccess
+ CloudWatchLogsFullAccess

Lambda関数の中でBacklogに登録ユーザのAPIキーを保存して読みだしています。適当に以下としてます。
+ シークレットの名前： Backlog
+ 暗号化キー： aws/secretsmanager
+ シークレットのキー： BacklogApiKey
+ シークレットの値： Backlogで発行したAPIキー

Lambdaが実行されると、エクセル管理表にその日登録するべきタスクがあれば下記のようにBacklog登録してくれます。
![BacklogIssueSample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49353/c3d59e5b-9276-402a-a2ee-b4b4048d9285.png)

### 注意事項
+ EventBridge で Lambda をスケジュール実行させることを忘れないでください。
https://docs.aws.amazon.com/ja_jp/eventbridge/latest/userguide/eb-run-lambda-schedule.html
+ LambdaからBacklog API にアクセスできるように、Lambda VPC内に置いてNatGateway経由でインターネットに出るようにしてください。
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/configuration-vpc.html
+ Backlog API についてはヌーラボ社の下記を参考にして下さい。
https://developer.nulab.com/ja/docs/backlog/
+ Backlog 管理画面で接続元IPを制限している可能性があるので、静的IPを割り当てて許可してもらってください。
https://repost.aws/ja/knowledge-center/lambda-static-ip



以上