AWS Lambda 関数¶
はじめに¶
本サイトにつきまして、【IT技術の知見】はじめに - 俺の技術ノート をご認識のほど宜しくお願いいたします。
01. ハンドラ関数¶
ハンドラ関数とは¶
自身から起動することはなく、外部から要求されて実行される関数のこと。
AWS Lambda ハンドラ関数¶
▼ 非同期ハンドラ関数 (Async handlers)¶
AWS Lambda はハンドラ関数を非同期関数としてコールし、引数のオブジェクト (event) に値をわたす。
ハンドラ関数の初期名は handler() 関数であるが別名でもよい。
return または throw を使用して、AWS Lambda のコール元にレスポンスを返信する。
レスポンスとして、Promise オブジェクトを送信もできる。
JavaScript の場合を示す。
exports.handler = async (event) => {
const response = {
statusCode: null,
body: null,
};
response.statusCode = 200;
response.body = "Hello World!";
// もしくはthrowを使用して、レスポンスを返信する。
return response;
};
import aws from "aws-sdk";
const s3 = new aws.S3();
exports.handler = async function (event) {
// Promiseオブジェクトをレスポンスとして送信する。
return s3.listBuckets().promise();
};
exports.handler = async (event) => {
// Promiseオブジェクトをレスポンスとして送信する。
return new Promise((resolve, reject) => {
// 何らかの処理
});
};
▼ 同期ハンドラ関数 (Non-async handlers)¶
AWS Lambda はハンドラ関数を同期関数としてコールし、引数 (event オブジェクト、context オブジェクト、callback 関数) に値をわたす。
このオブジェクトには関数とプロパティを持つ。
ハンドラ関数の初期名は handler() であるが別名でもよい。
callback() 関数を使用して、AWS Lambda のコール元に Promise オブジェクトのレスポンスを返信する。
(※『Non』が翻訳をおかしくしているため、英語版を推奨)
JavaScript の場合を示す。
レスポンスを返信するには、done() 関数、succeed() 関数、callback() 関数が必要である。
また、処理を終える場合は return で返却する必要がある。
exports.handler = (event, context, callback) => {
// なんらかの処理
// context以前の処理を待機はしない
context.done(null /*レスポンス*/);
// 処理を終える場合
// return context.done(null, /*レスポンス*/)
};
exports.handler = (event, context, callback) => {
// なんらかの処理
// context以前の処理を待機はしない
context.succeed(/*レスポンス*/);
// 処理を終える場合
// return context.succeed( /*レスポンス*/ )
};
exports.handler = (event, context, callback) => {
// なんらかの処理
// callback以前の処理を待機する。
callback(null /*レスポンス*/);
// 処理を終える場合
// return callback(null, /*レスポンス*/)
};
▼ 予約された引数の説明¶
| 引数 | 説明 | 補足 |
|---|---|---|
| event オブジェクト | HTTP リクエストに関するデータが代入されている。 | AWS Lambda にリクエストを送信する AWS リソースごとに、オブジェクトの構造が異なる。構造は以下の通り。 ・https://docs.aws.amazon.com/lambda/latest/dg/lambda-services.html |
| context オブジェクト | AWS Lambda に関するデータ (名前、バージョンなど) を取得できる関数とプロパティが代入されている。 | オブジェクトの構造は以下の通り ・https://docs.aws.amazon.com/lambda/latest/dg/nodejs-context.html |
| callback 関数 | 代入されている関数の実体は不明である。すべての処理が終了するまで実行が待機され、AWS Lambda のコール元にレスポンスを返信する。 | ・https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html |
▼ テストとデバッグ¶
AWS Lambda で関数を作成すると、Amazon CloudWatch Logs のロググループに、『/aws/lambda/<関数名>』というグループが自動的に作成される。AWS Lambda の関数内で発生したエラーや console.log() 関数のログはここに出力されるため、都度確認すること。
▼ ベストプラクティス¶
02. Go による実装¶
aws-lambda-go¶
▼ aws-lambda-go とは¶
Go を使用して、AWS Lambda-API へリクエストを送信し、AWS リソースを操作できる。
▼ Start() 関数¶
Lambda のエントリーポイント関数を実行するための関数。
Start() 関数に渡すパラメーターには、必ず 1 つでも error インターフェースの実装が含まれている必要がある。
もし含まれていない場合は、AWS Lambda で内部エラーが起こる。
package main
import (
"context"
"fmt"
"github.com/aws/aws-lambda-go/lambda"
)
type MyEvent struct {
Name string `json:"name"`
}
// HandleRequest リクエストをハンドリングします。
func HandleRequest(ctx context.Context, name MyEvent) (string, error) {
return fmt.Sprintf("Hello %s!", name.Name), nil
}
func main() {
// AWS Lambda関数を実行します。
lambda.Start(HandleRequest)
}
▼ パラメータ¶
context オブジェクトと event オブジェクトをパラメーターとして使用できる。
event オブジェクトの種類¶
▼ 全種類¶
▼ Amazon SNS イベントの場合¶
package main
import (
"context"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-lambda-go/lambdacontext"
)
/**
* AWS Lambdaハンドラー関数
*/
func HandleRequest(context context.Context, event events.SNSEvent) (string, error) {
}
func main() {
lambda.Start(HandleRequest)
}
▼ Amazon CloudWatch イベントの場合¶
package main
import (
"context"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-lambda-go/lambdacontext"
)
/**
* AWS Lambdaハンドラー関数
*/
func HandleRequest(context context.Context, event events.CloudWatchEvent) (string, error) {
}
func main() {
lambda.Start(HandleRequest)
}
▼ Amazon API Gateway イベントの場合¶
package main
import (
"context"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-lambda-go/lambdacontext"
)
/**
* AWS Lambdaハンドラー関数
*/
func HandleRequest(context context.Context, event events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
}
func main() {
lambda.Start(HandleRequest)
}
レスポンス¶
▼ 正常系¶
正常系レスポンスの構成要素については以下のリンクを参考にせよ。
文字列を返却すると、AWS Lambda はその文字列をそのまま返信する。
また、JSON をレスポンスもできる。
▼ 異常系¶
AWS Lambda のエラーレスポンスのステータスコードについては以下のリンクを参考にせよ。
エラーレスポンスのメッセージボディには以下の JSON が割り当てられる。
{"errorMessage": "<エラーメッセージ>", "errorType": "<エラータイプ>"}
errors パッケージの New() 関数を使用すると、内部で発生したエラーメッセージをオーバーライドできる。
package main
import (
"errors"
"github.com/aws/aws-lambda-go/lambda"
)
func HandleRequest() (string, error) {
return "", errors.New("something went wrong!")
}
func main() {
lambda.Start(OnlyErrors)
}
/* 結果
{
"errorMessage": "something went wrong!",
"errorType": "errorString"
}
*/
ログ¶
▼ レポートログ¶
| 機能名 | |
|---|---|
| RequestId | リクエスト ID |
| Duration | イベントの処理時間 |
| Billed Duration | AWS Lambda の課金対象の時間 |
| Memory Size | AWS Lambda のメモリサイズ |
| Max Memory Used | AWS Lambda が実際に使用するメモリの最大サイズ |
▼ ログの出力方法¶
標準パッケージの fmt、または任意のロギングパッケージを使用して、標準出力/標準エラー出力に出力する。
Amazon CloudWatch Logs にてこれを確認する。
02-02. 関数例¶
AWS Amplify ➡️ Amazon EventBridge ➡️ AWS Lambda ➡️ Slack-API¶
03. JavaScript による実装¶
デフォルトで使用できるパッケージ¶
以下のパッケージでは、npm を使用する必要はない。
パッケージから提供されるパッケージの関数のほとんどが非同期処理として実装されている。
もし後続の処理で非同期処理の結果を使用したい場合、非同期処理の状態を Promise オブジェクトで管理する必要がある。
| パッケージ名 | 説明 | 補足 |
|---|---|---|
| JavaScript の標準パッケージ | JavaScript にビルトイン関数を使用できる | ・https://nodejs.org/api/index.html |
aws-sdk.js |
JavaScript を使用して、AWS-API へリクエストを送信し、AWS リソースを操作できる。 | ・https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/index.html |
非同期処理の成否の管理¶
▼ JavaScript の標準パッケージの場合¶
▼ aws-sdk.js の場合¶
各 AWS オブジェクトの関数の後に、promise() 関数をチェーンできる。
これにより、各関数の非同期処理の成否を Promise オブジェクトで管理できるようになる。
"use strict";
import aws from "aws-sdk";
/**
* @param event
* @returns Promise<json>
*/
exports.handler = async (event) => {
const ec2 = new aws.EC2({apiVersion: "2014-10-01"});
// Promiseオブジェクトを返却する
const ec2Instances = ec2.describeInstances().promise();
ec2Instances.then(
(data) => {
// 非同期処理が成功した時の後続処理
},
(error) => {
// 非同期処理が失敗した時の後続処理
},
);
};
03-02. 関数例¶
AWS Amplify ➡️ Amazon EventBridge ➡️ AWS Lambda ➡️ Slack-API¶
AWS Amplify のイベントを Amazon EventBridge でキャッチし、これを AWS Lambda にフォワーディングする。AWS Lambda では、メッセージを構成し、Slack-API に送信する。
"use strict";
const aws = require("aws-sdk");
const https = require("https");
const { format } = require("util");
/**
* @param event
* @returns Promise<json>
*/
exports.handler = async (event) => {
console.log(JSON.stringify({event}, null, 2));
const amplify = new aws.Amplify({apiVersion: "2017-07-25"});
const option = {
appId: event.detail.appId,
branchName: event.detail.branchName,
};
let result;
try {
// AWS Amplifyのブランチ情報を取得します。
const app = await amplify.getBranch(option).promise();
console.log(JSON.stringify({app}, null, 2));
const message = buildMessage(event, app);
console.log(message);
result = await postMessageToSlack(message);
} catch (error) {
console.error(error);
}
console.log(JSON.stringify({result}, null, 2));
return result;
};
/**
* メッセージを作成します。
*
* @param event
* @param app
* @returns string
*/
const buildMessage = (event, app) => {
return JSON.stringify({
channel: process.env.SLACK_CHANNEL_ID,
text: "develop環境 通知",
attachments: [
{
color: event.detail.jobStatus === "SUCCEED" ? "#00FF00" : "#ff0000",
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: format(
"%s環境",
event.detail.appId === process.env.AMPLIFY_APP_ID_PC
? ":computer: PC"
: ":iphone: SP",
),
},
},
{
type: "context",
elements: [
{
type: "mrkdwn",
text: format(
"*結果*: %s",
event.detail.jobStatus === "SUCCEED" ? "成功" : "失敗",
),
},
],
},
{
type: "context",
elements: [
{
type: "mrkdwn",
text: format("*ブランチ名*: %s", event.detail.branchName),
},
],
},
{
type: "context",
elements: [
{
type: "mrkdwn",
text: format(
"*プルリクエストURL*: https://github.com/foo-repository/compare/%s",
event.detail.branchName,
),
},
],
},
{
type: "context",
elements: [
{
type: "mrkdwn",
text: format(
"*検証URL*: https://%s.%s.amplifyapp.com",
app.branch.displayName,
event.detail.appId,
),
},
],
},
{
type: "context",
elements: [
{
type: "mrkdwn",
text: format(
":amplify: <https://%s.console.aws.amazon.com/amplify/home?region=%s#/%s/%s/%s|*AWS Amplifyコンソール画面はこちら*>",
event.region,
event.region,
event.detail.appId,
app.branch.displayName,
event.detail.jobId,
),
},
],
},
{
type: "divider",
},
],
},
],
});
};
/**
* メッセージを送信します。
*
* @param message
* @returns Promise<json>
*/
const postMessageToSlack = (message) => {
// 非同期処理を持つ関数をコンストラクタに渡し、非同期処理を管理します。
return new Promise((resolve, reject) => {
const options = {
host: "slack.com",
path: "/api/chat.postMessage",
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + process.env.SLACK_API_TOKEN,
"Content-Length": Buffer.byteLength(message),
},
};
// 非同期処理
const request = https.request(options, (response) => {
console.info({response}, null, 2);
let tmp = "";
// 正常なレスポンスからデータを取り出します。
response.on("data", (data) => {
tmp += data.toString();
});
// 異常なレスポンスからエラーを取り出します。
response.on("error", (error) => {
reject(error);
});
// data、error、endの間でawaitの効力は横断できない。
// そのため、できるだけendで事後処理を実装し、awaitを使用する。
response.on("end", async () => {
const body = JSON.parse(tmp);
const result = {
statusCode: response.statusCode,
body: body,
};
if (response.statusCode !== 200 || !body.ok) {
return reject(result);
}
return resolve(result);
});
});
request.on("error", (error) => {
console.error(JSON.stringify({error}, null, 2));
});
// メッセージボディを設定して、リクエストを送信します。
request.write(message);
request.end();
console.log(JSON.stringify({request}, null, 2));
});
};
Amazon API Gateway ➡️ AWS Lambda ➡️ Amazon S3¶
Amazon API Gateway でリクエストを受信し、それに応じて特定のデータを Amazon S3 に保管する。
AWS Lambda が Amazon S3 に対してアクションを実行できるように、事前に、AWS 管理ポリシーの『LambdaExecute』が紐付けられたロールを AWS Lambda に紐付けしておく必要がある。
"use strict";
import aws from "aws-sdk";
const s3 = new aws.S3();
exports.handler = (event, context, callback) => {
// Amazon API Gatewayとのプロキシ統合を意識したJSON構造にする。
// レスポンスの初期値
const response = {
statusCode: null,
body: null,
};
// 認証バリデーション
if (event.headers["X-API-Key"] !== process.env.X_API_KEY) {
response.statusCode = 401;
response.body = "An API key is invalid.";
return callback(null, response);
}
// リクエストバリデーション
if (!event.headers || !event.body) {
response.statusCode = 400;
response.body = "Parameters are not found.";
return callback(null, response);
}
s3.putObject(
{
Bucket: "<バケット名>",
Key: "<パスを含む保管先ファイル>",
Body: "<保管データ>",
},
(err, data) => {
if (err) {
response.statusCode = 500;
response.body = "[ERROR] " + err;
return callback(null, response);
}
response.statusCode = 200;
response.body = "OK";
return callback(null, response);
},
);
};
Amazon CloudFront ➡️ AWS Lambda@Edge ➡️ Amazon S3¶

event オブジェクトの domainName と host.value に代入されたバケットのドメイン名によって、フォワーディング先のバケットが決まる。
そのため、この値を切り替えれば動的オリジンを実現できる。
注意点として、各バケットには同じオリジンアクセスアイデンティティを設定する必要がある。
"use strict";
exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
// ログストリームに変数を出力する。
console.log(JSON.stringify({request}, null, 2));
const headers = request.headers;
const s3Backet = getBacketBasedOnDeviceType(headers);
request.origin.s3.domainName = s3Backet;
request.headers.host[0].value = s3Backet;
// ログストリームに変数を出力する。
console.log(JSON.stringify({request}, null, 2));
return callback(null, request);
};
/**
* デバイスタイプを基に、オリジンを切り替える。
*
* @param {Object} headers
* @param {string} env
* @returns {string} pcBucket|spBucket
*/
const getBacketBasedOnDeviceType = (headers) => {
const pcBucket = env + "-bucket.s3.amazonaws.com";
const spBucket = env + "-bucket.s3.amazonaws.com";
if (
headers["cloudfront-is-desktop-viewer"] &&
headers["cloudfront-is-desktop-viewer"][0].value === "true"
) {
return pcBucket;
}
if (
headers["cloudfront-is-tablet-viewer"] &&
headers["cloudfront-is-tablet-viewer"][0].value === "true"
) {
return pcBucket;
}
if (
headers["cloudfront-is-mobile-viewer"] &&
headers["cloudfront-is-mobile-viewer"][0].value === "true"
) {
return spBucket;
}
return spBucket;
};
オリジンリクエストは、以下の event オブジェクトの JSON 型データにマッピングされている。
注意点として、一部のキーは省略している。
{
"Records":
[
{
"cf":
{
"request":
{
"body":
{
"action": "read-only",
"data": "",
"encoding": "base64",
"inputTruncated": "false",
},
"clientIp": "*.*.*.*",
"headers":
{
"host":
[
{
"key": "Host",
"value": "sp-bucket.s3.ap-northeast-1.amazonaws.com",
},
],
"cloudfront-is-mobile-viewer":
[{"key": "CloudFront-Is-Mobile-Viewer", "value": "true"}],
"cloudfront-is-tablet-viewer":
[{"key": "loudFront-Is-Tablet-Viewer", "value": "false"}],
"cloudfront-is-smarttv-viewer":
[
{
"key": "CloudFront-Is-SmartTV-Viewer",
"value": "false",
},
],
"cloudfront-is-desktop-viewer":
[
{
"key": "CloudFront-Is-Desktop-Viewer",
"value": "false",
},
],
"user-agent":
[{"key": "User-Agent", "value": "Amazon CloudFront"}],
},
"method": "GET",
"origin":
{
"s3":
{
"authMethod": "origin-access-identity",
"customHeaders":
{"env": [{"key": "env", "value": "prd"}]},
"domainName": "sp-bucket.s3.amazonaws.com",
"path": "",
"port": 443,
"protocol": "https",
"region": "ap-northeast-1",
},
},
"querystring": "",
"uri": "/images/12345",
},
},
},
],
}
04. Python による実装¶
04-02. 関数例¶
AWS Lambda ➡️ Amazon RDS¶
▼ 自動起動¶
import boto3
import json
def lambda_handler(event, context):
client = boto3.client('rds')
try:
instances = client.describe_db_instances()
instances = client.describe_db_instances()
for instance in instances['DBInstances']:
if instance['DBInstanceStatus'] == "stopped":
tags = client.list_tags_for_resource(ResourceName=instance['DBInstanceArn'])
for tag in tags['TagList']:
# 起動タグがある場合
if tag['Key'] == 'enabled_start_scheduling' and tag['Value'] == 'true':
client.start_db_instance(DBInstanceIdentifier=instance['DBInstanceIdentifier'])
return {
'statusCode': 200,
'body': json.dumps("Amazon RDS instance start operation successfully")
}
except Exception as e:
print(f"Error: {repr(e)}", flush=True)
return {
'statusCode': 500,
'body': json.dumps(f"Error: {repr(e)}")
}
▼ 自動停止¶
import boto3
import json
def lambda_handler(event, context):
client = boto3.client('rds')
try:
instances = client.describe_db_instances()
instances = client.describe_db_instances()
for instance in instances['DBInstances']:
if instance['DBInstanceStatus'] == "available":
tags = client.list_tags_for_resource(ResourceName=instance['DBInstanceArn'])
for tag in tags['TagList']:
# 停止タグがある場合
if tag['Key'] == 'enabled_stop_scheduling' and tag['Value'] == 'true':
client.stop_db_instance(DBInstanceIdentifier=instance['DBInstanceIdentifier'])
return {
'statusCode': 200,
'body': json.dumps("Amazon RDS instance stop operation successfully")
}
except Exception as e:
print(f"Error: {repr(e)}", flush=True)
return {
'statusCode': 500,
'body': json.dumps(f"Error: {repr(e)}")
}