API Gateway で受信したメッセージを SNS 経由で Slack へ通知する
概要
記事一覧はこちらです。
API Gateway で受信したメッセージを Lambda で SNS へ転送し、SNS から Lambda で Slack へメッセージを送信してみます。
API Gateway → SNS 連携は AWS Service Proxy という機能を使えば直接送信できるそうですが、今回は Lambda 経由で送信します。
以下の仕様で実装します。
- API Gateway にアクセスする時の URL は
https://<deploy 時に表示されるドメイン名>/dev/send-msg
。 - request body には message の項目だけが記述された JSON をセットします。
- message にセットされた文字列を Slack の
#general
に送信します。
参照したサイト・書籍
SNS
https://www.serverless.com/framework/docs/providers/aws/events/sns/AWS - Resources
https://www.serverless.com/framework/docs/providers/aws/guide/resources/- 以下の内容を参考にしました。
- resources に定義する AWS リソースに DependsOn が定義できる。
- serverless.xml 内で定義した lambda function を他の AWS リソースから参照する時に、
- 末尾に
LambdaFunction
を追加した文字列で参照できる。 - Override AWS CloudFormation Resource に function 名は normalizedFunctionName にする(先頭は大文字に変える、
-
と_
は文字列に変える)。 - 例えば notifySlack という function ならば、先頭を大文字に変えて末尾に
LambdaFunction
を追加した NotifySlackLambdaFunction というリソース名になる。
- 末尾に
- 以下の内容を参考にしました。
AWS CloudFormation でプッシュベースのイベントソースに AWS Lambda 関数をサブスクライブするにはどうすればよいですか?
https://aws.amazon.com/jp/premiumsupport/knowledge-center/lambda-subscribe-push-cloudformation/AWS: Publish SNS message for Lambda function via boto3 (Python2)
https://stackoverflow.com/questions/34029251/aws-publish-sns-message-for-lambda-function-via-boto3-python2Requests: HTTP for Humans
https://requests.readthedocs.io/en/master/- 今回から urllib3 ではなく requests を使ってみます。
- urllib3 は boto3 の依存関係にあるので追加インストールの必要がありませんが、requests は個別にインストールする必要があります。
Amazon SNS (from AWS) - The Ultimate Guide
https://www.serverless.com/amazon-sns/Sending messages using Incoming Webhooks
https://api.slack.com/messaging/webhooksウェブフックを使用して Amazon SNS メッセージを Amazon Chime、Slack や Microsoft Teams に発行する方法を教えてください。
https://aws.amazon.com/jp/premiumsupport/knowledge-center/sns-lambda-webhooks-chime-slack-teams/Requests: HTTP for Humans
https://requests.readthedocs.io/en/master/Variables
https://www.serverless.com/framework/docs/providers/aws/guide/variables/Encrypting messages published to Amazon SNS with AWS KMS
https://aws.amazon.com/jp/blogs/compute/encrypting-messages-published-to-amazon-sns-with-aws-kms/moto(boto3のmockモジュール)の使い方:SQS/SNS編
https://qiita.com/ck_fm0211/items/08a7bc5a0c98de112cb7django setting environment variables in unittest tests
https://stackoverflow.com/questions/31195183/django-setting-environment-variables-in-unittest-testsAn Introduction to Mocking in Python
https://www.toptal.com/python/an-introduction-to-mocking-in-pythonnotify-slack
https://registry.terraform.io/modules/terraform-aws-modules/notify-slack/aws/3.3.0- 書き終わるころに見つけました。メッセージを Slack に送信する手段が欲しいだけならば Terraform の module にしておいて簡単に作成できるようにしておくのもありですね。
目次
- apigw-sns-slack-project プロジェクトを作成する
- Serverless Framework で message_service サブプロジェクトを作成し serverless.yml を変更する
- requirements.txt を作成する
- API Gateway から呼び出す Lambda を実装する
- Slack App を作成し Webhook URL を生成する
- SNS から呼び出す Lambda を実装する
- ユニットテストを作成する
- deploy する
- 動作確認する
手順
apigw-sns-slack-project プロジェクトを作成する
以下の手順で apigw-sns-slack-project プロジェクトを作成します。具体的な手順は IntelliJ IDEA+Node.js+npm+serverless framework+Python の組み合わせで開発環境を構築して AWS Lambda を作成してみる 参照。
- apigw-sns-slack-project の Empty Project を作成する。
- Python の仮想環境を作成する。
- Serverless Framework をローカルインストールする。
- .envrc を作成する。
npm install --save-dev serverless-python-requirements
- IDEA で Terminal を起動して boto3、requests、moto をインストールする。
pip install boto3
pip install requests
pip install moto
Serverless Framework で message_service サブプロジェクトを作成し serverless.yml を変更する
プロジェクトのルートディレクトリの下で npx sls create --template aws-python3 --path message_service
を実行して message_service サブプロジェクトを作成します。
生成された serverless.yml を以下の内容に変更します。
service: message-service plugins: - serverless-python-requirements custom: pythonRequirements: dockerizePip: true provider: name: aws runtime: python3.8 stage: dev region: ap-northeast-1 iamRoleStatements: - Effect: "Allow" Action: - "kms:GenerateDataKey" - "kms:Decrypt" - "sns:Publish" - "sns:Subscribe" Resource: - !Ref NotifySlackTopic functions: sendMsg: handler: apigw_handler.send_msg environment: TOPIC_ARN: !Ref NotifySlackTopic events: - http: path: send-msg method: post cors: true notifySlack: handler: sns_handler.notify_slack environment: SLACK_WEBHOOK_URL: ${env:SLACK_WEBHOOK_URL} events: # - sns: notify-slack-topic という書き方ではなく resources で定義した Topic に関連付けている # ただしこの書き方だと Subscription を作成してくれないし、Topic にメッセージが publish されても Lambda 関数が実行されない # ので resources で Subscription を作成して必要な権限を付与している - sns: arn: !Ref NotifySlackTopic topicName: notify-slack-topic resources: Resources: NotifySlackTopic: Type: AWS::SNS::Topic Properties: TopicName: notify-slack-topic # 以下のリソースは notifySlack 関数が作成された後に生成する # 既に作成されている Topic だと自動で Subscription を作成してくれないので、以下の定義で作成する NotifySlackSubscription: Type: AWS::SNS::Subscription DependsOn: # AWS - Resources # https://www.serverless.com/framework/docs/providers/aws/guide/resources/ # notifySlack function ならば、先頭を大文字に変えて末尾に LambdaFunction を付けた # NotifySlackLambdaFunction という名称で参照できる - NotifySlackLambdaFunction Properties: TopicArn: !Ref NotifySlackTopic Endpoint: Fn::GetAtt: - NotifySlackLambdaFunction - Arn Protocol: lambda # Subscription を作成するだけでは Lambda を実行できないので、以下の定義で実行できるようにする NotifySlackLambdaResourcePolicy: Type: AWS::Lambda::Permission DependsOn: - NotifySlackLambdaFunction Properties: FunctionName: !Ref NotifySlackLambdaFunction Principal: sns.amazonaws.com Action: "lambda:InvokeFunction" SourceArn: !Ref NotifySlackTopic
- Serverless Framework では SNS に関連付けたい場合 functions の events に
- sns: notify-slack-topic
と記述すればよいのですが、Topic へメッセージを publish する Lambda 関数も同じ serverless.xml に定義したい場合どうやって参照したらよいのかが分からなかったので、resources で NotifySlackSubscription を定義して、sendMsg と notifySlack の Lambda 関数から!Ref NotifySlackTopic
で参照できるようにしました。 - sendMsg ではメッセージを publish する時に NotifySlackTopic の arn が必要になるので、environment に
TOPIC_ARN: !Ref NotifySlackTopic
を定義し、環境変数で arn を渡しています。 - 通常 resources に定義された AWS リソースが全て作成されてから Lambda 関数が作成されますが、今回は NotifySlackTopic → sendMsg と notifySlack の Lambda 関数 → NotifySlackSubscription と NotifySlackLambdaResourcePolicy の順に作成する必要があるため、NotifySlackSubscription と NotifySlackLambdaResourcePolicy に DependsOn を定義して Lambda 関数が作成された後に作成されるようにしています。
- IAM Role は Lamda 関数毎に分けた方が良さそうですが、簡略化したかったので
sns:Publish
とsns:Subscribe
を1つの Role に入れました。
requirements.txt を作成する
message_service サブプロジェクトの下に requirements.txt を作成し、以下の内容を記述します。
requests==2.24.0
API Gateway から呼び出す Lambda を実装する
message_service サブプロジェクトを作成した時に作られている handler.py のファイル名を agigw_handler.py に変更した後、以下の内容を記述します。
import json import logging import os import boto3 logger = logging.getLogger() logger.setLevel(logging.INFO) def send_msg(event, context): # body に { "message": "..." } のフォーマットで Slack へ送信したいメッセージを格納する logger.info(event) body = json.loads(event['body']) logger.info(f"message={body['message']}") sns_client = boto3.client('sns') response = sns_client.publish( TopicArn=os.environ['TOPIC_ARN'], Message=body['message'] ) response_body = { "message": f"published message to SNS (MessageId={response['MessageId']})", "input": event } response = { "statusCode": 200, "body": json.dumps(response_body) } return response
Slack App を作成し Webhook URL を生成する
https://api.slack.com/ にアクセスした後、画面右上の「Your Apps」をクリックします。
以下の画像の画面が表示されるので「Create an App」ボタンをクリックします。
「Create a Slack App」ダイアログが表示されます。「App Name」に「SnsToSlack」を入力し、「Development Slack Workspace」を選択した後「Create App」ボタンをクリックします。
「SnsToSlack」App が作成されて「Basic Information」画面が表示されます。「Incoming Webhooks」をクリックします。
「Incoming Webhooks」画面が表示されます。右上のトグルを On に変えます。
画面の下に「Webhook URLs for Your Workspace」が表示されます。「Add New Webhook to Workspace」ボタンをクリックします。
以下の画面が表示されます。「#general」を選択してから「許可する」ボタンをクリックします。
「Webhook URLs for Your Workspace」に発行された Webhook URL が表示されるのでコピーします。
SNS から呼び出す Lambda を実装する
Webhook URL は直接 Lambda 内には記述せず環境変数 SLACK_WEBHOOK_URL で渡すことにします。
.envrc に export SLACK_WEBHOOK_URL=...
を追加します(AWS_PROFILE、SLACK_WEBHOOK_URL の ..... の部分には実際の値を記述します)。
export AWS_PROFILE=..... # Windows 上の Python で UTF-8 をデフォルトにする # https://qiita.com/methane/items/9a19ddf615089b071e71 export PYTHONUTF8=1 export SLACK_WEBHOOK_URL=https://hooks.slack.com/services/.....
serverless.yml の notifySlack 関数に環境変数 SLACK_WEBHOOK_URL の値を渡すよう記述を追加します。
notifySlack: handler: sns_handler.notify_slack environment: SLACK_WEBHOOK_URL: ${env:SLACK_WEBHOOK_URL} events: # - sns: notify-slack-topic という書き方ではなく resources で定義した Topic に関連付けている # ただしこの書き方だと Subscription を作成してくれないし、Topic にメッセージが publish されても Lambda 関数が実行されない # ので resources で Subscription を作成して必要な権限を付与している - sns: arn: !Ref NotifySlackTopic topicName: notify-slack-topic
message_service サブプロジェクトの下に sns_handler.py を新規作成し、以下の内容を記述します。
import json import logging import os import requests logger = logging.getLogger() logger.setLevel(logging.INFO) def notify_slack(event, context): msg = { 'text': event['Records'][0]['Sns']['Message'] } encoded_msg = json.dumps(msg).encode('utf-8') res = requests.post(os.environ['SLACK_WEBHOOK_URL'], data=encoded_msg) logger.info(res.status_code) logger.info(res.headers) logger.info(res.text)
ユニットテストを作成する
プロジェクトのルートディレクトリ直下に tests ディレクトリを作成し、中身が空の __init__.py
ファイルを作成します。
まずは sendMsg 関数のテストから。tests ディレクトリの下に test_apigw_handler.py を作成し、以下の内容を記述します。
import json import unittest from unittest.mock import patch import boto3 from moto import mock_sns from message_service import apigw_handler TOPIC_NAME = 'notify-slack-topic' @mock_sns class TestApigwHandler(unittest.TestCase): def setUp(self): sns_client = boto3.client('sns') response = sns_client.create_topic( Name=TOPIC_NAME ) self._topic_arn = response['TopicArn'] self.env = patch.dict('os.environ', { 'TOPIC_ARN': response['TopicArn'], }) def tearDown(self): sns_client = boto3.client('sns') sns_client.delete_topic( TopicArn=self._topic_arn ) def test_send_msg(self): with self.env: with open('tests/apigw_event.json', encoding='utf-8', mode='r') as f: apigw_event = json.load(f) response = apigw_handler.send_msg(apigw_event, None) self.assertEqual(response['statusCode'], 200)
apigw_event.json も作成し、以下の内容を記述します。
{ "body": "eyJ0ZXN0IjoiYm9keSJ9", "resource": "/{proxy+}", "path": "/path/to/resource", "httpMethod": "POST", "isBase64Encoded": true, "queryStringParameters": { "foo": "bar" }, "multiValueQueryStringParameters": { "foo": [ "bar" ] }, "pathParameters": { "proxy": "/path/to/resource" }, "stageVariables": { "baz": "qux" }, "headers": { "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Encoding": "gzip, deflate, sdch", "Accept-Language": "en-US,en;q=0.8", "Cache-Control": "max-age=0", "CloudFront-Forwarded-Proto": "https", "CloudFront-Is-Desktop-Viewer": "true", "CloudFront-Is-Mobile-Viewer": "false", "CloudFront-Is-SmartTV-Viewer": "false", "CloudFront-Is-Tablet-Viewer": "false", "CloudFront-Viewer-Country": "US", "Host": "1234567890.execute-api.ap-northeast-1.amazonaws.com", "Upgrade-Insecure-Requests": "1", "User-Agent": "Custom User Agent String", "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", "X-Forwarded-For": "127.0.0.1, 127.0.0.2", "X-Forwarded-Port": "443", "X-Forwarded-Proto": "https" }, "multiValueHeaders": { "Accept": [ "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" ], "Accept-Encoding": [ "gzip, deflate, sdch" ], "Accept-Language": [ "en-US,en;q=0.8" ], "Cache-Control": [ "max-age=0" ], "CloudFront-Forwarded-Proto": [ "https" ], "CloudFront-Is-Desktop-Viewer": [ "true" ], "CloudFront-Is-Mobile-Viewer": [ "false" ], "CloudFront-Is-SmartTV-Viewer": [ "false" ], "CloudFront-Is-Tablet-Viewer": [ "false" ], "CloudFront-Viewer-Country": [ "US" ], "Host": [ "0123456789.execute-api.ap-northeast-1.amazonaws.com" ], "Upgrade-Insecure-Requests": [ "1" ], "User-Agent": [ "Custom User Agent String" ], "Via": [ "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)" ], "X-Amz-Cf-Id": [ "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==" ], "X-Forwarded-For": [ "127.0.0.1, 127.0.0.2" ], "X-Forwarded-Port": [ "443" ], "X-Forwarded-Proto": [ "https" ] }, "requestContext": { "accountId": "123456789012", "resourceId": "123456", "stage": "prod", "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", "requestTime": "09/Apr/2015:12:34:56 +0000", "requestTimeEpoch": 1428582896000, "identity": { "cognitoIdentityPoolId": null, "accountId": null, "cognitoIdentityId": null, "caller": null, "accessKey": null, "sourceIp": "127.0.0.1", "cognitoAuthenticationType": null, "cognitoAuthenticationProvider": null, "userArn": null, "userAgent": "Custom User Agent String", "user": null }, "path": "/prod/path/to/resource", "resourcePath": "/{proxy+}", "httpMethod": "POST", "apiId": "1234567890", "protocol": "HTTP/1.1" }, "body": "{\r\n \"message\": \"これはテストです \"\r\n}", "isBase64Encoded": "False" }
IDEA からテストを実行して成功することを確認します。
次は notifySlack 関数のテストです。tests ディレクトリの下に test_sns_handler.py を作成し、以下の内容を記述します。
import json import os import unittest from unittest.mock import patch from message_service import sns_handler class TestApigwHandler(unittest.TestCase): def setUp(self): self.env = patch.dict('os.environ', { 'SLACK_WEBHOOK_URL': 'https:/localhost/service/test', }) def tearDown(self): None @patch('requests.post') def test_notify_slack(self, mock_requests): with self.env: with open('tests/sns_event.json', encoding='utf-8', mode='r') as f: sns_event = json.load(f) sns_handler.notify_slack(sns_event, None) msg = { 'text': 'これはテストです' } encoded_msg = json.dumps(msg).encode('utf-8') mock_requests.assert_called_with(os.environ['SLACK_WEBHOOK_URL'], data=encoded_msg)
sns_event.json も作成し、以下の内容を記述します。
{ "Records": [ { "EventSource": "aws:sns", "EventVersion": "1.0", "EventSubscriptionArn": "arn:aws:sns:ap-northeast-1:446693287859:notify-slack-topic:2e6206ff-f201-400c-86f8-5ad2a168d1d0", "Sns": { "Type": "Notification", "MessageId": "accf8eb7-8119-54a8-a2e4-d9db0cb190fb", "TopicArn": "arn:aws:sns:ap-northeast-1:446693287859:notify-slack-topic", "Subject": "None", "Message": "これはテストです", "Timestamp": "2020-06-23T23:08:24.463Z", "SignatureVersion": "1", "Signature": "INeRjQ+zIe3qolznKBSlM5p9c1IguSBa9CBkd3AXwmwJm3SEpqKD+g3+Xmg/KT5v2wg8MVpOMpu1UO6zYdQ4lSnU90DP6Q1e6Bngr9uvy5ypgpE7Hy5s5L24vUT5bdqQCpY8Ig+Pt4Fx1PMFaRw9WnASl+26JRlGR53hMPux7GZlugQIYrlAhPJ/ZUlD0gqP5+hTg86FLdQ4GYblruvV1TqI7nEzd7ou88lviXj/RC4YYUUK0fonaR5U9tLTJmHwyr/cV3oII2kw6FzsxASpEHi3GhalKRuy1bW4Lx+Y4a0ITavK+KvvCs5iK9jZ+W07E8HyWd/vjqtRjMjwa83kYQ==", "SigningCertUrl": "https://sns.ap-northeast-1.amazonaws.com/SimpleNotificationService-a86cb10b4e1f29c941702d737128f7b6.pem", "UnsubscribeUrl": "https://sns.ap-northeast-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:ap-northeast-1:446693287859:notify-slack-topic:2e6206ff-f201-400c-86f8-5ad2a168d1d0", "MessageAttributes": {} } } ] }
IDEA からテストを実行して成功することを確認します。
最後にコマンドラインの venv 環境下で python -m unittest -v
を実行してテストが成功することを確認します。
deploy する
message_service を deploy します。
動作確認する
Postman を使用して API Gateway にメッセージを送信します。赤枠の部分が設定した箇所です。
Slack の #general にメッセージが届きました。
npx sls logs -f sendMsg
、npx sls logs -f notifySlack --startTime 1h
(ログが出てこなかったため過去1時間を表示するようオプションを追加しています) を実行してログを確認しても特に問題はなさそうです。
npx sls remove -v
を実行して削除します。
履歴
2020/06/24
初版発行。
Spring Boot + npm + Geb で入力フォームを作ってテストする ( その92 )( http-proxy-middleware の createProxyMiddleware 関数の引数 context には Proxy させない URI を後に書く )
概要
記事一覧はこちらです。
- 今回の手順で確認できるのは以下の内容です。
Spring Boot + npm + Geb で入力フォームを作ってテストする ( その85 )( Node.js を 10.15.3 → 12.16.3 へ、npm を 6.9.0 → 6.14.5 へバージョンアップする ) で http-proxy-middleware を 0.19.1 → 1.0.3 にバージョンアップして createProxyMiddleware 関数を使うように変更したのですが、createProxyMiddleware 関数の引数 context に記述する URI の順番が今のままではダメなことに今更ながら気づきました。
Proxy させる URI を先に書いて Proxy させない URI をその後に書かないと、Proxy させないはずの URI も Proxy されていました。
期待した動作になるよう修正します。
参照したサイト・書籍
目次
- 今の記述だと Proxy させない URI も Proxy されてしまう
- createProxyMiddleware 関数の引数 context に Proxy させない URI を後に記述するよう修正する
手順
今の記述だと Proxy させない URI も Proxy されてしまう
bs-springboot-config.js の createProxyMiddleware 関数の記述は現在以下のように Proxy させない URI を先に、Proxy させる URI を後に記述していますが、
const {createProxyMiddleware} = require("http-proxy-middleware"); const proxy = createProxyMiddleware( [ // /css, /js, /vendor と *.html は Tomcat に転送しない "!/css/**/*", "!/js/**/*", "!/vendor/**/*", "!/**/*.html", "/**/*" ], {target: "http://localhost:8080"} ); ..........
npm run browser-sync:springboot
コマンドを実行して Browsersync だけ起動した後(Tomcat は起動しません)、
http://localhost:9080/css/common.css にアクセスすると "!/css/**/*"
の設定により Proxy されず /css/common.css が取得できると思っていましたが、Error occured while trying to proxy to: localhost:9080/css/common.css
のエラーメッセージが表示されます。
Browsersync から Tomcat に Proxy しようとして Tomcat にアクセスできないのでエラーになっているようです。
createProxyMiddleware 関数の引数 context に Proxy させない URI を後に記述するよう修正する
この問題は "/**/*"
の記述を一番最初にすることで解決できました。
bs-springboot-config.js を以下のように修正し、
const {createProxyMiddleware} = require("http-proxy-middleware"); const proxy = createProxyMiddleware( [ // /css, /js, /vendor と *.html は Tomcat に転送しない "/**/*", "!/css/**/*", "!/js/**/*", "!/vendor/**/*", "!/**/*.html" ], {target: "http://localhost:8080"} );
Browsersync を再起動してから http://localhost:9080/css/common.css にアクセスすると、今度は /css/common.css の内容が返ってきました。
Tomcat を起動して http://localhost:8080/inquiry/input/01 にアクセスすると入力画面1も表示されたので、問題ないようです。
履歴
2020/06/21
初版発行。
外部パッケージの Pillow と独自モジュール(.py ファイル)を Lambda Layer に配置する(後編)
概要
記事一覧はこちらです。
外部パッケージの Pillow と独自モジュール(.py ファイル)を Lambda Layer に配置する(前編) からの続きです。
参照したサイト・書籍
PyCharm and PYTHONPATH
https://stackoverflow.com/questions/28326362/pycharm-and-pythonpathInstall a Python package into a different directory using pip?
https://stackoverflow.com/questions/2915471/install-a-python-package-into-a-different-directory-using-pipAWS: Delete lambda layer still retains layer version history
https://stackoverflow.com/questions/60824745/aws-delete-lambda-layer-still-retains-layer-version-history
目次
- IntelliJ IDEA 上でユニットテストが成功するように設定する
- コマンドプロンプトから
python -m unittest -v
を実行してユニットテストが成功するように設定する - Docker コンテナから
python -m unittest -v
を実行してユニットテストが成功するように設定する - IntelliJ IDEA からユニットテストを実行する時に Docker コンテナで実行する(docker-compose 版)
- deploy する
- 画像をアップロードしてサムネイル画像を生成してみる
- 1度 Lambda を実行してから Lambda Layer 側の内容を変更して再度実行すると変更は反映されるのか?
- 最後に
手順
IntelliJ IDEA 上でユニットテストが成功するように設定する
今の状態だとモジュール検索のパスに my_module_layer/python が追加されていないので resize_service/handler.py に追加した from image_lib import resize_utils
に赤波線が表示されます。
IntelliJ IDEA でどうやって環境変数 PYTHONPATH を設定すればよいのか調べてみたのですが、my_module_layer/python でコンテキストメニューを表示してから「Mark Directory as」-「Sources Root」を選択して、
Source Folder(青色のフォルダーアイコンになります)にすれば、
resize_service/handler.py の from image_lib import resize_utils
から赤波線が消えました。
これで tests/test_resize.py で「Run 'Unittests for test_r...'」を選択してテストを実行すると、
テストが成功しました。breakpoint を設定して debug 実行することも出来ました。
コマンドプロンプトから python -m unittest -v
を実行してユニットテストが成功するように設定する
venv/Scripts/activate.bat、venv/Scripts/deactivate.bat 内で環境変数 PYTHONPATH に my_module_layer/python の絶対パス(自分の環境では D:\project-serverless\ksbysample-serverless\python-lambda-layer-project\my_module_layer\python
)をセットします。
venv/Scripts/activate.bat の最後に以下の記述を追加します。
if defined PYTHONPATH ( set _OLD_PYTHONPATH=%PYTHONPATH% ) set PYTHONPATH=%PYTHONPATH%;D:\project-serverless\ksbysample-serverless\python-lambda-layer-project\my_module_layer\python
venv/Scripts/deactivate.bat の最後に以下の記述を追加します。
set PYTHONPATH= if defined _OLD_PYTHONPATH ( set PYTHONPATH=%_OLD_PYTHONPATH% ) set _OLD_PYTHONPATH=
これでコマンドプロンプトから python -m unittest -v
を実行してテストが成功するようになります。
ただし IntelliJ IDEA の Terminal からは成功しませんでした。(venv)
の文字が表示されていますが、venv/Scripts/activate.bat で activate している訳ではないようです。
Settings ダイアログの「Tools」-「Terminal」の「Environment Variables」に PYTHONPATH=D:\project-serverless\ksbysample-serverless\python-lambda-layer-project\my_module_layer\python
をセットすれば成功しましたが、あちこち設定するのは好みではないので Terminlal には設定せず pip install 専用にしようと思います。
Docker コンテナから python -m unittest -v
を実行してユニットテストが成功するように設定する
今回は volume で指定したいディレクトリが2ヶ所あるので docker-compose で実行します。
まずはプロジェクトのルートディレクトリ直下に Dockerfile を新規作成し、以下の内容を記述します。
FROM lambci/lambda:build-python3.8 COPY shared_package_layer/requirements.txt /tmp/requirements.txt RUN pip install --upgrade pip RUN pip install -r /tmp/requirements.txt RUN pip install moto ENV PYTHONPATH "${PYTHONPATH}:/opt/python"
次にプロジェクトのルートディレクトリ直下に docker-compose.yml を新規作成し、以下の内容を記述します。
# docker-compose build # docker-compose run --rm python-unittest version: '3' services: python-unittest: build: context: . image: lambci/lambda:build-python3.8-python-lambda-layer-project container_name: python-unittest volumes: - .:/var/task - ./my_module_layer/python:/opt/python command: python -m unittest -v
コマンドプロンプトを起動して docker-compose build
を実行し Docker Image を作成します。
最後に docker-compose run --rm python-unittest
を実行すると Docker コンテナで python -m unittest -v
が実行されてテストが成功します。
IntelliJ IDEA からユニットテストを実行する時に Docker コンテナで実行する(docker-compose 版)
IntelliJ IDEA のメインメニューから「File」-「Project Structure...」を選択して「Project Structure」ダイアログを表示します。
画面左側で「SDKs」を選択した後、中央で「+」-「Add Python SDK...」を選択します。
「Add Python Interpreter」ダイアログが表示されます。画面左側で「Docker Compose」を選択した後、右側の設定を以下の画像のようにしてから「OK」ボタンをクリックします。
「Name」に長い名前が設定されるので Remote Python 3.8.3 Docker Compose (python-lambda-layer-project)
に変更して「OK」ボタンをクリックしダイアログを閉じます。
IntelliJ IDEA のメインメニューから「Run」-「Edit Configurations...」を選択して「Run/Debug Configurations」ダイアログを表示します。
画面左側で「Templates」-「Python tests」-「Unittests」を選択し、以下の内容を設定して「OK」ボタンをクリックします。
- 「Python interpreter」で「Use specified interpreter」を選択した後「Remote Python 3.8.3 Docker Compose (python-lambda-layer-project)」を選択します。
- 「Working directory」に
D:\project-serverless\ksbysample-serverless\python-lambda-layer-project
を入力します。
設定は以上で完了です。エディタから「Debug 'Unittests for test_r...'」を選択して1回 debug 実行します。
この時テストは成功しますが、まだ docker-compose で実行されていません。
再度「Run/Debug Configurations」ダイアログを表示すると左側に「Python tests」-「Unittests for test_resize.TestResizeService.test_resize」が追加されているので、「Python interpreter」を「Use SDK of module」→「Use specified interpreter」に変更します。
再度エディタから「Debug 'Unittests for test_r...'」を選択して debug 実行します。
Console に以下のように表示されて、
「Step Into」「Step Over」で進めると my_module_layer/python/image_lib/resize_utils.py の resize_image 関数まで進めることができます。
Services Tool Window を見ると python-unittest コンテナが実行されており、
Debug Tool Window で「Resume Program」ボタンを押してテストを最後まで実行すると成功します。
Services Tool Window を見ると pycharm_helptest_IU コンテナが残っているのは docker コマンドで実行した時と同じですが、python-unittest コンテナまで残っています。。。
「Run/Debug Configurations」ダイアログから docker-compose 実行時のオプションを確認すると --rm
オプションがありません。「Command and options」で設定しようとしましたが、何かエラーが出て設定できませんでした。どうやったら設定できるのだろう?
手動で残っているコンテナを削除することにします。自動で削除できないなら docker-compose ではなく docker コマンドで実行する方式に変更した方が良さそうです。
deploy する
resize_service を delploy します。
マネジメントコンソールで deploy された resize-service-dev-resize を見ると shared-package-layer と my-module-layer の2つの Lambda Layer が使用されていることが分かります。
画像をアップロードしてサムネイル画像を生成してみる
ksbysample-upload-bucket に sample2.jpg をアップロードすると、
ksbysample-resize-bucket に sample2_thumb.jpg が生成されています。
npx sls logs -f resize
を実行してログを見ても特にエラーは出ていませんでした。
1度 Lambda を実行してから Lambda Layer 側のみ内容を変更して再度実行すると変更は反映されるのか?
my_module_layer/python/image_lib/resize_utils.py で thumbnail_size の値をログに出力するよう変更してから、
import logging from PIL import Image logger = logging.getLogger() logger.setLevel(logging.INFO) thumbnail_size = 320, 180 def resize_image(image_path, resized_path): logger.info(thumbnail_size) with Image.open(image_path) as image: image.thumbnail(thumbnail_size) image.save(resized_path)
thumbnail_size = 320, 180
→ thumbnail_size = 160, 90
に変更して my_module_layer のみ deploy したら変更が反映されるのか確認します。
まずは念の為 resize_service → my_module_layer → shared_package_layer の順に remove して、逆の順番で deploy し直します。thumbnail_size = 320, 180
の状態です。
sample.jpg をアップロードしてログを確認すると (320, 180)
と出力されており、初回なので Init Duration
も出力されています。
thumbnail_size = 160, 90
に変更して my_module_layer のみ deploy してから sample.jpg をアップロードすると、
ログに出力される値は (320, 180)
のまま変わらず、既にインスタンスが生成されて再利用されているので Init Duration
は出力されません。
生成済みのインスタンスが破棄されたら変更された Lambda Layer の内容が反映されるのか確認したいので、20分程度何もせずに放置します。
約20分後に sample.jpg をアップロードすると、
Init Duration
が出ているにもかかわらず (320, 180)
のままでした。。。
原因は、マネジメントコンソールで my-module-layer のバージョンを見ると現在 12 なのですが、
resize-service-dev-resize 関数が参照している my-module-layer のバージョンは 11 で、最新の 12 に更新されていないためのようです。latest のような指定が出来ないのかも見てみましたが、設定できるところが見当たりませんでした。
ちなみに resize_service を何も変更せずに delploy すると、関数が参照している my-module-layer のバージョンは 12 に更新されて、
sample.jpg をアップロードすると今度は (160, 90)
が出力されました。
Lambda Layer を deploy した場合には、それを利用する関数側も deploy しないと反映されない、という結果でした。latest で指定できるようにならないかな。。。
最後に
まとめてみると、
- Python + Serverless Framework で Lambda Layer を使うなら serverless-python-requirements プラグインの「Lambda Layer」の説明を見ること。Serverless Framework のドキュメントの AWS - Layers ではない。
- Lambda Layer を deploy したら、それを利用する関数も deploy し直すこと。そうしないと更新された Layer の最新バージョンを参照してくれない。
- Lambda Layer は Python のサイズの大きなパッケージを配置する程度に留めた方がよいのかもしれない(deploy 時間短縮のため)。
履歴
2020/06/20
初版発行。
外部パッケージの Pillow と独自モジュール(.py ファイル)を Lambda Layer に配置する(前編)
概要
記事一覧はこちらです。
前々々回、前々回、前回の記事で作成した resize-image-app-project プロジェクト をベースに別プロジェクトを作成して、外部パッケージの Pillow と独自モジュール(.py ファイル)を Lambda Layer に配置するサンプルを作成します。
長いので2回に分けます。
尚、動作させてみて分かりましたが、Lambda Layer を更新した場合、利用している Lambda 関数の方も deploy し直さないと新しいバージョンの Lambda Layer を利用してくれません。利用する Lambda Layer は latest ではなくバージョン固定で指定されていて(マネジメントコンソールで AWS Lambda の関数を見るとどのバージョンを利用しているのか表示されます)、Lambda Layer を更新しても Lambda 関数が参照する Layer のバージョンは変わらないからです。
Pillow のようなパッケージなら delploy 時間を短縮するために Lambda Layer に配置するのはありだと思いますが、ちょっとした独自の共有モジュール程度なら serverless-package-external でいいんじゃないかなという気がします。
参照したサイト・書籍
AWS - Layers
https://www.serverless.com/framework/docs/providers/aws/guide/layers/Serverless Python Requirements
https://www.serverless.com/plugins/serverless-python-requirements/- 「Lambda Layer」の記述を参照しました。
Getting started with AWS Lambda Layers for Python
https://medium.com/@adhorn/getting-started-with-aws-lambda-layers-for-python-6e10b1f9a5dHow to publish and use AWS Lambda Layers with the Serverless Framework
https://www.serverless.com/blog/publish-aws-lambda-layers-serverless-framework/Reference CloudFormation Outputs
https://www.serverless.com/framework/docs/providers/aws/guide/variables#reference-cloudformation-outputs
目次
- python-lambda-layer-project プロジェクトを作成する
- resize-image-app-project プロジェクトから resize_service、tests ディレクトリをコピーし、boto3、Pillow、moto をインストールする
- 外部パッケージの Pillow を配置する Lambda Layer 用の shared_package_layer サブプロジェクトを作成する
- 独自モジュール(.py ファイル)を配置する Lambda Layer 用の my_module_layer サブプロジェクトを作成する
- resize_service ディレクトリ内のファイルを変更する
- 後編に続く
手順
python-lambda-layer-project プロジェクトを作成する
以下の手順で python-lambda-layer-project プロジェクトを作成します。具体的な手順は IntelliJ IDEA+Node.js+npm+serverless framework+Python の組み合わせで開発環境を構築して AWS Lambda を作成してみる 参照。
- python-lambda-layer-project の Empty Project を作成する。
- Python の仮想環境を作成する。
- Serverless Framework をローカルインストールする。
- .envrc を作成する。
npm install --save-dev serverless-python-requirements
resize-image-app-project プロジェクトから resize_service、tests ディレクトリをコピーし、boto3、Pillow、moto をインストールする
resize-image-app-project プロジェクトから resize_service、tests ディレクトリをコピーします。resize_service の下の .serverless、pycache ディレクトリは削除します。
IDEA の Terminal を起動して boto3、Pillow、moto をインストールします。
pip install boto3
pip install Pillow
pip install moto
コマンドラインと IntelliJ IDEA からユニットテストが成功することを確認します。
deploy が成功することも確認します(キャプチャは省略)。
外部パッケージの Pillow を配置する Lambda Layer 用の shared_package_layer サブプロジェクトを作成する
まずは外部パッケージの Pillow を Lambda Layer に配置します。ポイントは、Serverless Framework のドキュメントの AWS - Layers の設定は一切使わず、serverless-python-requirements プラグインのドキュメント の「Lambda Layer」に記載されている設定を使う点です。これ、分からなくて結構悩みました。。。
プロジェクトのルートディレクトリの下で npx sls create --template aws-python3 --path shared_package_layer
を実行して shared_package_layer サブプロジェクトを作成します。
serverless.yml を以下の内容に変更します。
service: shared-package-layer plugins: - serverless-python-requirements custom: pythonRequirements: dockerizePip: true # Lambda Layer の定義は pythonRequirements の下に記述する layer: name: shared-package-layer description: 共通パッケージ用 Lambda Layer provider: name: aws runtime: python3.8 stage: dev region: ap-northeast-1 resources: Outputs: # 他の Stack から Lambda Layer を参照できるようにする # Value に記載している "PythonRequirementsLambdaLayer" はこの文字列固定である SharedPackageLayer: Value: Ref: PythonRequirementsLambdaLayer
以下のファイルを削除・移動します。
- handler.py は不要なので削除します。
- resize_service/requirements.txt を shared_package_layer ディレクトリの下に移動します。
ここまででディレクトリ構成は以下のようになります。
deploy します。
マネジメントコンソールで確認すると Lambda Layer が作成されており(バージョンが 1 でないのは何度も試しているからです)、
「ダウンロード」ボタンをクリックして zip ファイルの中身を確認すると、
python ディレクトリの下に Pillow のパッケージが入っていました。
独自モジュール(.py ファイル)を配置する Lambda Layer 用の my_module_layer サブプロジェクトを作成する
外部パッケージだけでなく自分で作成したモジュール(.py ファイル)も Lambda Layer に配置してみます。ポイントは以下の2点です。
- こちらは Serverless Framework のドキュメントの AWS - Layers の設定を使用する。
- python ディレクトリの下に配置する。
プロジェクトのルートディレクトリの下で npx sls create --template aws-python3 --path my_module_layer
を実行して my_module_layer サブプロジェクトを作成します。
serverless.yml を以下の内容に変更します。
service: my-module-layer provider: name: aws runtime: python3.8 stage: dev region: ap-northeast-1 package: include: - ./python/** layers: # こちらの名称には "Layer" は付けない # "my-module-layer" の前半の "my-module" だけ取り出して MyModule にする MyModule: path: . name: my-module-layer description: 独自モジュール用 Lambda Layer compatibleRuntimes: - python3.8 resources: Outputs: # How to publish and use AWS Lambda Layers with the Serverless Framework # https://www.serverless.com/blog/publish-aws-lambda-layers-serverless-framework/ # # こちらの名称には "Layer" を付ける # "my-module-layer" から MyModuleLayer にする # 他の Stack から参照する時のキーになる MyModuleLayer: Value: # layers に書いた "MyModule" + "LambdaLayer" の文字列で参照する Ref: MyModuleLambdaLayer
handler.py は不要なので削除し、my_module_layer ディレクトリの下に python ディレクトリを作成します。
独自モジュール(.py ファイル)は my_module_layer/python の下に image_lib ディレクトリを作成し、その下に resize_utils.py を作成して、resize_service/handler.py の中に定義していた resize_image 関数を持ってくることにします。
my_module_layer/python/image_lib/resize_utils.py に以下の内容を記述します。
from PIL import Image thumbnail_size = 320, 180 def resize_image(image_path, resized_path): with Image.open(image_path) as image: image.thumbnail(thumbnail_size) image.save(resized_path)
ここまででディレクトリ構成は以下のようになります。
deploy します。
マネジメントコンソールで確認すると Lambda Layer が作成されており(バージョンが 1 でないのは何度も試しているからです)、
「ダウンロード」ボタンをクリックして zip ファイルの中身を確認すると、python ディレクトリの下に image_lib/resize_utils.py が入っていました。
resize_service ディレクトリ内のファイルを変更する
serverless.yml の以下の点を変更します。
service: resize-service provider: name: aws runtime: python3.8 stage: dev region: ap-northeast-1 iamRoleStatements: - Effect: "Allow" Action: - "s3:GetObject" Resource: - "arn:aws:s3:::ksbysample-upload-bucket/*" - Effect: "Allow" Action: - "s3:PutObject" Resource: - "arn:aws:s3:::ksbysample-resize-bucket/*" functions: resize: handler: handler.resize events: # Using existing buckets # https://www.serverless.com/framework/docs/providers/aws/events/s3#using-existing-buckets - s3: ksbysample-upload-bucket layers: # Stack名はマネジメントコンソールの CloudFormation > スタック で確認する - ${cf:shared-package-layer-dev.SharedPackageLayer} - ${cf:my-module-layer-dev.MyModuleLayer} resources: Resources: KsbysampleResizeBucket: Type: AWS::S3::Bucket Properties: BucketName: ksbysample-resize-bucket
- serverless-python-requirements プラグインを使用しないので plugins、custom の定義を削除します。
- functions - resize の下に layers を追加し、以下の2行を追加します。他の Stack の Outputs を参照する方法は Reference CloudFormation Outputs に記載があります。
- ${cf:shared-package-layer.SharedPackageLayer}
- ${cf:my-module-layer.MyModuleLayer}
handler.py の以下の点を変更します。
import logging import os import re import uuid from urllib.parse import unquote_plus import boto3 from image_lib import resize_utils logger = logging.getLogger() logger.setLevel(logging.INFO) def resize(event, context): s3_client = boto3.client('s3') for record in event['Records']: bucket = record['s3']['bucket']['name'] key = unquote_plus(record['s3']['object']['key']) # ディレクトリの場合には何もしない if key.endswith('/'): return tmpkey = key.replace('/', '') download_path = '/tmp/{}{}'.format(uuid.uuid4(), tmpkey) resized_path = '/tmp/resized-{}'.format(tmpkey) filename = key.split('/')[-1] dirname = re.sub(filename + '$', '', key) basename, ext = os.path.splitext(filename) resized_key = '{}{}_thumb{}'.format(dirname, basename, ext) s3_client.download_file(bucket, key, download_path) resize_utils.resize_image(download_path, resized_path) s3_client.upload_file(resized_path, "ksbysample-resize-bucket", resized_key) logger.info('サムネイルを生成しました({})'.format(resized_key))
- my_module_layer/python/image_lib/resize_utils.py に移動した resize_image 関数を削除します。
- 以下の記述も削除します。
from PIL import Image
thumbnail_size = 320, 180
- 以下の記述を追加します。
from image_lib import resize_utils
resize_image(download_path, resized_path)
→resize_utils.resize_image(download_path, resized_path)
に変更します。
後編に続く
履歴
2020/06/20
初版発行。
resize-image-app-project プロジェクトで作成した AWS Lambda のユニットテストを Docker コンテナ上で動作させる
概要
記事一覧はこちらです。
resize-image-app-project プロジェクトで作成した AWS Lambda のユニットテストを作成する(local動作版) でユニットテストを作成しましたが、Pillow は OS 依存のバイナリがあるので lambci/lambda:build-python3.8 の Docker Image 上でユニットテストを実行して動作確認する方法を調べてみます。
最初 lambci/lambda:python3.8 を利用してユニットテストを動かそうとしたのですが、
- lambci/lambda:python3.8 で Lambda の関数(resize_service/handler.resize)を呼び出す方法は分かったのですが、
python -m unittest -v
コマンドを実行する方法が全然分かりません。 - lambci / docker-lambda の Build Examples を見ると
docker run -it lambci/lambda:build-python3.8 bash
と記述されており、これで bash を起動するとpython -m unittest -v
コマンドを実行することが出来ました。
という理由で lambci/lambda:build-python3.8 の Docker Image を利用することにしました。
参照したサイト・書籍
lambci / docker-lambda
https://github.com/lambci/docker-lambdaIn Git for windows “git bash”, how to “print working directory” in Windows path format that is usable by cmd and Windows explorer?
https://stackoverflow.com/questions/44842275/in-git-for-windows-git-bash-how-to-print-working-directory-in-windows-path/48777179Configure an interpreter using Docker
https://www.jetbrains.com/help/pycharm/using-docker-as-a-remote-interpreter.html
目次
- lambci/lambda:build-python3.8 をベースに Pillow、moto をインストールしたカスタム Docker Image を作成する
- Docker コンテナでユニットテストを実行する
- IntelliJ IDEA からユニットテストを debug 実行する時にコードが Docker コンテナで動くようにする
- (メモ書き)lambci/lambda:python3.8 をベースにカスタム Docker Image を作成する
手順
lambci/lambda:build-python3.8 をベースに Pillow、moto をインストールしたカスタム Docker Image を作成する
プロジェクトのルートディレクトリ直下に Dockerfile を作成して以下の内容を記述します。
FROM lambci/lambda:build-python3.8 COPY resize_service/requirements.txt /tmp/requirements.txt RUN pip install --upgrade pip RUN pip install -r /tmp/requirements.txt RUN pip install moto
コマンドプロンプト(今回は git-bash)で docker build . -t lambci/lambda:build-python3.8-resize-image-app-project
を実行してカスタム Docker Image を作成します。
Docker コンテナでユニットテストを実行する
docker run --rm -it -v ``pwd -W``:/var/task:rw,delegated lambci/lambda:build-python3.8-resize-image-app-project bash
(pwd -W 前後の ` は1つだけに変更すること)を実行して Docker コンテナで bash を起動します。
/var/task
にプロジェクトのルートディレクトリがマウントされていることが確認できます。
pwd -W
の部分は Web の記事を見ると $(pwd)
と書かれていますが、
- Windows 上の git-bash だと
$(pwd)
は/d/project-serverless/ksbysample-serverless/resize-image-app-project
を返す。 - Windows で docker コマンドの -v オプションにマウント元を渡す時には
/d/...
ではなくD:\...
で渡さないとマウントされない(エラーになる)。
という理由で pwd -W
に変えました。試しに git-bash で実行してみると以下のようになります。
python -m unittest -v
を実行するとテストが1件成功しました。
exit コマンドで Docker コンテナから抜けます。
IntelliJ IDEA からユニットテストを debug 実行する時にコードが Docker コンテナで動くようにする
IntelliJ IDEA のメインメニューから「File」-「Project Structure...」を選択して「Project Structure」ダイアログを表示します。
画面左側で「SDKs」を選択した後、中央のリストの上部から「+」-「Add Python SDK...」を選択します。
「Add Python Interpreter」ダイアログが表示されます。画面左側で「Docker」を選択してから、画面右側の「Image name」で作成したカスタム Docker Image(lambci/lambda:build-python3.8-resize-image-app-project)を選択して「OK」ボタンをクリックします。
「Project Structure」ダイアログに戻ると「Remote Python 3.8.3 Docker (lambci/lambda:build-python3.8-resize-image-app-project)」が追加されています。「OK」ボタンをクリックしてダイアログを閉じます。
IntelliJ IDEA のメインメニューから「Run」-「Edit Configurations...」を選択して「Run/Debug Configurationis」ダイアログを表示します。
前回作成した Python tests の設定で以下の点を変更します。
- 「Python initerpreter」を「Use SDK of module」→「Use specified interpreter」に変更した後、「Remote Python 3.8.3 Docker (lambci/lambda:build-python3.8-resize-image-app-project)」を選択します。
次に「Docker container settings」で右側のフォルダアイコンをクリックします。
「Edit Docker Container Settings」ダイアログが開くのでマウント先の Container Path を「/opt/project」→「/var/task」に変更します。
「Run/Debug Configurationis」ダイアログに戻ってから「Docker container settings」の設定が以下の画像のようになっていることを確認した後「OK」ボタンをクリックしてダイアログを閉じます。
以上で設定は完了です。次は debug 実行してみます。
まず IntelliJ IDEA の Services Tool Window で Docker コンテナが1つも存在しないことを確認してから、
IntelliJ IDEA のエディタ上で handler.resize(event, None)
の行に breakpoint を設定して「Debug 'Unittests for test_r...'」を選択します。
Console に以下の画像のように表示された後、breakpoint で止まります。
Services Tool Window を見ると debug 用に起動していると思われるコンテナ(実行中)と /pycharm_helpers_IU...
から始まるコンテナ(停止している)がありました。
Debugger で「Step Info」「Step Over」で処理を進めることができることも確認できます。
debug 実行を終了させると、
コンテナは /pycharm_helpers_IU...
から始まるもののみ残っていました。こちらは自動で削除されないようです。
(メモ書き)lambci/lambda:python3.8 をベースにカスタム Docker Image を作成する
今回は作成しませんでしたが build-
が付かない lambci/lambda:python3.8 をベースにカスタム Docker Image を作成する時は Dockerfile の書き方が少し異なるのでメモ書きとして残しておきます。
FROM に lambci/lambda:python3.8
を記述した Dockerfile を作成し、
FROM lambci/lambda:python3.8 COPY resize_service/requirements.txt /tmp/requirements.txt RUN pip install --upgrade pip RUN pip install -r /tmp/requirements.txt RUN pip install moto
docker build . -t lambci/lambda:python3.8-resize-image-app-project
を実行すると ERROR が出て Docker Image を作成できません。
Dockerfile に USER root
を追加します。
FROM lambci/lambda:python3.8 USER root COPY resize_service/requirements.txt /tmp/requirements.txt RUN pip install --upgrade pip RUN pip install -r /tmp/requirements.txt RUN pip install moto
再度 docker build . -t lambci/lambda:python3.8-resize-image-app-project
を実行すると今度は Docker Image が作成できます。
(..........途中省略..........)
履歴
2020/06/13
初版発行。
resize-image-app-project プロジェクトで作成した AWS Lambda のユニットテストを作成する(local動作版)
概要
記事一覧はこちらです。
S3 にアップロードされた画像ファイルから Lambda でサムネイル画像を生成してみる で作成した AWS Lambda のユニットテストを作成してみます。
前回 deploy するための外部ライブラリを収集するのに lambci/lambda:build-python3.8 という Docker Image を利用したので、おそらく Docker 上で動作確認することもできると思いますが、Testing Serverless Services という Web ページを見つけて local で動作させることもできるようなので試してみます。
参照したサイト・書籍
Testing Serverless Services
https://towardsdatascience.com/testing-serverless-services-59c688812a0dPython実践入門 ── 言語の力を引き出し、開発効率を高める WEB+DB PRESS plus
- unittest モジュールの使い方が記載されていて参考にしました。
spulec / moto
https://github.com/spulec/motoWhat is the fastest way to empty s3 bucket using boto3?
https://stackoverflow.com/questions/43326493/what-is-the-fastest-way-to-empty-s3-bucket-using-boto3
目次
- ディレクトリ名を resize-service → resize_service に変更する
- プロジェクトのルートディレクトリ直下に tests ディレクトリを作成してテストのサンプルを作成・実行する
- moto モジュールをインストールする
- test_resize.py にテストを実装して実行してみる。。。が NoCredentialsError が発生する
- NoCredentialsError のエラーを解消する
- IntelliJ IDEA から debug 実行してみる
- 最後にディレクトリ構成を記載する
手順
ディレクトリ名を resize-service → resize_service に変更する
プロジェクト名に -
(ハイフン) が含まれているとテストのファイルから AWS Lambda のファイルを import できません。IDEA で import 文を書いた時に赤波線が表示されて気づきました。
PEP 8 -- Style Guide for Python Code の Package and Module Names には all-lowercase names
にするか Underscores
が使用できると記載されていますが、ディレクトリ名を _
(アンダースコア) に変更して serverless.yml の service 名も同じに変更すると今度は deploy が成功しません。
The stack service name "resize_service-dev" is not valid. A service name should only contain alphanumeric (case sensitive) and hyphens. It should start with an alphabetic character and shouldn't exceed 128 characters.
のエラーが出ます。
ディレクトリ名は _
(アンダースコア) に変更し(resize_service)、serverless.yml の service 名は -
(ハイフン)のままにします(resize-service)。
service: resize-service ..........
ちなみに npx sls create --template aws-python3 --path xxxxxxxx_service
と create コマンドに渡すディレクトリ名に _
(アンダースコア)を含めた場合、
serverless.yml の service 名は -
(ハイフン)になりました。
プロジェクトのルートディレクトリ直下に tests ディレクトリを作成してテストのサンプルを作成・実行する
プロジェクトのルートディレクトリ直下に tests ディレクトリを作成し、その下に __init__.py
というファイルを作成します(中身は空)。
test_resize.py というファイルも作成し以下の内容を記述してから、
import unittest class TestResizeService(unittest.TestCase): def test_resize(self): None
コマンドプロンプトを起動して以下のコマンドを実行すると、unittest モジュールにテストのファイルが認識されて1件成功します。
venv\Scripts\activate
python -m unittest -v
テストを実行すると tests ディレクトリの下に __pycache__
というディレクトリが作成されますが git で管理する必要のないディレクトリなので .gitignore に __pycache__/
の記述を追加して無視されるようにします。
.......... # Ignore Python unittest __pycache__/ ..........
ちなみに __init__.py
というファイルがないと unittest モジュールにテストが認識されません。最初テストのファイルだけ作成して認識されなくて少し悩みました。。。
moto モジュールをインストールする
AWS Service をモック化してくれる moto モジュールをインストールします。
pip install moto
を実行します。依存するモジュールがいろいろインストールされるので少し時間がかかります。
test_resize.py にテストを実装して実行してみる。。。が NoCredentialsError が発生する
tests ディレクトリの下にテストで使用する sample.jpg をコピーし、s3_event.json というファイルを新規作成して S3 のイベントの JSON を記述します。
{ "Records": [ { "eventVersion": "2.1", "eventSource": "aws:s3", "awsRegion": "ap-northeast-1", "eventTime": "2020-06-10T00:45:25.995Z", "eventName": "ObjectCreated:Put", "userIdentity": { "principalId": "AWS:XXXXXXXXXXXXXXXXXXXXX" }, "requestParameters": { "sourceIPAddress": "xxx.xxx.xxx.xxx" }, "responseElements": { "x-amz-request-id": "11F4BEB2A6E4D924", "x-amz-id-2": "k2RJ6yeU8sk5tRp7ntfyM8QDmR58rF1TIdacJimdExhLOOSQ3N6pOtIAWXyk6pGjTeBGOBx995ELqaK53zhdAo6Ky/PZb08e" }, "s3": { "s3SchemaVersion": "1.0", "configurationId": "66e09001-2a37-4900-9a98-06da126cc896", "bucket": { "name": "ksbysample-upload-bucket", "ownerIdentity": { "principalId": "XXXXXXXXXXXXXX" }, "arn": "arn:aws:s3:::ksbysample-upload-bucket" }, "object": { "key": "sample.jpg", "size": 669286, "eTag": "febe0aaf4d817457776b6be293973442", "sequencer": "005EE02D28714EE2DC" } } } ] }
test_resize.py にテストを実装します。
import json import unittest import boto3 from moto import mock_s3 from resize_service import handler @mock_s3 class TestResizeService(unittest.TestCase): UPLOAD_BUCKET = 'ksbysample-upload-bucket' RESIZE_BUCKET = 'ksbysample-resize-bucket' def setUp(self): s3_client = boto3.client('s3') s3_client.create_bucket(Bucket=TestResizeService.UPLOAD_BUCKET) s3_client.create_bucket(Bucket=TestResizeService.RESIZE_BUCKET) def tearDown(self): s3 = boto3.resource('s3') upload_bucket = s3.Bucket(TestResizeService.UPLOAD_BUCKET) upload_bucket.objects.all().delete() upload_bucket.delete() resize_bucket = s3.Bucket(TestResizeService.RESIZE_BUCKET) resize_bucket.objects.all().delete() resize_bucket.delete() def test_resize(self): s3_client = boto3.client('s3') s3_client.upload_file('tests/sample.jpg', TestResizeService.UPLOAD_BUCKET, 'sample.jpg') with open('tests/s3_event.json', 'r') as f: event = json.load(f) handler.resize(event, None) thumb_object = s3_client.get_object(Bucket=TestResizeService.RESIZE_BUCKET, Key='sample_thumb.jpg') self.assertEqual(thumb_object['ResponseMetadata']['HTTPStatusCode'], 200) self.assertGreater(int(thumb_object['ResponseMetadata']['HTTPHeaders']['content-length']), 0) # 生成されたサムネイル画像をダウンロードすることも出来る(実際に作成される) # s3_client.download_file(TestResizeService.RESIZE_BUCKET, 'sample_thumb.jpg', # 'tests/sample_thumb.jpg')
python -m unittest -v
を実行してみると raise NoCredentialsError
のエラーが出てテストが失敗しました。。。
NoCredentialsError のエラーを解消する
Web で調べると AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY が設定されていないことが原因と書かれている記事を見かけるのですが、解決したという人としなかったという人を見かける上に、テストの方はモックの S3 に問題なくアクセスできていて今ひとつ良く分かりません。
試しに AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY をいくつかの方法で設定してみると以下の結果でした。
- test_resize メソッド内で AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY の環境変数を設定するのは NG。
- resize_service/handler.py で
s3_client = boto3.client('s3')
→s3_client = boto3.client('s3', aws_access_key_id='test', aws_secret_access_key='test')
に変更すると OK。 python -m unittest -v
を実行するコマンドプロンプトで AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY の環境変数を設定すると OK。
何となく原因が分かりました。おそらく以下の動作になっているのでしょう。
- test_resize.py が読み込まれる。
from resize_service import handler
により resize_service/handler.py が読み込まれる。- resize_service/handler.py のグローバルで
s3_client = boto3.client('s3')
が実行される。この時は boto3 の s3 クライアントはまだモックではなく Credentials の情報がセットされない。 - test_resize.py の TestResizeService.test_resize メソッドが実行される。この時は mock_s3 デコレータ により boto3 の s3 クライアントはモック化されて Credentials の情報がセットされる。
- 結果として test_resize.py 内の処理ではモックの S3 に問題なくアクセスできるが、resize_service/handler.py 内の処理でモックの S3 にアクセスしようとすると
raise NoCredentialsError
のエラーが出る。
resize_service/handler.py の s3_client = boto3.client('s3')
を記述する位置を resize 関数の最初に変更します。
.......... logger = logging.getLogger() logger.setLevel(logging.INFO) def resize_image(image_path, resized_path): with Image.open(image_path) as image: image.thumbnail(thumbnail_size) image.save(resized_path) def resize(event, context): s3_client = boto3.client('s3') for record in event['Records']: bucket = record['s3']['bucket']['name'] ..........
python -m unittest -v
を実行すると今度は成功しました。
IntelliJ IDEA から debug 実行してみる
test_resize.py 内の handler.resize(event, None)
の行に breakpoint をセットしてから、IDEA のエディタの左側に表示されている矢印をクリックして Debug 'Unittests for test_r...'
を選択します。
初回は FileNotFoundError: [WinError 3] 指定されたパスが見つかりません。: 'tests/sample.jpg'
のエラーが出てエラーになります。
IDEA のメインメニューから「Run」-「Edit Configurations...」を選択して「Run/Debug Configurations」ダイアログを表示した後、「Python tests」の下に表示されている項目を選択してから画面右側の「Working directory」の設定を D:\project-serverless\ksbysample-serverless\resize-image-app-project\tests
→ D:\project-serverless\ksbysample-serverless\resize-image-app-project
に変更します(プロジェクトのルートディレクトリにする)。
再度 Debug 'Unittests for test_r...'
を選択して debug 実行すると今度は breakpoint で止まり、「Step Into」ボタンを押せば resize_service/handler.py の resize 関数に進みます。
最後にディレクトリ構成を記載する
Python には標準で unittest モジュールが用意されていたり、moto という AWS Service をモック化するモジュールが存在したり、IntelliJ IDEA で Lambda のソースを debug 実行できたりして、Serverless は debug しにくいと思っていましたが結構開発しやすくて意外でした。
履歴
2020/06/13
初版発行。
IntelliJ IDEA で Java Flight Recorder を有効にして実行する
記事一覧はこちらです。
会社の IntelliJ IDEA で開発をしていた時に画面右上のボタンに Run with Java Flight Recorder のようなボタンを見かけた気がしていて、家に帰ってから IntellJ IDEA を起動して確認するとそんなボタンが見当たりません。
位置的には下の画像の赤枠のボタンのはずなのですが、「Run with Profiler」というラベルが出るだけでボタンが押せません。。。
調べてみると Profiling Tools and IntelliJ IDEA Ultimate のページが見つかりました。書かれたのは March 6, 2020 で最近ですね。
Settings ダイアログを開いて Java Profiler の設定を見ると中央のリストには何も表示されていません。ボタンが押せないはずです。
AdoptOpenJDK の Migration Guide を見ると Java Flight Recorder は OpenJDK 11 に入っているようなので試してみます。
Settings ダイアログの Java Profiler の設定の中央のリストで「+」ボタンを押すと「Java Flight Recorder」が出てきたので選択します。
中央のリストに「Java Flight Recorder」が追加されます。設定は「Default」のままにして「OK」ボタンを押してダイアログを閉じます。
画面右上の「Run with Profiler」のドロップダウンリストをクリックすると「Run 'Application' with 'Java Flight Recorder'」のメニューが表示されました。クリックして実行します。
Tomcat が起動して、更に初めて見る「Profiler」タブが表示されました。
「Profiler」タブをクリックすると以下の画面が表示されて、
「Stop Profiling and Show Results」リンクをクリックすると Profile 結果が表示されました。残念ながら見方が全然分かりません。。。
Java Flight Recorder が Java 11 から OpenJDK でも使えるようになったというのは見かけていましたが、使うのが面倒そうだったので1度も試していなかったんですよね。IntelliJ IDEA から実行できるようになったのであれば試してみようと思います。