Foto von Lukáš Vaňátko auf Unsplash
Go war aufgrund seiner Einfachheit schon immer eine meiner Lieblingssprachen. Kürzlich habe ich beschlossen, herauszufinden, was nötig ist, um ein einfaches serverloses Boilerplate-Projekt mit in Go geschriebenen Lambda-Funktionen zu erstellen. Ich war neugierig auf die Tools und die Entwicklererfahrung.
Ich möchte eine REST-API erstellen, die die Postgres-Datenbank als Datenschicht verwendet. Meine anfänglichen Anforderungen sind die folgenden
Sie müssten Go sowie AWS SAM installiert haben. Wenn Sie Ihr Projekt auf AWS bereitstellen, werden Ihnen möglicherweise die von Ihnen erstellten Ressourcen in Rechnung gestellt. Denken Sie also daran, Ihre Ressourcen freizugeben, wenn Sie sie nicht benötigen.
Für DB verwende ich supabase
Der Code ist in diesem Repo verfügbar
Beginnen wir mit der Ausführung von sam init. Ich habe die Vorlage „Hello World“ ausgewählt und verwende die bereitgestellte al.2023-Umgebung. Früher gab es eine verwaltete Laufzeit für Go, aber heutzutage ist sie veraltet.
Das API-Schema als OpenApi-Spezifikation definiert zu haben, hat einige offensichtliche Vorteile. Wir können damit Dokumentation erstellen, Clients, Server usw. erstellen. Ich verwende es auch zum Definieren der Form des AWS HttpApi Gateway.
Mein Schema ist unkompliziert. Der einzig interessante Teil ist die Eigenschaft x-amazon-apigateway-integration, die eine Verbindung mit der Lambda-Integration ermöglicht. Das Setup ist sprachunabhängig.
Sie finden die Schemadatei im Repo
# 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' # ....
Wie oben erwähnt, gibt es hier nichts Spezifisches. Das HttpApi Gateway wird auf Basis von OpenApi erstellt.
Es gibt auch ein Geheimnis zum Speichern der Verbindungszeichenfolge. Ich werde seinen Wert nach der Bereitstellung aktualisieren
AWS SAM-Unterstützung für Go ist ziemlich großartig. Ich kann den CodeUri mit dem Lambda-Handler auf den Ordner verweisen und die Build-Methode als go1.x
definieren.In Go integrierte Lambda-Funktionen verwenden die bereitgestellte.al2023-Laufzeit, da sie eine einzelne eigenständige Binärdatei erzeugen.
Die Definition der Funktion sieht folgendermaßen aus:
# 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 # ....
Dank der SAM-Magie wird die Verbindung zwischen HttpApi Gateway und der Lambda-Funktion mit allen erforderlichen Berechtigungen hergestellt.
Um ehrlich zu sein, ist die Ordnerstruktur wahrscheinlich nicht idiomatisch. Aber ich habe versucht, allgemeinen Go-Mustern zu folgen
lambda_handlers |--/api |--/cmd |--/internal |--/tools |--go.mod |--go.sum
cmd ist der Hauptordner mit den tatsächlichen Lambda-Handlern
intern enthält den von den Handlern gemeinsam genutzten Code
tools definiert zusätzliche Tools, die in den Projekten verwendet werden sollen
API für OpenAPI-Generatorkonfiguration und generierte Modelle
Das anfängliche Boilerplate für den Lambda-Handler sieht folgendermaßen aus:
// ... func handleRequest(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { // handler logic } func main() { lambda.Start(handleRequest) }
Normalerweise ist die erste Frage, die wir stellen müssen, wo die Initialisierung von AWS SDK-Clients, die Datenbankverbindung und andere Dinge, mit denen wir uns während des Kaltstarts befassen möchten, untergebracht werden sollen.
Wir haben hier Optionen. Zunächst müssen Sie dem Muster aus dem AWS-Dokumentationsbeispiel folgen und Dienste innerhalb der Funktion init() initialisieren. Ich mag diesen Ansatz nicht, weil er es schwieriger macht, den Handler in den Unit-Tests zu verwenden.
Dank der Tatsache, dass die Methode lambda.Start() eine Funktion als Eingabe akzeptiert, kann ich sie in die benutzerdefinierte Struktur einschließen und sie mit den von mir benötigten Diensten initialisieren. In meinem Fall sieht der Code so aus:
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) }
In der Funktion main() (die beim Kaltstart ausgeführt wird) erhalte ich das Geheimnis vom Secretsmanager und initialisiere dann die Verbindung mit DB. Beide Funktionalitäten werden in internen Ordnern als gemeinsame Helfer definiert, sodass sie in anderen Handlern wiederverwendet werden können. Schließlich wird mein ItemsService mit der erstellten Datenbankverbindung initialisiert und zum Erstellen eines Lambda-Handlers verwendet.
HandleRequest analysiert die ID aus dem Pfadparameter und ruft ItemsService auf, um ein Element aus der Datenbank abzurufen.
Da die Funktion einfach ist, gibt es nicht viel Geschäftslogik. Der ItemsServise ruft einfach die Datenbank für das spezifische Element auf
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 }
Zu diesem Zeitpunkt brauchen wir hier nichts mehr.
Mein Ziel ist es, zusätzliche Tools zu verwenden, die an die Projektabhängigkeiten angehängt werden könnten, sodass es nicht notwendig ist, sich auf Tools zu verlassen, die auf dem Computer des Entwicklers installiert sind.
In Go besteht eine Möglichkeit darin, oapi-codegen im Tools-Paket zu behalten
//go:build tools // build tools package main import ( _ "github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen" )
Und rufen Sie es aus api_gen.go auf
//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=cfg.yaml ../../api.yaml package api
Auf diese Weise kann ich go generate ausführen, ohne die Binärdateien von oapi-codegen separat zu installieren.
Mein Build-Prozess erfordert zwei Schritte: Generieren von Modellen aus OpenAPI und Erstellen der Projekte selbst. Letzteres überlasse ich AWS SAM.
Hier ist mein 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
Für mich ist der einfachste Weg, API Gateway lokal zu testen, die Ausführung von sam local start-api
Da unsere Funktion auf Umgebungsvariablen basiert, habe ich die Datei paramters.json erstellt, um Umgebungsvariablen an sam local zu übergeben
Wenn Sie serverlos entwickeln, möchten Sie vielleicht irgendwann damit beginnen, Cloud-Ressourcen auch für die lokale Entwicklung zu nutzen. In meinem Fall werde ich sofort den Secrets Manager verwenden, um die Verbindungszeichenfolge für die Datenbank zu speichern. Das bedeutet, dass ich den Stack zuerst bereitstellen muss, damit ich ihn in der lokalen Entwicklung verwenden kann.
Ich führe makeploy aus, aber im Moment überprüfe ich nicht die gesamte Bereitstellung, sondern hole mir einfach einen geheimen Namen aus der Konsole. Ich muss auch das Geheimnis in der Konsole aktualisieren, damit es die richtige Verbindungszeichenfolge enthält.
Zum Testen habe ich eine Datenbank auf Supabase erstellt und diese mit ein paar Dummy-Datensätzen gesät
Nachdem ich make local ausgeführt habe, kann ich die API lokal testen
Da Go eine kompilierte Sprache ist, muss ich nach jeder Änderung das Projekt neu erstellen und start-api erneut ausführen. Angesichts der erstaunlichen Geschwindigkeit des Go-Compilers ist das keine große Sache.
Die API-Gateway-URL wurde nach der Bereitstellung in der Konsole ausgedruckt und kann auch direkt von der AWS-Konsole abgerufen werden.
Ich rufe den Endpunkt auf und er funktioniert wie erwartet:
Der Kaltstart ist etwas langwierig, da die Initialisierung etwa 300 ms dauert, hauptsächlich weil sie das Entnehmen des Geheimnisses und den Aufbau einer Verbindung zur Datenbank umfasst. Aber um ehrlich zu sein ist es ein mehr als ordentliches Ergebnis.
Das angegebene Projekt ist ein Ausgangspunkt für die Erstellung einer serverlosen REST-API in Go. Es verwendet OpenAPI für das Schema und AWS SAM für die Verwaltung der Bereitstellung und lokalen Tests.
Ich habe eine externe Postgres-Datenbank und ein AWS SDK verwendet, um die Verbindungszeichenfolge vom Secrets Manager abzurufen.
Es gibt auch Unit-Tests für den Lambda-Handler und den Item-Service
Die meiste Zeit habe ich damit verbracht, den AWS-Teil zu konfigurieren, der für alle Sprachen gleich wäre. Der Go-Code ist ziemlich einfach (für diesen einfachen Anwendungsfall).
Haftungsausschluss: Alle bereitgestellten Ressourcen stammen teilweise aus dem Internet. Wenn eine Verletzung Ihres Urheberrechts oder anderer Rechte und Interessen vorliegt, erläutern Sie bitte die detaillierten Gründe und legen Sie einen Nachweis des Urheberrechts oder Ihrer Rechte und Interessen vor und senden Sie ihn dann an die E-Mail-Adresse: [email protected] Wir werden die Angelegenheit so schnell wie möglich für Sie erledigen.
Copyright© 2022 湘ICP备2022001581号-3