Mục tiêu: Master CI/CD pipeline cho .NET, Docker workflows, và .NET Aspire cho cloud-native development.
1. CI/CD Pipeline Patterns
GitHub Actions cho .NET
yaml# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
DOTNET_VERSION: '9.0.x'
REGISTRY: myregistry.azurecr.io
IMAGE_NAME: order-service
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --no-restore --configuration Release
- name: Test
run: dotnet test --no-build --configuration Release
--logger "trx;LogFileName=test-results.trx"
--collect:"XPlat Code Coverage"
-- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: '**/*.trx'
- name: Code Coverage Report
uses: codecov/codecov-action@v3
with:
files: '**/coverage.opencover.xml'
docker-build:
needs: build-and-test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login to ACR
uses: azure/docker-login@v1
with:
login-server: ${{ env.REGISTRY }}
username: ${{ secrets.ACR_USERNAME }}
password: ${{ secrets.ACR_PASSWORD }}
- name: Build and push
run: |
docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} .
docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest .
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
deploy-staging:
needs: docker-build
runs-on: ubuntu-latest
environment: staging
steps:
- uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Deploy to Container Apps
uses: azure/container-apps-deploy-action@v1
with:
containerAppName: order-service
resourceGroup: rg-staging
imageToDeploy: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production # Requires manual approval
steps:
- uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Deploy to Production
uses: azure/container-apps-deploy-action@v1
with:
containerAppName: order-service
resourceGroup: rg-production
imageToDeploy: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
Azure DevOps Pipeline
yaml# azure-pipelines.yml
trigger:
branches:
include: [main]
pool:
vmImage: 'ubuntu-latest'
variables:
buildConfiguration: 'Release'
dotnetVersion: '9.0.x'
stages:
- stage: Build
jobs:
- job: BuildAndTest
steps:
- task: UseDotNet@2
inputs:
version: $(dotnetVersion)
- script: dotnet build --configuration $(buildConfiguration)
- script: dotnet test --configuration $(buildConfiguration) --logger trx
- task: PublishTestResults@2
inputs:
testResultsFormat: 'VSTest'
testResultsFiles: '**/*.trx'
- stage: DeployStaging
dependsOn: Build
jobs:
- deployment: DeployToStaging
environment: staging
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
inputs:
azureSubscription: 'Azure-Connection'
appName: 'myapp-staging'
package: '$(Pipeline.Workspace)/**/*.zip'
2. Docker Best Practices cho .NET
dockerfile# Production Dockerfile - multi-stage, optimized FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build WORKDIR /src # Copy csproj files first → layer caching COPY src/Api/Api.csproj src/Api/ COPY src/Domain/Domain.csproj src/Domain/ COPY src/Application/Application.csproj src/Application/ COPY src/Infrastructure/Infrastructure.csproj src/Infrastructure/ RUN dotnet restore src/Api/Api.csproj # Copy source and build COPY src/ src/ RUN dotnet publish src/Api/Api.csproj \ -c Release \ -o /app/publish \ --no-restore \ /p:UseAppHost=false # Runtime image - minimal FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS runtime WORKDIR /app # Security: non-root user RUN adduser --disabled-password --no-create-home appuser USER appuser COPY --from=build /app/publish . EXPOSE 8080 ENV ASPNETCORE_URLS=http://+:8080 ENV ASPNETCORE_ENVIRONMENT=Production HEALTHCHECK --interval=30s --timeout=3s --start-period=10s \ CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 ENTRYPOINT ["dotnet", "Api.dll"]
yaml# docker-compose.yml cho local development
services:
api:
build: .
ports: ["5001:8080"]
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ConnectionStrings__Default=Server=db;Database=MyApp;User=sa;Password=P@ss1234;TrustServerCertificate=true
depends_on:
db: { condition: service_healthy }
rabbitmq: { condition: service_healthy }
db:
image: mcr.microsoft.com/mssql/server:2022-latest
environment:
- ACCEPT_EULA=Y
- SA_PASSWORD=P@ss1234
ports: ["1433:1433"]
healthcheck:
test: /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P P@ss1234 -Q "SELECT 1"
interval: 10s
timeout: 3s
rabbitmq:
image: rabbitmq:3-management
ports: ["5672:5672", "15672:15672"]
healthcheck:
test: rabbitmq-diagnostics -q ping
interval: 10s
3. .NET Aspire - Cloud-Native Development
.NET Aspire = development orchestrator + cloud-native building blocks
┌──────────────────────────────────────────────────┐
│ Aspire Dashboard │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ Traces │ │ Logs │ │ Metrics │ │
│ │ (OTel) │ │ (Live) │ │ (Prometheus) │ │
│ └──────────┘ └──────────┘ └──────────────────┘ │
└──────────────────────────────────────────────────┘
AppHost (orchestrator):
→ Start all services
→ Wire up service discovery
→ Configure resources (DB, Redis, RabbitMQ)
→ Inject connection strings automatically
csharp// MyApp.AppHost/Program.cs - orchestrator
var builder = DistributedApplication.CreateBuilder(args);
// Infrastructure resources
var postgres = builder.AddPostgres("postgres")
.WithPgAdmin()
.AddDatabase("orderdb");
var redis = builder.AddRedis("redis")
.WithRedisInsight();
var rabbitmq = builder.AddRabbitMQ("messaging")
.WithManagementPlugin();
// Application services
var orderApi = builder.AddProject<Projects.OrderService>("order-service")
.WithReference(postgres)
.WithReference(redis)
.WithReference(rabbitmq)
.WithExternalHttpEndpoints();
var catalogApi = builder.AddProject<Projects.CatalogService>("catalog-service")
.WithReference(postgres)
.WithReference(redis);
// Frontend
builder.AddNpmApp("web", "../Web")
.WithReference(orderApi)
.WithReference(catalogApi)
.WithExternalHttpEndpoints()
.WithHttpEndpoint(port: 3000, env: "PORT");
builder.Build().Run();
csharp// Service project (e.g., OrderService/Program.cs)
var builder = WebApplication.CreateBuilder(args);
// Aspire auto-injects connection strings!
builder.AddServiceDefaults(); // OpenTelemetry, health checks, service discovery
builder.AddNpgsqlDbContext<OrderDbContext>("orderdb"); // Auto connection string
builder.AddRedisDistributedCache("redis");
builder.AddRabbitMQClient("messaging");
var app = builder.Build();
app.MapDefaultEndpoints(); // /health, /alive
app.Run();
Aspire Benefits
✅ 1-click start: tất cả services + infrastructure
✅ Auto service discovery (no manual URLs)
✅ Auto connection strings (no appsettings.json cho dev)
✅ Built-in dashboard: traces, logs, metrics
✅ Container management: auto-pull, auto-start
✅ Deployment manifest generation (Azure, AWS, Kubernetes)
4. Infrastructure as Code (Bicep)
bicep// main.bicep param location string = resourceGroup().location param appName string // App Service Plan resource appServicePlan 'Microsoft.Web/serverfarms@2023-01-01' = { name: '${appName}-plan' location: location sku: { name: 'P1v3', tier: 'PremiumV3' } properties: { reserved: true } // Linux } // App Service resource webApp 'Microsoft.Web/sites@2023-01-01' = { name: '${appName}-api' location: location properties: { serverFarmId: appServicePlan.id siteConfig: { linuxFxVersion: 'DOTNETCORE|9.0' alwaysOn: true healthCheckPath: '/health' } } identity: { type: 'SystemAssigned' } } // SQL Server resource sqlServer 'Microsoft.Sql/servers@2023-05-01-preview' = { name: '${appName}-sql' location: location properties: { administratorLogin: 'sqladmin' administratorLoginPassword: sqlPassword } } // Key Vault access resource keyVaultAccess 'Microsoft.KeyVault/vaults/accessPolicies@2023-07-01' = { parent: keyVault name: 'add' properties: { accessPolicies: [{ tenantId: webApp.identity.tenantId objectId: webApp.identity.principalId permissions: { secrets: ['get', 'list'] } }] } }
Tổng Kết Session 23
| Concept | Key Takeaway |
|---|---|
| CI/CD | GitHub Actions hoặc Azure DevOps, multi-stage |
| Docker | Multi-stage build, Alpine image, non-root user |
| .NET Aspire | Orchestrator for dev, auto-service discovery |
| Bicep | IaC cho Azure, type-safe, modular |
Bài tập: Setup .NET Aspire project với 2 services + PostgreSQL + Redis. Create GitHub Actions CI/CD pipeline. Deploy to Azure Container Apps.