← Notatki

Paweł · 10 września 2026

Cztery awarie jednego wdrożenia

Okno terminala z błędem rsynca: ścieżka z ukośnikami odwrotnymi i komunikat Invalid argument
Okno terminala z błędem rsynca: ścieżka z ukośnikami odwrotnymi i komunikat Invalid argument

Tekst: Paweł Gąsienica-Samek

Zadanie było proste: mała usługa zbierająca pomiary z urządzeń, .NET 10, Entity Framework, baza w Azure SQL, hosting na App Service. Kilka endpointów, dwie tabele. Zakładałem godzinę.

Kod napisał się w kwadrans. Cała reszta poszła na wdrożenie i to ono okazało się właściwą lekcją. Wywaliło się cztery razy, za każdym razem z innego powodu i w innej warstwie — od formatu archiwum po architekturę startu aplikacji.

Opisuję po kolei, bo każda z tych rzeczy dopadnie każdego, kto wdraża z Windowsa na Linuksa. Trzy z czterech miały tę samą cechę: komunikat wskazywał zupełnie inne miejsce niż przyczyna.

Punkt wyjścia

Aplikacja jest banalna. Rejestr urządzeń, pomiary, dwa endpointy do zapisu i odczytu. Wszystko w minimalnym API, bez kontrolerów.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<AppDbContext>(o =>
    o.UseSqlServer(
        builder.Configuration.GetConnectionString("Default"),
        sql => sql.EnableRetryOnFailure(
            maxRetryCount: 5,
            maxRetryDelay: TimeSpan.FromSeconds(30),
            errorNumbersToAdd: null)));

var app = builder.Build();

app.MapGet("/devices", async (AppDbContext db) =>
    await db.Devices
        .AsNoTracking()
        .Select(d => new DeviceDto(d.Serial, d.Customer, d.Location, d.Measurements.Count))
        .ToListAsync());

app.Run();
Program.cs — całość, dwadzieścia linii

Encje zwykłe, dwie klasy. Jedna rzecz warta uwagi dla kogoś, kto wraca do .NET po dłuższej przerwie: właściwości oznaczone jako wymagane. Kompilator wymusza ich ustawienie przy tworzeniu obiektu, więc nie trzeba już obiecywać mu wykrzyknikiem, że ktoś to kiedyś wypełni.

public class Device
{
    public int Id { get; set; }
    public required string Serial { get; set; }
    public required string Customer { get; set; }
    public string? Location { get; set; }

    public List<Measurement> Measurements { get; set; } = [];
}
Models.cs

Pierwsza: ukośniki w archiwum

Zbudowałem paczkę, wysłałem, dostałem błąd wdrożenia bez sensownego komunikatu — samo „Deployment failed" i kod 400.

Windows PowerShell
PS C:\dev\MyDeviceHub> dotnet publish -c Release -o publish
  MyDeviceHub net10.0 powodzenie (4,1s) -> publish\

PS C:\dev\MyDeviceHub> Compress-Archive -Path publish\* -DestinationPath publish.zip
PS C:\dev\MyDeviceHub> az webapp deploy --src-path publish.zip --type zip
Initiating deployment
Deploying from local path: publish.zip
Deployment failed.
Error Code  : DeploymentFailed
Kudu Status : 400
Pierwsza próba

Kod 400 nie mówi nic. Prawdę widać dopiero w dzienniku wdrożenia po stronie serwera — a tam wysypało się kilkadziesiąt linii tego samego wzorca.

bash
$ az webapp log deployment show -n <app> -g <grupa>

rsync: [generator] recv_generator: failed to stat
  "/home/site/wwwroot/runtimes\linux-x64\native\libe_sqlite3.so": Invalid argument (22)
rsync: [generator] recv_generator: failed to stat
  "/home/site/wwwroot/runtimes\win-x64\native\Microsoft.Data.SqlClient.SNI.dll": Invalid argument (22)
rsync: [generator] recv_generator: failed to stat
  "/home/site/wwwroot/pl\Microsoft.Data.SqlClient.resources.dll": Invalid argument (22)

rsync error: some files/attrs were not transferred (code 23) at main.c(1338)
Deployment Failed. deployer = OneDeploy
Dziennik Kudu, fragment

Popatrz na ścieżki. Ukośniki odwrotne w środku, tam gdzie powinny być zwykłe. Standard ZIP wymaga ukośników zwykłych w ścieżkach wewnętrznych — Linux widzi więc nie ścieżkę, tylko jedną nazwę pliku zawierającą znaki, których w nazwie być nie może. Stąd „Invalid argument".

Windows PowerShell 5.1 stoi na starym .NET Framework, który tego nie przestrzega i zapisuje separator systemowy. Pliki z korzenia paczki przechodzą, wszystko z podkatalogów pada. A w publikacji .NET podkatalogów jest sporo, bo sterownik SQL Servera ciągnie natywne biblioteki dla wszystkich platform świata.

Zamiana na klasę z przestrzeni System.IO.Compression nic nie dała — ta sama biblioteka pod spodem, ten sam efekt. Działa dopiero jedno z dwóch:

# 1. tar z Windowsa - zapisuje zgodnie ze standardem
tar.exe -a -c -f publish.zip -C publish .

# 2. albo PowerShell 7, ktory stoi na nowoczesnym .NET
winget install Microsoft.PowerShell
pwsh -c 'Compress-Archive -Path publish\* -DestinationPath publish.zip'
Dwa sposoby na poprawne archiwum

Druga: limit procesora na darmowym planie

Po poprawieniu paczki wdrożenie zaczęło zwracać 403 ze stroną „Web App - Unavailable". Jedno polecenie wyjaśniło wszystko.

Windows PowerShell
PS C:\dev\MyDeviceHub> az webapp show -n <app> -g <grupa> --query state -o tsv
QuotaExceeded

Plan F1 daje sześćdziesiąt minut czasu procesora na dobę. Brzmi dużo, dopóki nie zauważysz, że rozpakowanie paczki i start aplikacji też z tego korzystają — a każda nieudana próba zaczyna od zera. Trzy nieudane wdrożenia i limit zniknął. Aplikacja wraca dopiero po północy czasu UTC.

Wniosek nie jest taki, że darmowy plan jest zły. Jest taki, że darmowy plan nie nadaje się do iterowania. Do obejrzenia portalu wystarczy, do pracy nie.

az appservice plan update --name <plan> --resource-group <grupa> --sku B1

Trzecia: niekompletny katalog po nieudanym rsyncu

Trzecia próba dała błąd, którego wcześniej nie było. Aplikacja wstawała i natychmiast padała — na bibliotece, którą przecież wysyłałem.

bash
Running the command: dotnet "MyDeviceHub.dll"

Unhandled exception. System.TypeInitializationException:
  The type initializer for 'SqlServerTypeMappingSource' threw an exception.
 ---> System.IO.FileNotFoundException: Could not load file or assembly
      'Microsoft.Data.SqlClient, Version=6.0.0.0'. The system cannot find the file.

   at MyDeviceHub.AppDbContext.get_Devices() in AppDbContext.cs:line 11
   at Program.<Main>$(String[] args) in Program.cs:line 28

/opt/startup/startup.sh: line 20: Aborted (core dumped) dotnet "MyDeviceHub.dll"
Container has finished running with exit code: 134.
Dziennik kontenera

Przyczyna była pozostałością po pierwszej awarii. Rsync skopiował to, co umiał — czyli pliki z korzenia — i przerwał na podkatalogach, zostawiając katalog docelowy w stanie mieszanym. Kolejne wdrożenia dokładały do bałaganu, bo domyślnie nie czyszczą katalogu, tylko synchronizują różnice.

Lekarstwo to jedna flaga:

az webapp deploy --resource-group <grupa> --name <app> \
  --src-path publish.zip --type zip \
  --clean true --restart true

Warto o niej wiedzieć zawczasu, bo objaw — brakująca biblioteka — kieruje podejrzenia na pakiety i zależności, czyli w zupełnie złą stronę. Straciłem na to najwięcej czasu z całej czwórki.

Czwarta: baza przy starcie aplikacji

Ostatnia jest najciekawsza, bo to nie usterka narzędzi, tylko mój błąd projektowy.

W kodzie miałem blok zasiewający dane startowe: przy uruchomieniu aplikacja sprawdzała, czy w bazie są urządzenia, i jeśli nie — dodawała dwa przykładowe. Wygodne lokalnie.

using var scope = app.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();

if (!await db.Devices.AnyAsync())      // baza niedostepna => wyjatek
{                                      // => proces sie wywraca
    db.Devices.AddRange(/* ... */);    // => kontener nigdy nie wstaje
    await db.SaveChangesAsync();
}
Wersja, która wywraca aplikację

W chmurze oznacza to, że niedostępna baza zamienia się w niedziałającą aplikację. A baza bywa niedostępna zupełnie normalnie: Azure SQL w wariancie serverless usypia po godzinie bezczynności i wybudza się kilkanaście sekund.

Windows PowerShell
PS C:\dev\MyDeviceHub> dotnet ef database update

System.InvalidOperationException: An exception has been raised that is likely
due to a transient failure. Consider enabling transient error resiliency by
adding 'EnableRetryOnFailure' to the 'UseSqlServer' call.
 ---> Microsoft.Data.SqlClient.SqlException (0x80131904):
      Database 'devicehub' is not currently available. Please retry later.

Numer bledu: 40613, stan: 1, klasa: 20
Błąd 40613 — baza właśnie wstaje

Dwie poprawki. Pierwsza to obudowanie zasiewu w obsługę błędu, żeby awaria bazy trafiała do logów zamiast wywracać proces. Druga to ponawianie po stronie dostawcy — mechanizm zna listę błędów uznanych za przejściowe i 40613 jest na niej.

try
{
    using var scope = app.Services.CreateScope();
    var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    if (!await db.Devices.AnyAsync())
    {
        db.Devices.AddRange(/* ... */);
        await db.SaveChangesAsync();
    }
}
catch (Exception ex)
{
    app.Logger.LogError(ex, "Nie udalo sie zasiac danych startowych");
}
Wersja, która wstaje mimo śpiącej bazy

Jedna pułapka przy ponawianiu, o którą pytają na rozmowach: przy włączonej strategii nie wolno otwierać transakcji wprost. Skoro operacja może zostać powtórzona, strategia musi wiedzieć, co dokładnie powtórzyć — a transakcja rozpoczęta ręcznie tego nie mówi.

var strategy = db.Database.CreateExecutionStrategy();

await strategy.ExecuteAsync(async () =>
{
    await using var tx = await db.Database.BeginTransactionAsync();
    // ...
    await tx.CommitAsync();
});
Transakcja przy włączonym ponawianiu

Co z tego zostaje

Trzy z czterech awarii miały wspólną cechę: komunikat wskazywał zupełnie inne miejsce niż przyczyna. Brakująca biblioteka to był nieposprzątany katalog. Kod 403 to był limit procesora. Nieudane rozpakowanie to był separator ścieżki w archiwum zbudowanym na innym systemie.

Żadnej z nich nie dałoby się rozwiązać zgadywaniem i przy żadnej nie pomogłoby powtórzenie tej samej czynności. Za każdym razem trzeba było zejść do logów — do dziennika wdrożenia, do stanu zasobu, do dziennika kontenera — i przeczytać, co system naprawdę mówi.

To zresztą jedyna umiejętność, która przy wdrożeniach naprawdę się liczy. Reszta to znajomość konkretnych flag, a tych i tak nikt nie pamięta.

Aha, i jeszcze jedno. Pierwsza publikacja tego tekstu zwróciła 404, bo ustawiłem datę pięć minut w przyszłość, a silnik nie pokazuje wpisów, których czas jeszcze nie nadszedł. Piąta awaria, w tekście o czterech.