バイセル Tech Blog

バイセル Tech Blogは株式会社BuySellTechnologiesのエンジニア達が知見・発見を共有する技術ブログです。

見習いエンジニアが入社1ヶ月でLambdaを動かした話

こんにちは!テクノロジー開発部の杉田です。
弊社には数多くの優秀なエンジニアが揃っていますが、私はまったくの異業種からの転職者で、未経験として入社したのが4月1日。前職の会社ではエイプリルフールは毎年先輩たちにどんな嘘をつこうか考えながら呑気に出社していましたが、今年は出勤初日ということで、緊張して嘘どころではなかったのを覚えています。
そんな私が入社1ヶ月でAWSのLambdaに触れる機会に恵まれたので、今日はそのお話を書きたいと思います。

AWS Lambda

Lambdaとは

まず、入社前まではLambda(ラムダ)のラの字も知らなかった私。1ヶ月前の自分に「Lamdaとは」を一言で説明するなら...「AWSが提供してくれているサービスのひとつで、なにかのイベントをトリガーに、事前に定義しておいた関数(自分が書いたプログラム)をサーバーレスで実行してくれるスグレモノ」となります。このLambdaのトリガーに設定するとめちゃくちゃ便利なAPI Gatewayという別のサービスもAmazonさんは用意してくださってまして、これは「簡単にAPIの作成&管理ができるスグレモノ」という説明で概ね合っていると思います。外部サービスのWebhook先をAPI Gatewayで作ったAPIのエンドポイントにしておけば、外部サービスでのアクションをトリガーにLambdaを起動する、といったことが簡単に実現できてしまいます。私は今回のことをきっかけにLambda&API Gatewayコンビの優秀さに感動し、Lambda大好きマンになりました!

実装

前提として、弊社では全社でのコミュニケーションツールにChatworkを採用しつつ、社内の問い合わせシステムにJiraを導入しています。今回実装したプログラムは、この問い合わせシステムの質問スレ(Jiraチケット)に回答者から回答コメントがついたら、質問者のChatworkに通知を飛ばす、というものです。一見シンプルなのですが、ここで1番のミソは「Jira側から取得した質問者のメールアドレスをキーに、その質問者のChatworkのroom_idを探し出す必要があること」。社員のメールアドレスとroom_idが対になったデータが既にあればそこまで難しいことはなかったと思うのですが、生憎そのようなデータが存在していなかったので、まずはそれを生成する必要がありました。そこで、下図のようにAサイドとBサイドで処理を分けることにしました。

全体図

Aサイド

Aサイドでは、Chatwork APIから取得したコンタクト一覧と社員データを、Chatworkのaccount_idをキーにしてmergeすることにしました。

Chatwork APIから情報取得

Chatwork APIからは色々な情報が取得できるのですが、情報が多過ぎても逆に扱いにくいので、account_idとroom_idだけのシンプルなdictのリストを生成します。(以下コーディングは全てPythonで行なっています。)

import requests

# Chatwork APIを叩いてコンタクト一覧からaccount_idとroom_idのペアを取得
api_token = 'YOUR_API_TOKEN'
api_url = 'https://api.chatwork.com/v2/contacts'
headers = {'X-ChatWorkToken': api_token}
resp = requests.get(api_url, headers=headers).json()

contacts_list = []
for row in resp:
    contacts_list.append({'account_id': row['account_id'], 'room_id': row['room_id']})

社員データ(spreadsheet)を読み込む

Google spreadsheetを読みにいくのも、Lambda上で動かすので個人アカウントではなくサービスアカウントを使用します。
また、こちらでも必要最低限の情報であるaccount_idとemailだけのdictのリストを生成します。

from google.oauth2 import service_account
from googleapiclient.discovery import build
import os

# 社内データからChatworkのaccount_idとメールアドレスのペアを取得
SCOPES = ['https://www.googleapis.com/auth/spreadsheets.readonly']
key_file = 'YOUR_CREDENTIAL_FILE'
creds = service_account.Credentials.from_service_account_file(key_file, scopes=SCOPES)
service = build('sheets', 'v4', credentials=creds, cache_discovery=False)

spread_sheet_id = os.getenv('MEMBERS_SPREADSHEET_ID')
range = os.getenv('RANGE')
sheet = service.spreadsheets()
result = sheet.values().get(spreadsheetId=spread_sheet_id, range=range).execute()
values = result.get('values', [])

if not values:
    print('No data found.')
else:
    members_list = []
    for row in values:
        members_list.append({'account_id': row[0], 'email': row[2]})

merge

色々試行錯誤した結果、上記の2つをmergeするにはpandasでDataFrameに変換してしまうのが1番簡単そうだったので、そうします。DataFrameをspreadsheetに書き込むのにはpygsheetsというモジュールが便利でした。

import pygsheets
import pandas as pd

# がっちゃんこしてメールアドレスとroom_idのペアを生成し、新たなspreadsheetに書き込む
gc = pygsheets.authorize(service_file=key_file)

df_contacts = pd.io.json.json_normalize(contacts_list)
df_members = pd.io.json.json_normalize(members_list)

df_contacts['account_id'] = df_contacts['account_id'].astype(str) # mergeするにあたってINTカラムをSTRカラムに変換
merged_df = pd.merge(df_contacts, df_members, on='account_id', how='inner')

sh = gc.open_by_key(os.getenv('FINAL_SPREADSHEET_ID'))
wks = sh[0]
wks.set_dataframe(merged_df, (1,1)) # set_dataframe(書き込むデータフレーム, 書き込み開始点)

Bサイド

Aサイドができてしまえば、あとはBサイドでspreadsheetを読みにいってChatworkにメッセージ送信するだけです!

Aサイドで書き出したspreadsheetを読み込む

spreadsheetを読みにいく部分はほぼAサイドのコードのコピペなので割愛します(笑)最後の部分だけ、emailがキーでroom_idがvalueになるdictを生成しています。(先述の通り、Jira側から取得した質問者のメールアドレスをキーに、その質問者のChatworkのroom_idを探し出す必要があるため)

final_list = []
for row in values:
    final_list.append((row[1], row[0]))
final_dict = dict(final_list)

Chatworkにメッセージ送信

API GatewayはLambdaのhandler(自分が定義した関数)を起動します。その際に、予め用意されているeventという変数に値(この場合はJiraから送られてきたjson)が入ってくるので、 event['キー名'] という形で欲しい値にアクセスすることができます。jsonの階層が深い場合は表記のうっかりミスに要注意です!

import requests

# Chatworkにメッセージ送信
email = event['issue']['fields']['creator']['emailAddress']
if event['comment']['author']['emailAddress'] != email and event['comment']['jsdPublic'] == True: # 質問者自身のコメント&隠しコメントは通知しない
    if email in final_dict:
        room_id = str(final_dict[email]) # emailからroom_idを取得
        msg = 'あなたの質問へ回答がありました。内容を確認してください。'
    else:
        room_id = str(os.getenv('SYSTEM_ROOM_ID'))
        msg = '送信先room_idエラー'
    api_token = 'YOUR_API_TOKEN'
    url = 'https://api.chatwork.com/v2/rooms/'
    api_url = url + room_id + '/messages'
    headers = {'X-ChatWorkToken': api_token}
    params = {'body': msg}
    req = requests.post(api_url, headers=headers, params=params)

終わりに

いかがでしたでしょうか?API GatewayとLambda(とPythonのモジュールたち)が優秀過ぎて、私のような初心者でもちょこっとコードを書いただけでこんな便利機能を作ることができてしまいました!
実は最初はただ「カッコ良いから」という理由だけでGo言語で書き始めていたのですが、静的型付け言語は私にとってまだまだハードルが高く、あまりにも無謀そうだったので途中からPythonに切り替えたという経緯があります😂とは言えPythonもほぼ書いたことがなかったので大きな挑戦でしたが...先輩方やGoogle先生を頼りまくり、なんとか完成まで辿り着くことができました。LambdaやAPI Gatewayというもの自体の学習から始まり、探り探りコードを書き始めてから2週間。これだけのコードを生産するのに2週間なんて時間がかかり過ぎかもしれませんが、自分でトライ&エラーを繰り返せたお陰で、学びも非常に大きかったです。途中で躓くことがあっても、「もっと知りたい」とか「悔しい」とか「楽しい」という気持ちが1番の原動力になりますよね。
入社1ヶ月でしかも未経験者の私にこんな楽しい学びの機会を与えてくださったマネージャーの松榮さんや、アドバイスをくださった弊社のNo.1エンジニアである村上さんに感謝すると共に、今後も努力を続けながらエンジニアリングを楽しんでいきたいと思いました。そして早くしっかりとした戦力になれるように引き続き頑張ります!(Go言語の勉強も続けます笑)