«Если рабочий хочет хорошо выполнять свою работу, он должен сначала заточить свои инструменты» — Конфуций, «Аналитики Конфуция. Лу Лингун»
титульная страница > программирование > AWS Lambda с Go, исходный шаблон

AWS Lambda с Go, исходный шаблон

Опубликовано 6 ноября 2024 г.
Просматривать:943

Фото Лукаша Ваньятко на Unsplash

Введение

Go всегда был одним из моих любимых языков из-за своей простоты. Недавно я решил разобраться, что нужно для создания простого шаблонного бессерверного проекта с лямбда-функциями, написанными на Go. Мне было интересно узнать об инструментах и ​​опыте разработки.

Цель

Я хочу создать REST API, который использует базу данных Postgres в качестве уровня данных. Мои первоначальные требования следующие

  • Спецификация, которая будет определена с помощью OpenApi, и модели, созданные на ее основе
  • Используйте AWS SAM
  • Каждая конечная точка должна обрабатываться отдельной лямбда-функцией
  • Местная разработка максимально проста
  • То же самое для развертывания

Предварительные условия

Вам потребуется установить Go, а также AWS SAM. Если вы развернете свой проект на AWS, вам может быть выставлен счет за создаваемые вами ресурсы, поэтому не забывайте очищать свои ресурсы, когда они вам не нужны.
Для БД я использую супабазу

Создание проекта

Код доступен в этом репозитории

Давайте начнем с запуска sam init. Я выбрал шаблон Hello World Go с предоставленной окр. al.2023. Раньше для Go существовала управляемая среда выполнения, но сейчас она устарела.

Схема OpenApi

Наличие схемы API, определенной как спецификация OpenApi, имеет несколько очевидных преимуществ. Мы можем использовать его для создания документации, создания клиентов, серверов и т. д. Я также использую его для определения формы шлюза AWS HttpApi.

Моя схема проста. Единственная интересная часть — это свойство x-amazon-apigateway-integration, которое позволяет подключаться к лямбда-интеграции. Настройка не зависит от языка.

Файл схемы можно найти в репозитории

Шаблон SAM

HttpApi и секрет

# template.yaml
# ....
ItemsAPI:
    Type: AWS::Serverless::HttpApi
    Properties:
      StageName: Prod
      DefinitionBody:
        'Fn::Transform':
          Name: 'AWS::Include'
          Parameters:
            Location: './api.yaml'
      FailOnWarnings: false
DBSecret:
    Type: AWS::SecretsManager::Secret
    Properties:
      Name: my-db-secret
      Description: Postgres config string
      SecretString: 'host=172.17.0.1 port=5431 user=admin password=root dbname=lambdas sslmode=disable'
# ....

Как упоминалось выше, здесь нет ничего особенного для Go. Шлюз HttpApi создан на основе OpenApi.

Также существует секрет хранения строки подключения. Я обновлю его значение после развертывания

Лямбда-функция

Поддержка AWS SAM для Go просто великолепна. Я могу указать CodeUri на папку с лямбда-обработчиком и определить метод сборки как go1.x

Лямбда-функции, встроенные в Go, используют среду выполнения предоставленного.al2023, поскольку они создают один самодостаточный двоичный файл.

Определение функции выглядит следующим образом:

# template.yaml
# ....
  GetItemFunction:
    Type: AWS::Serverless::Function
    Metadata:
      BuildMethod: go1.x
    Properties:
      Tracing: Active
      CodeUri: lambda_handlers/cmd/get_item/
      Handler: bootstrap
      Runtime: provided.al2023
      Architectures:
        - x86_64
      Environment:
        Variables:
          DB_SECRET_NAME: !Ref DBSecret
          API_STAGE: Prod
      Events:
        HttpApiEvents:
          Type: HttpApi
          Properties:
            Path: /item/{id}
            Method: GET
            ApiId: !Ref ItemsAPI
      Policies: 
        - AWSLambdaBasicExecutionRole
        - AWSSecretsManagerGetSecretValuePolicy:
            SecretArn: !Ref DBSecret
# ....

Благодаря магии SAM соединение между шлюзом HttpApi и лямбда-функцией будет установлено со всеми необходимыми разрешениями.

Код функции

Структура проекта

Честно говоря, структура папок, вероятно, не идиоматическая. Но я старался следовать общим шаблонам Go

lambda_handlers
|--/api
|--/cmd
|--/internal
|--/tools
|--go.mod
|--go.sum

cmd — основная папка с реальными лямбда-обработчиками
внутренний содержит код, общий для обработчиков
инструменты определяет дополнительные инструменты, которые будут использоваться в проектах
API для конфигурации генератора OpenAPI и сгенерированных моделей

Обработчик функции

Исходный шаблон лямбда-обработчика выглядит следующим образом:

// ...
func handleRequest(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    // handler logic
}

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

Обычно первый вопрос, который нужно задать, — это где разместить инициализацию клиентов AWS SDK, подключение к базе данных и другие вещи, с которыми мы хотим разобраться во время холодного запуска.

Здесь у нас есть варианты. Во-первых, нужно следовать шаблону из примера документации AWS и инициализировать сервисы внутри функции init(). Мне не нравится этот подход, потому что он усложняет использование обработчика в модульных тестах.

Благодаря тому, что метод лямбда.Start() принимает функцию в качестве входных данных, я могу обернуть ее в собственную структуру и инициализировать ее с помощью необходимых мне сервисов. В моем случае код выглядит так:

package main

// imports ...

type GetItemsService interface {
    GetItem(id int) (*api.Item, error)
}

type LambdaHandler struct {
    svc GetItemsService
}

func InitializeLambdaHandler(svc GetItemsService) *LambdaHandler {
    return &LambdaHandler{
        svc: svc,
    }
}

func (h *LambdaHandler) HandleRequest(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {

    strId, err := helpers.GetPathParameter("id", request.PathParameters)

    if err != nil {
        return helpers.SendResponse("id is required", 400), nil
    }

    id, err := strconv.Atoi(*strId)

    if err != nil {
        return helpers.SendResponse("id must be an integer", 400), nil
    }

    log.Printf("id: %d", id)

    result, err := h.svc.GetItem(id)

    if err != nil {
        if err.Error() == "Item not found" {
            return helpers.SendResponse("Item not found", 404), nil
        }
        return helpers.SendResponse("error", 500), nil
    }

    jsonRes, err := json.Marshal(result)

    if err != nil {
        log.Printf("error marshalling response: %s", err.Error())
        return helpers.SendResponse("internal server error", 500), nil
    }

    return helpers.SendResponse(string(jsonRes), 200), nil
}

func main() {

    dbSecretName := os.Getenv("DB_SECRET_NAME")

    log.Printf("dbSecretName: %s", dbSecretName)

    cfg, err := awssdkconfig.InitializeSdkConfig()

    if err != nil {
        log.Fatal(err)
    }

    secretsClient := awssdkconfig.InitializeSecretsManager(cfg)

    connString, err := secretsClient.GetSecret(dbSecretName)

    if err != nil {
        log.Fatal(err)
    }

    conn, err := db.InitializeDB(connString)

    if err != nil {
        log.Fatal(err)
    }

    defer conn.Close()

    log.Println("successfully connected to db")

    svc := items.InitializeItemsService(conn)

    handler := InitializeLambdaHandler(svc)

    lambda.Start(handler.HandleRequest)
}

В функции main() (которая запускается при холодном запуске) я получаю секрет от secretsmanager, а затем инициализирую соединение с БД. Обе функции определены внутри внутренних папок как общие помощники, поэтому их можно повторно использовать в других обработчиках. Наконец, мой ItemsService инициализируется с помощью созданного соединения с базой данных и используется для создания лямбда-обработчика.

HandleRequest анализирует идентификатор из параметра пути и вызывает ItemsService, чтобы получить элемент из БД.

Внутренние модули

Поскольку функция проста, в ней не так много бизнес-логики. ItemsServise просто вызывает базу данных для конкретного элемента

package items

import (
    "database/sql"
    "errors"
    api "lambda_handlers/api"
    "log"
)

type ItemsService struct {
    conn *sql.DB
}

func InitializeItemsService(conn *sql.DB) *ItemsService {
    return &ItemsService{
        conn: conn,
    }
}

func (svc ItemsService) GetItem(id int) (*api.Item, error) {

    log.Printf("Getting item id %v", id)

    query := `SELECT * FROM items WHERE id = $1`

    var item api.Item

    err := svc.conn.QueryRow(query, id).Scan(&item.Id, &item.Name, &item.Price)

    log.Printf("Got item id %v", id)

    if err != nil {
        log.Printf("Error getting item %v: %v", id, err)
        if err == sql.ErrNoRows {
            return nil, errors.New("Item not found")
        }
        return nil, err
    }

    return &item, nil

}

На данный момент нам больше ничего не нужно.

Инструменты

Моя цель — использовать дополнительные инструменты, которые можно было бы прикрепить к зависимостям проекта, чтобы не было необходимости полагаться на инструменты, установленные на машине разработчика.

В Go один из способов сделать это — сохранить oapi-codegen в пакете инструментов

//go:build tools
//  build tools

package main

import (
    _ "github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen"
)

И вызовите его изнутри api_gen.go

//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=cfg.yaml ../../api.yaml

package api

Таким образом, я могу запустить gogenerate без отдельной установки двоичных файлов oapi-codegen.

Местное развитие

Строить

Мой процесс сборки состоит из двух этапов: создания моделей из OpenAPI и создания самих проектов. С последним я позволяю AWS SAM разобраться.

Вот мой Makefile

.PHONY: build local deploy

generate:
    cd lambda_handlers && go generate ./...

build: generate
    rm -rf .aws-sam
    sam build

local: build
    sam local start-api --env-vars parameters.json

deploy: build
    sam deploy

Первоначальное развертывание

Для меня самый простой способ протестировать API-шлюз локально — запустить sam local start-api
Поскольку наша функция зависит от переменных среды, я создал файл paramters.json для передачи переменных env в sam local

При разработке бессерверных приложений в какой-то момент вам может потребоваться начать использовать облачные ресурсы даже для локальной разработки. В моем случае я сразу воспользуюсь менеджером секретов для хранения строки подключения к БД. Это означает, что сначала мне нужно развернуть стек, чтобы я мог использовать его в локальной разработке.

Я запускаю make Deploy, но пока не проверяю все развертывание, просто получаю секретное имя из консоли. Мне также нужно обновить секрет в консоли, чтобы он содержал правильную строку подключения.

Для тестирования я создал БД на Supabase и заполнил ее несколькими фиктивными записями

Локальное тестирование

После запуска make local я могу протестировать API локально

AWS Lambda with Go, initial boilerplate

Поскольку Go — компилируемый язык, после каждого изменения мне нужно пересобирать проект и заново запускать start-api. Учитывая потрясающую скорость компилятора Go, это не имеет большого значения.

Тестирование на AWS

URL-адрес шлюза API был распечатан в консоли после развертывания, его также можно получить напрямую из консоли AWS.

Я вызываю конечную точку, и она работает как положено:

AWS Lambda with Go, initial boilerplate

Холодный старт немного долгий, так как инициализация занимает ~300 мс, главным образом потому, что он включает в себя получение секрета и установление соединения с БД. Но, честно говоря, это более чем достойный результат.

Краткое содержание

Данный проект является отправной точкой для создания бессерверного REST API на Go. Он использует OpenAPI для схемы и AWS SAM для управления развертыванием и локальным тестированием.

Я использовал внешнюю базу данных Postgres и AWS SDK для получения строки подключения от менеджера секретов.

Также имеются модульные тесты для лямбда-обработчика и службы элементов

Большую часть времени я потратил на настройку части AWS, которая была бы одинаковой для всех языков. Код Go довольно прост (для этого простого варианта использования).

Заявление о выпуске Эта статья воспроизведена по адресу: https://dev.to/aws-builders/aws-lambda-with-go-initial-boilerplate-2787?1. В случае нарушения авторских прав свяжитесь с [email protected], чтобы удалить ее.
Последний учебник Более>

Изучайте китайский

Отказ от ответственности: Все предоставленные ресурсы частично взяты из Интернета. В случае нарушения ваших авторских прав или других прав и интересов, пожалуйста, объясните подробные причины и предоставьте доказательства авторских прав или прав и интересов, а затем отправьте их по электронной почте: [email protected]. Мы сделаем это за вас как можно скорее.

Copyright© 2022 湘ICP备2022001581号-3