Фото Лукаша Ваньятко на Unsplash
Go всегда был одним из моих любимых языков из-за своей простоты. Недавно я решил разобраться, что нужно для создания простого шаблонного бессерверного проекта с лямбда-функциями, написанными на Go. Мне было интересно узнать об инструментах и опыте разработки.
Я хочу создать REST API, который использует базу данных Postgres в качестве уровня данных. Мои первоначальные требования следующие
Вам потребуется установить Go, а также AWS SAM. Если вы развернете свой проект на AWS, вам может быть выставлен счет за создаваемые вами ресурсы, поэтому не забывайте очищать свои ресурсы, когда они вам не нужны.
Для БД я использую супабазу
Код доступен в этом репозитории
Давайте начнем с запуска sam init. Я выбрал шаблон Hello World Go с предоставленной окр. al.2023. Раньше для Go существовала управляемая среда выполнения, но сейчас она устарела.
Наличие схемы API, определенной как спецификация OpenApi, имеет несколько очевидных преимуществ. Мы можем использовать его для создания документации, создания клиентов, серверов и т. д. Я также использую его для определения формы шлюза AWS HttpApi.
Моя схема проста. Единственная интересная часть — это свойство x-amazon-apigateway-integration, которое позволяет подключаться к лямбда-интеграции. Настройка не зависит от языка.
Файл схемы можно найти в репозитории
# 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 локально
Поскольку Go — компилируемый язык, после каждого изменения мне нужно пересобирать проект и заново запускать start-api. Учитывая потрясающую скорость компилятора Go, это не имеет большого значения.
URL-адрес шлюза API был распечатан в консоли после развертывания, его также можно получить напрямую из консоли AWS.
Я вызываю конечную точку, и она работает как положено:
Холодный старт немного долгий, так как инициализация занимает ~300 мс, главным образом потому, что он включает в себя получение секрета и установление соединения с БД. Но, честно говоря, это более чем достойный результат.
Данный проект является отправной точкой для создания бессерверного REST API на Go. Он использует OpenAPI для схемы и AWS SAM для управления развертыванием и локальным тестированием.
Я использовал внешнюю базу данных Postgres и AWS SDK для получения строки подключения от менеджера секретов.
Также имеются модульные тесты для лямбда-обработчика и службы элементов
Большую часть времени я потратил на настройку части AWS, которая была бы одинаковой для всех языков. Код Go довольно прост (для этого простого варианта использования).
Отказ от ответственности: Все предоставленные ресурсы частично взяты из Интернета. В случае нарушения ваших авторских прав или других прав и интересов, пожалуйста, объясните подробные причины и предоставьте доказательства авторских прав или прав и интересов, а затем отправьте их по электронной почте: [email protected]. Мы сделаем это за вас как можно скорее.
Copyright© 2022 湘ICP备2022001581号-3