Photo de Lukáš Vaňátko sur Unsplash
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.
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
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
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.
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
# 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
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.
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
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.
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.
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.
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
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
Après avoir exécuté make local, je peux tester l'API localement
É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.
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 :
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.
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).
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