Git, CI/CD, Containers, Cloud and Observability

A CI pipeline runs on every push/PR: restore → build → test → static analysis → package → publish artifacts.

Concept

Source Control with Git git init, git clone, git status, git add, git commit -m, git log

git switch -c feature/x; short-lived feature branches off main

git merge feature/x — creates a merge commit, preserves history

git rebase main — replays your commits on top of main for linear history; never rebase shared branches

Code review + CI checks before merging into main git tag -a v1.2.0 -m "..." marks releases

release/1.2 for stabilization and hotfixes Edit the conflicted file (markers <<<<<<<, =======, >>>>>>>), git add, commit

Keep bin/, obj/, .vs/, local secrets out of the repository

A CI pipeline runs on every push/PR: restore → build → test → static analysis → package → publish artifacts. CD deploys artifacts through environments (dev → staging → production) with environment-specific configuration and secrets from the CI system's secret store. GitHub Actions, Azure DevOps Pipelines, GitLab CI and Jenkins all express the same steps.

Docker packages an app with its runtime into an image; a container is a running instance. A Dockerfile describes the image; multi-stage builds compile with the large SDK image and ship the small aspnet/runtime image. Configure containers with environment variables (ConnectionStrings__Db), map ports (-p 8080:8080), and persist data in volumes. .NET can also build images without a Dockerfile: dotnet publish -t:PublishContainer.

App Service, Container Apps, AKS Elastic Beanstalk, ECS, EKS Cloud Run, GKE Azure SQL, Cosmos DB Cloud SQL, Firestore Service Bus, Event Hubs SQS/SNS, Kinesis Application Insights / Azure Monitor CloudWatch, X-Ray Cloud Monitoring/Trace

Cloud apps scale horizontally (more instances), so they must be stateless (state in databases/caches) and configured externally.

The three pillars: logs (Module 45), metrics and traces (Module 48). OpenTelemetry is the vendor-neutral standard (OpenTelemetry.Extensions.Hosting + exporters for OTLP, Prometheus, Azure Monitor). Correlation IDs/trace IDs connect log lines across services (distributed tracing, W3C traceparent header). Health checks expose liveness (is the process alive? → restart if not) and readiness (can it serve traffic? → remove from the load balancer if not) for orchestrators like Kubernetes.

Why It Matters

Software that isn't versioned, built and tested automatically, deployed repeatably and observable in production isn't finished. These practices are expected of every professional .NET developer, including in automotive (ASPICE requires traceable, reproducible builds).

Syntax

git switch -c feature/gateway && git commit -am "Add gateway" && git push -u origin feature/gateway
dotnet publish -c Release -o out
docker build -t diag-api:1.0 . && docker run -p 8080:8080 -e ConnectionStrings__Db=... diag-api:1.0
builder.Services.AddHealthChecks().AddCheck("database", () => ..., tags: ["ready"]);
app.MapHealthChecks("/health/ready", new HealthCheckOptions { Predicate = c => c.Tags.Contains("ready") });

Example

59.1 A Git workflow with a feature branch, a merge conflict and a release tag

The Program.cs contents at each step, in order: initial commit, the feature branch change, the change on main, and the manual conflict resolution:

59.2 A CI pipeline (GitHub Actions) and a multi-stage Dockerfile

These two files are reference configurations (they run on GitHub's servers and in Docker, not in this course's environment):

59.3 Liveness and readiness health checks

Console.WriteLine("ECU list: VCU, BMS");
Console.WriteLine("ECU list: VCU, BMS, GW");
Console.WriteLine("ECU list: BMS, VCU (sorted)");
Console.WriteLine("ECU list: BMS, GW, VCU (sorted)");
bin/
obj/
*.user
.vs/
appsettings.*.local.json
# .github/workflows/build.yml - a typical CI pipeline for a .NET solution
name: build
on:
  push: { branches: [ main ] }
  pull_request:
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with: { dotnet-version: '10.0.x' }
      - run: dotnet restore --locked-mode              # reproducible restore (lock files)
      - run: dotnet format --verify-no-changes         # style gate
      - run: dotnet build -c Release --no-restore -warnaserror
      - run: dotnet test -c Release --no-build
      - run: dotnet list package --vulnerable --include-transitive
      - run: dotnet publish src/Diag.Api -c Release -o out
      - uses: actions/upload-artifact@v4
        with: { name: diag-api, path: out }
# Multi-stage build: the SDK image builds, the small runtime image runs
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY src/Diag.Api/Diag.Api.csproj src/Diag.Api/
RUN dotnet restore src/Diag.Api/Diag.Api.csproj          # cached layer while dependencies don't change
COPY . .
RUN dotnet publish src/Diag.Api -c Release -o /app --no-restore

FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app
COPY --from=build /app .
ENV ASPNETCORE_HTTP_PORTS=8080
EXPOSE 8080
USER $APP_UID                                             # run as non-root
HEALTHCHECK CMD ["dotnet", "Diag.Api.dll", "--health"]
ENTRYPOINT ["dotnet", "Diag.Api.dll"]
#:sdk Microsoft.NET.Sdk.Web
#:property PublishAot=false
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.Diagnostics.HealthChecks;

var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.WebHost.UseUrls("http://127.0.0.1:0");
var db = new FakeDependency("database", healthy: true);
var bench = new FakeDependency("hil-bench", healthy: false);
builder.Services.AddHealthChecks()
    .AddCheck("self", () => HealthCheckResult.Healthy("process is running"), tags: ["live"])
    .AddCheck("database", () => db.Healthy ? HealthCheckResult.Healthy() : HealthCheckResult.Unhealthy("no connection"), tags: ["ready"])
    .AddCheck("hil-bench", () => bench.Healthy ? HealthCheckResult.Healthy() : HealthCheckResult.Degraded("bench offline, simulated mode"), tags: ["ready"]);

var app = builder.Build();
// Liveness: is the process alive? (restart it if not). Readiness: can it serve traffic? (take it out of the load balancer if not)
app.MapHealthChecks("/health/live", new HealthCheckOptions { Predicate = c => c.Tags.Contains("live") });
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = c => c.Tags.Contains("
…

Expected Result

Both branches changed the same line, so Git stopped with a conflict (UU = unmerged); git diff showed both versions between the markers. After the manual resolution the history graph shows the branch and the merge, tagged v1.0.0.

Liveness stayed healthy while readiness reported Degraded (still 200 — serving in simulated mode) and then Unhealthy with HTTP 503 when the database failed, which tells a load balancer to stop sending traffic to this instance.

> git init -q -b main
> git config user.name "Asha Dev"
> git config user.email [email protected]
> git config core.autocrlf false
> git add .
> git commit -q -m "Add ECU list"
> git switch -q -c feature/gateway
> git commit -q -am "Add gateway ECU"
> git switch -q main
> git commit -q -am "Sort ECU list"
> git merge feature/gateway
Auto-merging Program.cs
CONFLICT (content): Merge conflict in Program.cs
Automatic merge failed; fix conflicts and then commit the result.
> git status --short
UU Program.cs
> git diff
diff --cc Program.cs
index 0fe7f07,b8a0e59..0000000
--- a/Program.cs
+++ b/Program.cs
@@@ -1,1 -1,1 +1,5 @@@
++<<<<<<< HEAD
 +Console.WriteLine("ECU list: BMS, VCU (sorted)");
++=======
+ Console.WriteLine("ECU list: VCU, BMS, GW");
++>>>>>>> feature/gateway
> git add Program.cs
> git commit -q -m "Merge feature/gateway"
> git tag -a v1.0.0 -m "Release 1.0.0"
> git log --graph --format=%s%d
*   Merge feature/gateway (HEAD -> main, tag: v1.0.0)
|\  
| * Add gateway ECU (feature/gateway)
* | Sort ECU list
|/  
* Add ECU list
GET /health/live   -> 200 Healthy
GET /health/ready  -> 200 {"status":"Degraded","checks":[{"name":"database","status":"Healthy","description":null},{"name":"hil-bench","status":"Degraded","description":"bench offline, simulated mode"}]}
GET /health/ready  -> 503 {"status":"Unhealthy","checks":[{"name":"database","status":"Unhealthy","description":"no connection"},{"name":"hil-bench","status":"Degraded","description":"bench offline, simulated mode"}]}

Common Mistakes

Committing bin//obj/, secrets or huge binaries; missing .gitignore.

Long-lived branches that diverge for weeks → painful merges.

Rebasing or force-pushing shared branches.

CI that doesn't run tests, or deploys artifacts different from the ones tested.

Secrets baked into Docker images; containers running as root.

One health endpoint that checks every dependency and gets the process restarted when the database blips.

Logs without correlation IDs; metrics with unbounded label values.

Best Practices

Small commits with clear messages; short-lived branches; PRs with required CI checks.

Build once, deploy the same artifact everywhere; configuration via environment.

Multi-stage, non-root, minimal container images; scan them for vulnerabilities.

Separate liveness and readiness probes.

OpenTelemetry for logs, metrics and traces from day one.

Interview Relevance

High: "merge vs rebase?", "how do you resolve a merge conflict?", "what does your CI pipeline do?", "what is a multi-stage Docker build?", "liveness vs readiness?", "logs vs metrics vs traces?", "how do you manage secrets in CI and in the cloud?".

Practice Questions

Repeat the Git workflow using git rebase main on the feature branch instead of a merge and compare the graph.

Add a job to the pipeline that publishes the NuGet package from Module 46 on tags only.

Build the health-check app into a container with dotnet publish -t:PublishContainer.

Add OpenTelemetry tracing with the console exporter to the ASP.NET Core API from Module 51.

Quick Quiz

Q1. (Multiple choice) Which probe failure should cause the orchestrator to restart the container? A) Readiness B) Liveness C) Startup D) Metrics Answer: B.

Q2. (Predict the output) What does git status --short print for a file with an unresolved merge conflict?

Answer: UU Program.cs (verified).

Q3. (True/False) A multi-stage Dockerfile ships the .NET SDK in the final image.

Answer: False. Only the last stage (the runtime image plus published output) becomes the final image.

Q4. (Find the bug) ENV ConnectionStrings__Db="Server=db;Password=Hunter2" in the Dockerfile.

Answer: The secret is baked into the image layers; inject it at run time from a secret store or orchestrator secret.

Coding Challenge

Write a custom health check class DiskSpaceHealthCheck : IHealthCheck that reports Degraded below 10 GB and Unhealthy below 1 GB of free space on the system drive, and run it once.

#:sdk Microsoft.NET.Sdk.Web
#:property PublishAot=false
using Microsoft.Extensions.Diagnostics.HealthChecks;

var check = new DiskSpaceHealthCheck(Path.GetPathRoot(Environment.SystemDirectory)!);
HealthCheckResult result = await check.CheckHealthAsync(new HealthCheckContext());
Console.WriteLine($"status {result.Status}, has description: {result.Description is not null}");

public sealed class DiskSpaceHealthCheck(string drive) : IHealthCheck
{
    public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
    {
        double freeGb = new DriveInfo(drive).AvailableFreeSpace / 1e9;
        HealthCheckResult result = freeGb switch
        {
            < 1 => HealthCheckResult.Unhealthy($"{freeGb:F1} GB free"),
            < 10 => HealthCheckResult.Degraded($"{freeGb:F1} GB free"),
            _ => HealthCheckResult.Healthy($"{freeGb:F1} GB free"),
        };
        return Task.FromResult(result);
    }
}