C# w .NET: najlepsze biblioteki i narzędzia do pracy w chmurze

0
10
Rate this post

Nawigacja:

C# i .NET w chmurze – od „zwykłej” aplikacji do cloud-native

Co w praktyce znaczy „praca w chmurze” dla C# i .NET

Dla programisty C# hasło „aplikacja w chmurze” przestaje się dziś kojarzyć z samym przeniesieniem kodu na maszynę wirtualną. Chodzi o zupełnie inny sposób projektowania: aplikacja ma być skalowalna, odporna na błędy, łatwa do wdrożenia na nowych środowiskach i dobrze zintegrowana z usługami platformy chmurowej.

Warto uporządkować podstawowe modele, bo od nich zależy dobór bibliotek i narzędzi:

  • IaaS (Infrastructure as a Service) – masz klasyczne VM-ki (np. Azure Virtual Machines, EC2 w AWS) i sam instalujesz .NET, serwery, wszystko. To najmniej „chmurowy” wariant, często tylko lekko poprawia obecną sytuację on‑prem.
  • PaaS (Platform as a Service) – np. Azure App Service, Azure SQL, Azure Cache for Redis. Kod C# skupia się na logice biznesowej, a platforma bierze na siebie zarządzanie infrastrukturą.
  • Serverless / Functions – Azure Functions, AWS Lambda, Google Cloud Functions. Piszesz funkcje reagujące na zdarzenia (HTTP, kolejki, timery), a skalowanie i infrastruktura są „po drugiej stronie mocy”.
  • Aplikacje kontenerowe – Docker + Kubernetes (AKS w Azure, EKS w AWS, GKE w GCP). Kod .NET działa w kontenerach, które są zarządzane przez orkiestrator.

„Praca w chmurze” w C# to więc nie tylko inne środowisko uruchomieniowe, ale szereg decyzji: czy używasz App Service, Functions, kontenerów, jak przechowujesz dane, jak obsługujesz kolejki, jak logujesz i monitorujesz działanie aplikacji. Tu wchodzą do gry konkretne biblioteki i narzędzia .NET, które potrafią zadecydować o tym, czy projekt będzie utrzymywalny, czy raczej zamieni się w kosztowny spaghetti‑as‑a‑Service.

Od monolitu on-prem do rozproszonej aplikacji cloud-native

Tradycyjna aplikacja on-premise to najczęściej jeden monolit: API, logika biznesowa, dostęp do danych, przetwarzanie wsadowe – wszystko w jednej paczce. W chmurze pojawia się pokusa (a czasem konieczność) rozbicia tego na wiele usług: API, mikroserwisy, worker services, funkcje serverless, kolejki, eventy.

To wprost przekłada się na sposób, w jaki myślisz o narzędziach:

  • potrzebujesz bibliotek do komunikacji asynchronicznej (Azure.Messaging.ServiceBus, Azure.Messaging.EventHubs),
  • musisz dbać o odporność na błędy sieciowe – retry, circuit breakers (np. Polly),
  • konfiguracja nie może być „na sztywno” – musi wspierać zmienne środowiskowe, Key Vault, różne środowiska,
  • logowanie i monitoring przestają być dodatkiem; stają się „zmysłem wzroku” aplikacji.

Przesiadka na podejście cloud-native to też zmiana nawyków. Nagle:

  • lepiej projektować API pod skalowanie poziome (stateless, bez sesji trzymanej w pamięci serwera),
  • częściej wprowadza się komunikację event-driven: zdarzenia trafiają do Event Hubs czy Service Busa, a różne komponenty aplikacji reagują na nie niezależnie,
  • logika, która wcześniej była „zadaniem CRON-a” na jednym serwerze, teraz działa jako Azure Function wyzwalana timerem lub komunikatem z kolejki.

Główne platformy chmurowe a C#/.NET

.NET jest naturalnie powiązany z Azure – w końcu obie technologie pochodzą z Microsoftu. Azure ma najbogatszy ekosystem gotowych bibliotek i narzędzi dla .NET: Azure SDK for .NET, świetna integracja z Visual Studio, Application Insights, Azure DevOps, Azure Functions z C# jako „pierwszym obywatelem”.

To jednak nie znaczy, że C# nie nadaje się do AWS czy GCP. Obydwie platformy udostępniają SDK dla .NET:

  • AWS SDK for .NET – obsługa S3, DynamoDB, SQS, SNS, Lambda i masy innych usług w jednym spójnym pakiecie.
  • Google Cloud .NET libraries – klienty do Pub/Sub, Cloud Storage, BigQuery itd.

Strategia wyboru zależy od projektu:

  • Jeśli środowisko firmowe jest „niebieskie” po korek – pójście „all in” na Azure zwykle jest najprostszym rozwiązaniem: lepsza integracja narzędzi, wsparcie, gotowe sample.
  • Jeśli organizacja stawia na multi-cloud lub vendor neutrality – warto trzymać warstwę domenową i integracje w poziomie abstrakcji, a SDK konkretnych chmur opakować we własne interfejsy.

Typowe scenariusze dla C# w chmurze

W codziennej praktyce najczęściej pojawiają się kilka powtarzalnych typów aplikacji .NET w chmurze:

  • API / backendy – ASP.NET Core w formie Minimal APIs lub klasycznego MVC, hostowane na App Service, w kontenerach (AKS) czy jako Functions HTTP.
  • Worker Services / background jobs – aplikacje .NET, które przetwarzają kolejki, integrują się z zewnętrznymi systemami, synchronizują dane.
  • Integracje B2B – usługi wymieniające dane przez kolejki, Event Grid, Service Bus, webhooki.
  • Serverless / event-driven – systemy, w których rdzeń stanowią Azure Functions reagujące na zdarzenia z kolejki, BLOB-a, Event Hubs.

W każdym z tych scenariuszy przydatny jest inny zestaw bibliotek: od Azure.Storage.* po narzędzia do monitoringu i bezpieczeństwa (Application Insights, Azure.Identity, Azure.Security.KeyVault). Dobrze dobrany „stack” to mniej kodu infrastrukturą, więcej kodu domeną.

Dlaczego dobór bibliotek i narzędzi ma większe znaczenie w chmurze

W środowisku on-prem złe decyzje technologiczne są bolesne, ale czasem da się je „przecierpieć”: serwer jest jeden, administrator ma do niego fizyczny dostęp, logi leżą na dysku. W chmurze każda pomyłka może się przełożyć na realne koszty i problemy z niezawodnością:

  • Złe retry – brak polityk ponawiania albo źle dobrane opóźnienia potrafią „zajechać” usługę chmurową lub wygenerować nadmiarowe koszty.
  • Brak centralnego monitoringu – bez Application Insights czy innego stacku observability, diagnoza problemów bywa jak zabawa w zgadywanie z logami na 10 różnych maszynach.
  • Sekrety w kodzie – connection string w appsettings.json synchronizowany do repo to proszenie się o kłopoty. Key Vault i towarzyszące biblioteki powstały głównie po to, aby takich sytuacji nie było.
  • Zbyt mocny vendor lock-in – bez warstwy abstrakcji na SDK można się „przyspawać” do jednego dostawcy do tego stopnia, że migracja będzie niemal nierealna.

Odpowiednio dobrane narzędzia .NET pomagają zapanować nad tym chaosem i przenieść ciężar z problemów infrastrukturalnych na rozwijanie funkcjonalności biznesowych. I tak, da się osiągnąć stan, w którym wdrożenie do produkcji w piątek popołudniu nie przyprawia o dreszcze – choć niekoniecznie polecam taki sport ekstremalny.

Zbliżenie na ekran laptopa z edytorem kodu w języku C# w chmurze
Źródło: Pexels | Autor: Daniil Komov

Podstawowy zestaw narzędzi .NET do pracy w chmurze

.NET SDK, CLI i wybór IDE do projektów cloud-native

Niezależnie od tego, czy pracujesz z Azure, AWS czy GCP, fundament jest ten sam: .NET SDK i .NET CLI. To one pozwalają tworzyć, kompilować, testować i publikować aplikacje w sposób automatyzowalny, co w chmurze jest absolutną podstawą (bez CI/CD nie ma zabawy).

Kilka elementów bazowych:

  • .NET SDK – najlepiej trzymać się aktualnie wspieranych LTS (np. .NET 8 LTS), szczególnie w środowiskach produkcyjnych.
  • .NET CLI (dotnet) – tworzenie projektów (dotnet new), zarządzanie pakietami (dotnet add package), publikacja (dotnet publish), testy (dotnet test).
  • IDE:
    • Visual Studio – najpełniejsza integracja z Azure, bogaty debugging, szablony do App Service, Functions, kontenerów.
    • Rider – świetny dla osób, które preferują środowisko JetBrains i multi-platform.
    • VS Code – lekki edytor z rozszerzeniami C# Dev Kit i Azure Tools, idealny pod CI/CD i lekkie środowiska.

W praktyce przy projektach chmurowych liczy się to, na ile IDE dobrze współpracuje z:

  • debuggowaniem zdalnym (np. Azure App Service Remote Debugging),
  • Azure Functions / kontenerami,
  • konfiguracją i sekretnymi danymi (user secrets, Key Vault, environment variables).

Azure CLI i Azure Developer CLI jako fundament automatyzacji

Ręczne klikanie w portalu Azure jest wygodne na start, ale w miarę rozwoju projektu staje się problematyczne: trudno odtworzyć środowisko, ciężko trzymać infrastrukturę pod kontrolą wersji, a „kliknięcia” znikają w niepamięci. Dlatego tak mocno promuje się podejście everything as code.

Dwie kluczowe linie komend dla programisty .NET w Azure:

  • Azure CLI (az) – ogólna konsola do zarządzania zasobami w Azure. Możesz z jej pomocą:
    • tworzyć resource group, App Service, Functions, bazy danych,
    • konfigurować ustawienia aplikacji, connection stringi, sloty wdrożeniowe,
    • wykonywać skrypty automatyzujące deployment (np. w GitHub Actions lub Azure DevOps).
  • Azure Developer CLI (azd) – narzędzie nastawione na developer experience:
    • używa szablonów projektów (aplikacja + infrastruktura jako kod),
    • pozwala bardzo szybko utworzyć środowisko dev/test/prod jednym poleceniem,
    • integruje się z Bicep/Terraform i repozytorium kodu.

Dla projektów .NET cloud-native sensowne jest podejście, w którym:

  • infrastrukturę opisujesz Bicepem lub Terraformem, wołanym z az lub azd,
  • kod aplikacji budujesz i publikujesz przez dotnet,
  • całość sklejają pipeline’y CI/CD (GitHub Actions, Azure DevOps).

Struktura projektów .NET pod chmurę

Nowoczesny projekt chmurowy w .NET rzadko jest jedną aplikacją. Częściej to solution złożone z kilku projektów:

  • API (ASP.NET Core Minimal API / Controllers),
  • Worker Service – przetwarzanie kolejek, integracje, zadania cykliczne,
  • Class Library z logiką domenową, modelami, walidacją,
  • osobne biblioteki dla integracji z zewnętrznymi usługami (np. warstwa „Infrastructure”).

Taka struktura ułatwia:

  • testowanie logiki biznesowej niezależnie od chmury,
  • wymianę konkretnej implementacji (np. Azure Service Bus na AWS SQS),
  • dzielenie kodu między API, workerami a Functions.

Przy ASP.NET Core coraz częściej stosuje się Minimal APIs, które w połączeniu z dependency injection i rozszerzeniami mogą tworzyć zaskakująco czytelny kod. W worker services przydaje się szablon Worker Service i interfejs IHostedService, pozwalający budować procesy działające w tle w tym samym modelu hostingu, co API.

Konfiguracja w chmurze – appsettings, environment i sekrety

Konfiguracja w aplikacjach chmurowych musi być elastyczna i bezpieczna. Standardowy mechanizm konfiguracji .NET świetnie się do tego nadaje – o ile jest poprawnie użyty.

Najczęściej spotykane elementy:

  • appsettings.json – konfiguracja domyślna, wersjonowana w repozytorium (ale bez sekretów!),
  • appsettings.<Environment>.json – np. appsettings.Development.json, appsettings.Production.json,
  • zmienne środowiskowe – nadpisują wartości z plików, idealne do konfiguracji różniącej się między środowiskami chmurowymi,
  • User secrets – mechanizm Secret Manager do przechowywania sekretów lokalnie w trybie development, bez wrzucania ich do repo,
  • Key Vault – docelowe miejsce przechowywania haseł, connection stringów, kluczy API w chmurze.

Typowy wzorzec:

  1. W kodzie używasz IConfiguration i Options pattern (IOptions<T>, IOptionsSnapshot<T>) do wstrzykiwania ustawień.
  2. Na etapie developmentu sekrety trzymasz w user secrets.
  3. Bezpieczne zarządzanie sekretami – Azure Key Vault i Azure.Identity

    Na etapie produkcji sekrety powinny trafić do dedykowanego magazynu. W ekosystemie Azure takim domyślnym miejscem jest Azure Key Vault, a po stronie .NET kluczową rolę odgrywa pakiet Azure.Identity oraz integracja z mechanizmem konfiguracji.

    Typowy zestaw bibliotek:

    • Azure.Identity – wspólny mechanizm uwierzytelniania (Managed Identity, Visual Studio, CLI, GitHub OIDC, itp.),
    • Azure.Security.KeyVault.Secrets, Azure.Security.KeyVault.Keys, Azure.Security.KeyVault.Certificates – operacje na sekretach, kluczach i certyfikatach,
    • provider konfiguracji do Key Vault – integruje Key Vault z IConfigurationBuilder.

    Najważniejszy element układanki to DefaultAzureCredential. W jednym kodzie ogarnia różne sposoby logowania:

    
    var credential = new DefaultAzureCredential();
    
    var client = new SecretClient(
        new Uri("https://<twoj-key-vault>.vault.azure.net/"),
        credential);
    
    KeyVaultSecret secret = await client.GetSecretAsync("DbConnection");
    string connString = secret.Value;
    

    Ten sam kod zadziała lokalnie (VS / Azure CLI / user secrets), w pipeline (np. federacja z Azure AD) oraz w chmurze (Managed Identity przypisana do App Service / Functions / VM). Bez ręcznego wklepywania haseł w konfigurację.

    W aplikacjach webowych i workerach często wygodniej wpiąć Key Vault bezpośrednio w pipeline konfiguracji:

    
    builder.Configuration
        .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
        .AddEnvironmentVariables();
    
    if (!builder.Environment.IsDevelopment())
    {
        var credential = new DefaultAzureCredential();
        var keyVaultUrl = new Uri(builder.Configuration["KeyVaultUrl"]!);
    
        builder.Configuration.AddAzureKeyVault(keyVaultUrl, credential);
    }
    

    Dzięki temu cały kod korzysta po prostu z IConfiguration, a to skąd pochodzą sekrety (Key Vault, env, pliki) przestaje być problemem aplikacji. Gdy ktoś wpisze connection string w appsettings.Production.json, system kontroli wersji i code review powinny zapalić wszystkie czerwone lampki.

    Observability – Application Insights, logging i tracing rozproszony

    Bez sensownego monitoringu aplikacja chmurowa działa tylko teoretycznie. W .NET warto zacząć od trzech elementów: logów, metryk i tracingu rozproszonego.

    Najpopularniejszy pakiet w ekosystemie Azure to Application Insights. Współpracuje z ASP.NET Core, Azure Functions, Worker Services, a w nowych projektach dobrze integruje się także z OpenTelemetry:

    • Microsoft.ApplicationInsights.AspNetCore – klasyczna integracja z ASP.NET Core,
    • Azure.Monitor.OpenTelemetry.AspNetCore – nowy sposób integracji z Azure Monitor przez OpenTelemetry,
    • OpenTelemetry.* – biblioteki do logów, metryk i trace’ów niezależne od chmury.

    Przykładowa konfiguracja z OpenTelemetry i wysyłką do Azure Monitor może wyglądać tak:

    
    builder.Services.AddOpenTelemetry()
        .UseAzureMonitor() // Azure Monitor / App Insights
        .WithMetrics(meterProviderBuilder =>
        {
            meterProviderBuilder.AddAspNetCoreInstrumentation();
            meterProviderBuilder.AddHttpClientInstrumentation();
        })
        .WithTracing(tracerProviderBuilder =>
        {
            tracerProviderBuilder.AddAspNetCoreInstrumentation();
            tracerProviderBuilder.AddHttpClientInstrumentation();
            tracerProviderBuilder.AddSource("MyApp.*");
        });
    

    Do logowania w .NET sensownym wyborem pozostaje Serilog, który ma bogaty ekosystem sinków (m.in. Application Insights, Seq, Elasticsearch, Event Hub):

    
    Log.Logger = new LoggerConfiguration()
        .ReadFrom.Configuration(builder.Configuration)
        .Enrich.FromLogContext()
        .WriteTo.Console()
        .CreateLogger();
    
    builder.Host.UseSerilog();
    

    Raz poprawnie spięty stack observability pozwala spokojniej spać w piątek po deployu – zamiast „domyślać się z ruchu na stronie”, masz wykresy, trace’y i konkretne logi z korelacją między usługami.

    Komunikacja z usługami chmurowymi – Azure SDK for .NET i alternatywy

    Nowy model Azure SDK – spójne klienty, retry i paginacja

    Aktualna generacja bibliotek Azure SDK dla .NET (nazwy w stylu Azure.Storage.Blobs, Azure.Messaging.ServiceBus) ma spójny model:

    • wszystko opiera się o klasy klienta (np. BlobClient, QueueClient),
    • wspólny mechanizm uwierzytelniania przez TokenCredential (np. DefaultAzureCredential),
    • wbudowane retry z politykami konfigurowanymi przez opcje klienta,
    • obsługa async/await, strumieni, paginacji (np. Pageable<T>).

    Konfiguracja klienta zwykle sprowadza się do przekazania poświadczeń i ewentualnej customizacji opcji:

    
    var options = new BlobClientOptions
    {
        Retry =
        {
            Mode = RetryMode.Exponential,
            MaxRetries = 5,
            Delay = TimeSpan.FromSeconds(1),
            MaxDelay = TimeSpan.FromSeconds(10)
        }
    };
    
    var blobServiceClient = new BlobServiceClient(
        new Uri("https://<account>.blob.core.windows.net"),
        new DefaultAzureCredential(),
        options);
    

    Większość SDK ma też wersje *ClientOptions, w których ustawisz polityki retry, logowanie, diagnostykę. Warto to trzymać w jednym miejscu (np. modul DI), aby nie kopiować konfiguracji po całym kodzie.

    HttpClient, Refit i inne podejścia do API REST

    Nie wszystko w chmurze to „gotowe” SDK. Bardzo często trzeba rozmawiać z zewnętrznymi API REST – czy to usług partnera, czy własnym microserwisem za bramką API. Bazą w .NET jest oczywiście HttpClient oraz fabryka IHttpClientFactory.

    Dobrą praktyką jest rejestrowanie klientów jako typed clients:

    
    builder.Services.AddHttpClient<IPaymentGatewayClient, PaymentGatewayClient>(client =>
    {
        client.BaseAddress = new Uri("https://api.payment-gateway.com");
        client.Timeout = TimeSpan.FromSeconds(10);
    });
    

    Do samej definicji kontraktów REST wygodnym narzędziem jest Refit – biblioteka generująca klienta na podstawie interfejsu w C#:

    
    public interface IPaymentGatewayApi
    {
        [Post("/payments")]
        Task<PaymentResponse> CreatePaymentAsync([Body] PaymentRequest request);
    
        [Get("/payments/{id}")]
        Task<PaymentStatus> GetStatusAsync(string id);
    }
    
    // Rejestracja:
    builder.Services
        .AddRefitClient<IPaymentGatewayApi>()
        .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.payment-gateway.com"));
    

    Refit sprawdza się tam, gdzie nie ma oficjalnego SDK lub jest ono bardzo ciężkie. Dobrym uzupełnieniem są biblioteki do retry i circuit breakerów, np. Polly, którą łatwo powiązać z IHttpClientFactory:

    
    builder.Services.AddHttpClient<IPaymentGatewayClient, PaymentGatewayClient>()
        .AddTransientHttpErrorPolicy(policy =>
            policy.WaitAndRetryAsync(3, retryAttempt =>
                TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))));
    

    Z takim podejściem logika biznesowa nie musi wiedzieć, czym dokładnie jest „cloud API” – dostaje interfejs, a pod spodem kryje się HTTP, retry, telemetry i wszystkie chmurowe detale.

    Abstrakcja nad dostawcą chmury – gdy Azure nie jest jedyną opcją

    Czasem projekt musi wspierać więcej niż jednego dostawcę (np. Azure + AWS u dużego klienta). Wtedy rozsądnie jest wprowadzić warstwę abstrakcji nad SDK dostawcy:

    • definiujesz interfejsy domenowe, np. ICloudBlobStorage, IMessageQueue,
    • po jednej implementacji na dostawcę: AzureBlobStorage, AwsS3Storage,
    • wstrzykujesz odpowiednią implementację na etapie konfiguracji (DI, feature flags, konfiguracja środowiska).

    Przykładowy interfejs dla prostego storage’u plików:

    
    public interface ICloudFileStorage
    {
        Task UploadAsync(string path, Stream content, string contentType, CancellationToken ct);
        Task<Stream> DownloadAsync(string path, CancellationToken ct);
        Task DeleteAsync(string path, CancellationToken ct);
    }
    

    W Azure implementacja oprze się na BlobClient, w AWS na AmazonS3Client. Wyższe warstwy nie muszą o tym wiedzieć – dla nich chmura to „miejsce, gdzie leżą pliki”. Zmiana dostawcy boli tylko w warstwie infrastrukturalnej, a nie w całym systemie.

    Laptop z wyświetlonym kodem w ciemnym pokoju obok kubka z kawą
    Źródło: Pexels | Autor: Daniil Komov

    Przechowywanie danych i kolejek – biblioteki do Storage, baz i komunikatów

    Azure Storage – Blob, Queue, Table, Files w wydaniu .NET

    Usługi klasy storage w Azure to często pierwszy przystanek każdej aplikacji. W .NET reprezentuje je rodzina Azure.Storage.*:

    • Azure.Storage.Blobs – przechowywanie plików, backupów, załączników,
    • Azure.Storage.Queues – proste kolejki komunikatów,
    • Azure.Data.Tables – NoSQL key-value / tabela,
    • Azure.Storage.Files.Shares – współdzielone udziały plikowe.

    Przykład pracy z BLOB-ami jest dość prosty:

    
    var containerClient = new BlobContainerClient(
        new Uri("https://<account>.blob.core.windows.net/invoices"),
        new DefaultAzureCredential());
    
    BlobClient blobClient = containerClient.GetBlobClient("2024/04/invoice-1234.pdf");
    
    await blobClient.UploadAsync(fileStream, overwrite: true);
    

    Do kolejek (Queue Storage) masz analogiczną bibliotekę Azure.Storage.Queues, która pozwala wysyłać / pobierać komunikaty w modelu „best effort”. Przy większych wymaganiach co do gwarancji dostarczenia lepiej spojrzeć w kierunku Azure Service Bus.

    Bazy danych – EF Core, Dapper i klienci specyficzni dla chmury

    Większość systemów i tak kończy z jakąś bazą danych, niezależnie od tego, ile mówi się o event sourcingu. W ekosystemie .NET najpopularniejsze są:

    • Entity Framework Core – pełnoprawny ORM, migracje, LINQ,
    • Dapper – „mikro ORM” nastawiony na wydajność i prostotę,
    • specyficzne biblioteki dostawców, np. Microsoft.Data.SqlClient, Npgsql (PostgreSQL), MongoDB.Driver.

    W chmurze dochodzą klienty dla usług zarządzanych:

    • Azure.Data.Tables – proste tabele NoSQL w Azure Storage,
    • Azure.Data.Cosmos – dostęp do Azure Cosmos DB,
    • drivery do „managed” baz jak Azure Database for PostgreSQL / MySQL (standardowe sterowniki, ale inne connection stringi i opcje).

    EF Core dobrze się sprawdza z klasycznymi bazami (SQL Server, PostgreSQL). W przypadku Cosmos DB bywa wygodny, ale często i tak kończy się sięganiem po model dokumentowy przez Azure.Data.Cosmos, aby mieć pełną kontrolę nad partycjonowaniem i zapytaniami.

    W aplikacjach chmurowych warto trzymać dostęp do bazy za interfejsami repozytoriów lub warstwą CQRS. Dzięki temu zmiana „SQL w Azure” na „SQL on-prem” albo odwrotnie nie rozleje się po całej aplikacji.

    Kolejki i messaging – Azure Service Bus, Event Hubs i alternatywy

    Kolejkowanie i publish-subscribe to kręgosłup wielu systemów cloud-native. W Azure główne opcje to:

    • Azure Service Bus – kolejki i tematy (topics) z gwarancjami dostarczenia, sesjami, dead-letteringiem,
    • Azure Event Hubs – strumienie zdarzeń (telemetria, logi, zdarzenia IoT),
    • Storage Queues – prostsza, tańsza kolejka (omówiona wyżej).

    Po stronie .NET kluczowe biblioteki to:

    • Azure.Messaging.ServiceBus – praca z Service Bus,
    • Azure.Messaging.EventHubs i Azure.Messaging.EventHubs.Processor – obsługa Event Hubs.

    Przykład wysyłania wiadomości do Azure Service Bus:

    
    var client = new ServiceBusClient("<connection-string>");
    ServiceBusSender sender = client.CreateSender("orders");
    
    var message = new ServiceBusMessage(JsonSerializer.Serialize(order))
    {
        ContentType = "application/json"
    };
    
    await sender.SendMessageAsync(message);
    

    W bardziej złożonych systemach dochodzą abstrakcje wyższego poziomu, np. MassTransit czy NServiceBus, które zapewniają:

    • mapowanie wiadomości na klasy .NET,
    • sagi, routing, retry, outbox,
    • wsparcie dla wielu transportów (Service Bus, RabbitMQ, Amazon SQS).

    To rozwiązania cięższe, ale jeśli system żyje głównie z przesyłania komunikatów między usługami, potrafią zaoszczędzić sporo czasu i kilku siwych włosów.

    .NET i serverless – Azure Functions oraz alternatywne platformy

    Azure Functions – model programistyczny dla C#

    W świecie .NET serverless najczęściej oznacza po prostu Azure Functions. Z punktu widzenia programisty to zwykła biblioteka klas w C#, ale z bardzo konkretnym modelem wywołań. Zamiast Main lub kontrolera MVC masz metody oznaczone atrybutami triggerów i bindingów.

    Prosty HTTP trigger w izolowanym modelu procesowym (dotnet-isolated, rekomendowany przy .NET 8+):

    
    public class GetOrderFunction
    {
        [Function("GetOrder")]
        public HttpResponseData Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", Route = "orders/{id}")] 
            HttpRequestData req,
            string id,
            FunctionContext executionContext)
        {
            var logger = executionContext.GetLogger("GetOrder");
            logger.LogInformation("Getting order {OrderId}", id);
    
            // ..logika, np. wywołanie serwisu domenowego..
    
            var response = req.CreateResponse(HttpStatusCode.OK);
            response.Headers.Add("Content-Type", "application/json");
            response.WriteString(JsonSerializer.Serialize(new { Id = id }));
            return response;
        }
    }
    

    Funkcja nie wie nic o serwerze, skalowaniu czy węzłach. Dostaje zdarzenie (HTTP, kolejka, timer) i ma je obsłużyć jak najszybciej, najlepiej bez side-effectów poza tymi, które naprawdę są potrzebne.

    Trigger’y i bindingi – klej między chmurą a funkcją

    W Azure Functions nie trzeba wszystkiego konfigurować ręcznie przez SDK. Większość typowych integracji jest dostępna jako bindings:

    • triggery – co wywołuje funkcję (HTTP, Queue Storage, Service Bus, Event Hub, Timer, Blob, Event Grid),
    • input bindings – skąd funkcja pobierze dane (np. dokument z Cosmos DB, BLOB, rekord z tabeli),
    • output bindings – gdzie funkcja zapisze efekt (np. wiadomość do kolejki, nowy dokument w Cosmos DB, BLOB).

    Klasyczny scenariusz: wiadomość z kolejki → przetworzenie → zapis wyniku do innej kolejki lub Cosmos DB, bez ręcznego klejenia klientów SDK:

    
    public class ProcessInvoiceFunction
    {
        [Function("ProcessInvoice")]
        [ServiceBusOutput("processed-invoices", Connection = "ServiceBusConnection")]
        public async Task<ProcessedInvoice> Run(
            [ServiceBusTrigger("incoming-invoices", Connection = "ServiceBusConnection")] 
            InvoiceMessage message,
            FunctionContext context)
        {
            var logger = context.GetLogger("ProcessInvoice");
            logger.LogInformation("Processing invoice {InvoiceId}", message.Id);
    
            // ...logika biznesowa...
    
            return new ProcessedInvoice
            {
                Id = message.Id,
                Status = "Processed"
            };
        }
    }
    

    W prostych scenariuszach bindingi ograniczają ilość „plumbing code”. W bardziej złożonych lepiej sięgać po jawne użycie SDK (np. Azure.Messaging.ServiceBus) i pełną kontrolę nad retry, transakcjami czy routingiem.

    Rozwój i debugowanie funkcji w .NET

    Do pracy lokalnej przydaje się Azure Functions Core Tools. Można nimi odpalać funkcje lokalnie, symulować triggery i testować całość bez dotykania chmury (i budżetu):

    • func init – tworzenie projektu funkcji,
    • func new – dodanie nowej funkcji z szablonu (HTTP, Service Bus, Timer itd.),
    • func start – lokalny runtime do debugowania z Visual Studio / VS Code / Rider.

    Konfiguracja opiera się głównie o local.settings.json (lokalnie) i Application Settings w Azure. Po stronie kodu używa się standardowego IConfiguration oraz DI znanego z ASP.NET Core – w izolowanym modelu host konfiguruje się w Program.cs:

    
    var host = new HostBuilder()
        .ConfigureFunctionsWorkerDefaults()
        .ConfigureServices(services =>
        {
            services.AddHttpClient();
            services.AddSingleton<IOrderService, OrderService>();
        })
        .Build();
    
    host.Run();
    

    Dzięki temu większość wzorców z klasycznych aplikacji .NET da się przenieść 1:1 – te same serwisy, te same rejestracje, inne tylko punkty wejścia.

    Wydajność i zimny start – co ma znaczenie w C#

    Serverless lubi lekkie projekty. W .NET oznacza to głównie:

    • nowoczesną wersję runtime (np. .NET 8, izolowany worker),
    • ograniczenie liczby zależności zewnętrznych do faktycznie potrzebnych,
    • unikanie kosztownych operacji w konstruktorach (ładowanie konfiguracji, duże grafy obiektów),
    • minimalizację statycznych singletonów, które długo się inicjalizują.

    Dobrym nawykiem jest użycie pre-warmingu (podtrzymywania instancji) i planu Premium/ASE dla krytycznych funkcji, gdzie zimny start jest nieakceptowalny. W wielu przypadkach wystarczy jednak klasyczny Consumption Plan plus rozsądna optymalizacja kodu (startup + zależności).

    Integracja z pozostałymi usługami Azure

    Funkcje rzadko żyją w izolacji. Najczęściej są wpięte w resztę platformy:

    • Azure API Management – jako bramka dla funkcji HTTP (autoryzacja, throttling, wersjonowanie),
    • Event Grid – wyzwalanie funkcji zdarzeniami z innych usług (np. nowy BLOB, zmiana w subskrypcji),
    • Key Vault – przechowywanie sekretów używanych przez funkcje (connection stringi, klucze API),
    • Application Insights – monitoring, logi i distributed tracing.

    Z poziomu C# większość integracji sprowadza się do użycia odpowiednich SDK (Azure.Messaging.EventGrid, Azure.Security.KeyVault.Secrets) oraz konfiguracji connection stringów w Application Settings z prefiksem @Microsoft.KeyVault(SecretUri=...). Funkcja myśli, że czyta zwykły setting, a runtime dogaduje się z Key Vaultem za kulisami.

    Alternatywne platformy serverless dla .NET

    C# nie jest zamknięty na jednego dostawcę. Jeśli projekt ląduje poza Azure, da się wykorzystać te same biblioteki domenowe na innych platformach.

    AWS Lambda z .NET

    AWS od dawna wspiera .NET w Lambda Functions. Aktualne runtime’y obsługują .NET 8, a aplikacje można pisać zarówno w podejściu „plain C#”, jak i jako minimal API hostowane w Lambdzie.

    Przykład prostego handlera:

    
    public class Functions
    {
        public APIGatewayProxyResponse Get(
            APIGatewayProxyRequest request, ILambdaContext context)
        {
            context.Logger.LogLine($"Request: {request.Path}");
    
            return new APIGatewayProxyResponse
            {
                StatusCode = 200,
                Body = "Hello from Lambda + .NET",
                Headers = new Dictionary<string, string>
                {
                    { "Content-Type", "text/plain" }
                }
            };
        }
    }
    

    Do wdrażania i zarządzania infrastrukturą można wykorzystać AWS SAM, CloudFormation albo CDK (gdzie część konfiguracji i tak pisze się w C#). Jeśli kod domenowy ma sensowną abstrakcję, różnice sprowadzają się głównie do warstwy wejścia/wyjścia (eventy z API Gateway, SQS, SNS, DynamoDB).

    Google Cloud Functions i Cloud Run

    W Google Cloud ekosystem .NET jest trochę mniej „pierwszej świeżości” niż w Azure/AWS, ale da się komfortowo żyć. Dla C# ciekawszą opcją niż klasyczne Cloud Functions jest zwykle Cloud Run – kontener serverless odpalany na żądanie.

    Aplikacja ASP.NET Core (np. minimal API) pakuje się do obrazu kontenera, publikuje w Artifact Registry, a Cloud Run dba o skalowanie i routing. Z punktu widzenia kodu to nadal „normalny” web host:

    
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddSingleton<IWeatherService, WeatherService>();
    
    var app = builder.Build();
    app.MapGet("/weather", (IWeatherService svc) => svc.GetForecast());
    
    app.Run();
    

    Cała magia dzieje się w konfiguracji uruchomienia kontenera (limit CPU, memory, concurrency, autoscaling). Biblioteki domenowe i klienci HTTP/SDK pozostają bez zmian.

    Open source: OpenFaaS, Knative, KEDA

    W środowiskach Kubernetes coraz częściej spotyka się platformy serverless oparte na open source. Dla C# najciekawsze są:

    • OpenFaaS – funkcje pisane w C#, pakowane jako kontenery, uruchamiane na K8s,
    • Knative – serwowanie kontenerów http scale-to-zero,
    • KEDA – autoskalowanie na podstawie zdarzeń (Azure Queue, Kafka, RabbitMQ, Prometheus itp.).

    Model jest podobny jak przy Cloud Run: piszesz normalną aplikację .NET (np. minimal API, gRPC, background service), a platforma zajmuje się skalowaniem na podstawie ruchu lub metryk. Jeśli architektura jest oparta na DI i kontraktach, migracja z Azure Functions do „funkcji na Kubernetesa” nie musi być dramatem – głównie zmienia się warstwa hostingu.

    Współdzielenie kodu między funkcjami a resztą systemu

    W większych projektach pojawia się pokusa: „skopiujmy trochę logiki do funkcji, przecież to tylko kilka linii”. Po trzeciej kopii człowiek zaczyna żałować. Przy serverless w .NET dobrze działa kilka prostych zasad:

    • logika domenowa w osobnych projektach (np. MyApp.Domain, MyApp.Application),
    • funkcje jako cienka warstwa adapterów wejścia/wyjścia (HTTP, kolejka, event),
    • współdzielone biblioteki kontraktów (DTO, eventy) między funkcjami a microserwisami,
    • wspólna biblioteka „platformowa” na cross-cutting (telemetria, obsługa błędów, retry, logowanie korelacji).

    Dzięki temu ta sama komenda CQRS (CreateOrderCommand) może być wywoływana zarówno z kontrolera HTTP w API, jak i z funkcji reagującej na wiadomość z kolejki. Funkcje nie stają się „drugim backendem”, tylko kolejnym sposobem dostarczenia tych samych use-case’ów.

    Infrastruktura jako kod dla środowisk serverless

    Serverless nie zwalnia z ogarnięcia infrastruktury, tylko przerzuca ją w inne miejsce. Zamiast plików ARM/Terraform wyklikiwanych w portalu lepiej postawić na Infrastructure as Code:

    • Bicep / ARM – natywny język deklaratywny dla Azure,
    • Terraform – przenośny, działający na Azure/AWS/GCP,
    • Pulumi – definicja infrastruktury w C#, co bywa atrakcyjne dla zespołów .NET.

    Przykład bardzo uproszczonej definicji Function App w Bicep:

    
    param functionAppName string
    param location string = resourceGroup().location
    
    resource storage 'Microsoft.Storage/storageAccounts@2023-01-01' = {
      name: 'st${uniqueString(resourceGroup().id)}'
      location: location
      sku: {
        name: 'Standard_LRS'
      }
      kind: 'StorageV2'
    }
    
    resource plan 'Microsoft.Web/serverfarms@2023-01-01' = {
      name: 'asp-${functionAppName}'
      location: location
      sku: {
        name: 'Y1'
        tier: 'Dynamic'
      }
    }
    
    resource func 'Microsoft.Web/sites@2023-01-01' = {
      name: functionAppName
      location: location
      kind: 'functionapp'
      properties: {
        serverFarmId: plan.id
        siteConfig: {
          appSettings: [
            {
              name: 'AzureWebJobsStorage'
              value: storage.properties.primaryEndpoints.blob
            }
          ]
        }
      }
    }
    

    Po stronie .NET łatwo to zszyć z pipeline’ami CI/CD (GitHub Actions, Azure DevOps, GitLab CI), tak by wdrożenie nowej funkcji automatycznie aktualizowało też infrastrukturę. Mniej ręcznego klikania, mniej „magicznych” różnic między środowiskami.

    Obserwowalność serverless w .NET – logi, metryki, ślady

    Przy kilkunastu funkcjach można jeszcze „ogarniać na oko”. Przy kilkudziesięciu albo setce – bez telemetrii robi się loteria. W ekosystemie .NET trzon stanowią:

    • Application Insights (Azure Monitor) – logi, metryki, distributed tracing,
    • OpenTelemetry – wspólny standard eksportu danych obserwowalności,
    • serwisy docelowe: Azure Monitor, Prometheus, Jaeger, Grafana Cloud itp.

    W Azure Functions integracja z Application Insights jest prawie automatyczna – wystarczy dodać connection string w konfiguracji. W izolowanym workerze można dodatkowo dołożyć ILogger<T> i manualne śledzenie:

    
    public class ChargeCustomerFunction
    {
        private readonly IPaymentService _payments;
        private readonly ILogger<ChargeCustomerFunction> _logger;
    
        public ChargeCustomerFunction(IPaymentService payments, ILogger<ChargeCustomerFunction> logger)
        {
            _payments = payments;
            _logger = logger;
        }
    
        [Function("ChargeCustomer")]
        public async Task Run(
            [ServiceBusTrigger("payments", Connection = "ServiceBus")] PaymentCommand cmd)
        {
            using var scope = _logger.BeginScope(new Dictionary<string, object>
            {
                ["PaymentId"] = cmd.PaymentId
            });
    
            _logger.LogInformation("Charging customer for payment {PaymentId}", cmd.PaymentId);
    
            await _payments.ChargeAsync(cmd);
        }
    }
    

    Jeśli projekt korzysta z OpenTelemetry w innych microserwisach, nic nie stoi na przeszkodzie, żeby dołączyć do tego też funkcje – biblioteki .NET obsługują HTTP klienta, ASP.NET Core, gRPC, a także własne źródła Activity i Meter.

    Dobór narzędzi chmurowych w projektach .NET

    Najczęściej zadawane pytania (FAQ)

    Jak zacząć pracę w chmurze z C# i .NET, jeśli mam klasyczną aplikację on-premise?

    Punkt startowy to zrozumienie, w jakim modelu chmury chcesz działać: IaaS (maszyny wirtualne), PaaS (np. Azure App Service) czy serverless/kontenery. Najszybsza, ale najmniej „chmurowa” droga to przeniesienie aplikacji na VM w Azure lub AWS – wymaga najmniej zmian w kodzie, ale niewiele zyskujesz poza elastyczniejszą infrastrukturą.

    Docelowo sensowniej jest przepakować aplikację jako ASP.NET Core (jeśli jeszcze nie jest) i wystawić ją jako Web App w Azure App Service lub w kontenerze Docker uruchamianym w Kubernetes (AKS/EKS/GKE). Dodatkowo dobrze jest od razu wynieść konfigurację poza kod (zmienne środowiskowe, Key Vault) i podpiąć centralne logowanie, np. Application Insights.

    Jakie biblioteki .NET są najważniejsze przy tworzeniu aplikacji cloud-native w Azure?

    W typowym projekcie cloud-native na Azure często pojawia się ten sam „zestaw startowy” bibliotek. Do komunikacji z usługami chmurowymi używa się Azure SDK for .NET, m.in.: Azure.Storage.Blobs, Azure.Messaging.ServiceBus, Azure.Messaging.EventHubs, Azure.Data.Tables. Do uwierzytelniania względem usług przydaje się Azure.Identity.

    Do bezpieczeństwa sekretów dobrze sprawdzają się Azure.Security.KeyVault.Secrets oraz integracja Key Vault z konfiguracją ASP.NET Core. Do logowania i monitoringu – Microsoft.ApplicationInsights.AspNetCore oraz wbudowane rozszerzenia loggingu w .NET. Na deser warto dorzucić Polly do retry i circuit breakerów, bo w chmurze sieć psuje dzień częściej, niżby się chciało.

    Jaka jest różnica między hostowaniem .NET na VM (IaaS) a w App Service (PaaS)?

    Na maszynie wirtualnej (IaaS) zarządzasz wszystkim sam: systemem operacyjnym, aktualizacjami, instalacją .NET, konfiguracją serwera (IIS, Kestrel + reverse proxy). To daje pełną kontrolę, ale też pełną odpowiedzialność – trochę jak własny serwer w szafie, tylko w cudzej serwerowni.

    App Service (PaaS) zdejmuje z ciebie większość zadań administratorskich. Wrzucasz paczkę z aplikacją lub kontener, ustawiasz konfigurację, autoskalowanie, sloty wdrożeniowe i skupiasz się na kodzie. Minusem może być mniejsza elastyczność w porównaniu z „gołą” VM, ale w zamian dostajesz szybsze wdrożenia, prostszy scaling i integrację z innymi usługami Azure.

    Kiedy lepiej użyć Azure Functions / AWS Lambda zamiast klasycznego API w ASP.NET Core?

    Funkcje serverless mają sens, gdy logika jest mocno zdarzeniowa: reagujesz na komunikaty z kolejki, pliki wrzucane do storage, timery, webhooki. Sprawdzają się też przy nieregularnym ruchu – płacisz głównie za wykonania, więc nie trzymasz cały czas „rozgrzanych” serwerów tylko po to, żeby obsłużyć kilka żądań na godzinę.

    Klasyczne API w ASP.NET Core lepiej nadaje się do systemów z przewidywalnym lub stałym ruchem, z większym, spójnym kontraktem HTTP (np. rozbudowane API B2B). Łatwiej też utrzymać jedną aplikację API niż 50 funkcji, jeśli zespół dopiero uczy się podejścia event-driven. Częste rozwiązanie hybrydowe: publiczne API w ASP.NET Core, a cięższe lub opóźnione przetwarzanie w Functions/Lambda.

    Jak unikać vendor lock-in, używając C# i .NET w chmurze (Azure, AWS, GCP)?

    Podstawą jest oddzielenie logiki domenowej od kodu, który mówi „jak dogadać się z konkretną chmurą”. W praktyce oznacza to wprowadzenie interfejsów i własnych adapterów dla SDK chmurowych. Kod domenowy korzysta np. z IFileStorage czy IMessageBus, a dopiero implementacja wie, że pod spodem jest Azure Blob Storage, S3 czy Google Cloud Storage.

    Drugi element to trzymanie konfiguracji usług (connection stringi, nazwy kolejek, topiki, endpointy) poza kodem – w konfiguracji środowisk, Key Vault/Secret Managerach. Dzięki temu w razie migracji nie refaktoryzujesz połowy repozytorium, tylko podmieniasz adapter i ustawienia. I, co ważne, nie korzystaj z unikatowych funkcji dostawcy bez świadomości, że za jakiś czas może to „przyspawać” projekt do danej platformy.

    Jakie narzędzia do logowania i monitoringu są polecane dla aplikacji .NET w chmurze?

    Najpopularniejszy zestaw w świecie Azure to Application Insights + wbudowany logging w ASP.NET Core. Pozwala to śledzić requesty, zależności, wyjątki, metryki wydajności i tworzyć alerty (np. gdy wzrośnie liczba błędów 5xx). Dobrze jest skonfigurować korelację między usługami (operation id), żeby móc prześledzić cały „lot” żądania przez mikroserwisy i funkcje.

    Poza tym często stosuje się narzędzia „cloud-agnostic”: Serilog lub NLog z wysyłką logów do Elastic Stack, Seq, Datadoga, czy Prometheusa + Grafanę. W praktyce kończy się tak, że masz jedno centralne miejsce, gdzie trafiają logi i metryki ze wszystkich usług – inne rozwiązania zwykle kończą się nocnym polowaniem na logi na kilku maszynach jednocześnie.

    Jak bezpiecznie przechowywać connection stringi i hasła w aplikacjach .NET w chmurze?

    Zamiast trzymać sekrety w appsettings.json czy – o zgrozo – w kodzie, lepiej użyć mechanizmów dostarczanych przez chmurę. W Azure będzie to Azure Key Vault + zarządzane tożsamości (Managed Identity), w AWS – Secrets Manager lub Systems Manager Parameter Store, a w GCP – Secret Manager. ASP.NET Core potrafi wczytywać te wartości bezpośrednio do konfiguracji.

    Dodatkowo przydaje się zasada: aplikacja uwierzytelnia się do chmury przy pomocy tożsamości (Identity), a nie „wypalonego” klucza. Dzięki temu nie rozsyłasz haseł po repozytorium i plikach konfiguracyjnych, a rotacja sekretów nie kończy się ręcznym przepisywaniem wartości w kilkunastu projektach.

Poprzedni artykułAI w monitoringu pracowników: granice kontroli i prywatności
Następny artykułRecenzja myszy ergonomicznej: czy realnie pomaga na ból nadgarstka?
Sebastian Król
Sebastian Król specjalizuje się w DevOps, chmurze i automatyzacji. Na FPID.org.pl pokazuje, jak budować powtarzalne środowiska, wdrażać aplikacje i monitorować je bez zbędnej teorii. Pracuje na realnych przykładach, weryfikuje komendy w praktyce i dopisuje kontekst: kiedy dane rozwiązanie ma sens, a kiedy lepiej wybrać prostszą drogę. Ceni transparentność konfiguracji, dobre praktyki bezpieczeństwa i czytelne logi. Jego poradniki powstają na bazie doświadczeń z utrzymania usług oraz analizy dokumentacji i zmian w narzędziach.