AWS Lambda が HTTPS エンドポイントをサポートしたので試してみた。
はじめに
追記
日本語の記事が無くなってしまったようです
2022 年 4 月 6 日(米国時間)、Lambda Function URLs の一般提供についてお知らせします。Lambda Function URLs は、任意の Lambda 関数に HTTPS エンドポイントを追加し、オプションで Cross-Origin Resource Sharing (CORS) ヘッダーを設定できるようにする新機能です。
これを使用することで、可用性が高く、スケーラブルで安全な HTTPS サービスの設定とモニタリングを当社が行うため、お客様は重要な業務に集中できます。
今までは API Gateway や LB を使ってマッピングしていましたが、Lambda 単体で HTTPS のエンドポイントを生やせる様になりました。管理する物が減るのは良い事です。
IAM 認証または CORS によるアクセス制限を使う事ができます。
サーバ側の実装
試しにマイクロサービスとして使えそうな物を作ってみました。コードは短いので全体を貼っておきます。
package main
import (
"bytes"
"context"
"embed"
"encoding/base64"
"encoding/json"
"fmt"
"image"
"image/jpeg"
_ "image/png"
"io/ioutil"
"log"
"net/http"
"strings"
"github.com/aws/aws-lambda-go/lambda"
pigo "github.com/esimov/pigo/core"
"github.com/nfnt/resize"
"golang.org/x/image/draw"
)
var (
maskImg image.Image
classifier *pigo.Pigo
)
//go:embed data
var fs embed.FS
func init() {
f, err := fs.Open("data/mask.png")
if err != nil {
log.Fatal("cannot open mask.png:", err)
}
defer f.Close()
maskImg, _, err = image.Decode(f)
if err != nil {
log.Fatal("cannot decode mask.png:", err)
}
f, err = fs.Open("data/facefinder")
if err != nil {
log.Fatal("cannot open facefinder:", err)
}
defer f.Close()
b, err := ioutil.ReadAll(f)
if err != nil {
log.Fatal("cannot read facefinder:", err)
}
pigo := pigo.NewPigo()
classifier, err = pigo.Unpack(b)
if err != nil {
log.Fatal("cannot unpack facefinder:", err)
}
}
type Payload struct {
Body string `json:"body"`
IsBase64Encoded bool `json:"isBase64Encoded"`
}
type Request struct {
ImgURL string `json:"img_url"`
}
func main() {
lambda.Start(func(ctx context.Context, payload Payload) (string, error) {
var req Request
if payload.IsBase64Encoded {
b, err := base64.StdEncoding.DecodeString(payload.Body)
if err != nil {
return "", fmt.Errorf("cannot decode payload: %v: %v", err, payload.Body)
}
err = json.NewDecoder(bytes.NewReader(b)).Decode(&req)
if err != nil {
return "", fmt.Errorf("cannot decode payload: %v: %v", err, payload.Body)
}
} else {
err := json.NewDecoder(strings.NewReader(payload.Body)).Decode(&req)
if err != nil {
return "", fmt.Errorf("cannot decode payload: %v: %v", err, payload.Body)
}
}
resp, err := http.Get(req.ImgURL)
if err != nil {
return "", fmt.Errorf("cannot get image: %v: %v", err, req.ImgURL)
}
defer resp.Body.Close()
img, _, err := image.Decode(resp.Body)
if err != nil {
return "", fmt.Errorf("cannot decode input image: %v: %v", err, req.ImgURL)
}
bounds := img.Bounds().Max
param := pigo.CascadeParams{
MinSize: 20,
MaxSize: 2000,
ShiftFactor: 0.1,
ScaleFactor: 1.1,
ImageParams: pigo.ImageParams{
Pixels: pigo.RgbToGrayscale(pigo.ImgToNRGBA(img)),
Rows: bounds.Y,
Cols: bounds.X,
Dim: bounds.X,
},
}
faces := classifier.RunCascade(param, 0)
faces = classifier.ClusterDetections(faces, 0.18)
canvas := image.NewRGBA(img.Bounds())
draw.Draw(canvas, img.Bounds(), img, image.Point{0, 0}, draw.Over)
for _, face := range faces {
pt := image.Point{face.Col - face.Scale/2, face.Row - face.Scale/2}
fimg := resize.Resize(uint(face.Scale), uint(face.Scale), maskImg, resize.NearestNeighbor)
log.Println(pt.X, pt.Y, face.Scale)
draw.Copy(canvas, pt, fimg, fimg.Bounds(), draw.Over, nil)
}
var buf bytes.Buffer
err = jpeg.Encode(&buf, canvas, &jpeg.Options{Quality: 100})
if err != nil {
return "", fmt.Errorf("cannot encode output image: %v", err)
}
return base64.StdEncoding.EncodeToString(buf.Bytes()), nil
})
}
冒頭の部分が長いのは、Go の embed という機能を使って画像ファイルを埋め込んでいるからです。気を付ける点としては、aws cli がアップロードする時と同様にペイロードが base64 であったりなかったりする点です。
type Payload struct {
Body string `json:"body"`
IsBase64Encoded bool `json:"isBase64Encoded"`
}
type Request struct {
ImgURL string `json:"img_url"`
}
var req Request
if payload.IsBase64Encoded {
b, err := base64.StdEncoding.DecodeString(payload.Body)
if err != nil {
return "", fmt.Errorf("cannot decode payload: %v: %v", err, payload.Body)
}
err = json.NewDecoder(bytes.NewReader(b)).Decode(&req)
if err != nil {
return "", fmt.Errorf("cannot decode payload: %v: %v", err, payload.Body)
}
} else {
err := json.NewDecoder(strings.NewReader(payload.Body)).Decode(&req)
if err != nil {
return "", fmt.Errorf("cannot decode payload: %v: %v", err, payload.Body)
}
}
この様に isBase64Encoded
の値を見て、body が base64 エンコードされているかを判定する必要があります。
あとは pigo を使って画像の中にある顔を探し、絵を描いて base64 エンコードしてクライアントに返却します。
google/ko が楽
話はちょっとズレますが、こういった Go のコードを簡単に Lambda に登録するには google/ko を使うのが便利です。
Google Cloud Run 向けに作られた物ですが、AWS Lambda にも使えます。
$ aws lambda update-function-code \
--function-name=my-function-name \
--image-uri=$(ko build ./cmd/app)
気を付ける点としては ko がデフォルトで指定しているベースイメージ gcr.io/distroless/base:nonroot
は AWS では動かないという点です。AWS が指定している public.ecr.aws/lambda/provided:al2
や alpine
を使うと良いです。ちなみに scratch
も動きません。
僕の場合、Windows から使いたかったので以下の様なバッチファイルを用意しました。
@echo off
setlocal
set PATH=%PATH%;c:\Program Files\Amazon\AWSCLIV2
set CMD=ko build --bare .
set KO_DEFAULTBASEIMAGE=alpine
set KO_DOCKER_REPO=[Lambda で指定されているレジストリのURL]
for /f "delims=;" %%1 in ('%CMD%') do (
aws lambda update-function-code --function-name [関数名] --image-uri=%%1
)
クライアントの実装
HTML、CSS、JS はそれぞれ以下の通り。
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
<script src="app.js"></script>
<title>AnonymousFace</title>
</head>
<body>
<h1>AnonymousFace</h1>
<div class="content">
<p>Image URL</p>
<input type="url" id="input" value="">
<button type="submit" id="submit">Submit</button><br/>
<br/>
<img id="image"/>
<div id="response"></div>
</div>
</body>
</html>
body {margin: 6pt; text-align: center;}
#input {margin: auto; width: 500px; margin: auto}
#image {display: none; max-width: 600px}
#submit {margin-top:6pt}
async function postData(url = '', data = {}) {
const response = await fetch(url, {
method: 'POST',
mode: 'cors',
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
},
redirect: 'follow',
referrerPolicy: 'no-referrer',
body: JSON.stringify(data)
})
return response.text();
}
window.addEventListener('DOMContentLoaded', e => {
document.querySelector('#submit').addEventListener('click', e => {
const url = '[Lambda の HTTPS エンドポイント]';
const pred = document.querySelector('#response');
const image = document.querySelector('#image');
const input = document.querySelector('#input');
image.src = '';
image.style.display = 'none';
pred.innerHTML = '<h2>Loading...</h2>';
postData(url, { img_url: input.value }).then(data => {
pred.innerHTML = '';
image.src = 'data:image/jpeg;base64,' + data;
image.style.display = 'inline-block';
});
});
});
Lambda の設定
Lambda の設定にて CORS を使う為、認証方式を NONE にします。
課金も影響するのでアクセス許可する URL を指定すると良いでしょう。あと許可ヘッダに content-type
を足しておきます。
実行結果
入力エリアに画像の URL を入力し submit ボタンを押すと Lambda にリクエストが送られ、以下の様にアノニマスのお面が付いた画像が返されます。
おわりに
AWS Lambda の HTTPS エンドポイントを使って、簡単なウェブアプリケーションを実装してみました。API Gateway 等を考えなくても済むので、すこーしだけ楽になった気がします。リクエスト数制限(使用量プラン)などが掛けられないので手放しで「便利」とは言いづらいですが、用法用量を守って正しく使えば便利になるかもしれません。
Discussion
リンクが変わってる、かつ日本語じゃなくなっているっぽいです(元のリンク先を見る前に 404 になってしまったので、もともと日本語だったのかを知らないのですが・・・)。
ありがとうございます。英語の方に差し替えました。