iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🧰

Trying Out AWS Lambda's New HTTPS Endpoint Support

に公開
2

Introduction

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

Update

It seems the Japanese version of the article is no longer available.

On April 6, 2022 (US time), we announced the general availability of Lambda Function URLs. Lambda Function URLs is a new feature that adds an HTTPS endpoint to any Lambda function and optionally allows you to configure Cross-Origin Resource Sharing (CORS) headers.

By using this, we take care of configuring and monitoring a highly available, scalable, and secure HTTPS service, so you can focus on your core business.

Previously, we used API Gateway or Load Balancers (LB) for mapping, but now we can expose HTTPS endpoints with Lambda alone. It is a good thing that there are fewer components to manage.

You can use IAM authentication or access restrictions via CORS.

Server-side Implementation

I tried creating something that could be used as a microservice. The code is short, so I will include the entire file.

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
	})
}

The beginning part is long because I am using Go's embed feature to embed the image file. One thing to note is that the payload may or may not be base64-encoded, similar to when uploading via the AWS CLI.

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)
	}
}

In this way, you need to check the value of isBase64Encoded to determine if the body is base64-encoded.

After that, I use Pigo to find faces in the image, draw on it, base64-encode the result, and return it to the client.

google/ko is Convenient

As a side note, it is convenient to use google/ko to easily register this kind of Go code into Lambda.

https://github.com/google/ko

While it was created for Google Cloud Run, it can also be used for AWS Lambda.

$ aws lambda update-function-code \
  --function-name=my-function-name \
  --image-uri=$(ko build ./cmd/app)

One thing to note is that the default base image specified by ko, gcr.io/distroless/base:nonroot, does not work on AWS. It is better to use public.ecr.aws/lambda/provided:al2 or alpine, which are specified by AWS. Incidentally, scratch also does not work.

In my case, I wanted to use it from Windows, so I prepared a batch file like the following:

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=[Registry URL specified in Lambda]
for /f "delims=;" %%1 in ('%CMD%') do (
  aws lambda update-function-code --function-name [function name] --image-uri=%%1
)

Client-side Implementation

The HTML, CSS, and JS are as follows.

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 Endpoint]';
    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 Configuration

In the Lambda settings, the authentication method is set to NONE to use CORS.

Since this also affects billing, it is a good idea to specify the allowed origin URLs. Also, add content-type to the allowed headers.

Execution Results

When you enter an image URL in the input area and click the submit button, a request is sent to Lambda, and an image with an Anonymous mask is returned as follows.

Conclusion

I implemented a simple web application using the HTTPS endpoint of AWS Lambda. It feels like things have become a little bit easier because I don't have to worry about services like API Gateway. While it's hard to unconditionally call it "convenient" because you can't set request limits (usage plans), it might be useful if used properly and appropriately.

Discussion