تصوير لوكاش فاناتكو على Unsplash
كانت Go دائمًا إحدى اللغات المفضلة لدي نظرًا لبساطتها. قررت مؤخرًا معرفة ما يلزم لإنشاء مشروع بسيط بدون خادم مع وظائف lambda المكتوبة بلغة Go. كنت مهتمًا بالأدوات وتجربة المطورين.
أريد إنشاء واجهة برمجة تطبيقات REST، التي تستخدم قاعدة بيانات postgres كطبقة بيانات. متطلباتي الأولية هي ما يلي
ستحتاج إلى تثبيت Go، بالإضافة إلى AWS SAM. إذا قمت بنشر مشروعك على AWS، فقد تتم محاسبتك على الموارد التي تقوم بإنشائها، لذا تذكر مسح الموارد الخاصة بك عندما لا تحتاج إليها.
بالنسبة لقاعدة البيانات أستخدم Supabase
الكود موجود في هذا الريبو
لنبدأ بتشغيل sam init. لقد اخترت قالب Hello World، استخدمه مع بيئة al.2023 المتوفرة. في السابق كان هناك وقت تشغيل مُدار لـ Go، ولكن في الوقت الحاضر تم إهماله.
إن تعريف مخطط واجهة برمجة التطبيقات (API) على أنه مواصفات OpenApi له بعض المزايا الواضحة. يمكننا استخدامه لإنشاء الوثائق وإنشاء العملاء والخوادم وما إلى ذلك. كما أستخدمه أيضًا لتحديد شكل بوابة AWS HttpApi.
مخططي واضح ومباشر. الجزء الوحيد المثير للاهتمام هو خاصية x-amazon-apigateway-integration، والتي تسمح بالاتصال بتكامل lambda. الإعداد حيادي للغة.
يمكنك العثور على ملف المخطط في الريبو
# 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' # ....
كما ذكرنا أعلاه، لا يوجد شيء محدد للذهاب هنا. تم إنشاء بوابة HttpApi بناءً على OpenApi.
هناك أيضًا سر لتخزين سلسلة الاتصال. سأقوم بتحديث قيمته بعد النشر
يعد دعم AWS SAM لـ Go رائعًا جدًا. يمكنني توجيه CodeUri إلى المجلد باستخدام معالج lambda وتحديد طريقة الإنشاء على أنها go1.x
تستخدم وظائف Lambda المضمنة في 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 ووظيفة lambda مع جميع الأذونات المطلوبة.
لأكون صادقًا، ربما لا تكون بنية المجلد اصطلاحية. لكنني حاولت اتباع أنماط Go العامة
lambda_handlers |--/api |--/cmd |--/internal |--/tools |--go.mod |--go.sum
cmd هو المجلد الرئيسي الذي يحتوي على معالجات لامدا الفعلية
داخلي يحمل الكود المشترك بين المعالجات
تحدد الأدوات الأدوات الإضافية التي سيتم استخدامها في المشاريع
واجهة برمجة التطبيقات لتكوين مولد openapi والنماذج التي تم إنشاؤها
يبدو النموذج الأولي لمعالج لامدا كما يلي:
// ... func handleRequest(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { // handler logic } func main() { lambda.Start(handleRequest) }
عادةً، السؤال الأول الذي يجب طرحه هو مكان وضع تهيئة عملاء AWS SDK، والاتصال بقاعدة البيانات، وأشياء أخرى، نرغب في التعامل معها أثناء البداية الباردة.
لدينا خيارات هنا. الأول هو اتباع النمط من مثال وثائق AWS وتهيئة الخدمات داخل وظيفة init(). لا أحب هذا الأسلوب، لأنه يجعل من الصعب استخدام المعالج في اختبارات الوحدة.
بفضل حقيقة أن طريقة lambda.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) }
في الوظيفة الرئيسية () (التي تعمل أثناء البداية الباردة) أحصل على السر من Secretsmanager ثم أقوم بتهيئة الاتصال بقاعدة البيانات. يتم تعريف كلتا الوظيفتين داخل المجلدات الداخلية كمساعدين مشتركين بحيث يمكن إعادة استخدامها في معالجات أخرى. أخيرًا، تمت تهيئة ItemsService الخاصة بي باستخدام اتصال db الذي تم إنشاؤه، واستخدامه لإنشاء معالج lambda.
يقوم 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
بهذه الطريقة يمكنني تشغيل go generator دون تثبيت ثنائيات 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 Gateway محليًا هي تشغيل sam local start-api
نظرًا لأن وظيفتنا تعتمد على متغيرات البيئة، فقد قمت بإنشاء ملف paramters.json لتمرير env vars إلى sam local
عند التطوير بدون خادم، قد ترغب في مرحلة ما في البدء في استخدام الموارد السحابية حتى للتطوير المحلي. في حالتي، سأستخدم مدير الأسرار على الفور لتخزين سلسلة الاتصال لقاعدة البيانات. هذا يعني أنني بحاجة إلى نشر المكدس أولاً، حتى أتمكن من استخدامه في التطوير المحلي.
أجري عملية نشر ولكن في الوقت الحالي لا أتحقق من النشر بالكامل، فقط احصل على اسم سري من وحدة التحكم. أحتاج أيضًا إلى تحديث السر في وحدة التحكم، بحيث يحمل سلسلة الاتصال الصحيحة.
للاختبار، قمت بإنشاء قاعدة بيانات على Supabase وقمت بزرعها مع بعض السجلات الوهمية
بعد تشغيل make local يمكنني اختبار واجهة برمجة التطبيقات محليًا
نظرًا لأن Go هي لغة مجمعة، بعد كل تغيير أحتاج إلى إعادة بناء المشروع وتشغيل start-api مرة أخرى. وبالنظر إلى السرعة المذهلة لمترجم Go، فإن ذلك ليس بالأمر الكبير.
تمت طباعة عنوان URL لبوابة API في وحدة التحكم بعد النشر، ويمكن أيضًا الحصول عليه من وحدة تحكم AWS مباشرةً.
أتصل بنقطة النهاية، وهي تعمل كما هو متوقع:
البداية الباردة طويلة بعض الشيء، حيث تستغرق التهيئة حوالي 300 مللي ثانية، ويرجع ذلك في الغالب إلى أنها تتضمن أخذ السر وإنشاء اتصال بقاعدة البيانات. ولكن لنكون صادقين، فهي أكثر من مجرد نتيجة لائقة.
يعد المشروع المحدد نقطة انطلاق لإنشاء واجهة برمجة تطبيقات REST بدون خادم في Go. ويستخدم OpenAPI للمخطط وAWS SAM لإدارة النشر والاختبار المحلي.
لقد استخدمت قاعدة بيانات postgres خارجية وAWS SDK للحصول على سلسلة الاتصال من مدير الأسرار.
هناك أيضًا اختبارات وحدة لمعالج لامدا وخدمة العناصر
قضيت معظم الوقت في تكوين جزء AWS، والذي سيكون هو نفسه بالنسبة لجميع اللغات. يعد رمز Go واضحًا جدًا (لحالة الاستخدام البسيطة هذه).
تنصل: جميع الموارد المقدمة هي جزئيًا من الإنترنت. إذا كان هناك أي انتهاك لحقوق الطبع والنشر الخاصة بك أو الحقوق والمصالح الأخرى، فيرجى توضيح الأسباب التفصيلية وتقديم دليل على حقوق الطبع والنشر أو الحقوق والمصالح ثم إرسالها إلى البريد الإلكتروني: [email protected]. سوف نتعامل مع الأمر لك في أقرب وقت ممكن.
Copyright© 2022 湘ICP备2022001581号-3