使用Localstack在Golang应用程序中调用AWS Lambda函数

301 阅读2分钟

多年来,我们一直投入大量的个人时间和精力,与大家分享我们的知识。然而,我们现在需要你的帮助来维持这个博客的运行。你所要做的只是点击网站上的一个广告,否则它将由于托管等费用而不幸被关闭。谢谢你。

在这个例子中,我们将创建一个AWS Lambda函数,并使用Golang应用程序来调用它。这个例子依赖于一个Localstack环境。

// main.go

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/aws/aws-lambda-go/lambda"
)

type Request struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

type Response struct {
	OK   bool      `json:"ok"`
	Time time.Time `json:"time"`
}

func Login(ctx context.Context, req Request) (Response, error) {
	select {
	case <-ctx.Done():
		return Response{OK: false, Time: time.Now()}, ctx.Err()
	default:
	}

	log.Printf("%+v\n", req)

	if req.Username != "hello" || req.Password != "world" {
		return Response{OK: false, Time: time.Now()}, fmt.Errorf("invalid attempt")
	}

	return Response{OK: true, Time: time.Now()}, nil
}

func main() {
	lambda.Start(Login)
}

创建并测试函数

$ GOOS=linux CGO_ENABLED=0 go build -ldflags "-s -w" -o main main.go

$ zip main.zip main

$ aws --profile localstack --endpoint-url http://localhost:4566 lambda create-function --function-name login --handler main --runtime go1.x --role create-role --zip-file fileb://main.zip

$ aws --profile localstack --endpoint-url http://localhost:4566 lambda invoke --function-name login --cli-binary-format raw-in-base64-out --payload '{"username":"hello","password":"world"}' response.json

cloud/session.go

package cloud

import (
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
)

func NewAWSSession() (*session.Session, error) {
	return session.NewSessionWithOptions(session.Options{
		Profile: "localstack",
		Config: aws.Config{
			Region:   aws.String("eu-west-1"),
			Endpoint: aws.String("http://localhost:4566"),
		},
	})
}

cloud/lambda.go

package cloud

import (
	"fmt"
	"regexp"
	"strings"

	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/lambda"
)

// errMsg is used to extract key-value pair for the JSON key `errorMessage` in
// the `invoke` result string.
var errMsg = regexp.MustCompile(`"errorMessage":[^,}]*`)

// NewLambdaClient returns a new Lambda client.
func NewLambdaClient(ses *session.Session) *lambda.Lambda {
	return lambda.New(ses)
}

// CheckLambdaError accepts a result from a Lambda function call in order to work
// out if there was an error or not.
func CheckLambdaError(result map[string]interface{}) (string, error) {
	msg, ok := result["errorMessage"]
	if !ok {
		return "", nil
	}

	match := errMsg.FindStringSubmatch(msg.(string))
	if len(match) == 0 {
		return "", fmt.Errorf("error result does not contain a json data")
	}

	parts := strings.Split(match[0], ":")
	if msg := parts[1]; msg == "" || strings.TrimSpace(msg) == "" {
		return "", fmt.Errorf("error message is missing in result")
	}

	return parts[1][1 : len(parts[1])-1], nil
}

Lambda错误看起来如下,所以这就是为什么我们要在上面做一些数据提取,以便获得实际的错误信息。

Lambda process returned with error. Result: {"errorType":"errorString","errorMessage":"invalid attempt"}. Output:
START RequestId: 68d51f06-e6d6-141f-e660-60274d2ca777 Version: $LATEST
END RequestId: 68d51f06-e6d6-141f-e660-60274d2ca777
REPORT RequestId: 68d51f06-e6d6-141f-e660-60274d2ca777  Init Duration: 125.07 ms        Duration: 9.88 ms       Billed Duration: 10 ms  Memory Size: 1536 MB    Max Memory Used: 19 MB

user/login.go

package user

import (
	"context"
	"encoding/json"
	"fmt"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/lambda"
	"github.com/you/client/cloud"
)

type LoginRequest struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

type LoginResponse struct {
	OK    bool   `json:"ok,omitempty"`
	Time  string `json:"time,omitempty"`
	Error string `json:"error,omitempty"`
}

func Login(ctx context.Context, client *lambda.Lambda, request LoginRequest) (LoginResponse, error) {
	payload, err := json.Marshal(request)
	if err != nil {
		return LoginResponse{}, fmt.Errorf("marshal request: %w", err)
	}

	input := &lambda.InvokeInput{
		FunctionName: aws.String("login"),
		Payload:      payload,
	}

	output, err := client.InvokeWithContext(ctx, input)
	if err != nil {
		return LoginResponse{}, fmt.Errorf("invoke: %w", err)
	}

	// fmt.Printf("OUTPUT: %+v\n", output)

	var result map[string]interface{}
	if err := json.Unmarshal(output.Payload, &result); err != nil {
		return LoginResponse{}, fmt.Errorf("unmarshal result: %w", err)
	}

	// fmt.Printf("RESULT: %+v\n", result)

	msg, err := cloud.CheckLambdaError(result)
	if err != nil {
		return LoginResponse{}, fmt.Errorf("check lambda error: %w", err)
	}
	if msg != "" {
		return LoginResponse{Error: msg}, nil
	}

	var response LoginResponse

	if ok := result["ok"]; ok != nil {
		response.OK = ok.(bool)
	}
	if tm := result["time"]; tm != nil {
		response.Time = tm.(string)
	}

	return response, nil
}

main.go

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"

	"github.com/you/client/cloud"
	"github.com/you/client/user"
)

func main() {
	ses, err := cloud.NewAWSSession()
	if err != nil {
		log.Fatalln(err)
	}

	res, err := user.Login(context.Background(), cloud.NewLambdaClient(ses), user.LoginRequest{
		Username: "hello",
		Password: "world",
	})
	if err != nil {
		log.Fatalln(err)
	}

	dat, _ := json.MarshalIndent(res, "", "  ")
	fmt.Println(string(dat))
}

测试

// Send some invalid username or password other than hello:world

$ go run main.go 
{
  "error": "invalid attempt"
}
// Send hello:world

$ go run main.go
{
  "ok": true,
  "time": "2021-12-30T21:42:28.4341183Z"
}

AWS SDK for Go V2 version

Lambda

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/aws/aws-lambda-go/lambda"
)

type Request struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

type Response struct {
	OK bool `json:"ok,omitempty"`
}

func main() {
	lambda.Start(HandleEvent)
}

func HandleEvent(ctx context.Context, req Request) (Response, error) {
	select {
	case <-ctx.Done():
		return Response{OK: false}, ctx.Err()
	default:
	}

	log.Printf("%+v\n", req)

	if req.Username == "error" {
		return Response{}, fmt.Errorf("some system error")
	}

	if req.Username != "hello" || req.Password != "world" {
		return Response{OK: false}, nil
	}

	return Response{OK: true}, nil
}

Configuration

$ GOOS=linux CGO_ENABLED=0 go build -ldflags "-s -w" -o main lambda/main.go

$ zip main.zip main

$ aws --profile localstack --endpoint-url http://localhost:4566 lambda create-function --function-name login --handler main --runtime go1.x --role create-role --zip-file fileb://main.zip

$ aws --profile localstack --endpoint-url http://localhost:4566 lambda invoke --function-name login --cli-binary-format raw-in-base64-out --payload '{"username":"hello","password":"world"}' response.json

Golang application

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/lambda"
)

type Request struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

type Response struct {
	OK bool `json:"ok,omitempty"`
}

func main() {
	ctx := context.Background()

	// AWS CONFIG
	cfg, err := config.LoadDefaultConfig(ctx,
		config.WithRegion("eu-west-1"),
		config.WithSharedConfigProfile("localstack"),
		config.WithEndpointResolverWithOptions(aws.EndpointResolverWithOptionsFunc(func(s, r string, o ...interface{}) (aws.Endpoint, error) {
			return aws.Endpoint{URL: "http://localhost:4566"}, nil
		})),
	)
	if err != nil {
		log.Fatalln(err)
	}

	// AWS LAMBDA
	lmb := lambda.NewFromConfig(cfg)

	out, err := lmb.Invoke(ctx, &lambda.InvokeInput{
		FunctionName: aws.String("login"),
		Payload:      []byte(*aws.String(`{"username":"hello","password":"world"}`)), // You could pass Request here
	})
	if err != nil {
		log.Fatalln(err)
	}

	var res Response
	if err := json.Unmarshal(out.Payload, &res); err != nil {
		log.Fatalln(err)
	}
	fmt.Println("OK:", res.OK)
}