こんにちは、長塚電話工業所の佐藤です。
今回はみなさん大好きなAWSを使って、kintoneアプリ内のレコードを指定した条件に従って抽出し、メール通知するまでを実装したいと思います。
前回の記事の内容をAWS向けにアップデートし、実運用まで持っていきます!
PR
すみません、本題に入る前に自社製品の紹介を少しだけさせてください笑
実は今年、enterpriseヘッドセットに接続する新しいアダプターを開発・販売を開始しました。USB-A端子に対応しています。詳細はリンク先に記載しますので、興味がありましたら是非チェックしてみてください。
必要な環境
では気を取り直して本題に入りますね。必要な環境は以下の通りです。
- Pythonの実行環境(ローカル開発時)
- AWSアカウント(有効化済みのもの)
やりたいこと
- アプリに登録されているレコード一覧を取得
- 条件に一致したレコードそれぞれにアクセスできるURLを作成
- その結果を特定のメールアドレスに送信する
それでは、早速やっていきましょう!
構成

前もって作成した資料のスクショ貼り付けで失礼します。
今回の構成を組むにあたり、前回作成したソースコードをAWS仕様に書き換える必要があります。
画像の通り、プログラム側はAWS lambdaで実装するのですが、行いたい処理をlambda関数の内部に格納してあげます。
次に、AWS Syetems Manager(パラメータストア)を使って、パスワードやAPIキーの暗号化および安全な保存を行います。
自社のサーバーに置いておくだけなら、ソースコード内に重要な情報が書かれていてもギリギリセーフかもしれませんが(本当はよろしくない)、、しっかりとセキュリティ対策を講じましょう。
最後に、Amazon EventBridgeを利用して、プログラムが定期的に実行できるよう設定してあげます。
より具体的な構成

今回は、このようなイメージで開発を進めました。
①のソースコード開発およびテストについては前回の記事で既に完了しておりますので、lambda関数の実装の箇所から見ていきましょう。
実装
IAMでポリシーおよびロールの作成
上記構成の実装をするその前に!実はやらなくてはならないことがあります。IAMでのポリシー、ロールの作成です。
何だそれ?という方はこちらをご確認ください。
https://qiita.com/montama/items/90bb8a3973d101be4690
AWSのサービスは本来独立しています。連携するためには、必ずと言ってよいほどIAMでのロール、ポリシー作成が必要になります。今回の場合だと、
・AWS LambdaとSystemManager間のアクセス許可
を行うポリシーおよびロールが必要になります。
AWS LambdaとEventBridge間のやり取りについては、lambda側で設定します(後述)。
AWS lambdaとパラメータストア間のやり取りには非常に細かな決まり事があり、それに沿って実装を進めます。
https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/ps-integration-lambda-extensions.html
では、IAMを検索します。

アクセス管理→ポリシーより、ポリシーの作成をクリックします。
今回はJSONを記述し、上記ドキュメントに沿ってダイレクトにポリシーを設定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | { "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": [ "ssm:GetParameter", "kms:Decrypt" ], "Resource": "*" } ] } |
もし、余計な改行がある場合は適宜削除してください。以下の記述に則っています。
Parameter Store リクエストを承認および認証するために、拡張機能は Lambda 関数自体の実行に使用されたものと同じ認証情報を使用します。そのため、関数の実行に使用する AWS Identity and Access Management (IAM) ロールには、Parameter Store と対話するための次の権限が必要です。ssm:GetParameter
— Parameter Store からパラメータの取得に必要kms:Decrypt
— Parameter Store からSecureString
パラメータを取得するために必要
ポリシーを作成し終わったら、ロール作成画面へ移行します。こちらでも新規にロールを作成し、適切に名前を設定後、先ほど作成したポリシーを割り振ります。
信頼されたエンティティタイプを「AWSのサービス」、ユースケースを「lambda」とします。許可を追加というタブに移動するので、先ほど作成したポリシーを付与したら完了です。
パラメータストアの作成
次に、Lambda関数で使用するセンシティブな情報を保存するために、パラメータストアにアクセスします。

「パラメータの作成」より、プログラムに必要な変数やAPIキーを作成していきましょう。画像では、APIキーの値を作成しています。

タイプを「安全な文字列」とすると「SecureString」形式となり文字列が暗号化されます。先ほどのjsonファイル内に、「”kms:Decrypt”」と記載したと思うのですが、これを書かないとSecureStringキーをlambdaが読み取るすることができず、関数の実行時にエラーになります。
同じように、プログラム上必要になりかつ機密性の高い値については、どんどんパラメータストアに保存していきましょう。
ここで一つ注意なのですが、それぞれの値の「名前」はプログラム上でも呼び出し時に必要になります。ですのであまり適当な名前にはしないようにしておきましょう。
Lambda関数の作成

次はLambdaを検索します。リージョンは東京としておきます。

「関数の作成」をクリックします。

このような画面になりますので、適宜関数名などを入力していきます。今回は「KintoneAccess」とします。ランタイムの箇所は、今回はPythonを使用してプログラムを作成していますので「Python 3.11」とします。

「デフォルトの実行ロールの変更」タブをクリックし、先ほど作成したロールを選択します。もしロールが出てこない場合は、IAMの設定を再度確認してみてください。
詳細設定はスルーして、「関数の作成」をクリックします。

するとこのように、Lambda関数が作成されます。ではさっそく、前回作成したコードを修正しながら関数の中身を作成していきます。
「コード」タブより、lambda_function.pyを開きます。ちなみにですが、このプログラム名は変更しないでください。
プログラムの実装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | import os import json import requests import pandas as pd from datetime import datetime import smtplib from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText # パラメータと変数のマッピング param_to_var = { 'API_TOKEN': 'API_TOKEN', 'KINTONE_DOMAIN': 'KINTONE_DOMAIN', 'from_email': 'from_email', 'smtp_password': 'smtp_password', 'smtp_user': 'smtp_user', 'to_email': 'to_email' } def get_parameters(): # 初期化 globals().update({var: None for var in param_to_var.values()}) # GETリクエストを行うために必要なトークンの情報 headers = { 'X-Aws-Parameters-Secrets-Token': os.environ.get('AWS_SESSION_TOKEN') } # 各パラメータ名に対してGETリクエストを行い、結果をそれぞれの変数に格納する for param, var in param_to_var.items(): endpoint = f'http://localhost:2773/systemsmanager/parameters/get/?name={param}&withDecryption=true' try: res = requests.get(endpoint, headers=headers) res.raise_for_status() # パラメータ値をJSONとして解析し、"Parameter" -> "Value" の値を取得 parameter_value = json.loads(res.text).get("Parameter", {}).get("Value", "").strip() globals()[var] = parameter_value except requests.exceptions.HTTPError as err: print(f"HTTP error occurred while fetching parameter {param}: {err}") print(f"Response content: {res.text}") # エラーレスポンスの本文を出力 raise # エラーを再スローして処理を中断 parameters = {var: globals()[var] for var in param_to_var.values()} # パラメータを辞書として取得 print(f"Parameters: {parameters}") # パラメータをログに出力 return parameters # パラメータの辞書を返す ####### ####### ####### #ここからKintoneおよびメール送信の連携になります。 APP_ID = xx #適宜入力してください。 def fetch_kintone_data(API_TOKEN, KINTONE_DOMAIN): url = f"https://{KINTONE_DOMAIN}/k/v1/records.json" headers = { "X-Cybozu-API-Token": API_TOKEN, } all_records = [] offset = 0 limit = 100 while True: params = { 'app': APP_ID, 'query': f'order by $id asc limit {limit} offset {offset}', } response = requests.get(url, headers=headers, params=params) data = json.loads(response.text) if 'records' in data: records = data['records'] all_records.extend(records) if len(records) < limit: break offset += limit else: print(f"Error occurred: {data['message']}") break return all_records def filter_data(all_records, KINTONE_DOMAIN): current_year = datetime.now().year current_month = datetime.now().month df = pd.DataFrame.from_records(all_records) for column in df.columns: df[column] = df[column].map(lambda x: x['value'] if isinstance(x, dict) else x) df['aa'] = pd.to_datetime(df['aa']) df['bb'] = df['aa'].dt.year df_filtered = df[ (df['bb'] <= current_year) & (df['aa'].dt.month == current_month) & (df['cc'] == 'dd') & (df['ee'] == 'ff') ] df_filtered = df_filtered.copy() df_filtered['gg'] = f"https://{KINTONE_DOMAIN}/k/{APP_ID}/show#record=" + df_filtered['$id'].astype(str) return df_filtered def send_email(df_filtered, from_email, smtp_user, smtp_password, to_email): df_print = df_filtered[['hh', 'ii', 'jj', 'kk']] html = df_print.to_html() subject = "【件名】" body = f"本文がここに入ります。\n{html}" msg = MIMEMultipart() msg["From"] = from_email to_email_list = json.loads(to_email) # to_emailをリストに変換 msg["To"] = ", ".join(to_email_list) # メールアドレスを文字列に結合 msg["Subject"] = subject msg.attach(MIMEText(body, "html")) server = smtplib.SMTP_SSL("smtp.example.jp", 111, timeout=120) server.login(smtp_user, smtp_password) server.sendmail(from_email, to_email_list, msg.as_string()) # メールアドレスをリストとして渡す server.quit() def lambda_handler(event, context): try: params = get_parameters() print(f"Params: {params}") all_records = fetch_kintone_data(params['API_TOKEN'], params['KINTONE_DOMAIN']) df_filtered = filter_data(all_records, params['KINTONE_DOMAIN']) send_email(df_filtered, params['from_email'], params['smtp_user'], params['smtp_password'], params['to_email']) except Exception as e: print(f"An error occurred: {e}") return { 'statusCode': 500, 'body': json.dumps({'message': str(e)}) } return { 'statusCode': 200, 'body': json.dumps('Email Sent Successfully!') } |
いくつか、さほど重要ではないが見せるほどでもない、というレベル感のパラメータなどがあったのでそちらについては適当な文字列で置き換えています。パラメータストアの値などもあるため流石にコピペでは動きません。何卒ご了承ください。
主なロジックは前回説明済みですが、上記プログラムが行っていることを大まかに解説します。
まず、パラメータストアから引っ張ってくる値をプログラム上で使用するための準備を行います。次にAWSのお作法に従い、パラメータを取得してKintoneにリクエストを出し、条件に沿ったデータをpandasに落とし込みます。データの整形・抽出が終わったら、それをhtml化し、メールサーバーにアクセスしてメール送信、という寸法です。
おそらく上記プログラムと全く同じものを作る方はいらっしゃらないと思うのですが、パラメータストアに保存された暗号化キー複数に対して、Pythonからどのようにアプローチするかという箇所のコードは良かったら参考にしてみてください。また、Lambdaの設定から適宜、タイムアウトや必要メモリ数を変更してください。
作成し終わったら「Deploy」ボタンを押して保存しておきましょう。たまにしっかりと保存されないときがありますのでよくご確認ください。
レイヤーの設定
さて、プログラムは作成できたのですが、おそらくまだ実際に動かすことはできません。実はPythonのプログラム上で「pandas」「Request」といった外部ライブラリを利用しており、これらはデフォルトのではLambda側に組み込まれていません。コードが正常でも、実行すると「そんなライブラリ無いよ!」とエラーを吐いてしまいます。
そこで、レイヤーと呼ばれるLambdaの機能を使って、外部ライブラリを明示的に指定します。また、パラメータストアの値を使えるようにするためのレイヤーもありますので、それらを追加します。
関数のホーム画面を下にスクロールし「レイヤーを追加」をクリックします。

layersをクリックすると飛べます。今回のプログラムでは合計3つ追加します。
AWS-Parameters-and-Secrets-Lambda-Extension

このように選択し、追加します。
Klayers-p311-requests
https://api.klayers.cloud/api/v2/p3.11/layers/latest/ap-northeast-1/html
こちらを確認し、requestsのarnをコピーします。これは有志の方が作ってくださった外部ライブラリを読み込むためのarnでして、おかげさまでライブラリをzip化して読み込んだり、自作したりということをしなくても良くなっています。

上記画像のように指定し、追加します。
Klayers-p311-pandas
requestsライブラリと同様、pandasについても先ほどのURLより追加を行ってください。
Lambda単体テスト
ここまで長らくの作業、大変お疲れ様でした。やっとLambdaの単体テストを行うことができます。コードタブの隣にある「テスト」を選択し、画面右側の「テスト」をクリックします。今回はトリガー時にLambdaに渡す値がありませんので、イベントJSONなどは気にしなくても大丈夫です。

成功すると、イベントコード200およびプログラム内で指定した文言が返されるはずです。ここまで来る頃にはおそらく、プログラムの内容はみなさまと大きく異なっているはずですので、エラーの解消については各自にて対応をお願いいたします。
とはいえ、そこまで大がかりでなく似たようなソースコードであれば恐らく対応可能ですので、詰まってしまった箇所がある、Lambda関数の作成を委託したいなどの場合は、お問い合わせフォームよりご相談いただければと思います。
Amazon EventBridgeの設定
さて、ソースコードは完成しましたが、毎回Lambdaにアクセスして関数を実行するのはさすがに面倒ですよね?そこでEventBridgeの登場です。EventBridgeはWindowsでいうところのタスクスケジューラのようなものです。早速、LambdaとEventBridgeを紐づけましょう。
EventBridgeを検索します。


EventBridgeスケジュールの箇所を選択後、「スケジュールを作成」をクリックします。

このような画面になりますので、適宜スケジュール名を入力してください。またスケジュールのパターンですが、今回は毎月1日10時に1回だけ実行させるものとし、以下のように設定します。cron式の設定の詳細についてはAWS上にドキュメントがありますので「こちら」を参考にしてください。

その下にあるフレックスタイムウィンドウについてはお好みで設定してください。

次にターゲット選択の画面になりますので、AWS Lambdaを選びます。
あらかじめ作成したLambda関数を選択し、次へ、をクリックします。

ほとんど触る項目はありませんが、EventBridgeからLambdaへのアクセスを行うロールを作成していない場合、ここで新しく作成してしまっても良いでしょう。
最後に内容を確認して「スケジュールを作成」をクリックしたら完了です。LambdaへアクセスしEventBridgeが紐づいていることを確認してください。
さいごに
ここまで非常に長い道のりでしたね…そしてかなりの文章量になってしまいました。ですがオンプレで稼働していたプログラムをクラウド移行する一部始終をお見せすることができたかと思います。また今回の記事では、パラメータストアの値をPythonで読み込んだ後の処理については詳しく解説していません。Pythonのお作法やpandasライブラリの具体的な使い方などについては、別途有志の方が素晴らしい記事を公開されていますので、そちらをご確認いただければと思います。
最後までお読みいただき、ありがとうございました!