Module 1 · Cloud-Native Foundations · Lesson 1 of 13
What Cloud-Native Means for a .NET Service: Twelve-Factor Config, Logs and Disposability
Learning outcome
You can explain "cloud-native" in concrete terms an interviewer accepts: a service that is packaged as an immutable image, configured from its environment, stateless, observable from the outside, and safe to start, stop, scale and replace at any time. You can map the twelve-factor ideas that matter most to ASP.NET Core code, and you can show them in a running Kestrel app.
Intuition
A platform such as Kubernetes treats your service like cattle, not a pet: it starts copies, kills them, moves them between machines and replaces them during every deploy. Your app survives that only if nothing important lives inside one copy. Settings come from outside, data lives in a database or cache, logs leave through stdout, and a stop signal is handled politely.
flowchart LR
IMG["image 1.4.2 (same bytes everywhere)"] --> P1["pod A"]
IMG --> P2["pod B"]
CFG["env vars + secrets"] --> P1
CFG --> P2
P1 -->|stdout JSON logs| COL["log/OTel pipeline"]
P2 -->|stdout JSON logs| COL
P1 --> DB[("database / cache: the state")]
P2 --> DBDeep dive
- What the term means. The CNCF definition (v1.1, 2024) describes systems that are "loosely coupled" and "secure, resilient, manageable, sustainable, and observable", typically built from some combination of containers, service meshes, multi-tenancy, microservices, immutable infrastructure, serverless and declarative APIs. Microservices are optional: a single well-built API on Kubernetes is cloud-native too.
- Twelve-factor, the parts interviewers ask about:
- Codebase / build, release, run: one image is built once and promoted through environments. Only configuration changes between staging and production, never the bytes.
- Config in the environment: ASP.NET Core's default host already reads environment variables, and
__maps to:, soOrders__MaxPageSizebecomesOrders:MaxPageSize. - Backing services as attached resources: the database is reached through a connection string from config, so swapping it is a config change.
- Port binding: the app listens on a port given to it (
ASPNETCORE_HTTP_PORTS; the official .NET 8+ images default to 8080). - Processes are stateless: no in-memory session or local file that a second replica would not see.
- Disposability: fast start, graceful stop on SIGTERM (Lesson 9).
- Logs as event streams: write to stdout; the platform ships them. Do not manage log files inside the container.
- Dev/prod parity: the same backing services locally, which is where .NET Aspire helps (Lesson 6).
- What twelve-factor does not cover: health endpoints, telemetry, and security posture (non-root, read-only file system). Modern checklists add them; this course covers each.
- Where the platform takes over: restarts, scaling, rollout and traffic routing belong to Kubernetes or a managed service. Your code must make those operations safe; it should not reimplement them.
Code example
The program sets the variables a platform would inject, starts a real Kestrel server, calls it, writes a structured log line and stops.
// L01: a 12-factor-style ASP.NET Core service, run for real on Kestrel (no container needed to see the behaviour).
// Config comes from environment variables, the port is injected, logs go to stdout as JSON, state lives outside.
using System.Net.Http.Json;
// What a container platform would inject. Set here so the demo is self-contained.
Environment.SetEnvironmentVariable("ASPNETCORE_HTTP_PORTS", "5088"); // port binding from the environment
Environment.SetEnvironmentVariable("Orders__MaxPageSize", "50"); // nested config: "__" maps to ":"
Environment.SetEnvironmentVariable("ConnectionStrings__orders", "Host=pg;Database=orders");
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.Logging.AddJsonConsole(o => o.JsonWriterOptions = new() { Indented = false }); // logs are an event stream on stdout
builder.Logging.AddFilter("Microsoft.AspNetCore", LogLevel.Warning);
var app = builder.Build();
int maxPage = app.Configuration.GetValue<int>("Orders:MaxPageSize");
string conn = app.Configuration.GetConnectionString("orders")!;
app.MapGet("/config", () => new { maxPage, connectionTarget = conn.Split(';')[0]});
app.MapGet("/orders", (int page) => Results.Ok(new { page, pageSize = maxPage }));
await app.StartAsync();
Console.WriteLine($"listening on: {string.Join(", ", app.Urls)}");
using var http = new HttpClient { BaseAddress = new Uri("http://localhost:5088") };
Console.WriteLine("GET /config -> " + await http.GetStringAsync("/config"));
Console.WriteLine("GET /orders?page=2 -> " + await http.GetStringAsync("/orders?page=2"));
app.Logger.LogInformation("Order {OrderId} shipped in {ElapsedMs} ms", 1042, 37);
await app.StopAsync(); // what SIGTERM triggers in a container: graceful stop (Lesson 9)
Console.WriteLine("stopped cleanly");Output (.NET 10, Release)
{"EventId":14,"LogLevel":"Information","Category":"Microsoft.Hosting.Lifetime","Message":"Now listening on: http://[::]:5088","State":{"address":"http://[::]:5088","{OriginalFormat}":"Now listening on: {address}"}}
{"EventId":0,"LogLevel":"Information","Category":"Microsoft.Hosting.Lifetime","Message":"Application started. Press Ctrl\u002BC to shut down.","State":{"{OriginalFormat}":"Application started. Press Ctrl\u002BC to shut down."}}
{"EventId":0,"LogLevel":"Information","Category":"Microsoft.Hosting.Lifetime","Message":"Hosting environment: Production","State":{"EnvName":"Production","{OriginalFormat}":"Hosting environment: {EnvName}"}}
{"EventId":0,"LogLevel":"Information","Category":"Microsoft.Hosting.Lifetime","Message":"Content root path: /app","State":{"ContentRoot":"/app","{OriginalFormat}":"Content root path: {ContentRoot}"}}
listening on: http://[::]:5088
GET /config -> {"maxPage":50,"connectionTarget":"Host=pg"}
GET /orders?page=2 -> {"page":2,"pageSize":50}
{"EventId":0,"LogLevel":"Information","Category":"L01","Message":"Order 1042 shipped in 37 ms","State":{"OrderId":1042,"ElapsedMs":37,"{OriginalFormat}":"Order {OrderId} shipped in {ElapsedMs} ms"}}
{"EventId":0,"LogLevel":"Information","Category":"Microsoft.Hosting.Lifetime","Message":"Application is shutting down...","State":{"{OriginalFormat}":"Application is shutting down..."}}
stopped cleanlyCode walkthrough
- The
Content root pathline originally showed the build machine's working directory; it was replaced with/app(a conventional application directory) so no build-box path is published. Nothing else in the output was changed. ASPNETCORE_HTTP_PORTS=5088made Kestrel listen onhttp://[::]:5088with no code change; the official .NET images preset the same variable (Lesson 3 shows the value).Orders__MaxPageSize=50andConnectionStrings__ordersarrived asOrders:MaxPageSizeand theordersconnection string:/configreturned"maxPage":50and"connectionTarget":"Host=pg"./orders?page=2used the configured page size of 50. Nothing about the environment is hard-coded.- Every log line is one JSON object on stdout, including the app's own
Order 1042 shipped in 37 ms, whoseStatekeepsOrderIdandElapsedMsas fields a log backend can query. StopAsyncis what SIGTERM triggers inside a container. The host loggedApplication is shutting down...and the program printedstopped cleanly.
Failure modes
- Baking environment-specific settings into the image (an
appsettings.Production.jsonwith real hosts or secrets), so the image cannot be promoted. - Storing sessions, uploads or caches on the pod's local disk; they vanish on reschedule and differ between replicas.
- Writing log files inside the container instead of stdout; they are lost with the pod and fill its writable layer.
- Hard-coding
UseUrls("http://localhost:5000"): inside a container, localhost is unreachable from outside the pod. - Ignoring SIGTERM, so every deploy drops in-flight requests.
- Calling a monolith "not cloud-native" by definition; the properties matter, not the number of services.
Interview angle
"What makes a .NET service cloud-native?" Answer: "It is built once into an immutable image and promoted unchanged; all configuration and secrets come from the environment; it keeps no state in the process; it logs structured events to stdout and exports traces and metrics; it exposes health endpoints; it starts fast and shuts down gracefully on SIGTERM; and it runs as a non-root user with explicit resource requests. Then the platform can scale, restart and roll it out safely."
Practice questions
- Which environment variable sets
Orders:MaxPageSize?
Answer: Orders__MaxPageSize; double underscore is the hierarchy separator because : is not valid in all shells.
- Why should the same image go to staging and production?
Answer: So what you tested is exactly what runs; only configuration differs, and rollback means redeploying a known image tag or digest.
- Where should a containerised .NET service write logs?
Answer: To stdout/stderr, ideally as structured JSON; the node agent or OpenTelemetry pipeline collects them.
- Is a single ASP.NET Core API on AKS cloud-native?
Answer: It can be, if it has the properties (immutable image, external config and state, observability, disposability). Microservices are not required.
Revision checklist
- One image, many environments; config from env vars (
__=:). - Stateless processes; state in backing services.
- Port from the environment (8080 in the official images).
- JSON logs to stdout; no log files in the container.
- Graceful stop on SIGTERM.
Related on TechInterviewNotes
Analogy
Hotel rooms, not your apartment
A hotel room is identical to the one next door, cleaned between guests, and you bring nothing permanent into it. Your luggage (state) and your instructions (config) come with you. Cloud-native pods are hotel rooms: any one can be swapped for another without you noticing.
Quick reference
The cloud-native checklist for an ASP.NET Core service
- Immutable image, promoted by tag or digest.
ASPNETCORE_HTTP_PORTS,ConnectionStrings__x,Section__Keyfrom the environment.- No local state; health endpoints; OpenTelemetry.
- JSON console logs; graceful SIGTERM handling.
- Non-root user, resource requests and limits.