🧰

AWS Lambda が HTTPS エンドポイントをサポートしたので試してみた。

2022/04/15に公開
2

はじめに

https://aws.amazon.com/jp/blogs/aws/announcing-aws-lambda-function-urls-built-in-https-endpoints-for-single-function-microservices/

追記

日本語の記事が無くなってしまったようです

2022 年 4 月 6 日(米国時間)、Lambda Function URLs の一般提供についてお知らせします。Lambda Function URLs は、任意の Lambda 関数に HTTPS エンドポイントを追加し、オプションで Cross-Origin Resource Sharing (CORS) ヘッダーを設定できるようにする新機能です。

これを使用することで、可用性が高く、スケーラブルで安全な HTTPS サービスの設定とモニタリングを当社が行うため、お客様は重要な業務に集中できます。

今までは API Gateway や LB を使ってマッピングしていましたが、Lambda 単体で HTTPS のエンドポイントを生やせる様になりました。管理する物が減るのは良い事です。

IAM 認証または CORS によるアクセス制限を使う事ができます。

サーバ側の実装

試しにマイクロサービスとして使えそうな物を作ってみました。コードは短いので全体を貼っておきます。

main.go
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 を使うのが便利です。

https://github.com/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:al2alpine を使うと良いです。ちなみに scratch も動きません。

僕の場合、Windows から使いたかったので以下の様なバッチファイルを用意しました。

update.bat
@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 はそれぞれ以下の通り。

index.html
<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>
style.css
body {margin: 6pt; text-align: center;}
#input {margin: auto; width: 500px; margin: auto}
#image {display: none; max-width: 600px}
#submit {margin-top:6pt}
app.js
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