إرشادات التعليمة البرمجية: تطبيق بلا خادم باستخدام Functions

Azure Event Hubs
Azure Functions

نماذج بلا خادم تعليمات برمجية مجردة من البنية الأساسية الرئيسية للحساب، ما يسمح للمطورين بالتركيز على منطق الأعمال دون إعداد مكثف. تعمل التعليمة البرمجية بلا خادم على تقليل التكاليف، لأنك تدفع فقط مقابل موارد تنفيذ التعليمة البرمجية والمدة.

يناسب النموذج بلا خادم المواقف التي يؤدي فيها حدث معين إلى إجراء محدد. على سبيل المثال، يؤدي تلقي رسالة جهاز واردة إلى تشغيل التخزين للاستخدام لاحقاً، أو يؤدي تحديث قاعدة البيانات إلى إجراء مزيد من المعالجة.

لمساعدتك في استكشاف تقنيات Azure بلا خادم في Azure، طورت Microsoft واختبرت تطبيقاً بلا خادم يستخدم Microsoft Azure Functions . تستعرض هذه المقالة التعليمة البرمجية لحل Functions بلا خادم، وتصف قرارات التصميم، وتفاصيل التنفيذ، وبعض "مشكلات التعثر" التي قد تواجهها.

اكتشاف الحل

يصف الحل المكون من جزأين نظاماً افتراضياً لإيصال الطائرات من دون طيار. ترسل الطائرات من دون طيار حالة الرحلة إلى السحابة التي تخزن هذه الرسائل لاستخدامها لاحقاً. يتيح تطبيق الويب للمستخدمين استرداد الرسائل للحصول على أحدث حالة للأجهزة.

يمكنك تنزيل التعليمة البرمجية الخاص بهذا الحل من GitHub.

تفترض هذه الإرشادات الإلمام الأساسي بالتقنيات التالية:

لست بحاجة إلى أن تكون خبيراً في Functions أو مراكز الأحداث، ولكن يجب أن تفهم ميزاتها على مستوى عالٍ. إليك بعض الموارد الجيدة للبدء:

فَهم السيناريو

رسم تخطيطي للكتل الوظيفية

تدير شركة Fabrikam أسطولاً من الطائرات من دون طيار لخدمة توصيل الطلبات من دون طيار (سائق). يتكون التطبيق من مجالين وظيفيين رئيسيين:

  • عرض الحدث. أثناء الرحلة، ترسل طائرات من دون طيار رسائل حالة إلى نقطة نهاية سحابية. يعالج التطبيق هذه الرسائل ويعالجها، ويكتب النتائج إلى قاعدة بيانات خلفية (Azure Cosmos DB). ترسل الأجهزة رسائل بتنسيق بروتوكول المخزن المؤقت للبروتوكول (protobuf). إن Protobuf تنسيق إنشاء تسلسل فعال وذاتي الوصف.

    تحتوي هذه الرسائل على تحديثات جزئية. في فاصل زمني ثابت، ترسل كل طائرة من دون طيار رسالة "إطار رئيسي" تحتوي على جميع حقول الحالة. بين الإطارات الرئيسية، تتضمن رسائل الحالة فقط الحقول التي تغيرت منذ الرسالة الأخيرة. هذا السلوك نموذجي للعديد من أجهزة إنترنت الأشياء التي تحتاج إلى الحفاظ على عرض النطاق الترددي والطاقة.

  • تطبيق الويب. يتيح تطبيق الويب للمستخدمين البحث عن جهاز والاستعلام عن آخر حالة معروفة للجهاز. يجب على المستخدمين تسجيل الدخول إلى التطبيق والمصادقة باستخدام معرف Microsoft Entra. يسمح التطبيق فقط للطلبات الواردة من المستخدمين المصرح لهم بالوصول إلى التطبيق.

إليك لقطة شاشة لتطبيق الويب، تعرض نتيجة الاستعلام:

لقطة شاشة لتطبيق العميل

تصميم التطبيق

قررت شركة Fabrikam استخدام Azure Functions لتنفيذ منطق عمل التطبيق. تعد Azure Functions مثالاً على "الوظائف كخدمة" (FaaS). في نموذج الحوسبة هذا، تعد function جزءاً من التعليمة البرمجية التي يتم توزيعها في السحابة ويتم تشغيلها في بيئة استضافة. تقوم بيئة الاستضافة هذه بتجريد الخوادم التي تقوم بتشغيل التعليمة البرمجية بشكل كامل.

لماذا تختار نهج بلا خادم؟

التصميم بلا خادم مع Functions هو مثال على بنية مدفوعة بالأحداث. يتم تشغيل التعليمة البرمجية للوظيفة بواسطة حدث ما خارج الوظيفة - في هذه الحالة، إما رسالة من طائرة من دون طيار وإما طلب HTTP من تطبيق عميل. باستخدام تطبيق وظيفي، لن تحتاج إلى كتابة أي التعليمة البرمجية للمشغل. أنت تكتب فقط التعليمة البرمجية التي يتم تشغيلها استجابةً للمشغل. هذا يعني أنه يمكنك التركيز على منطق عملك، بدلاً من كتابة الكثير من التعليمة البرمجية للتعامل مع مخاوف البنية الأساسية مثل الرسائل.

هناك أيضاً بعض المزايا التشغيلية لاستخدام بنية بلا خادم:

  • ليست هناك حاجة لإدارة الخوادم.
  • حساب الموارد يتم تخصيصها ديناميكيا حسب الحاجة.
  • تتم محاسبتك فقط على موارد الحساب المستخدمة لتنفيذ التعليمة البرمجية الخاصة بك.
  • مقياس موارد الحساب حسب الطلب بناءً على نسبة استخدام الشبكة.

بناء الأنظمة

يوضح الرسم البياني التالي البنية عالية المستوى للتطبيق:

رسم تخطيطي يوضح البنية عالية المستوى لتطبيق الوظائف بلا خادم.

في أحد تدفقات البيانات، تعرض الأسهم الرسائل التي تنتقل من الأجهزة إلى مراكز الأحداث وتقوم بتشغيل تطبيق الوظيفة. من التطبيق، يُظهر سهم واحد الرسائل المهملة التي تنتقل إلى قائمة انتظار التخزين، وسهم آخر يُظهر الكتابة إلى قاعدة بيانات (Azure Cosmos DB). في تدفق بيانات آخر، تُظهر الأسهم تطبيق الويب للعميل وهو يحصل على ملفات ثابتة من استضافة الويب الثابتة لتخزين كائن ثنائي كبير الحجم (Blob)، من خلال شبكة تسليم المحتوى (CDN). يظهر سهم آخر لطلب HTTP للعميل يمر عبر إدارة واجهة برمجة التطبيقات APIM. من إدارة واجهة برمجة التطبيقات APIM، يُظهر سهم واحد تشغيل التطبيق الوظيفي وقراءة البيانات من قاعدة بيانات (Azure Cosmos DB). يظهر سهم آخر المصادقة من خلال معرف Microsoft Entra. يقوم المستخدم أيضا بتسجيل الدخول إلى معرف Microsoft Entra.

تناول الحدث:

  1. يتم استيعاب رسائل الطائرات من دون طيار بواسطة مراكز الأحداث.
  2. تقوم مراكز الأحداث بإنتاج سلسلة من الأحداث التي تحتوي على بيانات الرسالة.
  3. تؤدي هذه الأحداث إلى تشغيل تطبيق Azure Functions لمعالجتها.
  4. يتم تخزين النتائج في Azure Cosmos DB.

التطبيق على شبكة الإنترنت:

  1. يتم تقديم الملفات الثابتة من قِبل CDN من تخزين كائن ثنائي كبير الحجم (Blob).
  2. يقوم مستخدم بتسجيل الدخول إلى تطبيق الويب باستخدام معرف Microsoft Entra.
  3. تعمل إدارة واجهة برمجة تطبيقات Azure APIM كبوابة تعرض نقطة نهاية واجهة برمجة تطبيقات (REST API).
  4. تؤدي طلبات HTTP من العميل إلى تشغيل تطبيق Azure Functions الذي يقرأ من Azure Cosmos DB ويعيد النتيجة.

يعتمد هذا التطبيق على علامتين مرجعيتين، تتوافقان مع الكتلتين الوظيفيتين الموصوفتين أعلاه:

يمكنك قراءة هذه المقالات لمعرفة المزيد حول البنية عالية المستوى وخدمات Azure المستخدمة في الحل واعتبارات قابلية التوسع والأمان والموثوقية.

وظيفة بيانات تتبع الاستخدام من دون طيار

لنبدأ بإلقاء نظرة على الوظيفة التي تعالج رسائل الطائرات من دون طيار من مراكز الأحداث. تم تحديد الوظيفة في فئة تسمى RawTelemetryFunction:

namespace DroneTelemetryFunctionApp
{
    public class RawTelemetryFunction
    {
        private readonly ITelemetryProcessor telemetryProcessor;
        private readonly IStateChangeProcessor stateChangeProcessor;
        private readonly TelemetryClient telemetryClient;

        public RawTelemetryFunction(ITelemetryProcessor telemetryProcessor, IStateChangeProcessor stateChangeProcessor, TelemetryClient telemetryClient)
        {
            this.telemetryProcessor = telemetryProcessor;
            this.stateChangeProcessor = stateChangeProcessor;
            this.telemetryClient = telemetryClient;
        }
    }
    ...
}

تحتوي هذه الفئة على العديد من التبعيات، والتي يتم حقنها في المُنشئ باستخدام إدخال التبعية:

  • تحدد واجهات ITelemetryProcessor وIStateChangeProcessor عنصرين مساعدين. كما سنرى، تقوم هذه العناصر بمعظم العمل.

  • يعد TelemetryClient جزءاً من تطبيق عدة تطوير البرامج (Application Insights SDK). يتم استخدامه لإرسال قياسات التطبيق المخصصة إلى Application Insights.

لاحقاً، سننظر في كيفية تكوين إدخال التبعية. في الوقت الحالي، افترض فقط أن هذه التبعيات موجودة.

تكوين مشغل مراكز الأحداث

يتم تنفيذ المنطق في الوظيفة كطريقة غير متزامنة تسمى RunAsync. ها هو توقيع الطريقة:

[FunctionName("RawTelemetryFunction")]
[StorageAccount("DeadLetterStorage")]
public async Task RunAsync(
    [EventHubTrigger("%EventHubName%", Connection = "EventHubConnection", ConsumerGroup ="%EventHubConsumerGroup%")]EventData[] messages,
    [Queue("deadletterqueue")] IAsyncCollector<DeadLetterMessage> deadLetterMessages,
    ILogger logger)
{
    // implementation goes here
}

تأخذ الطريقة المعلمات التالية:

  • messages عبارة عن مجموعة من رسائل مركز الأحداث.
  • deadLetterMessages عبارة عن قائمة انتظار Azure Storage، تُستخدم لتخزين الرسائل المهملة.
  • يوفر logging واجهة تسجيل لكتابة سجلات التطبيق. يتم إرسال هذه السجلات إلى Azure Monitor.

تعمل السمة EventHubTrigger في المعلمة messages على تكوين المشغل. تحدد خصائص السمة اسم محور الحدث وسلسلة الاتصال و مجموعة المستهلكين . (تعد consumer group طريقة عرض معزولة لتدفق أحداث مراكز الأحداث. يسمح هذا التجريد للعديد من المستهلكين في نفس مركز الأحداث.)

لاحظ علامات النسبة المئوية (%) في بعض خصائص السمة. تشير هذه إلى أن الخاصية تحدد اسم إعداد التطبيق، وأن القيمة الفعلية مأخوذة من إعداد هذا التطبيق في وقت التشغيل. خلاف ذلك، من دون علامات النسبة المئوية، تعطي الخاصية القيمة الحرفية.

تعتبر الخاصية Connection استثناءً. تحدد هذه الخاصية دائماً اسم إعداد التطبيق، وليس قيمة حرفية أبداً، لذلك ليست هناك حاجة إلى علامة النسبة المئوية. سبب هذا التمييز هو أن سلسلة الاتصال سرية ولا ينبغي أبداً التحقق منها في التعليمة البرمجية المصدري.

في حين أن الخاصيتين الأخريين (اسم مركز الحدث ومجموعة المستهلكين) ليستا بيانات حساسة مثل سلسلة الاتصال، فلا يزال من الأفضل وضعها في إعدادات التطبيق، بدلاً من الترميز الثابت. بهذه الطريقة، يمكن تحديثها دون إعادة ترجمة التطبيق.

لمزيد من المعلومات حول تكوين هذا المشغل، راجع روابط Azure Event Hubs لـ Azure Functions .

منطق معالجة الرسائل

إليك طريقة تنفيذ RawTelemetryFunction.RunAsync التي تعالج مجموعة من الرسائل:

[FunctionName("RawTelemetryFunction")]
[StorageAccount("DeadLetterStorage")]
public async Task RunAsync(
    [EventHubTrigger("%EventHubName%", Connection = "EventHubConnection", ConsumerGroup ="%EventHubConsumerGroup%")]EventData[] messages,
    [Queue("deadletterqueue")] IAsyncCollector<DeadLetterMessage> deadLetterMessages,
    ILogger logger)
{
    telemetryClient.GetMetric("EventHubMessageBatchSize").TrackValue(messages.Length);

    foreach (var message in messages)
    {
        DeviceState deviceState = null;

        try
        {
            deviceState = telemetryProcessor.Deserialize(message.Body.Array, logger);

            try
            {
                await stateChangeProcessor.UpdateState(deviceState, logger);
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Error updating status document", deviceState);
                await deadLetterMessages.AddAsync(new DeadLetterMessage { Exception = ex, EventData = message, DeviceState = deviceState });
            }
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Error deserializing message", message.SystemProperties.PartitionKey, message.SystemProperties.SequenceNumber);
            await deadLetterMessages.AddAsync(new DeadLetterMessage { Exception = ex, EventData = message });
        }
    }
}

عندما يتم استدعاء الوظيفة، تحتوي المعلمة messages على مصفوفة من الرسائل من مركز الحدث. ستؤدي معالجة الرسائل على دفعات بشكل عام إلى أداء أفضل من قراءة رسالة واحدة في كل مرة. ومع ذلك، يجب عليك التأكد من أن الوظيفة مرنة وتتعامل مع حالات الفشل والاستثناءات بأمان. وإلا، إذا قامت الوظيفة بإلقاء استثناء غير معالج في منتصف المجموعة، فقد تفقد الرسائل المتبقية. تمت مناقشة هذا الاعتبار بمزيد من التفصيل في قسم معالجة الأخطاء.

ولكن إذا تجاهلت معالجة الاستثناءات، فإن منطق المعالجة لكل رسالة يكون بسيطاً:

  1. اتصل بـ ITelemetryProcessor.Deserialize لإلغاء تسلسل الرسالة التي تحتوي على تغيير في حالة الجهاز.
  2. اتصل بـ IStateChangeProcessor.UpdateState لمعالجة تغيير الحالة.

لنلقِ نظرة على هاتين الطريقتين بمزيد من التفصيل، بدءاً بالطريقة Deserialize.

طريقة نزع التسلسل

تأخذ الطريقة TelemetryProcess.Deserialize مصفوفة بايت تحتوي على حمولة الرسالة. يقوم بإلغاء تسلسل هذه الحمولة وإرجاع عنصر DeviceState، والذي يمثل حالة الطائرة من دون طيار. قد تمثل الولاية تحديثاً جزئياً، يحتوي فقط على دلتا من آخر حالة معروفة. لذلك، تحتاج الطريقة إلى معالجة حقول null في الحمولة غير المتسلسلة.

public class TelemetryProcessor : ITelemetryProcessor
{
    private readonly ITelemetrySerializer<DroneState> serializer;

    public TelemetryProcessor(ITelemetrySerializer<DroneState> serializer)
    {
        this.serializer = serializer;
    }

    public DeviceState Deserialize(byte[] payload, ILogger log)
    {
        DroneState restored = serializer.Deserialize(payload);

        log.LogInformation("Deserialize message for device ID {DeviceId}", restored.DeviceId);

        var deviceState = new DeviceState();
        deviceState.DeviceId = restored.DeviceId;

        if (restored.Battery != null)
        {
            deviceState.Battery = restored.Battery;
        }
        if (restored.FlightMode != null)
        {
            deviceState.FlightMode = (int)restored.FlightMode;
        }
        if (restored.Position != null)
        {
            deviceState.Latitude = restored.Position.Value.Latitude;
            deviceState.Longitude = restored.Position.Value.Longitude;
            deviceState.Altitude = restored.Position.Value.Altitude;
        }
        if (restored.Health != null)
        {
            deviceState.AccelerometerOK = restored.Health.Value.AccelerometerOK;
            deviceState.GyrometerOK = restored.Health.Value.GyrometerOK;
            deviceState.MagnetometerOK = restored.Health.Value.MagnetometerOK;
        }
        return deviceState;
    }
}

تستخدم هذه الطريقة واجهة مساعدة أخرى، ITelemetrySerializer<T>، لإلغاء تسلسل الرسالة الأولية. يتم تحويل النتائج بعد ذلك إلى نموذج POCO يسهل التعامل معه. يساعد هذا التصميم في عزل منطق المعالجة عن تفاصيل تنفيذ التسلسل. يتم تعريف واجهة ITelemetrySerializer<T> في مكتبة مشتركة، والتي يتم استخدامها أيضاً بواسطة محاكي الجهاز لإنشاء أحداث جهاز محاكاة وإرسالها إلى مراكز الأحداث.

using System;

namespace Serverless.Serialization
{
    public interface ITelemetrySerializer<T>
    {
        T Deserialize(byte[] message);

        ArraySegment<byte> Serialize(T message);
    }
}

طريقة UpdateState

تطبق طريقة StateChangeProcessor.UpdateState تغييرات الحالة. يتم تخزين الحالة الأخيرة المعروفة لكل طائرة بدون طيار كمستند JSON في Azure Cosmos DB. نظراً لأن الطائرات من دون طيار ترسل تحديثات جزئية، لا يمكن للتطبيق ببساطة الكتابة فوق المستند عندما يحصل على تحديث. بدلاً من ذلك، يحتاج إلى إحضار الحالة السابقة، ودمج الحقول، ثم إجراء عملية upsert.

public class StateChangeProcessor : IStateChangeProcessor
{
    private IDocumentClient client;
    private readonly string cosmosDBDatabase;
    private readonly string cosmosDBCollection;

    public StateChangeProcessor(IDocumentClient client, IOptions<StateChangeProcessorOptions> options)
    {
        this.client = client;
        this.cosmosDBDatabase = options.Value.COSMOSDB_DATABASE_NAME;
        this.cosmosDBCollection = options.Value.COSMOSDB_DATABASE_COL;
    }

    public async Task<ResourceResponse<Document>> UpdateState(DeviceState source, ILogger log)
    {
        log.LogInformation("Processing change message for device ID {DeviceId}", source.DeviceId);

        DeviceState target = null;

        try
        {
            var response = await client.ReadDocumentAsync(UriFactory.CreateDocumentUri(cosmosDBDatabase, cosmosDBCollection, source.DeviceId),
                                                            new RequestOptions { PartitionKey = new PartitionKey(source.DeviceId) });

            target = (DeviceState)(dynamic)response.Resource;

            // Merge properties
            target.Battery = source.Battery ?? target.Battery;
            target.FlightMode = source.FlightMode ?? target.FlightMode;
            target.Latitude = source.Latitude ?? target.Latitude;
            target.Longitude = source.Longitude ?? target.Longitude;
            target.Altitude = source.Altitude ?? target.Altitude;
            target.AccelerometerOK = source.AccelerometerOK ?? target.AccelerometerOK;
            target.GyrometerOK = source.GyrometerOK ?? target.GyrometerOK;
            target.MagnetometerOK = source.MagnetometerOK ?? target.MagnetometerOK;
        }
        catch (DocumentClientException ex)
        {
            if (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
            {
                target = source;
            }
        }

        var collectionLink = UriFactory.CreateDocumentCollectionUri(cosmosDBDatabase, cosmosDBCollection);
        return await client.UpsertDocumentAsync(collectionLink, target);
    }
}

تستخدم هذه التعليمة البرمجية الواجهة IDocumentClient لإحضار مستند من Azure Cosmos DB. إذا كان المستند موجوداً، يتم دمج قيم الحالة الجديدة في المستند الحالي. خلاف ذلك، يتم إنشاء مستند جديد. يتم التعامل مع كلتا الحالتين بواسطة الطريقة UpsertDocumentAsync.

تم تحسين هذا التعليمة البرمجية للحالة التي يوجد بها المستند بالفعل ويمكن دمجها. في أول رسالة تتبع عن بعد من طائرة من دون طيار معينة، ستطرح طريقة ReadDocumentAsync استثناءً، لأنه لا يوجد مستند لتلك الطائرة من دون طيار. بعد الرسالة الأولى، سيكون المستند متاحاً.

لاحظ أن هذه الفئة تستخدم حقن التبعية لإدخال IDocumentClient ل Azure Cosmos DB و IOptions<T> مع إعدادات التكوين. سنرى كيفية إعداد إدخال التبعية لاحقاً.

إشعار

تدعم Azure Functions ربط إخراج ل Azure Cosmos DB. يتيح هذا الربط لتطبيق الوظائف كتابة المستندات في Azure Cosmos DB دون أي تعليمة برمجية. ومع ذلك، لن يعمل ربط الإخراج لهذا السيناريو المحدد، بسبب منطق upert المخصص المطلوب.

معالجة الخطأ

كما ذكرنا سابقاً، يعالج تطبيق الوظيفة RawTelemetryFunction مجموعة من الرسائل في حلقة. هذا يعني أن الوظيفة تحتاج إلى التعامل مع أي استثناءات بأمان ومواصلة معالجة بقية الدُفعة. خلاف ذلك، قد يتم إسقاط الرسائل.

في حالة مواجهة استثناء عند معالجة رسالة، تضع الوظيفة الرسالة في قائمة انتظار الرسائل المهملة:

catch (Exception ex)
{
    logger.LogError(ex, "Error deserializing message", message.SystemProperties.PartitionKey, message.SystemProperties.SequenceNumber);
    await deadLetterMessages.AddAsync(new DeadLetterMessage { Exception = ex, EventData = message });
}

يتم تحديد قائمة انتظار الرسائل المهملة باستخدام ربط الإخراج بقائمة انتظار التخزين:

[FunctionName("RawTelemetryFunction")]
[StorageAccount("DeadLetterStorage")]  // App setting that holds the connection string
public async Task RunAsync(
    [EventHubTrigger("%EventHubName%", Connection = "EventHubConnection", ConsumerGroup ="%EventHubConsumerGroup%")]EventData[] messages,
    [Queue("deadletterqueue")] IAsyncCollector<DeadLetterMessage> deadLetterMessages,  // output binding
    ILogger logger)

هنا تحدد السمة Queue ربط الإخراج، وتحدد السمة StorageAccount اسم إعداد التطبيق الذي يحمل سلسلة الاتصال لحساب التخزين.

نصيحة التوزيع: في قالب Resource Manager الذي ينشئ حساب التخزين، يمكنك تلقائياً ملء أحد إعدادات التطبيق بسلسلة الاتصال. الحيلة هي استخدام وظيفة listKeys.

هذا هو قسم القالب الذي يُنشئ حساب التخزين لقائمة الانتظار:

    {
        "name": "[variables('droneTelemetryDeadLetterStorageQueueAccountName')]",
        "type": "Microsoft.Storage/storageAccounts",
        "location": "[resourceGroup().location]",
        "apiVersion": "2017-10-01",
        "sku": {
            "name": "[parameters('storageAccountType')]"
        },

هذا هو قسم القالب الذي يقوم بإنشاء تطبيق الوظيفة.


    {
        "apiVersion": "2015-08-01",
        "type": "Microsoft.Web/sites",
        "name": "[variables('droneTelemetryFunctionAppName')]",
        "location": "[resourceGroup().location]",
        "tags": {
            "displayName": "Drone Telemetry Function App"
        },
        "kind": "functionapp",
        "dependsOn": [
            "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]",
            ...
        ],
        "properties": {
            "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]",
            "siteConfig": {
                "appSettings": [
                    {
                        "name": "DeadLetterStorage",
                        "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('droneTelemetryDeadLetterStorageQueueAccountName'), ';AccountKey=', listKeys(variables('droneTelemetryDeadLetterStorageQueueAccountId'),'2015-05-01-preview').key1)]"
                    },
                    ...

يعرّف هذا إعداد تطبيق يسمى DeadLetterStorage يتم ملء قيمته باستخدام الوظيفة listKeys. من المهم جعل مورد تطبيق الوظيفة يعتمد على مورد حساب التخزين (راجع عنصر dependsOn). يضمن هذا إنشاء حساب التخزين أولاً وإتاحة سلسلة الاتصال.

إعداد إدخال التبعية

تقوم التعليمة البرمجية التالية بإعداد إدخال التبعية لوظيفة RawTelemetryFunction:

[assembly: FunctionsStartup(typeof(DroneTelemetryFunctionApp.Startup))]

namespace DroneTelemetryFunctionApp
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddOptions<StateChangeProcessorOptions>()
                .Configure<IConfiguration>((configSection, configuration) =>
                {
                    configuration.Bind(configSection);
                });

            builder.Services.AddTransient<ITelemetrySerializer<DroneState>, TelemetrySerializer<DroneState>>();
            builder.Services.AddTransient<ITelemetryProcessor, TelemetryProcessor>();
            builder.Services.AddTransient<IStateChangeProcessor, StateChangeProcessor>();

            builder.Services.AddSingleton<IDocumentClient>(ctx => {
                var config = ctx.GetService<IConfiguration>();
                var cosmosDBEndpoint = config.GetValue<string>("CosmosDBEndpoint");
                var cosmosDBKey = config.GetValue<string>("CosmosDBKey");
                return new DocumentClient(new Uri(cosmosDBEndpoint), cosmosDBKey);
            });
        }
    }
}

يمكن أن تستخدم Azure Functions المكتوبة لـ .NET إطار عمل إدخال التبعية الأساسية لـ ASP.NET. الفكرة الأساسية هي أنك تعلن عن طريقة بدء التشغيل للتجميع الخاص بك. تأخذ الطريقة واجهة IFunctionsHostBuilder، والتي تُستخدم لتعريف التبعيات لـ DI. يمكنك القيام بذلك عن طريق استدعاء الأسلوب Add* على العنصر Services. عندما تضيف تبعية، فإنك تحدد عمرها:

  • يتم إنشاء عناصر عابرة في كل مرة يتم طلبها.
  • يتم إنشاء عناصر محددة النطاق مرة واحدة لكل تنفيذ دالة.
  • يتم إعادة استخدام عناصر Singleton عبر عمليات تنفيذ الوظائف، خلال عمر مضيف الوظيفة.

في هذا المثال، تم التصريح عن العنصرين TelemetryProcessor وStateChangeProcessor على أنهما عابران. هذا مناسب للخدمات خفيفة الوزن وعديمة الجنسية. من ناحية أخرى، يجب أن تكون الفئة DocumentClient مفردة للحصول على أفضل أداء. لمزيد من المعلومات، راجع نصائح أداء Azure Cosmos DB و.NET .

إذا عدت إلى التعليمة البرمجية RawTelemetryFunction، فسترى هناك تبعية أخرى لا تظهر في التعليمة البرمجية لإعداد DI، وهي فئة TelemetryClient المستخدمة لتسجيل قياسات التطبيق. يقوم وقت تشغيل Functions بتسجيل هذه الفئة تلقائياً في حاوية DI، لذلك لا تحتاج إلى تسجيلها بشكل صريح.

لمزيد من المعلومات حول DI في Azure Functions، راجع المقالات التالية:

تمرير إعدادات التكوين في DI

في بعض الأحيان يجب تكوين عنصر ببعض قيم التكوين. بشكل عام، يجب أن تأتي هذه الإعدادات من إعدادات التطبيق أو (في حالة الأسرار) من Azure Key Vault.

هناك مثالان في هذا التطبيق. أولا، DocumentClient تأخذ الفئة نقطة نهاية ومفتاح خدمة Azure Cosmos DB. بالنسبة لهذا العنصر، يسجل التطبيق لامدا التي سيتم استدعاؤها بواسطة حاوية DI. تستخدم lambda واجهة IConfiguration لقراءة قيم التكوين:

builder.Services.AddSingleton<IDocumentClient>(ctx => {
    var config = ctx.GetService<IConfiguration>();
    var cosmosDBEndpoint = config.GetValue<string>("CosmosDBEndpoint");
    var cosmosDBKey = config.GetValue<string>("CosmosDBKey");
    return new DocumentClient(new Uri(cosmosDBEndpoint), cosmosDBKey);
});

المثال الثاني هو فئة StateChangeProcessor. بالنسبة لهذا العنصر، نستخدم أسلوباً يسمى نمط الخيارات . إليك كيفية عمل هذا:

  1. حدد فئة T تحتوي على إعدادات التكوين الخاصة بك. في هذه الحالة، اسم قاعدة بيانات Azure Cosmos DB واسم المجموعة.

    public class StateChangeProcessorOptions
    {
        public string COSMOSDB_DATABASE_NAME { get; set; }
        public string COSMOSDB_DATABASE_COL { get; set; }
    }
    
  2. أضف الفئة T كفئة خيارات لـ DI.

    builder.Services.AddOptions<StateChangeProcessorOptions>()
        .Configure<IConfiguration>((configSection, configuration) =>
        {
            configuration.Bind(configSection);
        });
    
  3. في منشئ الفئة التي يتم تهيئتها، قم بتضمين معلمة IOptions<T>.

    public StateChangeProcessor(IDocumentClient client, IOptions<StateChangeProcessorOptions> options)
    

سيقوم نظام DI تلقائياً بتعبئة فئة الخيارات بقيم التكوين وتمريرها إلى المُنشئ.

هناك عدة مزايا لهذا الأسلوب:

  • افصل الفئة عن مصدر قيم التكوين.
  • قم بإعداد مصادر تكوين مختلفة بسهولة، مثل متغيرات البيئة أو ملفات تكوين JSON.
  • تبسيط اختبار الوحدة.
  • استخدم فئة خيارات مكتوبة بشدة، والتي تكون أقل عرضة للخطأ من مجرد تمرير القيم العددية.

دالة GetStatus

يقوم تطبيق Functions الآخر في هذا الحل بتنفيذ واجهة برمجة تطبيقات REST بسيطة للحصول على آخر حالة معروفة للطائرة من دون طيار. يتم تحديد هذه الوظيفة في فئة تسمى GetStatusFunction. هذا هو التعليمة البرمجية الكامل للوظيفة:

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using System.Security.Claims;
using System.Threading.Tasks;

namespace DroneStatusFunctionApp
{
    public static class GetStatusFunction
    {
        public const string GetDeviceStatusRoleName = "GetStatus";

        [FunctionName("GetStatusFunction")]
        public static IActionResult Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)]HttpRequest req,
            [CosmosDB(
                databaseName: "%COSMOSDB_DATABASE_NAME%",
                collectionName: "%COSMOSDB_DATABASE_COL%",
                ConnectionStringSetting = "COSMOSDB_CONNECTION_STRING",
                Id = "{Query.deviceId}",
                PartitionKey = "{Query.deviceId}")] dynamic deviceStatus,
            ClaimsPrincipal principal,
            ILogger log)
        {
            log.LogInformation("Processing GetStatus request.");

            if (!principal.IsAuthorizedByRoles(new[] { GetDeviceStatusRoleName }, log))
            {
                return new UnauthorizedResult();
            }

            string deviceId = req.Query["deviceId"];
            if (deviceId == null)
            {
                return new BadRequestObjectResult("Missing DeviceId");
            }

            if (deviceStatus == null)
            {
                return new NotFoundResult();
            }
            else
            {
                return new OkObjectResult(deviceStatus);
            }
        }
    }
}

تستخدم هذه الوظيفة مشغل HTTP لمعالجة طلب HTTP GET. تستخدم الدالة ربط إدخال Azure Cosmos DB لجلب المستند المطلوب. أحد الاعتبارات هو أن هذا الربط سيتم تشغيله قبل تنفيذ منطق التفويض داخل الوظيفة. إذا طلب مستخدم غير مصرح له مستنداً، فسيظل ربط الوظيفة يجلب المستند. ثم ستعيد التعليمة البرمجية للتخويل 401، لذلك لن يرى المستخدم المستند. ما إذا كان هذا السلوك مقبولاً قد يعتمد على متطلباتك. على سبيل المثال، قد يجعل هذا النهج من الصعب تدقيق الوصول إلى البيانات للبيانات الحساسة.

المصادقة والتخويل

يستخدم تطبيق الويب معرف Microsoft Entra لمصادقة المستخدمين. نظراً لأن التطبيق عبارة عن تطبيق من صفحة واحدة (SPA) يعمل في المتصفح، فإن تدفق المنح الضمني يكون مناسباً:

  1. يعيد تطبيق الويب توجيه المستخدم إلى موفر الهوية (في هذه الحالة، معرف Microsoft Entra).
  2. يقوم المستخدم بإدخال بيانات اعتماده.
  3. يقوم موفر الهوية بإعادة التوجيه إلى تطبيق الويب باستخدام رمز وصول.
  4. يرسل تطبيق الويب طلباً إلى واجهة برمجة تطبيقات الويب ويتضمن رمز الوصول في رأس التفويض.

رسم تخطيطي للتدفق الضمني

يمكن تكوين تطبيق الوظيفة لمصادقة المستخدمين بصفر تعليمة برمجية. لمزيد من المعلومات، راجع المصادقة والتخويل في Azure App Service.

من ناحية أخرى، يتطلب التفويض عموماً بعض منطق الأعمال. يدعم معرف Microsoft Entra المصادقة المستندة إلى المطالبات. في هذا النموذج، يتم تمثيل هوية المستخدم كمجموعة من المطالبات التي تأتي من موفر الهوية. يمكن أن تكون المطالبة أي جزء من المعلومات حول المستخدم، مثل اسمه أو عنوان بريده الإلكتروني.

يحتوي رمز الوصول على مجموعة فرعية من مطالبات المستخدم. من بين هذه الأدوار أي تطبيق تم تعيين المستخدم إليه.

المعلمة principal للدالة هي عنصر ClaimsPrincipal يحتوي على المطالبات من رمز الوصول. كل مطالبة هي زوج مفتاح / قيمة من نوع المطالبة وقيمة المطالبة. يستخدم التطبيق هذه لتفويض الطلب.

تختبر طريقة الملحق التالية ما إذا كان عنصر ClaimsPrincipal يحتوي على مجموعة من الأدوار. تقوم بإرجاع false إذا كان أيٌّ من الأدوار المحددة مفقوداً. إذا عرضت هذه الطريقة خطأ، تقوم الوظيفة بإرجاع HTTP 401 (غير مصرح به).

namespace DroneStatusFunctionApp
{
    public static class ClaimsPrincipalAuthorizationExtensions
    {
        public static bool IsAuthorizedByRoles(
            this ClaimsPrincipal principal,
            string[] roles,
            ILogger log)
        {
            var principalRoles = new HashSet<string>(principal.Claims.Where(kvp => kvp.Type == "roles").Select(kvp => kvp.Value));
            var missingRoles = roles.Where(r => !principalRoles.Contains(r)).ToArray();
            if (missingRoles.Length > 0)
            {
                log.LogWarning("The principal does not have the required {roles}", string.Join(", ", missingRoles));
                return false;
            }

            return true;
        }
    }
}

لمزيد من المعلومات حول المصادقة والتفويض في هذا التطبيق، راجع قسم اعتبارات الأمان في البنية المرجعية.

الخطوات التالية

بمجرد أن تتعرَّف على كيفية عمل هذا الحل المرجعي، تعرَّف على أفضل الممارسات والتوصيات لحلول مماثلة.

Azure Functions هي مجرد خيار حساب Azure واحد. للمساعدة في اختيار تقنية الحوسبة، راجع اختيار خدمة حساب Azure لتطبيقك .