"Si un ouvrier veut bien faire son travail, il doit d'abord affûter ses outils." - Confucius, "Les Entretiens de Confucius. Lu Linggong"
Page de garde > La programmation > AWS Lambda avec Go, modèle initial

AWS Lambda avec Go, modèle initial

Publié le 2024-11-06
Parcourir:622

Photo de Lukáš Vaňátko sur Unsplash

Introduction

Go a toujours été l'un de mes langages préférés en raison de sa simplicité. Dernièrement, j'ai décidé de comprendre ce qu'il fallait pour créer un projet simple sans serveur avec des fonctions lambda écrites en Go. J'étais curieux de connaître les outils et l'expérience des développeurs.

But

Je souhaite créer une API REST, qui utilise la base de données postgres comme couche de données. Mes exigences initiales sont les suivantes

  • Spécification à définir avec OpenApi, et modèles générés à partir de celui-ci
  • Utiliser AWS SAM
  • Chaque point de terminaison doit être géré par une fonction lambda distincte
  • Le développement local aussi simple que possible
  • Idem pour le déploiement

Conditions préalables

Vous devez avoir installé Go, ainsi qu'AWS SAM. Si vous déployez votre projet sur AWS, vous pourriez être facturé pour les ressources que vous créez, alors n'oubliez pas d'effacer vos ressources lorsque vous n'en avez pas besoin.
Pour DB, j'utilise supabase

Construire le projet

Le code est disponible dans ce dépôt

Commençons par exécuter sam init. J'ai choisi le modèle Hello World, Go avec l'env al.2023 fourni. Auparavant, il existait un runtime géré pour Go, mais il est aujourd'hui obsolète.

Schéma OpenApi

Avoir un schéma API défini comme spécification OpenApi présente quelques avantages évidents. Nous pouvons l'utiliser pour générer de la documentation, créer des clients, des serveurs, etc. Je l'utilise également pour définir la forme de la passerelle AWS HttpApi.

Mon schéma est simple. La seule partie intéressante est la propriété x-amazon-apigateway-integration, qui permet la connexion avec l'intégration lambda. La configuration est indépendante de la langue.

Vous pouvez trouver le fichier de schéma dans le dépôt

Modèle SAM

HttpApi et secret

# 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'
# ....

Comme mentionné ci-dessus, il n'y a rien de spécifique à Go here. La passerelle HttpApi est créée sur la base d'OpenApi.

Il existe également un secret pour stocker la chaîne de connexion. Je mettrai à jour sa valeur après le déploiement

Fonction Lambda

La prise en charge d'AWS SAM pour Go est plutôt géniale. Je peux pointer le CodeUri vers le dossier avec le gestionnaire lambda et définir la méthode de construction comme go1.x

Les fonctions Lambda intégrées à Go utilisent le runtime fourni.al2023, car elles produisent un seul binaire autonome.

La définition de la fonction ressemble à ceci :

# 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
# ....

Grâce à la magie SAM, la connexion entre HttpApi Gateway et la fonction lambda sera établie avec toutes les autorisations requises.

Code de fonction

Structure du projet

Pour être honnête, la structure des dossiers n'est probablement pas idiomatique. Mais j'ai essayé de suivre les schémas généraux de Go

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

cmd est le dossier principal avec les véritables gestionnaires lambda
internal contient le code partagé entre les gestionnaires
tools définit des outils supplémentaires à utiliser dans les projets
API pour la configuration du générateur openapi et les modèles générés

Gestionnaire de fonctions

Le passe-partout initial du gestionnaire lambda ressemble à ceci :

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

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

Habituellement, la première question à poser est de savoir où placer l'initialisation des clients AWS SDK, la connexion à la base de données et d'autres éléments que nous souhaitons traiter lors du démarrage à froid.

Nous avons des options ici. La première consiste à suivre le modèle de l'exemple de documentation AWS et à initialiser les services dans la fonction init(). Je n'aime pas cette approche, car elle rend plus difficile l'utilisation du gestionnaire dans les tests unitaires.

Grâce au fait que la méthode lambda.Start() prend une fonction comme entrée, je peux l'envelopper dans la structure personnalisée et l'initialiser avec les services dont j'ai besoin. Dans mon cas, le code ressemble à ceci :

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

Dans la fonction main() (qui s'exécute lors d'un démarrage à froid), j'obtiens le secret de secretsmanager, puis j'initialise la connexion avec DB. Les deux fonctionnalités sont définies dans des dossiers internes en tant qu'assistants communs afin qu'elles puissent être réutilisées dans d'autres gestionnaires. Enfin, mon ItemsService est initialisé avec la connexion à la base de données créée et utilisé pour créer un gestionnaire lambda.

HandleRequest analyse l'ID du paramètre path et appelle ItemsService pour obtenir un élément de la base de données.

Modules internes

Comme la fonction est simple, il n'y a pas beaucoup de logique métier. ItemsServise appelle simplement la base de données pour l'élément spécifique

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

}

À ce stade, nous n'avons besoin de rien de plus ici.

Outils

Mon objectif est d'utiliser des outils supplémentaires, qui pourraient être attachés aux dépendances du projet, il n'est donc pas nécessaire de s'appuyer sur des outils installés sur la machine du développeur.

Dans Go, une façon de procéder est de conserver oapi-codegen dans le package d'outils

//go:build tools
//  build tools

package main

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

Et appelez-le depuis api_gen.go

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

package api

De cette façon, je peux exécuter go generate sans installer les binaires oapi-codegen séparément.

Développement local

Construire

Mon processus de construction nécessite deux étapes : générer des modèles à partir d'OpenAPI et créer les projets eux-mêmes. Je laisse AWS SAM s'occuper de ce dernier.

Voici mon 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

Déploiement initial

Pour moi, le moyen le plus simple de tester API Gateway localement est d'exécuter sam local start-api
Puisque notre fonction repose sur des variables d'environnement, j'ai créé le fichier paramters.json pour transmettre les variables d'environnement à sam local

Lors du développement sans serveur, vous souhaiterez peut-être à un moment donné commencer à utiliser des ressources cloud, même pour le développement local. Dans mon cas, j'utiliserai immédiatement le gestionnaire de secrets pour stocker la chaîne de connexion pour DB. Cela signifie que je dois d'abord déployer la pile, afin de pouvoir l'utiliser dans le développement local.

Je lance make déployer mais pour l'instant je ne vérifie pas l'intégralité du déploiement, je récupère simplement un nom secret dans la console. Je dois également mettre à jour le secret dans la console afin qu'il contienne la chaîne de connexion correcte.

Pour les tests, j'ai créé une base de données sur supabase et l'ai ensemencée avec quelques enregistrements factices

Tests locaux

Après avoir exécuté make local, je peux tester l'API localement

AWS Lambda with Go, initial boilerplate

Étant donné que Go est un langage compilé, après chaque modification, je dois reconstruire le projet et réexécuter start-api. Compte tenu de la vitesse incroyable du compilateur Go, ce n'est pas grave.

Tester sur AWS

L'URL de la passerelle API a été imprimée dans la console après le déploiement et elle peut également être récupérée directement à partir de la console AWS.

J'appelle le point de terminaison et il fonctionne comme prévu :

AWS Lambda with Go, initial boilerplate

Le démarrage à froid est un peu long, car l'initialisation prend environ 300 ms, principalement parce qu'elle inclut la prise du secret et l'établissement d'une connexion à la base de données. Mais pour être honnête, c'est plus qu'un résultat décent.

Résumé

Le projet donné est un point de départ pour créer une API REST sans serveur dans Go. Il utilise OpenAPI pour le schéma et AWS SAM pour gérer le déploiement et les tests locaux.

J'ai utilisé une base de données Postgres externe et un SDK AWS pour obtenir la chaîne de connexion du gestionnaire de secrets.

Il existe également des tests unitaires pour le gestionnaire lambda et le service d'articles

J'ai passé la plupart du temps à configurer la partie AWS, qui serait la même pour toutes les langues. Le code Go est assez simple (pour ce cas d'utilisation simple).

Déclaration de sortie Cet article est reproduit à l'adresse : https://dev.to/aws-builders/aws-lambda-with-go-initial-boilerplate-2787?1 En cas de violation, veuillez contacter [email protected] pour le supprimer.
Dernier tutoriel Plus>

Clause de non-responsabilité: Toutes les ressources fournies proviennent en partie d'Internet. En cas de violation de vos droits d'auteur ou d'autres droits et intérêts, veuillez expliquer les raisons détaillées et fournir une preuve du droit d'auteur ou des droits et intérêts, puis l'envoyer à l'adresse e-mail : [email protected]. Nous nous en occuperons pour vous dans les plus brefs délais.

Copyright© 2022 湘ICP备2022001581号-3