Panduan kode: Aplikasi tanpa server dengan Functions

Azure Event Hubs
Azure Functions

Model tanpa server kode abstrak dari infrastruktur komputasi yang mendasarinya, memungkinkan pengembang untuk fokus pada logika bisnis tanpa penyiapan yang ekstensif. Kode tanpa server mengurangi biaya, karena Anda hanya membayar untuk sumber daya dan durasi eksekusi kode.

Model berbasis peristiwa tanpa server cocok dengan situasi yang peristiwa tertentunya memicu tindakan yang ditentukan. Misalnya, menerima pesan perangkat yang masuk memicu penyimpanan untuk digunakan nanti, atau pembaruan database memicu beberapa pemrosesan lebih lanjut.

Untuk membantu Anda menjelajahi teknologi tanpa server Azure di Azure, Microsoft mengembangkan dan menguji aplikasi tanpa server yang menggunakan Azure Functions. Artikel ini menelusuri kode untuk solusi Functions tanpa server, dan menjelaskan keputusan desain, detail penerapan, dan beberapa "gotcha" yang mungkin Anda temui.

Menjelajahi solusinya

Solusi dua bagian menjelaskan sistem pengiriman drone hipotetis. Drone mengirim status dalam penerbangan ke cloud, yang menyimpan pesan-pesan ini untuk digunakan nanti. Aplikasi web memungkinkan pengguna mengambil pesan untuk mendapatkan status terbaru perangkat.

Anda dapat mengunduh kode untuk solusi ini dari GitHub.

Panduan ini mengasumsikan kemiripan dasar dengan teknologi berikut:

Anda tidak perlu menjadi ahli dalam Functions atau Azure Event Hubs, tetapi Anda harus memahami fitur-fiturnya pada tingkat yang tinggi. Berikut adalah beberapa sumber daya yang bagus untuk memulai:

Memahami skenario

Diagram blok fungsional

Fabrikam mengelola armada drone untuk layanan pengiriman drone. Aplikasi ini terdiri dari dua area fungsional utama:

  • Penyerapan peristiwa. Selama penerbangan, drone mengirim pesan status ke titik akhir cloud. Aplikasi menyerap dan memproses pesan-pesan ini, dan menulis hasilnya ke database back-end (Azure Cosmos DB). Perangkat mengirim pesan dalam format buffer protokol (protobuf). Protobuf adalah format serialisasi yang efisien dan menggambarkan diri sendiri.

    Pesan ini berisi pembaruan parsial. Pada interval tetap, setiap drone mengirimkan pesan "bingkai kunci" yang berisi semua bidang status. Di antara bingkai kunci, pesan status hanya menyertakan bidang yang berubah sejak pesan terakhir. Perilaku ini khas dari banyak perangkat IoT yang perlu menghemat bandwidth dan daya.

  • Aplikasi web. Aplikasi web memungkinkan pengguna untuk mencari perangkat dan menanyakan status terakhir perangkat yang diketahui. Pengguna harus masuk ke aplikasi dan mengautentikasi dengan ID Microsoft Entra. Aplikasi hanya mengizinkan permintaan dari pengguna yang berwenang untuk mengakses aplikasi.

Berikut adalah cuplikan layar aplikasi web, yang menunjukkan hasil kueri:

Cuplikan layar aplikasi klien

Desain aplikasi

Fabrikam telah memutuskan untuk menggunakan Azure Functions guna menerapkan logika bisnis aplikasi. Azure Functions adalah contoh dari "Functions sebagai layanan" (FaaS). Dalam model komputasi ini, fungsi adalah bagian kode yang disebarkan ke cloud dan berjalan di lingkungan hosting. Lingkungan hosting ini sepenuhnya mengabstraksi server yang menjalankan kode.

Mengapa memilih pendekatan tanpa server?

Arsitektur tanpa server dengan Functions adalah contoh arsitektur yang digerakkan oleh peristiwa. Kode fungsi dipicu oleh beberapa peristiwa yang berada di luar fungsi — dalam hal ini, baik pesan dari drone, maupun permintaan HTTP dari aplikasi klien. Dengan aplikasi fungsi, Anda tidak perlu menulis kode apa pun untuk pemicunya. Anda hanya menulis kode yang berjalan sebagai respons terhadap pemicu. Itu berarti Anda dapat fokus pada logika bisnis Anda, daripada menulis banyak kode untuk menangani masalah infrastruktur seperti olahpesan.

Ada juga beberapa keuntungan operasional menggunakan arsitektur tanpa server:

  • Tidak perlu mengelola server.
  • Sumber daya komputasi dialokasikan secara dinamis sesuai kebutuhan.
  • Anda hanya dikenakan biaya untuk sumber daya komputasi yang digunakan untuk menjalankan kode Anda.
  • Skala sumber daya komputasi sesuai permintaan berdasarkan lalu lintas.

Sistem

Diagram berikut menunjukkan arsitektur tingkat tinggi dari aplikasi:

Diagram yang menunjukkan arsitektur tingkat tinggi dari aplikasi Functions tanpa server.

Dalam satu aliran data, panah menunjukkan pesan yang berpindah dari Perangkat ke Azure Event Hubs dan memicu aplikasi fungsi. Dari aplikasi, satu panah menunjukkan pesan surat mati yang masuk ke antrean penyimpanan, dan panah lain menunjukkan penulisan ke Azure Cosmos DB. Di aliran data lain, panah menunjukkan aplikasi web klien mendapatkan file statik dari hosting web statik penyimpanan Blob, melalui CDN. Panah lain menunjukkan permintaan HTTP klien melalui API Management. Dari API Management, satu panah menunjukkan aplikasi fungsi yang memicu dan membaca data dari Azure Cosmos DB. Panah lain memperlihatkan autentikasi melalui ID Microsoft Entra. Pengguna juga masuk ke ID Microsoft Entra.

Penyerapan peristiwa:

  1. Pesan drone diserap oleh Azure Event Hubs.
  2. Azure Event Hubs menghasilkan aliran peristiwa yang berisi data pesan.
  3. Peristiwa tersebut memicu aplikasi Azure Functions untuk memprosesnya.
  4. Hasilnya disimpan di Azure Cosmos DB.

Aplikasi web:

  1. File statik dilayani oleh CDN dari penyimpanan Blob.
  2. Pengguna masuk ke aplikasi web menggunakan ID Microsoft Entra.
  3. Azure API Management bertindak sebagai gateway yang mengekspos titik akhir REST API.
  4. Permintaan HTTP dari klien memicu aplikasi Azure Functions yang membaca dari Azure Cosmos DB dan mengembalikan hasilnya.

Aplikasi ini didasarkan pada dua arsitektur referensi, sesuai dengan dua blok fungsional yang dijelaskan di atas:

Anda dapat membaca artikel tersebut untuk mempelajari selengkapnya tentang arsitektur tingkat tinggi, layanan Azure yang digunakan dalam solusi, dan pertimbangan untuk skalabilitas, keamanan, dan keandalan.

Fungsi telemetri drone

Mari kita mulai dengan melihat fungsi yang memproses pesan drone dari Azure Event Hubs. Fungsi ditentukan dalam kelas bernama 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;
        }
    }
    ...
}

Kelas ini memiliki beberapa dependensi, yang dimasukkan ke dalam konstruktor menggunakan injeksi dependensi:

  • Antarmuka ITelemetryProcessor dan IStateChangeProcessor menentukan dua objek pembantu. Seperti yang akan kita lihat, objek-objek ini melakukan sebagian besar pekerjaan.

  • TelemetryClient adalah bagian dari Application Insights SDK. Ini digunakan untuk mengirim metrik aplikasi kustom ke Application Insights.

Nanti, kita akan melihat cara mengkonfigurasi injeksi dependensi. Untuk saat ini, anggap saja dependensi ini ada.

Mengonfigurasi pemicu Azure Event Hubs

Logika dalam fungsi diterapkan sebagai metode asinkron bernama RunAsync. Berikut adalah tanda tangan metode:

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

Metode ini mengambil parameter berikut:

  • messages adalah array pesan hub peristiwa.
  • deadLetterMessages adalah Antrean Azure Storage, digunakan untuk menyimpan pesan surat mati.
  • logging menyediakan antarmuka pengelogan, untuk menulis log aplikasi. Log tersebut dikirim ke Azure Monitor.

Atribut EventHubTrigger pada parameter messages mengonfigurasi pemicu. Properti atribut menentukan nama hub peristiwa, string koneksi, dan grup konsumen. (Grup konsumen adalah tampilan terisolasi dari aliran peristiwa Azure Event Hubs. Abstraksi ini memungkinkan beberapa konsumen dari hub peristiwa yang sama.)

Perhatikan tanda persen (%) di beberapa properti atribut. Ini menunjukkan bahwa properti menentukan nama pengaturan aplikasi, dan nilai sebenarnya diambil dari pengaturan aplikasi tersebut pada durasi. Jika tidak, tanpa tanda persen, properti memberikan nilai literal.

Properti Connection adalah pengecualian. Properti ini selalu menentukan nama pengaturan aplikasi, bukan nilai literal, sehingga tanda persen tidak diperlukan. Alasan perbedaan ini adalah bahwa string koneksi bersifat rahasia dan tidak boleh diperiksa ke dalam kode sumber.

Sementara dua properti lainnya (nama hub peristiwa dan grup konsumen) bukanlah data sensitif seperti string koneksi, masih lebih baik untuk memasukkannya ke dalam pengaturan aplikasi, daripada hard coding. Dengan begitu, properti tersebut dapat diperbarui tanpa mengkompilasi ulang aplikasi.

Untuk informasi selengkapnya tentang mengonfigurasi pemicu ini, lihat Pengikatan Azure Event Hubs untuk Azure Functions.

Logika pemrosesan pesan

Berikut penerapan metode RawTelemetryFunction.RunAsync yang memproses batch pesan:

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

Saat fungsi dipanggil, parameter messages berisi array pesan dari hub peristiwa. Memproses pesan dalam batch umumnya akan menangguhkan performa yang lebih baik daripada membaca satu pesan pada satu waktu. Namun, Anda harus memastikan fungsinya tangguh dan menangani kegagalan dan pengecualian dengan benar. Jika tidak, jika fungsi memunculkan pengecualian yang tidak tertangani di tengah batch, Anda mungkin kehilangan pesan yang tersisa. Pertimbangan ini dibahas lebih detail di bagian Penanganan kesalahan.

Namun jika Anda mengabaikan penanganan pengecualian, logika pemrosesan untuk setiap pesan sederhana:

  1. Panggil ITelemetryProcessor.Deserialize untuk membatalkan serialisasi pesan yang berisi perubahan status perangkat.
  2. Panggil IStateChangeProcessor.UpdateState untuk memproses perubahan status.

Mari kita lihat kedua metode ini lebih detail, dimulai dengan metode Deserialize.

Metode pembatalan serialisasi

Metode TelemetryProcess.Deserialize mengambil array byte yang berisi muatan pesan. Ini membatalkan serialisasi payload ini dan mengembalikan objek DeviceState, yang mewakili status drone. Status dapat mewakili pembaruan parsial, yang hanya berisi delta dari status terakhir yang diketahui. Oleh karena itu, metode ini perlu menangani bidang null di payload yang dibatalkan serialisasinya.

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

Metode ini menggunakan antarmuka pembantu lain, ITelemetrySerializer<T>, untuk membatalkan serialisasi pesan mentah. Hasilnya kemudian diubah menjadi model POCO yang lebih mudah untuk dikerjakan. Desain ini membantu mengisolasi logika pemrosesan dari detail penerapan serialisasi. Antarmuka ITelemetrySerializer<T> ditentukan dalam pustaka bersama, yang juga digunakan oleh simulator perangkat untuk menghasilkan peristiwa perangkat yang disimulasikan dan mengirimkannya ke Azure Event Hubs.

using System;

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

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

Metode UpdateState

Metode StateChangeProcessor.UpdateState menerapkan perubahan status. Status terakhir yang diketahui untuk setiap drone disimpan sebagai dokumen JSON di Azure Cosmos DB. Karena drone mengirim pembaruan parsial, aplikasi tidak bisa begitu saja menimpa dokumen saat mendapat pembaruan. Sebagai gantinya, ini perlu mengambil status sebelumnya, menggabungkan bidang, lalu melakukan operasi 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);
    }
}

Kode ini menggunakan IDocumentClient antarmuka untuk mengambil dokumen dari Azure Cosmos DB. Jika dokumen ada, nilai status baru digabungkan ke dalam dokumen yang sudah ada. Jika tidak, dokumen baru dibuat. Kedua kasus ditangani dengan metode UpsertDocumentAsync.

Kode ini dioptimalkan untuk kasus yang dokumennya sudah ada dan dapat digabungkan. Pada pesan telemetri pertama dari drone yang diberikan, metode ReadDocumentAsync akan mengeluarkan pengecualian, karena tidak ada dokumen untuk drone tersebut. Setelah pesan pertama, dokumen akan tersedia.

Perhatikan bahwa kelas ini menggunakan injeksi dependensi untuk menyuntikkan IDocumentClient untuk Azure Cosmos DB dan IOptions<T> dengan pengaturan konfigurasi. Kita akan melihat cara mengatur injeksi dependensi nanti.

Catatan

Azure Functions mendukung pengikatan output untuk Azure Cosmos DB. Pengikatan ini memungkinkan aplikasi fungsi menulis dokumen di Azure Cosmos DB tanpa kode apa pun. Namun, pengikatan output tidak akan berfungsi untuk skenario khusus ini, karena logika upsert kustom yang diperlukan.

Penanganan kesalahan

Seperti yang disebutkan sebelumnya, aplikasi fungsi RawTelemetryFunction memproses batch pesan dalam satu perulangan. Ini berarti fungsi tersebut perlu menangani pengecualian apa pun dengan benar dan terus memproses sisa batch. Jika tidak, pesan mungkin akan dihilangkan.

Jika pengecualian ditemui saat memproses pesan, fungsi menempatkan pesan ke antrean surat mati:

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

Antrean surat mati ditentukan menggunakan pengikatan output ke antrean penyimpanan:

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

Di sini atribut Queue menentukan pengikatan output, dan atribut StorageAccount menentukan nama pengaturan aplikasi yang menyimpan string koneksi untuk akun penyimpanan.

Kiat penyebaran: Dalam templat Resource Manager yang membuat akun penyimpanan, Anda dapat secara otomatis mengisi pengaturan aplikasi dengan string koneksi. Caranya adalah dengan menggunakan fungsi listKeys.

Berikut adalah bagian dari templat yang membuat akun penyimpanan untuk antrean:

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

Berikut adalah bagian dari templat yang membuat aplikasi fungsi.


    {
        "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)]"
                    },
                    ...

Ini menentukan pengaturan aplikasi bernama DeadLetterStorage yang nilainya diisi menggunakan fungsi listKeys. Penting untuk membuat sumber daya aplikasi fungsi bergantung pada sumber daya akun penyimpanan (lihat elemen dependsOn). Ini menjamin bahwa akun penyimpanan dibuat terlebih dahulu dan string koneksi tersedia.

Menyiapkan injeksi dependensi

Kode berikut menyiapkan injeksi dependensi untuk fungsi 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 yang ditulis untuk .NET dapat menggunakan kerangka kerja injeksi dependensi ASP.NET Core. Ide dasarnya adalah Anda mendeklarasikan metode memulai untuk assembly Anda. Metode ini menggunakan antarmuka IFunctionsHostBuilder, yang digunakan untuk mendeklarasikan dependensi untuk DI. Anda melakukannya dengan memanggil metode Add* pada objek Services. Saat Anda menambahkan dependensi, Anda menentukan masa pakainya:

  • Objek sementara dibuat setiap kali diminta.
  • Objek cakupan dibuat satu kali per eksekusi fungsi.
  • Objek tunggal digunakan kembali di seluruh eksekusi fungsi, dalam masa pakai host fungsi.

Dalam contoh ini, objek TelemetryProcessor dan StateChangeProcessor dideklarasikan sebagai sementara. Ini sesuai untuk layanan ringan tanpa status. Kelas DocumentClient, di sisi lain, harus tunggal untuk performa terbaik. Untuk informasi selengkapnya, lihat Kiat performa untuk Azure Cosmos DB dan .NET.

Jika Anda merujuk kembali ke kode untuk RawTelemetryFunction, Anda akan melihat dependensi lain yang tidak muncul dalam kode penyiapan DI, yaitu kelas TelemetryClient yang digunakan untuk mencatat metrik aplikasi. Runtime Functions secara otomatis mendaftarkan kelas ini ke dalam kontainer DI, jadi Anda tidak perlu mendaftarkannya secara eksplisit.

Untuk informasi selengkapnya tentang DI di Azure Functions, lihat artikel berikut ini:

Melewati pengaturan konfigurasi di DI

Terkadang sebuah objek harus diinisialisasi dengan beberapa nilai konfigurasi. Umumnya, pengaturan ini harus berasal dari pengaturan aplikasi atau (dalam hal rahasia) dari Azure Key Vault.

Ada dua contoh dalam aplikasi ini. Pertama, DocumentClient kelas mengambil titik akhir dan kunci layanan Azure Cosmos DB. Untuk objek ini, aplikasi mendaftarkan lambda yang akan dipanggil oleh kontainer DI. Lambda ini menggunakan antarmuka IConfiguration untuk membaca nilai konfigurasi:

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

Contoh kedua adalah kelas StateChangeProcessor. Untuk objek ini, kita menggunakan pendekatan yang disebut pola opsi. Berikut adalah cara kerjanya:

  1. Tentukan kelas T yang berisi pengaturan konfigurasi Anda. Dalam hal ini, nama database dan nama koleksi Azure Cosmos DB.

    public class StateChangeProcessorOptions
    {
        public string COSMOSDB_DATABASE_NAME { get; set; }
        public string COSMOSDB_DATABASE_COL { get; set; }
    }
    
  2. Tambahkan kelas T sebagai kelas opsi untuk DI.

    builder.Services.AddOptions<StateChangeProcessorOptions>()
        .Configure<IConfiguration>((configSection, configuration) =>
        {
            configuration.Bind(configSection);
        });
    
  3. Di konstruktor kelas yang sedang dikonfigurasi, sertakan parameter IOptions<T>.

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

Sistem DI akan secara otomatis mengisi kelas opsi dengan nilai konfigurasi dan meneruskannya ke konstruktor.

Ada beberapa keuntungan dari pendekatan ini:

  • Memisahkan kelas dari sumber nilai konfigurasi.
  • Menyiapkan berbagai sumber konfigurasi dengan mudah, seperti variabel lingkungan atau file konfigurasi JSON.
  • Menyederhanakan pengujian unit.
  • Menggunakan kelas opsi yang diketik dengan kuat, yang kurang rawan kesalahan daripada hanya meneruskan nilai skalar.

Fungsi GetStatus

Aplikasi Fungsi lainnya dalam solusi ini menerapkan API REST sederhana untuk mendapatkan status drone yang terakhir diketahui. Fungsi ini ditentukan dalam kelas bernama GetStatusFunction. Berikut adalah kode lengkap untuk fungsi tersebut:

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

Fungsi ini menggunakan pemicu HTTP untuk memproses permintaan HTTP GET. Fungsi ini menggunakan pengikatan input Azure Cosmos DB untuk mengambil dokumen yang diminta. Satu pertimbangannya adalah bahwa pengikatan ini akan berjalan sebelum logika otorisasi dilakukan di dalam fungsi. Jika pengguna yang tidak sah meminta dokumen, pengikatan fungsi akan tetap mengambil dokumen. Kemudian kode otorisasi akan mengembalikan 401, sehingga pengguna tidak akan melihat dokumen tersebut. Apakah perilaku ini dapat diterima mungkin bergantung pada kebutuhan Anda. Misalnya, pendekatan ini mungkin mempersulit audit akses data untuk data sensitif.

Autentikasi dan otorisasi

Aplikasi web menggunakan ID Microsoft Entra untuk mengautentikasi pengguna. Karena aplikasi adalah aplikasi satu halaman (SPA) yang berjalan di browser, alur pemberian implisit sesuai:

  1. Aplikasi web mengalihkan pengguna ke penyedia identitas (dalam hal ini, ID Microsoft Entra).
  2. Pengguna memasukkan info masuk mereka.
  3. IdP mengalihkan kembali ke aplikasi web dengan token akses.
  4. Aplikasi web mengirimkan permintaan ke API web dan menyertakan token akses di header Otorisasi.

Diagram alur implisit

Aplikasi Fungsi dapat dikonfigurasi untuk mengautentikasi pengguna dengan kode nol. Untuk informasi selengkapnya, lihatAutentikasi dan otorisasi di Azure App Service.

Otorisasi, di sisi lain, umumnya memerlukan beberapa logika bisnis. MICROSOFT Entra ID mendukung autentikasi berbasis klaim. Dalam model ini, identitas pengguna direpresentasikan sebagai sekumpulan klaim yang berasal dari IdP. Klaim dapat berupa informasi apa pun tentang pengguna, seperti nama atau alamat email mereka.

Token akses berisi subset klaim pengguna. Di antaranya adalah peran aplikasi apa pun yang ditetapkan kepada pengguna.

Parameter principal dari fungsi tersebut adalah objek ClaimsPrincipal yang berisi klaim dari token akses. Setiap klaim adalah pasangan kunci/nilai dari jenis klaim dan nilai klaim. Aplikasi menggunakan ini untuk mengotorisasi permintaan.

Metode ekstensi berikut menguji apakah objek ClaimsPrincipal berisi sekumpulan peran. Ini mengembalikan false jika salah satu peran yang ditentukan tidak ada. Jika metode ini mengembalikan false, fungsi mengembalikan HTTP 401 (Tidak Diotorisasi).

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

Untuk informasi selengkapnya tentang autentikasi dan otorisasi dalam aplikasi ini, lihat bagian Pertimbangan keamanan dari arsitektur referensi.

Langkah berikutnya

Setelah Anda memahami cara kerja solusi referensi ini, pelajari praktik terbaik dan rekomendasi untuk solusi serupa.

Azure Functions hanyalah salah satu opsi komputasi Azure. Untuk bantuan memilih teknologi komputasi, lihat Memilih layanan komputasi Azure untuk aplikasi Anda.