はじめに
4/7に突如登場したLambda function URLs。
ついにAPI GatewayやALBを介さずにLambdaが直接HTTPサーバの機能を持ったので、お手軽に利用する幅が拡がることだろう。
今回は、実際どれだけお手軽になったか、どんなことが制約になり得るかを実際に構築しながら考察をしてみたい。
なお、事前知識としては以下を想定している。
- TerraformでLambda Functionを作成した経験がある
- Lambda Permissionsに関する知識が多少ある
Lambda関数の実装にはGolangを使っているが、基本的なことしかやっていないためそれほど重要ではない。
構成図
今回は以下の構成で作成をする。
- ServerとProxyのLambdaそれぞれにfunction URLsを設定する
- ServerはHTTPレスポンスのBodyで "Hello Lambda Function URLs!!" を応答する
- Proxyは、その名の通り、Serverへのリクエストとレスポンスのプロキシを行う
- Serverは、Proxyに設定したIAMのみ許容するfunction URLsを設定して、パブリックアクセスを行わせない
- Proxyは、パブリックアクセス可能なfunction URLsを設定する
Lambda関数
Server
Serverは超シンプルに以下の構成とする。
package main
import (
"context"
"encoding/json"
"net/http"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
)
func main() {
lambda.Start(handler)
}
func handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
jsonBytes, _ := json.Marshal(struct {
Detail string `json:"detail"`
}{
Detail: "Hello Lambda Function URLs!!",
})
return events.APIGatewayProxyResponse{
StatusCode: http.StatusOK,
IsBase64Encoded: false,
Body: string(jsonBytes),
}, nil
}
ポイントになるのは、ハンドラに渡されてくるイベントの型だ。
公式のデベロッパーガイドに
The request and response event formats follow the same schema as the Amazon API Gateway payload format version 2.0.
と記載されている通り、API Gatewayのイベントをそのまま流用できるようになっている。
既存のAPI Gatewayのバックエンドに配置された関数を用意に移植出来て良い感じだ。
Proxy
Proxyは以下の通り実装する。
package main
import (
"context"
"encoding/json"
"io/ioutil"
"log"
"net/http"
"os"
"time"
"github.com/aws/aws-sdk-go/aws/session"
v4 "github.com/aws/aws-sdk-go/aws/signer/v4"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
)
type responseBody struct {
Detail string `json:"detail"`
}
func main() {
lambda.Start(handler)
}
func handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
req, err := http.NewRequest("GET", os.Getenv("LAMBDA_SERVER_URL"), nil)
if err != nil {
jsonBytes, _ := json.Marshal(responseBody{
Detail: "http.Get() Failed",
})
return events.APIGatewayProxyResponse{
StatusCode: http.StatusInternalServerError,
IsBase64Encoded: false,
Body: string(jsonBytes),
}, nil
}
// リクエストに署名を付加
sess := session.Must(session.NewSession())
credential := sess.Config.Credentials
signer := v4.NewSigner(credential)
signer.Sign(req, nil, "lambda", "ap-northeast-1", time.Now())
resp, err := client.Do(req)
if err != nil {
jsonBytes, _ := json.Marshal(responseBody{
Detail: "client.Do() Failed",
})
return events.APIGatewayProxyResponse{
StatusCode: http.StatusInternalServerError,
IsBase64Encoded: false,
Body: string(jsonBytes),
}, nil
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
jsonBytes, _ := json.Marshal(responseBody{
Detail: "ioutil.ReadAll() Failed",
})
return events.APIGatewayProxyResponse{
StatusCode: http.StatusInternalServerError,
IsBase64Encoded: false,
Body: string(jsonBytes),
}, nil
}
return events.APIGatewayProxyResponse{
StatusCode: http.StatusOK,
IsBase64Encoded: false,
Body: string(body),
}, nil
}
Lambda function URLsは動的に生成されるため、冪等にするためには、環境変数で渡してあげて、http.NewRequest()
の引数でos.Getenv()
で取得するようにしよう。(試していないが、Route53のエイリアスレコードを作っても良いかもしれない)
特筆すべきは、
import (
// (中略)
"github.com/aws/aws-sdk-go/aws/session"
v4 "github.com/aws/aws-sdk-go/aws/signer/v4"
// (中略)
)
// (中略)
// リクエストに署名を付加
sess := session.Must(session.NewSession())
credential := sess.Config.Credentials
signer := v4.NewSigner(credential)
signer.Sign(req, nil, "lambda", "ap-northeast-1", time.Now())
の部分だ。
ProxyからServerへの通信の際はIAM認証を行う。通常のAWSサービス間の通信であれば、この手の認証情報はSDKが良い感じにラップしてくれていて利用者が意識するのはセッションを作るタイミングのみであるが、あくまでもLambda function URLsへの通信は通常のHTTPS通信の世界であるため、自分でSigV4の署名を設定する必要がある。
つまり、現在の構成によっては(例えばnginxプロキシの後段にAPI Gateway+Lambdaで実装している構成を置換するケースでは、nginxがSigV4の署名を付ける必要があり)、構成見直しのハードルが上がるので注意が必要だ。
IaC
あとは、粛々とIaCを書いていけば良い。
API Gatewayを作らなくて良いので非常に気楽だ。
################################################################################
# Lambda(Server) #
################################################################################
data "archive_file" "server" {
type = "zip"
source_dir = "../artifact/server"
output_path = "../outputs/lambda_server_function.zip"
}
resource "aws_lambda_function" "server" {
depends_on = [
aws_cloudwatch_log_group.lambda_server,
]
function_name = local.lambda_server_function_name
filename = data.archive_file.server.output_path
role = aws_iam_role.lambda.arn
handler = "server"
source_code_hash = data.archive_file.server.output_base64sha256
runtime = "go1.x"
memory_size = 128
timeout = 30
}
resource "aws_lambda_function_url" "server" {
function_name = aws_lambda_function.server.function_name
authorization_type = "AWS_IAM"
}
resource "aws_lambda_permission" "allow_function_url_auth_type_iam" {
statement_id = "FunctionURLAllowLambdaIAMAccess"
action = "lambda:InvokeFunctionUrl"
function_name = aws_lambda_function.server.function_name
principal = aws_iam_role.lambda.arn
function_url_auth_type = "AWS_IAM"
}
resource "aws_cloudwatch_log_group" "lambda_server" {
name = "/aws/lambda/${local.lambda_server_function_name}"
retention_in_days = 3
}
################################################################################
# Lambda(Proxy) #
################################################################################
data "archive_file" "proxy" {
type = "zip"
source_dir = "../artifact/proxy"
output_path = "../outputs/lambda_proxy_function.zip"
}
resource "aws_lambda_function" "proxy" {
depends_on = [
aws_cloudwatch_log_group.lambda_proxy,
]
function_name = local.lambda_proxy_function_name
filename = data.archive_file.proxy.output_path
role = aws_iam_role.lambda.arn
handler = "proxy"
source_code_hash = data.archive_file.proxy.output_base64sha256
runtime = "go1.x"
memory_size = 128
timeout = 30
environment {
variables = {
LAMBDA_SERVER_URL = aws_lambda_function_url.server.function_url
}
}
}
resource "aws_lambda_function_url" "proxy" {
function_name = aws_lambda_function.proxy.function_name
authorization_type = "NONE"
}
resource "aws_cloudwatch_log_group" "lambda_proxy" {
name = "/aws/lambda/${local.lambda_proxy_function_name}"
retention_in_days = 3
}
################################################################################
# IAM #
################################################################################
resource "aws_iam_role" "lambda" {
name = local.iam_role_name_lambda
assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
}
data "aws_iam_policy_document" "lambda_assume" {
statement {
effect = "Allow"
actions = [
"sts:AssumeRole",
]
principals {
type = "Service"
identifiers = [
"lambda.amazonaws.com",
]
}
}
}
resource "aws_iam_role_policy" "lambda_custom" {
name = local.iam_policy_name_lambda
role = aws_iam_role.lambda.id
policy = data.aws_iam_policy_document.lambda_service_account_custom.json
}
data "aws_iam_policy_document" "lambda_service_account_custom" {
statement {
effect = "Allow"
actions = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
]
resources = [
"*",
]
}
}
################################################################################
# Outputs #
################################################################################
output "lambda_server_function_url" {
value = aws_lambda_function_url.server.function_url
}
output "lambda_proxy_function_url" {
value = aws_lambda_function_url.proxy.function_url
}
なお、Lambda function URLsの記述は以下だけで良い。あとは自動でリソースポリシーとURLが設定される。
resource "aws_lambda_function_url" "server" {
function_name = aws_lambda_function.server.function_name
authorization_type = "AWS_IAM"
}
resource "aws_lambda_function_url" "proxy" {
function_name = aws_lambda_function.proxy.function_name
authorization_type = "NONE"
}
実際に動かしてみる
さて、ここまでで作ったGolangをビルドし、terraform apply
すると、
Outputs:
lambda_proxy_function_url = "https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.lambda-url.ap-northeast-1.on.aws/"
lambda_server_function_url = "https://yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy.lambda-url.ap-northeast-1.on.aws/"
といった感じで出力される。
これを、curlで直接実行した場合、lambda_server_function_urlは403応答をして、lambda_proxy_function_urlの場合は
{"detail":"Hello Lambda Function URLs!!"}
という応答が帰ってくるはずだ。
リソースポリシーの部分が多少詰まったが、それ以外は1時間もかからず構築できたので、Lambda利用の幅がまた広がったかと思う。