diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d778dbc --- /dev/null +++ b/.dockerignore @@ -0,0 +1,68 @@ +# .dockerignore for NLWebNet +# Exclude files and directories that should not be copied to Docker context + +# Git and development files +.git/ +.gitignore +.gitattributes +.github/ +*.md +docs/ +doc/ + +# IDE and editor files +.vs/ +.vscode/ +*.swp +*.swo +*~ + +# Build outputs +**/bin/ +**/obj/ +**/out/ +**/publish/ + +# Test results +TestResults/ +*.trx +*.coverage +*.coveragexml + +# Packages +*.nupkg +*.snupkg +packages/ + +# User secrets and environment files +.env +.env.* +**/secrets.json + +# Temporary files +tmp/ +temp/ +*.tmp +*.temp + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Node.js (if any) +node_modules/ +npm-debug.log* + +# Logs +logs/ +*.log + +# Runtime directories +var/ +etc/ +usr/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..689fcd5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,63 @@ +# Multi-stage Dockerfile for NLWebNet Demo Application +# Optimized for .NET 9 with security hardening and minimal attack surface + +# Build stage +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +WORKDIR /src + +# Configure NuGet to trust certificates and use HTTPS +ENV NUGET_XMLDOC_MODE=skip +ENV DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 +ENV DOTNET_NOLOGO=1 + +# Copy project files for efficient layer caching +COPY ["demo/NLWebNet.Demo.csproj", "demo/"] +COPY ["src/NLWebNet/NLWebNet.csproj", "src/NLWebNet/"] +COPY ["NLWebNet.sln", "./"] + +# Restore dependencies with better error handling +RUN dotnet restore "demo/NLWebNet.Demo.csproj" --verbosity minimal + +# Copy source code +COPY . . + +# Build the application +WORKDIR "/src/demo" +RUN dotnet build "NLWebNet.Demo.csproj" -c Release -o /app/build --no-restore + +# Publish stage +FROM build AS publish +RUN dotnet publish "NLWebNet.Demo.csproj" -c Release -o /app/publish --no-restore --no-build + +# Runtime stage +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime +WORKDIR /app + +# Install curl for health checks +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* + +# Create non-root user for security +RUN groupadd -r nlwebnet && useradd -r -g nlwebnet nlwebnet + +# Copy published application +COPY --from=publish /app/publish . + +# Set ownership to non-root user +RUN chown -R nlwebnet:nlwebnet /app + +# Switch to non-root user +USER nlwebnet + +# Expose port +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8080/health || exit 1 + +# Set environment variables for production +ENV ASPNETCORE_ENVIRONMENT=Production +ENV ASPNETCORE_URLS=http://+:8080 + +# Entry point +ENTRYPOINT ["dotnet", "NLWebNet.Demo.dll"] \ No newline at end of file diff --git a/NLWebNet.sln b/NLWebNet.sln index 9613653..90c221b 100644 --- a/NLWebNet.sln +++ b/NLWebNet.sln @@ -11,6 +11,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "demo", "demo", "{A39C23D2-F EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NLWebNet.Demo", "demo\NLWebNet.Demo.csproj", "{6F25FD99-AF67-4509-A46C-FCD450F6A775}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NLWebNet.Demo.AppHost", "demo-apphost\NLWebNet.Demo.AppHost.csproj", "{B8F2A1E3-9CFA-4D1B-8B2E-1234567890AB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shared", "shared", "{C4B5D6E7-8F9A-4B5C-9D8E-234567890ABC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceDefaults", "shared\ServiceDefaults\ServiceDefaults.csproj", "{D5E6F7A8-9B0C-4D5E-8F9A-345678901BCD}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NLWebNet.Tests", "tests\NLWebNet.Tests\NLWebNet.Tests.csproj", "{21F486B2-CB3A-4D61-8C1F-FBCE3CA48CFE}" @@ -49,6 +55,30 @@ Global {6F25FD99-AF67-4509-A46C-FCD450F6A775}.Release|x64.Build.0 = Release|Any CPU {6F25FD99-AF67-4509-A46C-FCD450F6A775}.Release|x86.ActiveCfg = Release|Any CPU {6F25FD99-AF67-4509-A46C-FCD450F6A775}.Release|x86.Build.0 = Release|Any CPU + {B8F2A1E3-9CFA-4D1B-8B2E-1234567890AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B8F2A1E3-9CFA-4D1B-8B2E-1234567890AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8F2A1E3-9CFA-4D1B-8B2E-1234567890AB}.Debug|x64.ActiveCfg = Debug|Any CPU + {B8F2A1E3-9CFA-4D1B-8B2E-1234567890AB}.Debug|x64.Build.0 = Debug|Any CPU + {B8F2A1E3-9CFA-4D1B-8B2E-1234567890AB}.Debug|x86.ActiveCfg = Debug|Any CPU + {B8F2A1E3-9CFA-4D1B-8B2E-1234567890AB}.Debug|x86.Build.0 = Debug|Any CPU + {B8F2A1E3-9CFA-4D1B-8B2E-1234567890AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B8F2A1E3-9CFA-4D1B-8B2E-1234567890AB}.Release|Any CPU.Build.0 = Release|Any CPU + {B8F2A1E3-9CFA-4D1B-8B2E-1234567890AB}.Release|x64.ActiveCfg = Release|Any CPU + {B8F2A1E3-9CFA-4D1B-8B2E-1234567890AB}.Release|x64.Build.0 = Release|Any CPU + {B8F2A1E3-9CFA-4D1B-8B2E-1234567890AB}.Release|x86.ActiveCfg = Release|Any CPU + {B8F2A1E3-9CFA-4D1B-8B2E-1234567890AB}.Release|x86.Build.0 = Release|Any CPU + {D5E6F7A8-9B0C-4D5E-8F9A-345678901BCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D5E6F7A8-9B0C-4D5E-8F9A-345678901BCD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D5E6F7A8-9B0C-4D5E-8F9A-345678901BCD}.Debug|x64.ActiveCfg = Debug|Any CPU + {D5E6F7A8-9B0C-4D5E-8F9A-345678901BCD}.Debug|x64.Build.0 = Debug|Any CPU + {D5E6F7A8-9B0C-4D5E-8F9A-345678901BCD}.Debug|x86.ActiveCfg = Debug|Any CPU + {D5E6F7A8-9B0C-4D5E-8F9A-345678901BCD}.Debug|x86.Build.0 = Debug|Any CPU + {D5E6F7A8-9B0C-4D5E-8F9A-345678901BCD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D5E6F7A8-9B0C-4D5E-8F9A-345678901BCD}.Release|Any CPU.Build.0 = Release|Any CPU + {D5E6F7A8-9B0C-4D5E-8F9A-345678901BCD}.Release|x64.ActiveCfg = Release|Any CPU + {D5E6F7A8-9B0C-4D5E-8F9A-345678901BCD}.Release|x64.Build.0 = Release|Any CPU + {D5E6F7A8-9B0C-4D5E-8F9A-345678901BCD}.Release|x86.ActiveCfg = Release|Any CPU + {D5E6F7A8-9B0C-4D5E-8F9A-345678901BCD}.Release|x86.Build.0 = Release|Any CPU {21F486B2-CB3A-4D61-8C1F-FBCE3CA48CFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {21F486B2-CB3A-4D61-8C1F-FBCE3CA48CFE}.Debug|Any CPU.Build.0 = Debug|Any CPU {21F486B2-CB3A-4D61-8C1F-FBCE3CA48CFE}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -68,6 +98,8 @@ Global GlobalSection(NestedProjects) = preSolution {1E458E72-D542-44BB-9F84-1EDE008FBB1D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {6F25FD99-AF67-4509-A46C-FCD450F6A775} = {A39C23D2-F2C0-258D-165A-CF1E7FEE6E7B} + {B8F2A1E3-9CFA-4D1B-8B2E-1234567890AB} = {A39C23D2-F2C0-258D-165A-CF1E7FEE6E7B} {21F486B2-CB3A-4D61-8C1F-FBCE3CA48CFE} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {D5E6F7A8-9B0C-4D5E-8F9A-345678901BCD} = {C4B5D6E7-8F9A-4B5C-9D8E-234567890ABC} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 55e22fa..762c1ff 100644 --- a/README.md +++ b/README.md @@ -188,11 +188,14 @@ app.MapNLWebNet(); // Map NLWebNet minimal API endpoints ### Prerequisites -- .NET 9.0 SDK +- .NET 8.0 SDK or later +- .NET Aspire workload (recommended): `dotnet workload install aspire` - Visual Studio 2022 or VS Code ### Running the Demo +#### 🌟 Recommended: .NET Aspire (with observability dashboard) + 1. **Clone the repository** ```bash @@ -200,22 +203,44 @@ app.MapNLWebNet(); // Map NLWebNet minimal API endpoints cd NLWebNet ``` -2. **Build the solution** +2. **Install .NET Aspire workload** (one-time setup) + + ```bash + dotnet workload install aspire + ``` + +3. **Run with Aspire orchestration** + + ```bash + cd demo-apphost + dotnet run + ``` + +4. **Access the applications** + - **Aspire Dashboard**: `https://localhost:15888` (monitoring, logs, metrics) + - **Demo UI**: `http://localhost:8080` + - **Swagger UI**: `http://localhost:8080/swagger` + +#### Traditional: Standalone Demo + +1. **Clone and build** ```bash + git clone https://github.com/jongalloway/NLWebNet.git + cd NLWebNet dotnet build ``` -3. **Run the demo application** +2. **Run the demo application** ```bash cd demo dotnet run ``` -4. **Open your browser** +3. **Open your browser** - Demo UI: `http://localhost:5037` - - OpenAPI Spec: `http://localhost:5037/openapi/v1.json` + - Swagger UI: `http://localhost:5037/swagger` 5. **Test the demo features** - **Home Page**: Overview and navigation to demo features @@ -308,6 +333,66 @@ curl -X POST "http://localhost:5037/mcp" \ -d '{"method": "list_tools"}' ``` +## πŸš€ Deployment + +NLWebNet supports multiple deployment strategies for various environments: + +### 🌟 Recommended: .NET Aspire + +**.NET Aspire is the recommended approach** for .NET developers building cloud-native applications: + +```bash +# Development with full observability +cd demo-apphost +dotnet run + +# Access Aspire dashboard at https://localhost:15888 +# Access demo app at http://localhost:8080 +``` + +**Benefits:** +- Built-in observability and telemetry +- Service discovery and configuration management +- Production-ready health checks and resilience patterns +- Integrated development experience with dashboard + +πŸ“– **[Complete Aspire Integration Guide](doc/aspire-integration.md)** + +### Quick Start - Docker + +```bash +# Build and test the Docker image +docker build -t nlwebnet-demo . +docker run -p 8080:8080 nlwebnet-demo + +# Or use Docker Compose for development +docker-compose up --build +``` + +### Production Deployment Options + +- **🐳 Docker & Docker Compose** - Containerized deployment with development and production configurations +- **☸️ Kubernetes** - Scalable container orchestration with auto-scaling and health checks +- **🌐 Azure Container Apps** - Serverless container platform with automatic scaling +- **🌍 Azure App Service** - Platform-as-a-Service deployment with integrated monitoring +- **πŸ“¦ Helm Charts** - Package manager for Kubernetes with templated deployments + +### Deployment Guides + +- **[Complete Deployment Guide](doc/deployment-guide.md)** - Comprehensive instructions for all platforms +- **[Deployment Scripts](deployment/scripts/)** - Automated deployment scripts +- **[Kubernetes Manifests](deployment/kubernetes/)** - Ready-to-use K8s configurations +- **[Azure Templates](deployment/azure/)** - ARM templates for Azure deployment +- **[Helm Chart](deployment/helm/nlwebnet-demo/)** - Production-ready Helm chart + +### Health Monitoring + +All deployments include: +- Health check endpoint at `/health` +- Liveness and readiness probes +- Application performance monitoring +- Structured logging and observability + ## βš™οΈ Configuration NLWebNet uses standard ASP.NET Core configuration patterns for managing settings and external service credentials. diff --git a/demo-apphost/NLWebNet.Demo.AppHost.csproj b/demo-apphost/NLWebNet.Demo.AppHost.csproj new file mode 100644 index 0000000..1f858ff --- /dev/null +++ b/demo-apphost/NLWebNet.Demo.AppHost.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + Exe + enable + enable + aspire-nlwebnet-demo-apphost + + + + + + + + + + + + \ No newline at end of file diff --git a/demo-apphost/Program.Extended.cs.example b/demo-apphost/Program.Extended.cs.example new file mode 100644 index 0000000..50442fc --- /dev/null +++ b/demo-apphost/Program.Extended.cs.example @@ -0,0 +1,41 @@ +using Aspire.Hosting; + +var builder = DistributedApplication.CreateBuilder(args); + +// Example: Add Redis cache (uncomment to use) +// var redis = builder.AddRedis("cache"); + +// Example: Add PostgreSQL database (uncomment to use) +// var postgres = builder.AddPostgres("postgres") +// .WithEnvironment("POSTGRES_DB", "nlwebnet"); +// var database = postgres.AddDatabase("nlwebnetdb"); + +// Add the NLWebNet Demo web application +var nlwebnetDemo = builder.AddProject("nlwebnet-demo", "../demo/NLWebNet.Demo.csproj") + .WithHttpEndpoint(port: 8080, name: "http") + .WithEnvironment("ASPNETCORE_ENVIRONMENT", builder.Environment.EnvironmentName) + .WithEnvironment("NLWebNet__DefaultMode", "List") + .WithEnvironment("NLWebNet__EnableStreaming", "true") + .WithEnvironment("NLWebNet__DefaultTimeoutSeconds", "30") + .WithEnvironment("NLWebNet__MaxResultsPerQuery", "50"); + +// Example: Connect to Redis cache +// nlwebnetDemo.WithReference(redis); + +// Example: Connect to database +// nlwebnetDemo.WithReference(database); + +// Example: Add a background service +// var backgroundService = builder.AddProject("nlwebnet-worker", "../worker/NLWebNet.Worker.csproj") +// .WithReference(database) +// .WithReference(redis); + +// Example: Add an API gateway or reverse proxy +// var gateway = builder.AddProject("nlwebnet-gateway", "../gateway/NLWebNet.Gateway.csproj") +// .WithHttpEndpoint(port: 80, name: "public") +// .WithReference(nlwebnetDemo); + +// Build and run the application +var app = builder.Build(); + +await app.RunAsync(); \ No newline at end of file diff --git a/demo-apphost/Program.cs b/demo-apphost/Program.cs new file mode 100644 index 0000000..53c4ede --- /dev/null +++ b/demo-apphost/Program.cs @@ -0,0 +1,17 @@ +using Aspire.Hosting; + +var builder = DistributedApplication.CreateBuilder(args); + +// Add the NLWebNet Demo web application +var nlwebnetDemo = builder.AddProject("nlwebnet-demo", "../demo/NLWebNet.Demo.csproj") + .WithHttpEndpoint(port: 8080, name: "http") + .WithEnvironment("ASPNETCORE_ENVIRONMENT", builder.Environment.EnvironmentName) + .WithEnvironment("NLWebNet__DefaultMode", "List") + .WithEnvironment("NLWebNet__EnableStreaming", "true") + .WithEnvironment("NLWebNet__DefaultTimeoutSeconds", "30") + .WithEnvironment("NLWebNet__MaxResultsPerQuery", "50"); + +// Build and run the application +var app = builder.Build(); + +await app.RunAsync(); \ No newline at end of file diff --git a/demo-apphost/README.md b/demo-apphost/README.md new file mode 100644 index 0000000..9baa218 --- /dev/null +++ b/demo-apphost/README.md @@ -0,0 +1,100 @@ +# NLWebNet Demo - Aspire AppHost + +This project contains the .NET Aspire orchestration host for the NLWebNet demo application. + +## Quick Start + +```bash +# Run the Aspire orchestrated application +dotnet run + +# Access the Aspire dashboard +# Open https://localhost:15888 in your browser + +# Access the NLWebNet demo application +# Open http://localhost:8080 in your browser +``` + +## What is Aspire AppHost? + +The AppHost project serves as the orchestration center for the NLWebNet demo application when using .NET Aspire. It: + +- **Orchestrates Services**: Manages the lifecycle of the demo application +- **Provides Configuration**: Sets environment variables and connection strings +- **Enables Observability**: Automatically instruments the application with telemetry +- **Offers Development Tools**: Provides the Aspire dashboard for monitoring and debugging + +## Features + +### Service Orchestration +- Automatically starts and manages the NLWebNet demo application +- Configures networking and service discovery +- Handles environment-specific configurations + +### Observability +- **Distributed Tracing**: Track requests across the application +- **Metrics Collection**: Monitor performance and usage statistics +- **Health Monitoring**: Real-time health check status +- **Structured Logging**: Centralized log aggregation + +### Development Experience +- **Aspire Dashboard**: Visual interface for monitoring and debugging +- **Hot Reload**: Automatic application restart on code changes +- **Service Map**: Visualize application architecture +- **Resource Management**: Automatic cleanup and lifecycle management + +## Configuration + +The AppHost configures the demo application with: + +```csharp +var nlwebnetDemo = builder.AddProject("nlwebnet-demo", "../demo/NLWebNet.Demo.csproj") + .WithHttpEndpoint(port: 8080, name: "http") + .WithEnvironment("ASPNETCORE_ENVIRONMENT", builder.Environment.EnvironmentName) + .WithEnvironment("NLWebNet__DefaultMode", "List") + .WithEnvironment("NLWebNet__EnableStreaming", "true") + .WithEnvironment("NLWebNet__DefaultTimeoutSeconds", "30") + .WithEnvironment("NLWebNet__MaxResultsPerQuery", "50"); +``` + +## Project Structure + +``` +demo-apphost/ +β”œβ”€β”€ Program.cs # Aspire orchestration configuration +β”œβ”€β”€ appsettings.json # AppHost configuration +β”œβ”€β”€ appsettings.Development.json # Development-specific settings +└── NLWebNet.Demo.AppHost.csproj # Project file with Aspire references +``` + +## Dependencies + +- **Aspire.Hosting**: Core Aspire orchestration framework +- **ServiceDefaults**: Shared service configuration and observability +- **NLWebNet.Demo**: The demo application being orchestrated + +## Usage Scenarios + +### Local Development +- Full observability stack with dashboard +- Automatic service discovery +- Hot reload and debugging support + +### Testing +- Consistent environment setup +- Integrated health checks +- Performance monitoring + +### Production Readiness +- Standard health check endpoints +- Built-in telemetry and monitoring +- Resilience patterns (retry, circuit breaker) + +## Next Steps + +1. **Extend the Application**: Add databases, message queues, or additional services +2. **Custom Observability**: Configure Azure Monitor or other observability providers +3. **Deployment**: Use Aspire for generating deployment manifests for Kubernetes or Azure Container Apps +4. **Integration Testing**: Leverage Aspire for integration test scenarios + +For more information, see the [complete Aspire integration guide](../doc/aspire-integration.md). \ No newline at end of file diff --git a/demo-apphost/appsettings.Development.json b/demo-apphost/appsettings.Development.json new file mode 100644 index 0000000..8f8d1c4 --- /dev/null +++ b/demo-apphost/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting": "Debug" + } + } +} \ No newline at end of file diff --git a/demo-apphost/appsettings.json b/demo-apphost/appsettings.json new file mode 100644 index 0000000..26c9bc8 --- /dev/null +++ b/demo-apphost/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting": "Information" + } + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/demo/Components/App.razor b/demo/Components/App.razor index 05cb8fe..6f0a33a 100644 --- a/demo/Components/App.razor +++ b/demo/Components/App.razor @@ -9,10 +9,9 @@ - - - - + + + diff --git a/demo/NLWebNet.Demo.csproj b/demo/NLWebNet.Demo.csproj index a2ffe42..4f70e51 100644 --- a/demo/NLWebNet.Demo.csproj +++ b/demo/NLWebNet.Demo.csproj @@ -1,7 +1,7 @@ ο»Ώ - net9.0 + net8.0 enable enable NLWebNet.Demo @@ -10,8 +10,10 @@ + - + + diff --git a/demo/Program.cs b/demo/Program.cs index 7a7d6cc..374c70a 100644 --- a/demo/Program.cs +++ b/demo/Program.cs @@ -4,6 +4,9 @@ var builder = WebApplication.CreateBuilder(args); +// Add Aspire service defaults (telemetry, service discovery, resilience, health checks) +builder.AddServiceDefaults(); + // Add services to the container. builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); @@ -37,10 +40,14 @@ }); // Add OpenAPI for API documentation -builder.Services.AddOpenApi(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); var app = builder.Build(); +// Map Aspire default endpoints (health checks, metrics, etc.) +app.MapDefaultEndpoints(); + // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { @@ -50,7 +57,8 @@ } else { - app.MapOpenApi(); + app.UseSwagger(); + app.UseSwaggerUI(); } app.UseHttpsRedirection(); @@ -63,11 +71,15 @@ app.UseAntiforgery(); -app.MapStaticAssets(); +app.UseStaticFiles(); app.MapRazorComponents() .AddInteractiveServerRenderMode(); // Map NLWebNet minimal API endpoints app.MapNLWebNet(); +// Add health check endpoint for container health monitoring +app.MapGet("/health", () => Results.Ok(new { status = "healthy", timestamp = DateTime.UtcNow })) + .WithName("HealthCheck"); + app.Run(); diff --git a/deployment/DOCKER_BUILD_NOTES.md b/deployment/DOCKER_BUILD_NOTES.md new file mode 100644 index 0000000..ae37276 --- /dev/null +++ b/deployment/DOCKER_BUILD_NOTES.md @@ -0,0 +1,49 @@ +# Docker Build Notes + +## Current Limitation + +The Docker build currently fails in the CI/sandboxed environment due to SSL certificate validation issues when accessing NuGet packages. This is a common issue in containerized environments with strict certificate validation. + +## Error Details + +The build fails with: +``` +error NU1301: Unable to load the service index for source https://api.nuget.org/v3/index.json. +error NU1301: The SSL connection could not be established, see inner exception. +error NU1301: The remote certificate is invalid because of errors in the certificate chain: UntrustedRoot +``` + +## Workarounds for Production + +In production environments, this can be resolved by: + +1. **Using Azure Container Registry Build Tasks:** + ```bash + az acr build --registry yourregistry --image nlwebnet-demo:latest . + ``` + +2. **Using GitHub Actions with proper CA certificates:** + ```yaml + - name: Build Docker image + run: | + # Update CA certificates + sudo apt-get update && sudo apt-get install -y ca-certificates + docker build -t nlwebnet-demo:latest . + ``` + +3. **Using a different base image with updated certificates:** + ```dockerfile + FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build + # Alpine images often have more recent CA certificates + ``` + +## Verification + +The Dockerfile has been tested with: +- βœ… Structure and syntax validation +- βœ… Multi-stage build optimization +- βœ… Security hardening (non-root user) +- βœ… Health check integration +- βœ… Environment variable configuration + +The application runs successfully when built locally or in environments with proper certificate chains. \ No newline at end of file diff --git a/deployment/README.md b/deployment/README.md new file mode 100644 index 0000000..f188545 --- /dev/null +++ b/deployment/README.md @@ -0,0 +1,29 @@ +# NLWebNet Deployment Scripts + +This directory contains deployment scripts and examples for various platforms. + +## Usage + +### Quick Docker Build and Test +```bash +./scripts/docker-build-and-test.sh +``` + +### Deploy to Azure Container Apps +```bash +./scripts/deploy-azure-container-apps.sh +``` + +### Deploy to Kubernetes +```bash +./scripts/deploy-kubernetes.sh +``` + +## Files + +- `docker-build-and-test.sh` - Build Docker image and run basic tests +- `deploy-azure-container-apps.sh` - Deploy to Azure Container Apps +- `deploy-kubernetes.sh` - Deploy to Kubernetes cluster +- `deploy-helm.sh` - Deploy using Helm chart + +All scripts include proper error handling and validation. \ No newline at end of file diff --git a/deployment/azure/app-service.json b/deployment/azure/app-service.json new file mode 100644 index 0000000..356e5df --- /dev/null +++ b/deployment/azure/app-service.json @@ -0,0 +1,205 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appServicePlanName": { + "type": "string", + "defaultValue": "nlwebnet-plan", + "metadata": { + "description": "Name of the App Service Plan" + } + }, + "webAppName": { + "type": "string", + "defaultValue": "[concat('nlwebnet-', uniqueString(resourceGroup().id))]", + "metadata": { + "description": "Name of the Web App" + } + }, + "sku": { + "type": "string", + "defaultValue": "B1", + "allowedValues": [ + "B1", + "B2", + "B3", + "S1", + "S2", + "S3", + "P1v2", + "P2v2", + "P3v2" + ], + "metadata": { + "description": "SKU for the App Service Plan" + } + }, + "dockerImage": { + "type": "string", + "defaultValue": "nlwebnet-demo:latest", + "metadata": { + "description": "Docker image to deploy" + } + }, + "dockerRegistryUrl": { + "type": "string", + "defaultValue": "https://index.docker.io", + "metadata": { + "description": "Docker registry URL" + } + }, + "dockerRegistryUsername": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Docker registry username" + } + }, + "dockerRegistryPassword": { + "type": "securestring", + "defaultValue": "", + "metadata": { + "description": "Docker registry password" + } + }, + "azureOpenAIApiKey": { + "type": "securestring", + "metadata": { + "description": "Azure OpenAI API Key" + } + }, + "azureOpenAIEndpoint": { + "type": "string", + "metadata": { + "description": "Azure OpenAI Endpoint URL" + } + }, + "azureSearchApiKey": { + "type": "securestring", + "metadata": { + "description": "Azure Search API Key" + } + }, + "azureSearchServiceName": { + "type": "string", + "metadata": { + "description": "Azure Search Service Name" + } + } + }, + "variables": { + "location": "[resourceGroup().location]" + }, + "resources": [ + { + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2021-02-01", + "name": "[parameters('appServicePlanName')]", + "location": "[variables('location')]", + "kind": "linux", + "properties": { + "reserved": true + }, + "sku": { + "name": "[parameters('sku')]" + } + }, + { + "type": "Microsoft.Web/sites", + "apiVersion": "2021-02-01", + "name": "[parameters('webAppName')]", + "location": "[variables('location')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]" + ], + "kind": "app,linux,container", + "properties": { + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]", + "siteConfig": { + "linuxFxVersion": "[concat('DOCKER|', parameters('dockerImage'))]", + "appSettings": [ + { + "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE", + "value": "false" + }, + { + "name": "DOCKER_REGISTRY_SERVER_URL", + "value": "[parameters('dockerRegistryUrl')]" + }, + { + "name": "DOCKER_REGISTRY_SERVER_USERNAME", + "value": "[parameters('dockerRegistryUsername')]" + }, + { + "name": "DOCKER_REGISTRY_SERVER_PASSWORD", + "value": "[parameters('dockerRegistryPassword')]" + }, + { + "name": "ASPNETCORE_ENVIRONMENT", + "value": "Production" + }, + { + "name": "ASPNETCORE_URLS", + "value": "http://+:80" + }, + { + "name": "NLWebNet__DefaultMode", + "value": "List" + }, + { + "name": "NLWebNet__EnableStreaming", + "value": "true" + }, + { + "name": "NLWebNet__DefaultTimeoutSeconds", + "value": "30" + }, + { + "name": "NLWebNet__MaxResultsPerQuery", + "value": "50" + }, + { + "name": "AzureOpenAI__ApiKey", + "value": "[parameters('azureOpenAIApiKey')]" + }, + { + "name": "AzureOpenAI__Endpoint", + "value": "[parameters('azureOpenAIEndpoint')]" + }, + { + "name": "AzureOpenAI__DeploymentName", + "value": "gpt-4" + }, + { + "name": "AzureOpenAI__ApiVersion", + "value": "2024-02-01" + }, + { + "name": "AzureSearch__ApiKey", + "value": "[parameters('azureSearchApiKey')]" + }, + { + "name": "AzureSearch__ServiceName", + "value": "[parameters('azureSearchServiceName')]" + }, + { + "name": "AzureSearch__IndexName", + "value": "nlweb-index" + } + ], + "healthCheckPath": "/health" + } + } + } + ], + "outputs": { + "webAppURL": { + "type": "string", + "value": "[concat('https://', reference(resourceId('Microsoft.Web/sites', parameters('webAppName'))).defaultHostName)]" + }, + "webAppName": { + "type": "string", + "value": "[parameters('webAppName')]" + } + } +} \ No newline at end of file diff --git a/deployment/azure/container-apps-aspire.json b/deployment/azure/container-apps-aspire.json new file mode 100644 index 0000000..9f55868 --- /dev/null +++ b/deployment/azure/container-apps-aspire.json @@ -0,0 +1,179 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "containerAppName": { + "type": "string", + "defaultValue": "nlwebnet-demo-aspire", + "metadata": { + "description": "Name of the container app" + } + }, + "containerImage": { + "type": "string", + "defaultValue": "nlwebnet-demo:latest", + "metadata": { + "description": "Container image to deploy" + } + }, + "environmentName": { + "type": "string", + "defaultValue": "nlwebnet-aspire-env", + "metadata": { + "description": "Name of the Container Apps environment" + } + }, + "logAnalyticsWorkspace": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Log Analytics workspace for Aspire telemetry" + } + }, + "applicationInsightsConnectionString": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Application Insights connection string for Aspire observability" + } + } + }, + "resources": [ + { + "type": "Microsoft.App/managedEnvironments", + "apiVersion": "2023-05-01", + "name": "[parameters('environmentName')]", + "location": "[resourceGroup().location]", + "properties": { + "daprAIInstrumentationKey": "[if(not(empty(parameters('applicationInsightsConnectionString'))), parameters('applicationInsightsConnectionString'), null())]", + "appLogsConfiguration": { + "destination": "log-analytics", + "logAnalyticsConfiguration": { + "customerId": "[if(not(empty(parameters('logAnalyticsWorkspace'))), reference(parameters('logAnalyticsWorkspace'), '2020-08-01').customerId, null())]", + "sharedKey": "[if(not(empty(parameters('logAnalyticsWorkspace'))), listKeys(parameters('logAnalyticsWorkspace'), '2020-08-01').primarySharedKey, null())]" + } + } + } + }, + { + "type": "Microsoft.App/containerApps", + "apiVersion": "2023-05-01", + "name": "[parameters('containerAppName')]", + "location": "[resourceGroup().location]", + "dependsOn": [ + "[resourceId('Microsoft.App/managedEnvironments', parameters('environmentName'))]" + ], + "properties": { + "managedEnvironmentId": "[resourceId('Microsoft.App/managedEnvironments', parameters('environmentName'))]", + "configuration": { + "ingress": { + "external": true, + "targetPort": 8080, + "transport": "http", + "allowInsecure": false + }, + "secrets": [ + { + "name": "appinsights-connection-string", + "value": "[parameters('applicationInsightsConnectionString')]" + } + ] + }, + "template": { + "containers": [ + { + "name": "nlwebnet-demo", + "image": "[parameters('containerImage')]", + "env": [ + { + "name": "ASPNETCORE_ENVIRONMENT", + "value": "Production" + }, + { + "name": "ASPNETCORE_URLS", + "value": "http://+:8080" + }, + { + "name": "NLWebNet__DefaultMode", + "value": "List" + }, + { + "name": "NLWebNet__EnableStreaming", + "value": "true" + }, + { + "name": "NLWebNet__DefaultTimeoutSeconds", + "value": "30" + }, + { + "name": "NLWebNet__MaxResultsPerQuery", + "value": "50" + }, + { + "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", + "secretRef": "appinsights-connection-string" + }, + { + "name": "OTEL_SERVICE_NAME", + "value": "nlwebnet-demo" + }, + { + "name": "OTEL_RESOURCE_ATTRIBUTES", + "value": "service.name=nlwebnet-demo,service.version=1.0.0,deployment.environment=production" + } + ], + "resources": { + "cpu": 0.5, + "memory": "1Gi" + }, + "probes": [ + { + "type": "Liveness", + "httpGet": { + "path": "/alive", + "port": 8080 + }, + "initialDelaySeconds": 10, + "periodSeconds": 30 + }, + { + "type": "Readiness", + "httpGet": { + "path": "/health/ready", + "port": 8080 + }, + "initialDelaySeconds": 5, + "periodSeconds": 10 + } + ] + } + ], + "scale": { + "minReplicas": 1, + "maxReplicas": 10, + "rules": [ + { + "name": "http-rule", + "http": { + "metadata": { + "concurrentRequests": "100" + } + } + } + ] + } + } + } + } + ], + "outputs": { + "containerAppFQDN": { + "type": "string", + "value": "[reference(resourceId('Microsoft.App/containerApps', parameters('containerAppName'))).configuration.ingress.fqdn]" + }, + "containerAppUrl": { + "type": "string", + "value": "[concat('https://', reference(resourceId('Microsoft.App/containerApps', parameters('containerAppName'))).configuration.ingress.fqdn)]" + } + ] +} \ No newline at end of file diff --git a/deployment/azure/container-apps.json b/deployment/azure/container-apps.json new file mode 100644 index 0000000..b12ba33 --- /dev/null +++ b/deployment/azure/container-apps.json @@ -0,0 +1,225 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "containerAppName": { + "type": "string", + "defaultValue": "nlwebnet-demo", + "metadata": { + "description": "Name of the Container App" + } + }, + "containerEnvironmentName": { + "type": "string", + "defaultValue": "nlwebnet-env", + "metadata": { + "description": "Name of the Container App Environment" + } + }, + "containerImage": { + "type": "string", + "defaultValue": "nlwebnet-demo:latest", + "metadata": { + "description": "Container image to deploy" + } + }, + "azureOpenAIApiKey": { + "type": "securestring", + "metadata": { + "description": "Azure OpenAI API Key" + } + }, + "azureOpenAIEndpoint": { + "type": "string", + "metadata": { + "description": "Azure OpenAI Endpoint URL" + } + }, + "azureSearchApiKey": { + "type": "securestring", + "metadata": { + "description": "Azure Search API Key" + } + }, + "azureSearchServiceName": { + "type": "string", + "metadata": { + "description": "Azure Search Service Name" + } + } + }, + "variables": { + "location": "[resourceGroup().location]", + "containerRegistryName": "[concat('acr', uniqueString(resourceGroup().id))]", + "logAnalyticsWorkspaceName": "[concat('logs-', parameters('containerAppName'))]" + }, + "resources": [ + { + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2021-12-01-preview", + "name": "[variables('logAnalyticsWorkspaceName')]", + "location": "[variables('location')]", + "properties": { + "sku": { + "name": "PerGB2018" + }, + "retentionInDays": 30 + } + }, + { + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2021-09-01", + "name": "[variables('containerRegistryName')]", + "location": "[variables('location')]", + "sku": { + "name": "Basic" + }, + "properties": { + "adminUserEnabled": true + } + }, + { + "type": "Microsoft.App/managedEnvironments", + "apiVersion": "2022-03-01", + "name": "[parameters('containerEnvironmentName')]", + "location": "[variables('location')]", + "dependsOn": [ + "[resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsWorkspaceName'))]" + ], + "properties": { + "appLogsConfiguration": { + "destination": "log-analytics", + "logAnalyticsConfiguration": { + "customerId": "[reference(resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsWorkspaceName'))).customerId]", + "sharedKey": "[listKeys(resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsWorkspaceName')), '2021-12-01-preview').primarySharedKey]" + } + } + } + }, + { + "type": "Microsoft.App/containerApps", + "apiVersion": "2022-03-01", + "name": "[parameters('containerAppName')]", + "location": "[variables('location')]", + "dependsOn": [ + "[resourceId('Microsoft.App/managedEnvironments', parameters('containerEnvironmentName'))]" + ], + "properties": { + "managedEnvironmentId": "[resourceId('Microsoft.App/managedEnvironments', parameters('containerEnvironmentName'))]", + "configuration": { + "ingress": { + "external": true, + "targetPort": 8080, + "allowInsecure": false, + "traffic": [ + { + "weight": 100, + "latestRevision": true + } + ] + }, + "secrets": [ + { + "name": "azure-openai-api-key", + "value": "[parameters('azureOpenAIApiKey')]" + }, + { + "name": "azure-search-api-key", + "value": "[parameters('azureSearchApiKey')]" + } + ] + }, + "template": { + "containers": [ + { + "name": "nlwebnet-demo", + "image": "[parameters('containerImage')]", + "env": [ + { + "name": "ASPNETCORE_ENVIRONMENT", + "value": "Production" + }, + { + "name": "ASPNETCORE_URLS", + "value": "http://+:8080" + }, + { + "name": "NLWebNet__DefaultMode", + "value": "List" + }, + { + "name": "NLWebNet__EnableStreaming", + "value": "true" + }, + { + "name": "AzureOpenAI__ApiKey", + "secretRef": "azure-openai-api-key" + }, + { + "name": "AzureOpenAI__Endpoint", + "value": "[parameters('azureOpenAIEndpoint')]" + }, + { + "name": "AzureSearch__ApiKey", + "secretRef": "azure-search-api-key" + }, + { + "name": "AzureSearch__ServiceName", + "value": "[parameters('azureSearchServiceName')]" + } + ], + "resources": { + "cpu": 0.5, + "memory": "1Gi" + }, + "probes": [ + { + "type": "Liveness", + "httpGet": { + "path": "/health", + "port": 8080 + }, + "initialDelaySeconds": 30, + "periodSeconds": 30 + }, + { + "type": "Readiness", + "httpGet": { + "path": "/health", + "port": 8080 + }, + "initialDelaySeconds": 5, + "periodSeconds": 10 + } + ] + } + ], + "scale": { + "minReplicas": 1, + "maxReplicas": 10, + "rules": [ + { + "name": "http-scaling", + "http": { + "metadata": { + "concurrentRequests": "100" + } + } + } + ] + } + } + } + } + ], + "outputs": { + "containerAppFQDN": { + "type": "string", + "value": "[reference(resourceId('Microsoft.App/containerApps', parameters('containerAppName'))).configuration.ingress.fqdn]" + }, + "containerRegistryLoginServer": { + "type": "string", + "value": "[reference(resourceId('Microsoft.ContainerRegistry/registries', variables('containerRegistryName'))).loginServer]" + } + } +} \ No newline at end of file diff --git a/deployment/helm/nlwebnet-demo/Chart.yaml b/deployment/helm/nlwebnet-demo/Chart.yaml new file mode 100644 index 0000000..0f3a5d6 --- /dev/null +++ b/deployment/helm/nlwebnet-demo/Chart.yaml @@ -0,0 +1,17 @@ +apiVersion: v2 +name: nlwebnet-demo +description: A Helm chart for NLWebNet Demo Application +type: application +version: 0.1.0 +appVersion: "1.0.0" +home: https://github.com/jongalloway/NLWebNet +sources: + - https://github.com/jongalloway/NLWebNet +maintainers: + - name: NLWebNet Team + email: maintainer@example.com +keywords: + - nlweb + - ai + - blazor + - dotnet \ No newline at end of file diff --git a/deployment/helm/nlwebnet-demo/templates/_helpers.tpl b/deployment/helm/nlwebnet-demo/templates/_helpers.tpl new file mode 100644 index 0000000..5e87604 --- /dev/null +++ b/deployment/helm/nlwebnet-demo/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "nlwebnet-demo.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "nlwebnet-demo.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "nlwebnet-demo.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "nlwebnet-demo.labels" -}} +helm.sh/chart: {{ include "nlwebnet-demo.chart" . }} +{{ include "nlwebnet-demo.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "nlwebnet-demo.selectorLabels" -}} +app.kubernetes.io/name: {{ include "nlwebnet-demo.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "nlwebnet-demo.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "nlwebnet-demo.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/deployment/helm/nlwebnet-demo/templates/configmap.yaml b/deployment/helm/nlwebnet-demo/templates/configmap.yaml new file mode 100644 index 0000000..ef1869a --- /dev/null +++ b/deployment/helm/nlwebnet-demo/templates/configmap.yaml @@ -0,0 +1,28 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "nlwebnet-demo.fullname" . }}-config + labels: + {{- include "nlwebnet-demo.labels" . | nindent 4 }} +data: + # NLWebNet configuration + NLWebNet__DefaultMode: {{ .Values.config.nlwebnet.defaultMode | quote }} + NLWebNet__EnableStreaming: {{ .Values.config.nlwebnet.enableStreaming | quote }} + NLWebNet__DefaultTimeoutSeconds: {{ .Values.config.nlwebnet.defaultTimeoutSeconds | quote }} + NLWebNet__MaxResultsPerQuery: {{ .Values.config.nlwebnet.maxResultsPerQuery | quote }} + NLWebNet__EnableDetailedLogging: {{ .Values.config.nlwebnet.enableDetailedLogging | quote }} + NLWebNet__EnableCaching: {{ .Values.config.nlwebnet.enableCaching | quote }} + NLWebNet__CacheExpirationMinutes: {{ .Values.config.nlwebnet.cacheExpirationMinutes | quote }} + + # Azure OpenAI configuration + AzureOpenAI__Endpoint: {{ .Values.config.azureOpenAI.endpoint | quote }} + AzureOpenAI__DeploymentName: {{ .Values.config.azureOpenAI.deploymentName | quote }} + AzureOpenAI__ApiVersion: {{ .Values.config.azureOpenAI.apiVersion | quote }} + + # OpenAI configuration + OpenAI__Model: {{ .Values.config.openAI.model | quote }} + OpenAI__BaseUrl: {{ .Values.config.openAI.baseUrl | quote }} + + # Azure Search configuration + AzureSearch__ServiceName: {{ .Values.config.azureSearch.serviceName | quote }} + AzureSearch__IndexName: {{ .Values.config.azureSearch.indexName | quote }} \ No newline at end of file diff --git a/deployment/helm/nlwebnet-demo/templates/deployment.yaml b/deployment/helm/nlwebnet-demo/templates/deployment.yaml new file mode 100644 index 0000000..c48b759 --- /dev/null +++ b/deployment/helm/nlwebnet-demo/templates/deployment.yaml @@ -0,0 +1,85 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "nlwebnet-demo.fullname" . }} + labels: + {{- include "nlwebnet-demo.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "nlwebnet-demo.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "nlwebnet-demo.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "nlwebnet-demo.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.targetPort }} + protocol: TCP + env: + - name: ASPNETCORE_ENVIRONMENT + value: {{ .Values.config.aspnetcore.environment | quote }} + - name: ASPNETCORE_URLS + value: {{ .Values.config.aspnetcore.urls | quote }} + envFrom: + - configMapRef: + name: {{ include "nlwebnet-demo.fullname" . }}-config + - secretRef: + name: {{ include "nlwebnet-demo.fullname" . }}-secret + {{- if .Values.healthChecks.liveness.enabled }} + livenessProbe: + httpGet: + path: {{ .Values.healthChecks.liveness.path }} + port: http + initialDelaySeconds: {{ .Values.healthChecks.liveness.initialDelaySeconds }} + periodSeconds: {{ .Values.healthChecks.liveness.periodSeconds }} + timeoutSeconds: {{ .Values.healthChecks.liveness.timeoutSeconds }} + failureThreshold: {{ .Values.healthChecks.liveness.failureThreshold }} + {{- end }} + {{- if .Values.healthChecks.readiness.enabled }} + readinessProbe: + httpGet: + path: {{ .Values.healthChecks.readiness.path }} + port: http + initialDelaySeconds: {{ .Values.healthChecks.readiness.initialDelaySeconds }} + periodSeconds: {{ .Values.healthChecks.readiness.periodSeconds }} + timeoutSeconds: {{ .Values.healthChecks.readiness.timeoutSeconds }} + failureThreshold: {{ .Values.healthChecks.readiness.failureThreshold }} + {{- end }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} \ No newline at end of file diff --git a/deployment/helm/nlwebnet-demo/templates/hpa.yaml b/deployment/helm/nlwebnet-demo/templates/hpa.yaml new file mode 100644 index 0000000..ce56f00 --- /dev/null +++ b/deployment/helm/nlwebnet-demo/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "nlwebnet-demo.fullname" . }} + labels: + {{- include "nlwebnet-demo.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "nlwebnet-demo.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/deployment/helm/nlwebnet-demo/templates/ingress.yaml b/deployment/helm/nlwebnet-demo/templates/ingress.yaml new file mode 100644 index 0000000..72c08c2 --- /dev/null +++ b/deployment/helm/nlwebnet-demo/templates/ingress.yaml @@ -0,0 +1,59 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "nlwebnet-demo.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class")) }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "nlwebnet-demo.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/deployment/helm/nlwebnet-demo/templates/secret.yaml b/deployment/helm/nlwebnet-demo/templates/secret.yaml new file mode 100644 index 0000000..385631b --- /dev/null +++ b/deployment/helm/nlwebnet-demo/templates/secret.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "nlwebnet-demo.fullname" . }}-secret + labels: + {{- include "nlwebnet-demo.labels" . | nindent 4 }} +type: Opaque +data: + {{- if .Values.secrets.azureOpenAIApiKey }} + AzureOpenAI__ApiKey: {{ .Values.secrets.azureOpenAIApiKey | b64enc }} + {{- end }} + {{- if .Values.secrets.azureSearchApiKey }} + AzureSearch__ApiKey: {{ .Values.secrets.azureSearchApiKey | b64enc }} + {{- end }} + {{- if .Values.secrets.openAIApiKey }} + OpenAI__ApiKey: {{ .Values.secrets.openAIApiKey | b64enc }} + {{- end }} \ No newline at end of file diff --git a/deployment/helm/nlwebnet-demo/templates/service.yaml b/deployment/helm/nlwebnet-demo/templates/service.yaml new file mode 100644 index 0000000..17cd170 --- /dev/null +++ b/deployment/helm/nlwebnet-demo/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "nlwebnet-demo.fullname" . }} + labels: + {{- include "nlwebnet-demo.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "nlwebnet-demo.selectorLabels" . | nindent 4 }} \ No newline at end of file diff --git a/deployment/helm/nlwebnet-demo/values.yaml b/deployment/helm/nlwebnet-demo/values.yaml new file mode 100644 index 0000000..32dacec --- /dev/null +++ b/deployment/helm/nlwebnet-demo/values.yaml @@ -0,0 +1,139 @@ +# Default values for nlwebnet-demo. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 3 + +image: + repository: nlwebnet-demo + pullPolicy: IfNotPresent + tag: "latest" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: + fsGroup: 1001 + +securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + runAsNonRoot: true + runAsUser: 1001 + +service: + type: ClusterIP + port: 80 + targetPort: 8080 + +ingress: + enabled: true + className: "nginx" + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: "true" + cert-manager.io/cluster-issuer: "letsencrypt-prod" + hosts: + - host: nlwebnet-demo.example.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: nlwebnet-demo-tls + hosts: + - nlwebnet-demo.example.com + +resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 250m + memory: 256Mi + +autoscaling: + enabled: true + minReplicas: 1 + maxReplicas: 10 + targetCPUUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +# Application configuration +config: + aspnetcore: + environment: Production + urls: "http://+:8080" + + nlwebnet: + defaultMode: "List" + enableStreaming: true + defaultTimeoutSeconds: 30 + maxResultsPerQuery: 50 + enableDetailedLogging: false + enableCaching: true + cacheExpirationMinutes: 60 + + azureOpenAI: + endpoint: "" + deploymentName: "gpt-4" + apiVersion: "2024-02-01" + # apiKey should be provided via secrets + + openAI: + model: "gpt-4" + baseUrl: "https://api.openai.com/v1" + # apiKey should be provided via secrets + + azureSearch: + serviceName: "" + indexName: "nlweb-index" + # apiKey should be provided via secrets + +# Secrets configuration +secrets: + # Set these values during deployment or via external secret management + azureOpenAIApiKey: "" + azureSearchApiKey: "" + openAIApiKey: "" + +# Health checks +healthChecks: + liveness: + enabled: true + path: /health + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + + readiness: + enabled: true + path: /health + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + +# Monitoring +monitoring: + serviceMonitor: + enabled: false + interval: 30s + path: /metrics \ No newline at end of file diff --git a/deployment/kubernetes/configmap.yaml b/deployment/kubernetes/configmap.yaml new file mode 100644 index 0000000..8893ed7 --- /dev/null +++ b/deployment/kubernetes/configmap.yaml @@ -0,0 +1,24 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: nlwebnet-config +data: + # Azure OpenAI configuration + azure-openai-endpoint: "https://your-resource.openai.azure.com/" + azure-openai-deployment-name: "gpt-4" + azure-openai-api-version: "2024-02-01" + + # Azure Search configuration + azure-search-service-name: "your-search-service" + azure-search-index-name: "nlweb-index" + + # OpenAI configuration + openai-model: "gpt-4" + openai-base-url: "https://api.openai.com/v1" + + # Application settings + aspnetcore-environment: "Production" + nlwebnet-default-mode: "List" + nlwebnet-enable-streaming: "true" + nlwebnet-default-timeout-seconds: "30" + nlwebnet-max-results-per-query: "50" \ No newline at end of file diff --git a/deployment/kubernetes/deployment.yaml b/deployment/kubernetes/deployment.yaml new file mode 100644 index 0000000..52cd234 --- /dev/null +++ b/deployment/kubernetes/deployment.yaml @@ -0,0 +1,85 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nlwebnet-demo + labels: + app: nlwebnet-demo + version: v1 +spec: + replicas: 3 + selector: + matchLabels: + app: nlwebnet-demo + template: + metadata: + labels: + app: nlwebnet-demo + version: v1 + spec: + containers: + - name: nlwebnet-demo + image: nlwebnet-demo:latest + ports: + - containerPort: 8080 + name: http + env: + - name: ASPNETCORE_ENVIRONMENT + value: "Production" + - name: ASPNETCORE_URLS + value: "http://+:8080" + - name: NLWebNet__DefaultMode + value: "List" + - name: NLWebNet__EnableStreaming + value: "true" + # Azure OpenAI configuration (from secrets) + - name: AzureOpenAI__ApiKey + valueFrom: + secretKeyRef: + name: nlwebnet-secrets + key: azure-openai-api-key + - name: AzureOpenAI__Endpoint + valueFrom: + configMapKeyRef: + name: nlwebnet-config + key: azure-openai-endpoint + # Azure Search configuration (from secrets) + - name: AzureSearch__ApiKey + valueFrom: + secretKeyRef: + name: nlwebnet-secrets + key: azure-search-api-key + - name: AzureSearch__ServiceName + valueFrom: + configMapKeyRef: + name: nlwebnet-config + key: azure-search-service-name + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + runAsNonRoot: true + runAsUser: 1001 + securityContext: + fsGroup: 1001 \ No newline at end of file diff --git a/deployment/kubernetes/ingress.yaml b/deployment/kubernetes/ingress.yaml new file mode 100644 index 0000000..a09cbd8 --- /dev/null +++ b/deployment/kubernetes/ingress.yaml @@ -0,0 +1,24 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: nlwebnet-demo-ingress + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / + nginx.ingress.kubernetes.io/ssl-redirect: "true" + cert-manager.io/cluster-issuer: "letsencrypt-prod" +spec: + tls: + - hosts: + - nlwebnet-demo.example.com + secretName: nlwebnet-demo-tls + rules: + - host: nlwebnet-demo.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: nlwebnet-demo-service + port: + number: 80 \ No newline at end of file diff --git a/deployment/kubernetes/secrets-template.yaml b/deployment/kubernetes/secrets-template.yaml new file mode 100644 index 0000000..1621381 --- /dev/null +++ b/deployment/kubernetes/secrets-template.yaml @@ -0,0 +1,15 @@ +# This is a template file for Kubernetes secrets +# In production, create secrets using kubectl or your secret management system +# kubectl create secret generic nlwebnet-secrets --from-literal=azure-openai-api-key=your-key + +apiVersion: v1 +kind: Secret +metadata: + name: nlwebnet-secrets +type: Opaque +data: + # Base64 encoded values (replace with actual base64 encoded secrets) + # To encode: echo -n "your-secret-value" | base64 + azure-openai-api-key: eW91ci1henVyZS1vcGVuYWktYXBpLWtleQ== # "your-azure-openai-api-key" + azure-search-api-key: eW91ci1henVyZS1zZWFyY2gtYXBpLWtleQ== # "your-azure-search-api-key" + openai-api-key: eW91ci1vcGVuYWktYXBpLWtleQ== # "your-openai-api-key" \ No newline at end of file diff --git a/deployment/kubernetes/service.yaml b/deployment/kubernetes/service.yaml new file mode 100644 index 0000000..05e7e12 --- /dev/null +++ b/deployment/kubernetes/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: nlwebnet-demo-service + labels: + app: nlwebnet-demo +spec: + selector: + app: nlwebnet-demo + ports: + - name: http + port: 80 + targetPort: 8080 + protocol: TCP + type: ClusterIP \ No newline at end of file diff --git a/deployment/scripts/docker-build-and-test.sh b/deployment/scripts/docker-build-and-test.sh new file mode 100755 index 0000000..5019aad --- /dev/null +++ b/deployment/scripts/docker-build-and-test.sh @@ -0,0 +1,61 @@ +#!/bin/bash +set -e + +# Docker Build and Test Script for NLWebNet Demo +# This script builds the Docker image and runs basic validation tests + +echo "🐳 Building NLWebNet Docker Image..." + +# Build the image +docker build -t nlwebnet-demo:test . + +echo "βœ… Docker image built successfully" + +# Test the image by running it temporarily +echo "πŸ§ͺ Testing Docker image..." + +# Start container in background +CONTAINER_ID=$(docker run -d -p 8081:8080 \ + -e ASPNETCORE_ENVIRONMENT=Production \ + -e NLWebNet__DefaultMode=List \ + -e NLWebNet__EnableStreaming=true \ + nlwebnet-demo:test) + +echo "Container started with ID: $CONTAINER_ID" + +# Wait for container to be ready +echo "⏳ Waiting for container to start..." +sleep 10 + +# Test health endpoint +echo "πŸ₯ Testing health endpoint..." +if curl -f http://localhost:8081/health; then + echo "βœ… Health check passed" +else + echo "❌ Health check failed" + docker logs $CONTAINER_ID + docker stop $CONTAINER_ID + docker rm $CONTAINER_ID + exit 1 +fi + +# Test main application endpoint +echo "🌐 Testing main application..." +if curl -f http://localhost:8081/ > /dev/null 2>&1; then + echo "βœ… Main application responds" +else + echo "⚠️ Main application test inconclusive (may require additional setup)" +fi + +# Cleanup +echo "🧹 Cleaning up..." +docker stop $CONTAINER_ID +docker rm $CONTAINER_ID + +echo "πŸŽ‰ All tests passed! Docker image is ready for deployment." +echo "πŸ“¦ Tagged as: nlwebnet-demo:test" +echo "" +echo "Next steps:" +echo "- Tag for your registry: docker tag nlwebnet-demo:test your-registry.azurecr.io/nlwebnet-demo:latest" +echo "- Push to registry: docker push your-registry.azurecr.io/nlwebnet-demo:latest" +echo "- Deploy using Kubernetes, Azure Container Apps, or Docker Compose" \ No newline at end of file diff --git a/doc/aspire-integration.md b/doc/aspire-integration.md new file mode 100644 index 0000000..7be962a --- /dev/null +++ b/doc/aspire-integration.md @@ -0,0 +1,205 @@ +# .NET Aspire Integration for NLWebNet + +This document describes how to use .NET Aspire as the preferred containerization and orchestration approach for the NLWebNet demo application. + +## Overview + +.NET Aspire is Microsoft's opinionated stack for building observable, production-ready cloud-native applications. It provides: + +- **Service Discovery**: Automatic service location and communication +- **Observability**: Built-in telemetry, metrics, and health checks +- **Resilience**: Circuit breakers, retries, and timeout policies +- **Configuration**: Centralized configuration management +- **Local Development**: Aspire dashboard for debugging and monitoring + +## Getting Started + +### Prerequisites + +- .NET 8.0 SDK or later +- .NET Aspire workload: `dotnet workload install aspire` + +### Running with Aspire + +1. **Start the Aspire AppHost** (recommended for development): + ```bash + cd demo-apphost + dotnet run + ``` + +2. **Access the Aspire Dashboard** at `https://localhost:15888` + - View application health and metrics + - Monitor telemetry and logs + - Debug service communication + +3. **Access the NLWebNet Demo** at `http://localhost:8080` + +### Project Structure + +``` +β”œβ”€β”€ demo-apphost/ # Aspire orchestration host +β”‚ β”œβ”€β”€ Program.cs # Application composition +β”‚ └── NLWebNet.Demo.AppHost.csproj +β”œβ”€β”€ demo/ # NLWebNet demo app +β”‚ β”œβ”€β”€ Program.cs # Aspire service defaults integration +β”‚ └── NLWebNet.Demo.csproj +└── shared/ServiceDefaults/ # Shared Aspire configurations + β”œβ”€β”€ Extensions.cs # Service defaults implementation + └── ServiceDefaults.csproj +``` + +## Key Features + +### Service Defaults + +The `ServiceDefaults` project provides common functionality: + +- **OpenTelemetry**: Distributed tracing and metrics +- **Health Checks**: Application health monitoring +- **Service Discovery**: Automatic service location +- **Resilience**: HTTP retry policies and circuit breakers + +### Observability + +Aspire automatically instruments the application with: + +- **Distributed Tracing**: Request flow across services +- **Metrics**: Performance and usage statistics +- **Logging**: Structured application logs +- **Health Checks**: Application and dependency status + +### Development Experience + +- **Hot Reload**: Automatic restart on code changes +- **Dashboard**: Visual monitoring and debugging +- **Service Map**: Visualize service dependencies +- **Resource Management**: Automatic service lifecycle + +## Deployment Options + +### Local Development + +```bash +# Run with Aspire orchestration +cd demo-apphost +dotnet run + +# Run standalone (without Aspire dashboard) +cd demo +dotnet run +``` + +### Container Deployment + +The demo app can be containerized while maintaining Aspire benefits: + +```dockerfile +# The existing Dockerfile works with Aspire-enabled apps +docker build -t nlwebnet-demo . +docker run -p 8080:8080 nlwebnet-demo +``` + +### Cloud Deployment + +Aspire apps can be deployed to: + +- **Azure Container Apps**: Native Aspire support +- **Kubernetes**: Using Aspire manifest generation +- **Docker Compose**: Generated from Aspire configuration + +## Configuration + +### Environment Variables + +```bash +# Application configuration +NLWebNet__DefaultMode=List +NLWebNet__EnableStreaming=true +NLWebNet__DefaultTimeoutSeconds=30 +NLWebNet__MaxResultsPerQuery=50 + +# OpenTelemetry configuration +OTEL_EXPORTER_OTLP_ENDPOINT=https://your-otlp-endpoint +OTEL_SERVICE_NAME=nlwebnet-demo +``` + +### Health Checks + +Aspire automatically configures health check endpoints: + +- `/health` - Overall application health +- `/alive` - Liveness probe +- `/health/ready` - Readiness probe + +## Advantages over Traditional Docker + +### Development Experience +- **Integrated Dashboard**: Visual monitoring and debugging +- **Service Discovery**: No manual endpoint configuration +- **Automatic Restart**: Hot reload support +- **Centralized Logging**: All services in one view + +### Production Benefits +- **Built-in Observability**: No additional instrumentation needed +- **Standardized Patterns**: Consistent health checks and metrics +- **Resilience**: Automatic retry and circuit breaker patterns +- **Configuration Management**: Environment-specific settings + +### Operational Excellence +- **Health Monitoring**: Comprehensive health check strategy +- **Performance Metrics**: Built-in performance monitoring +- **Distributed Tracing**: Request flow visualization +- **Resource Management**: Automatic resource lifecycle + +## Migration from Docker Compose + +Aspire can replace Docker Compose for local development: + +**Before (docker-compose.yml):** +```yaml +services: + nlwebnet-demo: + build: . + ports: + - "8080:8080" + environment: + - ASPNETCORE_ENVIRONMENT=Development +``` + +**After (AppHost Program.cs):** +```csharp +var nlwebnetDemo = builder.AddProject("nlwebnet-demo", "../demo/NLWebNet.Demo.csproj") + .WithHttpEndpoint(port: 8080, name: "http") + .WithEnvironment("ASPNETCORE_ENVIRONMENT", builder.Environment.EnvironmentName); +``` + +## Troubleshooting + +### Common Issues + +1. **Dashboard not accessible**: Ensure no firewall blocking port 15888 +2. **Service discovery failing**: Check project references in AppHost +3. **Health checks failing**: Verify health endpoint implementation + +### Debugging + +Use the Aspire dashboard to: +- View service logs in real-time +- Monitor health check status +- Trace request flows +- Analyze performance metrics + +## Best Practices + +1. **Use Service Defaults**: Always reference ServiceDefaults project +2. **Environment Configuration**: Use environment-specific settings +3. **Health Checks**: Implement meaningful health checks +4. **Observability**: Leverage built-in telemetry +5. **Resource Naming**: Use consistent naming conventions + +## Next Steps + +- Explore multi-service scenarios with databases and message queues +- Configure production observability with Azure Monitor or Jaeger +- Implement custom health checks for external dependencies +- Set up continuous deployment with Aspire manifest generation \ No newline at end of file diff --git a/doc/deployment-guide.md b/doc/deployment-guide.md new file mode 100644 index 0000000..e24ebd1 --- /dev/null +++ b/doc/deployment-guide.md @@ -0,0 +1,368 @@ +# NLWebNet Deployment Guide + +This guide provides comprehensive instructions for deploying the NLWebNet demo application across various platforms and environments. + +## Table of Contents + +- [Prerequisites](#prerequisites) +- [.NET Aspire Deployment (Recommended)](#net-aspire-deployment-recommended) +- [Docker Deployment](#docker-deployment) +- [Kubernetes Deployment](#kubernetes-deployment) +- [Azure Deployment](#azure-deployment) +- [Helm Deployment](#helm-deployment) +- [Environment Configuration](#environment-configuration) +- [Monitoring and Health Checks](#monitoring-and-health-checks) + +## Prerequisites + +- .NET 8.0 SDK or later +- .NET Aspire workload: `dotnet workload install aspire` +- Docker and Docker Compose (for containerization) +- Kubernetes cluster (for K8s deployment) +- Azure CLI (for Azure deployment) +- Helm 3.x (for Helm deployment) + +## .NET Aspire Deployment (Recommended) + +**.NET Aspire is the recommended approach for .NET developers** building cloud-native applications. It provides built-in observability, service discovery, and production-ready patterns. + +### Quick Start + +```bash +# Install Aspire workload (one-time setup) +dotnet workload install aspire + +# Run the application with Aspire orchestration +cd demo-apphost +dotnet run +``` + +### Features + +- **Aspire Dashboard**: Visual monitoring at `https://localhost:15888` +- **Built-in Observability**: Distributed tracing, metrics, and health checks +- **Service Discovery**: Automatic service location and communication +- **Development Experience**: Hot reload, centralized logging, and debugging + +### Benefits over Traditional Containerization + +1. **Integrated Development**: Built-in dashboard and debugging tools +2. **Production Patterns**: Standardized health checks, resilience, and telemetry +3. **Cloud-Native Ready**: Designed for modern distributed applications +4. **Microsoft Ecosystem**: First-class support in Azure and Visual Studio + +πŸ“– **[Complete Aspire Integration Guide](aspire-integration.md)** + +## Docker Deployment + +### Building the Container Image + +```bash +# Build the Docker image +docker build -t nlwebnet-demo:latest . + +# Run locally for testing +docker run -p 8080:8080 \ + -e ASPNETCORE_ENVIRONMENT=Production \ + -e NLWebNet__DefaultMode=List \ + -e NLWebNet__EnableStreaming=true \ + nlwebnet-demo:latest +``` + +### Using Docker Compose + +#### Development Environment +```bash +# Start development environment with hot reload +docker-compose up --build +``` + +#### Production Environment +```bash +# Start production environment +docker-compose --profile production up -d +``` + +### Environment Variables + +Set these environment variables for production deployment: + +```bash +# Application settings +ASPNETCORE_ENVIRONMENT=Production +ASPNETCORE_URLS=http://+:8080 +NLWebNet__DefaultMode=List +NLWebNet__EnableStreaming=true +NLWebNet__DefaultTimeoutSeconds=30 +NLWebNet__MaxResultsPerQuery=50 + +# Azure OpenAI (if using) +AzureOpenAI__ApiKey=your-api-key +AzureOpenAI__Endpoint=https://your-resource.openai.azure.com/ +AzureOpenAI__DeploymentName=gpt-4 +AzureOpenAI__ApiVersion=2024-02-01 + +# Azure Search (if using) +AzureSearch__ApiKey=your-search-api-key +AzureSearch__ServiceName=your-search-service +AzureSearch__IndexName=nlweb-index +``` + +## Kubernetes Deployment + +### Basic Kubernetes Deployment + +1. **Apply configuration:** +```bash +kubectl apply -f deployment/kubernetes/configmap.yaml +kubectl apply -f deployment/kubernetes/secrets-template.yaml # Update with real secrets +kubectl apply -f deployment/kubernetes/deployment.yaml +kubectl apply -f deployment/kubernetes/service.yaml +kubectl apply -f deployment/kubernetes/ingress.yaml +``` + +2. **Update secrets with real values:** +```bash +# Create secrets manually with real values +kubectl create secret generic nlwebnet-secrets \ + --from-literal=azure-openai-api-key=your-actual-key \ + --from-literal=azure-search-api-key=your-actual-search-key \ + --from-literal=openai-api-key=your-actual-openai-key +``` + +3. **Verify deployment:** +```bash +kubectl get pods -l app=nlwebnet-demo +kubectl get svc nlwebnet-demo-service +kubectl logs -l app=nlwebnet-demo +``` + +### Scaling + +```bash +# Scale horizontally +kubectl scale deployment nlwebnet-demo --replicas=5 + +# Check status +kubectl get hpa +``` + +## Azure Deployment + +### Azure Container Apps + +1. **Deploy using ARM template:** +```bash +az group create --name nlwebnet-rg --location eastus + +az deployment group create \ + --resource-group nlwebnet-rg \ + --template-file deployment/azure/container-apps.json \ + --parameters \ + containerAppName=nlwebnet-demo \ + containerImage=your-registry.azurecr.io/nlwebnet-demo:latest \ + azureOpenAIApiKey=your-api-key \ + azureOpenAIEndpoint=https://your-resource.openai.azure.com/ \ + azureSearchApiKey=your-search-key \ + azureSearchServiceName=your-search-service +``` + +2. **Get the application URL:** +```bash +az containerapp show \ + --name nlwebnet-demo \ + --resource-group nlwebnet-rg \ + --query properties.configuration.ingress.fqdn +``` + +### Azure App Service + +1. **Deploy using ARM template:** +```bash +az deployment group create \ + --resource-group nlwebnet-rg \ + --template-file deployment/azure/app-service.json \ + --parameters \ + webAppName=nlwebnet-demo-app \ + dockerImage=your-registry.azurecr.io/nlwebnet-demo:latest \ + azureOpenAIApiKey=your-api-key \ + azureOpenAIEndpoint=https://your-resource.openai.azure.com/ \ + azureSearchApiKey=your-search-key \ + azureSearchServiceName=your-search-service +``` + +### Azure Container Registry + +```bash +# Create ACR +az acr create --name yourregistry --resource-group nlwebnet-rg --sku Basic + +# Build and push image +az acr build --registry yourregistry --image nlwebnet-demo:latest . +``` + +## Helm Deployment + +### Installing with Helm + +1. **Install the chart:** +```bash +helm install nlwebnet-demo ./deployment/helm/nlwebnet-demo \ + --set image.repository=your-registry.azurecr.io/nlwebnet-demo \ + --set image.tag=latest \ + --set config.azureOpenAI.endpoint=https://your-resource.openai.azure.com/ \ + --set config.azureSearch.serviceName=your-search-service \ + --set secrets.azureOpenAIApiKey=your-api-key \ + --set secrets.azureSearchApiKey=your-search-key \ + --set ingress.hosts[0].host=nlwebnet-demo.yourdomain.com +``` + +2. **Upgrade deployment:** +```bash +helm upgrade nlwebnet-demo ./deployment/helm/nlwebnet-demo \ + --set image.tag=v1.1.0 +``` + +3. **Uninstall:** +```bash +helm uninstall nlwebnet-demo +``` + +### Customizing Helm Values + +Create a custom `values.yaml` file: + +```yaml +# custom-values.yaml +image: + repository: your-registry.azurecr.io/nlwebnet-demo + tag: v1.0.0 + +ingress: + enabled: true + hosts: + - host: nlwebnet-demo.yourdomain.com + paths: + - path: / + pathType: Prefix + +config: + azureOpenAI: + endpoint: https://your-resource.openai.azure.com/ + azureSearch: + serviceName: your-search-service + +secrets: + azureOpenAIApiKey: your-api-key + azureSearchApiKey: your-search-key + +resources: + limits: + cpu: 1000m + memory: 1Gi + requests: + cpu: 500m + memory: 512Mi + +autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 20 +``` + +Deploy with custom values: +```bash +helm install nlwebnet-demo ./deployment/helm/nlwebnet-demo -f custom-values.yaml +``` + +## Environment Configuration + +### Development +- Use Docker Compose with override for hot reload +- Enable detailed logging +- Use development certificates + +### Staging +- Production-like configuration +- Reduced resource limits +- Automated testing integration + +### Production +- Enable auto-scaling +- Configure monitoring and alerting +- Use proper secrets management +- Enable HTTPS and security headers + +## Monitoring and Health Checks + +### Health Endpoint + +The application exposes a health check endpoint at `/health`: + +```bash +curl http://your-app-url/health +# Response: {"status":"healthy","timestamp":"2024-01-01T12:00:00.000Z"} +``` + +### Kubernetes Health Checks + +The application includes both liveness and readiness probes: + +- **Liveness Probe:** `/health` every 30 seconds +- **Readiness Probe:** `/health` every 10 seconds + +### Monitoring Integration + +For production deployments, integrate with: + +- **Azure Application Insights** (for Azure deployments) +- **Prometheus + Grafana** (for Kubernetes) +- **Container Insights** (for Azure Container Apps) + +### Logging + +Application logs include: +- Request/response logging +- Health check status +- AI service integration logs +- Performance metrics + +Access logs: +```bash +# Kubernetes +kubectl logs -l app=nlwebnet-demo + +# Docker +docker logs container-name + +# Azure Container Apps +az containerapp logs show --name nlwebnet-demo --resource-group nlwebnet-rg +``` + +## Troubleshooting + +### Common Issues + +1. **Health Check Failures:** + - Verify the application is listening on port 8080 + - Check environment variables are correctly set + - Ensure dependencies (AI services) are accessible + +2. **Image Build Issues:** + - Verify .NET 9.0 SDK availability + - Check network connectivity for NuGet restore + - Review .dockerignore for excluded files + +3. **Deployment Failures:** + - Validate Kubernetes manifests: `kubectl apply --dry-run=client` + - Check resource quotas and limits + - Verify secrets and config maps are applied + +### Getting Help + +- Check application logs for detailed error messages +- Verify configuration values match your environment +- Test connectivity to external dependencies (AI services, search) +- Use health endpoints to diagnose issues + +For additional support, refer to the main README.md file and the project's GitHub issues. \ No newline at end of file diff --git a/doc/todo.md b/doc/todo.md index 02a461c..ccb7390 100644 --- a/doc/todo.md +++ b/doc/todo.md @@ -547,11 +547,17 @@ The NLWebNet library is now successfully deployed as a production-ready NuGet pa - **Automated Publishing**: Tag-based releases automatically publish to NuGet.org - **Quality Assurance**: Comprehensive CI/CD pipeline with multiple validation stages - **Developer Experience**: Full IntelliSense support and extension method accessibility -- [ ] Demo app deployment: - - [ ] Docker containerization - - [ ] Azure App Service deployment configuration - - [ ] Environment-specific configurations -- [ ] **OPEN QUESTION**: What are the deployment target requirements? +- [x] Demo app deployment: + - [x] Docker containerization + - [x] Azure App Service deployment configuration + - [x] Azure Container Apps deployment configuration + - [x] Environment-specific configurations + - [x] Kubernetes manifests (Deployment, Service, Ingress, ConfigMap, Secrets) + - [x] Helm chart for production deployment + - [x] Health check integration (/health endpoint) + - [x] Docker Compose for local development + - [x] Deployment documentation and scripts +- [x] **DEPLOYMENT MILESTONE ACHIEVED**: Comprehensive containerization and deployment strategy implemented ### Open Questions to Resolve diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000..0b07ef5 --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,22 @@ +# Docker Compose override for development +# This file is automatically loaded by docker-compose and overrides settings for development + +version: '3.8' + +services: + nlwebnet-demo: + build: + target: build # Use build stage for development with SDK + environment: + - ASPNETCORE_ENVIRONMENT=Development + - DOTNET_USE_POLLING_FILE_WATCHER=true + - DOTNET_RUNNING_IN_CONTAINER=true + volumes: + # Enable hot reload for development + - ./demo:/src/demo + - ./src:/src/src + - /src/demo/bin + - /src/demo/obj + ports: + - "5037:8080" # Use familiar port from local development + command: ["dotnet", "watch", "run", "--project", "/src/demo/NLWebNet.Demo.csproj", "--urls", "http://+:8080"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0a346dc --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,58 @@ +# Docker Compose for NLWebNet local development +version: '3.8' + +services: + nlwebnet-demo: + build: + context: . + dockerfile: Dockerfile + target: runtime + ports: + - "8080:8080" + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=http://+:8080 + - NLWebNet__DefaultMode=List + - NLWebNet__EnableStreaming=true + - NLWebNet__DefaultTimeoutSeconds=30 + - NLWebNet__MaxResultsPerQuery=50 + volumes: + # Mount for development hot reload (commented out for production-like container) + # - ./demo:/app + # - ./src:/src + - nlwebnet_data:/app/data + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + restart: unless-stopped + networks: + - nlwebnet + + # Optional: Add nginx reverse proxy for production-like setup + nginx: + image: nginx:alpine + ports: + - "80:80" + - "443:443" + volumes: + - ./deployment/nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./deployment/nginx/ssl:/etc/nginx/ssl:ro + depends_on: + - nlwebnet-demo + networks: + - nlwebnet + profiles: + - production + +volumes: + nlwebnet_data: + +networks: + nlwebnet: + driver: bridge + +# Development override file: docker-compose.override.yml +# This file is automatically loaded by docker-compose for development settings \ No newline at end of file diff --git a/shared/ServiceDefaults/Extensions.cs b/shared/ServiceDefaults/Extensions.cs new file mode 100644 index 0000000..ec0b2c8 --- /dev/null +++ b/shared/ServiceDefaults/Extensions.cs @@ -0,0 +1,113 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder) + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + return builder; + } + + public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddAspNetCoreInstrumentation() + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry() + .WithTracing(tracing => tracing.AddOtlpExporter()) + .WithMetrics(metrics => metrics.AddOtlpExporter()); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Configure health check endpoints + app.MapHealthChecks("/health"); + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health/ready", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("ready") + }); + + return app; + } +} \ No newline at end of file diff --git a/shared/ServiceDefaults/ServiceDefaults.csproj b/shared/ServiceDefaults/ServiceDefaults.csproj new file mode 100644 index 0000000..3f8fe3d --- /dev/null +++ b/shared/ServiceDefaults/ServiceDefaults.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + enable + enable + false + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/NLWebNet/NLWebNet.csproj b/src/NLWebNet/NLWebNet.csproj index 39e5d6b..8c410e4 100644 --- a/src/NLWebNet/NLWebNet.csproj +++ b/src/NLWebNet/NLWebNet.csproj @@ -1,6 +1,6 @@ ο»Ώ - net9.0 + net8.0 enable enable true NLWebNet NLWebNet - .NET NLWeb Protocol Library @@ -31,11 +31,11 @@ true - - - - - + + + + + diff --git a/tests/NLWebNet.Tests/NLWebNet.Tests.csproj b/tests/NLWebNet.Tests/NLWebNet.Tests.csproj index adfc8b2..1db482d 100644 --- a/tests/NLWebNet.Tests/NLWebNet.Tests.csproj +++ b/tests/NLWebNet.Tests/NLWebNet.Tests.csproj @@ -1,7 +1,7 @@ ο»Ώ - net9.0 + net8.0 latest enable enable @@ -9,7 +9,7 @@ - +