diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..71fbc44 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,82 @@ +# Docker ignore file for NLWebNet +# Optimizes Docker build context by excluding unnecessary files + +# Git +.git +.gitignore +.gitattributes + +# GitHub workflows and metadata +.github/ +copilot-setup-steps.yml + +# IDE and editor files +.vs/ +.vscode/ +*.user +*.suo +*.userosscache +*.sln.docstates +.idea/ + +# Build outputs +**/bin/ +**/obj/ +**/out/ +**/.vs/ + +# Test results +TestResults/ +test-results*.xml +coverage*.xml +*.coverage +*.coveragexml + +# NuGet +packages/ +*.nupkg +*.snupkg +**/packages/* +!**/packages/build/ + +# Runtime logs +logs/ +*.log + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Temporary files +tmp/ +temp/ +*.tmp +*.temp + +# Documentation (not needed in container) +README.md +doc/ +*.md + +# Scripts (not needed in runtime) +scripts/ + +# License files +LICENSE* +COPYING* + +# Docker files (avoid recursion) +Dockerfile* +docker-compose* +.dockerignore + +# Environment specific files (use volume mounts instead) +appsettings.*.json +!appsettings.json +*.env +.env* \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a6c414a..28a09d3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -266,3 +266,43 @@ jobs: tag_name: ${{ github.ref_name }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + docker-build: + runs-on: ubuntu-latest + needs: [check-changes, build] + if: needs.check-changes.outputs.should-skip != 'true' && (github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))) + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image (test build) + run: | + echo "๐Ÿณ Building Docker image for testing..." + docker build -t nlwebnet-demo:test . + echo "โœ… Docker build successful" + + - name: Test Docker image + run: | + echo "๐Ÿงช Testing Docker image..." + # Start container in background + docker run -d --name nlwebnet-test -p 8080:8080 nlwebnet-demo:test + + # Wait for container to start + sleep 10 + + # Test health endpoint + if curl -f http://localhost:8080/health; then + echo "โœ… Health check passed" + else + echo "โŒ Health check failed" + docker logs nlwebnet-test + exit 1 + fi + + # Cleanup + docker stop nlwebnet-test + docker rm nlwebnet-test diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e6bd409 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,60 @@ +# Multi-stage Dockerfile for NLWebNet Demo Application +# Based on .NET 9 runtime and optimized for production deployment + +# Build stage +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +WORKDIR /src + +# Copy solution file and project files for dependency resolution +COPY ["NLWebNet.sln", "./"] +COPY ["src/NLWebNet/NLWebNet.csproj", "src/NLWebNet/"] +COPY ["samples/Demo/NLWebNet.Demo.csproj", "samples/Demo/"] +COPY ["samples/AspireHost/NLWebNet.AspireHost.csproj", "samples/AspireHost/"] +COPY ["tests/NLWebNet.Tests/NLWebNet.Tests.csproj", "tests/NLWebNet.Tests/"] + +# Restore dependencies +RUN dotnet restore "samples/Demo/NLWebNet.Demo.csproj" + +# Copy source code +COPY . . + +# Build the demo application +WORKDIR "/src/samples/Demo" +RUN dotnet build "NLWebNet.Demo.csproj" -c Release -o /app/build + +# Publish stage +FROM build AS publish +RUN dotnet publish "NLWebNet.Demo.csproj" -c Release -o /app/publish /p:UseAppHost=false + +# Runtime stage +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final + +# Create a non-root user for security +RUN groupadd -r nlwebnet && useradd -r -g nlwebnet nlwebnet + +# Set working directory +WORKDIR /app + +# Copy published application +COPY --from=publish /app/publish . + +# Create directory for logs and ensure proper permissions +RUN mkdir -p /app/logs && chown -R nlwebnet:nlwebnet /app + +# Switch to non-root user +USER nlwebnet + +# Configure ASP.NET Core +ENV ASPNETCORE_ENVIRONMENT=Production +ENV ASPNETCORE_URLS=http://+:8080 +ENV ASPNETCORE_HTTP_PORTS=8080 + +# 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 + +# Entry point +ENTRYPOINT ["dotnet", "NLWebNet.Demo.dll"] \ No newline at end of file diff --git a/README.md b/README.md index 7a68530..5977e3d 100644 --- a/README.md +++ b/README.md @@ -402,6 +402,41 @@ builder.Services.Configure( builder.Configuration.GetSection("AzureSearch")); ``` +## ๐Ÿš€ Deployment + +NLWebNet supports multiple deployment options for different environments: + +### ๐Ÿณ Docker Deployment +```bash +# Quick start with Docker Compose +git clone https://github.com/jongalloway/NLWebNet.git +cd NLWebNet +docker-compose up --build +``` + +### โ˜๏ธ Azure Cloud Deployment +```bash +# Deploy to Azure Container Apps +./scripts/deploy/deploy-azure.sh -g myResourceGroup -t container-apps + +# Deploy to Azure App Service +./scripts/deploy/deploy-azure.sh -g myResourceGroup -t app-service +``` + +### โš™๏ธ Kubernetes Deployment +```bash +# Deploy to any Kubernetes cluster +kubectl apply -f k8s/ +``` + +### ๐Ÿ“ฆ Container Registry +Pre-built images available soon. For now, build locally: +```bash +./scripts/deploy/build-docker.sh -t latest +``` + +๐Ÿ“– **[Complete Deployment Guide](doc/deployment/README.md)** - Comprehensive instructions for all deployment scenarios. + ## ๐Ÿ› ๏ธ Development Status This is a **proof of concept implementation** of the NLWeb protocol, available as an **alpha prerelease package** for testing and evaluation purposes only. diff --git a/deploy/azure/app-service.bicep b/deploy/azure/app-service.bicep new file mode 100644 index 0000000..176e28f --- /dev/null +++ b/deploy/azure/app-service.bicep @@ -0,0 +1,224 @@ +@description('Application name used for resource naming') +param appName string = 'nlwebnet' + +@description('Location for all resources') +param location string = resourceGroup().location + +@description('Environment (dev, staging, prod)') +@allowed(['dev', 'staging', 'prod']) +param environment string = 'dev' + +@description('App Service Plan SKU') +@allowed(['F1', 'D1', 'B1', 'B2', 'B3', 'S1', 'S2', 'S3', 'P1V2', 'P2V2', 'P3V2', 'P1V3', 'P2V3', 'P3V3']) +param appServicePlanSku string = 'B1' + +@description('Container image name and tag') +param containerImage string = 'nlwebnet-demo:latest' + +@description('Container registry server') +param containerRegistryServer string = '' + +@description('Container registry username') +@secure() +param containerRegistryUsername string = '' + +@description('Container registry password') +@secure() +param containerRegistryPassword string = '' + +@description('Azure OpenAI API Key') +@secure() +param azureOpenAIApiKey string = '' + +@description('Azure OpenAI Endpoint') +param azureOpenAIEndpoint string = '' + +@description('Azure OpenAI Deployment Name') +param azureOpenAIDeploymentName string = 'gpt-4' + +@description('Azure Search API Key') +@secure() +param azureSearchApiKey string = '' + +@description('Azure Search Service Name') +param azureSearchServiceName string = '' + +@description('Azure Search Index Name') +param azureSearchIndexName string = 'nlweb-index' + +var appServicePlanName = '${appName}-plan-${environment}' +var appServiceName = '${appName}-app-${environment}' +var appInsightsName = '${appName}-insights-${environment}' +var logAnalyticsWorkspaceName = '${appName}-logs-${environment}' + +// Log Analytics Workspace +resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { + name: logAnalyticsWorkspaceName + location: location + properties: { + sku: { + name: 'PerGB2018' + } + retentionInDays: 30 + features: { + searchVersion: 1 + legacy: 0 + enableLogAccessUsingOnlyResourcePermissions: true + } + } +} + +// Application Insights +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { + name: appInsightsName + location: location + kind: 'web' + properties: { + Application_Type: 'web' + WorkspaceResourceId: logAnalyticsWorkspace.id + } +} + +// App Service Plan +resource appServicePlan 'Microsoft.Web/serverfarms@2023-12-01' = { + name: appServicePlanName + location: location + sku: { + name: appServicePlanSku + tier: appServicePlanSku == 'F1' ? 'Free' : appServicePlanSku == 'D1' ? 'Shared' : contains(appServicePlanSku, 'B') ? 'Basic' : contains(appServicePlanSku, 'S') ? 'Standard' : 'PremiumV2' + } + kind: 'linux' + properties: { + reserved: true // Required for Linux plans + } +} + +// App Service +resource appService 'Microsoft.Web/sites@2023-12-01' = { + name: appServiceName + location: location + properties: { + serverFarmId: appServicePlan.id + siteConfig: { + linuxFxVersion: 'DOCKER|${containerImage}' + appCommandLine: '' + alwaysOn: appServicePlanSku != 'F1' && appServicePlanSku != 'D1' // Not supported on Free/Shared tiers + httpLoggingEnabled: true + logsDirectorySizeLimit: 35 + detailedErrorLoggingEnabled: true + ftpsState: 'FtpsOnly' + minTlsVersion: '1.2' + http20Enabled: true + appSettings: concat( + [ + { + name: 'WEBSITES_ENABLE_APP_SERVICE_STORAGE' + value: 'false' + } + { + name: 'ASPNETCORE_ENVIRONMENT' + value: environment == 'prod' ? 'Production' : 'Development' + } + { + 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' + value: applicationInsights.properties.ConnectionString + } + { + name: 'ApplicationInsightsAgent_EXTENSION_VERSION' + value: '~3' + } + { + name: 'APPINSIGHTS_PROFILERFEATURE_VERSION' + value: '1.0.0' + } + { + name: 'APPINSIGHTS_SNAPSHOTFEATURE_VERSION' + value: '1.0.0' + } + ], + empty(containerRegistryServer) ? [] : [ + { + name: 'DOCKER_REGISTRY_SERVER_URL' + value: 'https://${containerRegistryServer}' + } + { + name: 'DOCKER_REGISTRY_SERVER_USERNAME' + value: containerRegistryUsername + } + { + name: 'DOCKER_REGISTRY_SERVER_PASSWORD' + value: containerRegistryPassword + } + ], + empty(azureOpenAIEndpoint) ? [] : [ + { + name: 'AzureOpenAI__Endpoint' + value: azureOpenAIEndpoint + } + { + name: 'AzureOpenAI__DeploymentName' + value: azureOpenAIDeploymentName + } + ], + empty(azureOpenAIApiKey) ? [] : [ + { + name: 'AzureOpenAI__ApiKey' + value: azureOpenAIApiKey + } + ], + empty(azureSearchServiceName) ? [] : [ + { + name: 'AzureSearch__ServiceName' + value: azureSearchServiceName + } + { + name: 'AzureSearch__IndexName' + value: azureSearchIndexName + } + ], + empty(azureSearchApiKey) ? [] : [ + { + name: 'AzureSearch__ApiKey' + value: azureSearchApiKey + } + ] + ) + } + httpsOnly: true + } +} + +// Configure health check endpoint +resource healthCheckConfig 'Microsoft.Web/sites/config@2023-12-01' = { + parent: appService + name: 'web' + properties: { + healthCheckPath: '/health' + } +} + +// Outputs +output appServiceUrl string = 'https://${appService.properties.defaultHostName}' +output appServiceName string = appService.name +output applicationInsightsInstrumentationKey string = applicationInsights.properties.InstrumentationKey +output applicationInsightsConnectionString string = applicationInsights.properties.ConnectionString \ No newline at end of file diff --git a/deploy/azure/container-apps.bicep b/deploy/azure/container-apps.bicep new file mode 100644 index 0000000..930aeaf --- /dev/null +++ b/deploy/azure/container-apps.bicep @@ -0,0 +1,273 @@ +@description('Application name used for resource naming') +param appName string = 'nlwebnet' + +@description('Location for all resources') +param location string = resourceGroup().location + +@description('Environment (dev, staging, prod)') +@allowed(['dev', 'staging', 'prod']) +param environment string = 'dev' + +@description('Container image name and tag') +param containerImage string = 'nlwebnet-demo:latest' + +@description('Container registry server') +param containerRegistryServer string = '' + +@description('Container registry username') +@secure() +param containerRegistryUsername string = '' + +@description('Container registry password') +@secure() +param containerRegistryPassword string = '' + +@description('Azure OpenAI API Key') +@secure() +param azureOpenAIApiKey string = '' + +@description('Azure OpenAI Endpoint') +param azureOpenAIEndpoint string = '' + +@description('Azure OpenAI Deployment Name') +param azureOpenAIDeploymentName string = 'gpt-4' + +@description('Azure Search API Key') +@secure() +param azureSearchApiKey string = '' + +@description('Azure Search Service Name') +param azureSearchServiceName string = '' + +@description('Azure Search Index Name') +param azureSearchIndexName string = 'nlweb-index' + +@description('Minimum number of replicas') +@minValue(0) +@maxValue(25) +param minReplicas int = 1 + +@description('Maximum number of replicas') +@minValue(1) +@maxValue(25) +param maxReplicas int = 3 + +var containerAppName = '${appName}-${environment}' +var containerAppEnvironmentName = '${appName}-env-${environment}' +var logAnalyticsWorkspaceName = '${appName}-logs-${environment}' +var appInsightsName = '${appName}-insights-${environment}' + +// Log Analytics Workspace +resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { + name: logAnalyticsWorkspaceName + location: location + properties: { + sku: { + name: 'PerGB2018' + } + retentionInDays: 30 + features: { + searchVersion: 1 + legacy: 0 + enableLogAccessUsingOnlyResourcePermissions: true + } + } +} + +// Application Insights +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { + name: appInsightsName + location: location + kind: 'web' + properties: { + Application_Type: 'web' + WorkspaceResourceId: logAnalyticsWorkspace.id + } +} + +// Container Apps Environment +resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2024-03-01' = { + name: containerAppEnvironmentName + location: location + properties: { + daprAIInstrumentationKey: applicationInsights.properties.InstrumentationKey + appLogsConfiguration: { + destination: 'log-analytics' + logAnalyticsConfiguration: { + customerId: logAnalyticsWorkspace.properties.customerId + sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey + } + } + } +} + +// Container App +resource containerApp 'Microsoft.App/containerApps@2024-03-01' = { + name: containerAppName + location: location + properties: { + managedEnvironmentId: containerAppEnvironment.id + configuration: { + ingress: { + external: true + targetPort: 8080 + allowInsecure: false + traffic: [ + { + weight: 100 + latestRevision: true + } + ] + } + registries: empty(containerRegistryServer) ? [] : [ + { + server: containerRegistryServer + username: containerRegistryUsername + passwordSecretRef: 'registry-password' + } + ] + secrets: concat( + empty(containerRegistryPassword) ? [] : [ + { + name: 'registry-password' + value: containerRegistryPassword + } + ], + empty(azureOpenAIApiKey) ? [] : [ + { + name: 'azure-openai-api-key' + value: azureOpenAIApiKey + } + ], + empty(azureSearchApiKey) ? [] : [ + { + name: 'azure-search-api-key' + value: azureSearchApiKey + } + ] + ) + } + template: { + containers: [ + { + image: containerImage + name: 'nlwebnet-demo' + env: concat( + [ + { + name: 'ASPNETCORE_ENVIRONMENT' + value: environment == 'prod' ? 'Production' : 'Development' + } + { + 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' + value: applicationInsights.properties.ConnectionString + } + ], + empty(azureOpenAIEndpoint) ? [] : [ + { + name: 'AzureOpenAI__Endpoint' + value: azureOpenAIEndpoint + } + { + name: 'AzureOpenAI__DeploymentName' + value: azureOpenAIDeploymentName + } + ], + empty(azureOpenAIApiKey) ? [] : [ + { + name: 'AzureOpenAI__ApiKey' + secretRef: 'azure-openai-api-key' + } + ], + empty(azureSearchServiceName) ? [] : [ + { + name: 'AzureSearch__ServiceName' + value: azureSearchServiceName + } + { + name: 'AzureSearch__IndexName' + value: azureSearchIndexName + } + ], + empty(azureSearchApiKey) ? [] : [ + { + name: 'AzureSearch__ApiKey' + secretRef: 'azure-search-api-key' + } + ] + ) + resources: { + cpu: json('0.5') + memory: '1Gi' + } + probes: [ + { + type: 'Liveness' + httpGet: { + path: '/health' + port: 8080 + scheme: 'HTTP' + } + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + } + { + type: 'Readiness' + httpGet: { + path: '/health' + port: 8080 + scheme: 'HTTP' + } + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + } + ] + } + ] + scale: { + minReplicas: minReplicas + maxReplicas: maxReplicas + rules: [ + { + name: 'http-rule' + http: { + metadata: { + concurrentRequests: '100' + } + } + } + ] + } + } + } +} + +// Outputs +output containerAppFQDN string = containerApp.properties.configuration.ingress.fqdn +output containerAppUrl string = 'https://${containerApp.properties.configuration.ingress.fqdn}' +output applicationInsightsInstrumentationKey string = applicationInsights.properties.InstrumentationKey +output applicationInsightsConnectionString string = applicationInsights.properties.ConnectionString \ No newline at end of file diff --git a/deploy/azure/container-apps.parameters.json b/deploy/azure/container-apps.parameters.json new file mode 100644 index 0000000..7ead5b1 --- /dev/null +++ b/deploy/azure/container-apps.parameters.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appName": { + "value": "nlwebnet" + }, + "environment": { + "value": "dev" + }, + "containerImage": { + "value": "nlwebnet-demo:latest" + }, + "minReplicas": { + "value": 1 + }, + "maxReplicas": { + "value": 3 + }, + "containerRegistryServer": { + "value": "" + }, + "containerRegistryUsername": { + "value": "" + }, + "containerRegistryPassword": { + "value": "" + }, + "azureOpenAIApiKey": { + "value": "" + }, + "azureOpenAIEndpoint": { + "value": "" + }, + "azureOpenAIDeploymentName": { + "value": "gpt-4" + }, + "azureSearchApiKey": { + "value": "" + }, + "azureSearchServiceName": { + "value": "" + }, + "azureSearchIndexName": { + "value": "nlweb-index" + } + } +} \ No newline at end of file diff --git a/doc/deployment/README.md b/doc/deployment/README.md new file mode 100644 index 0000000..dc2fa15 --- /dev/null +++ b/doc/deployment/README.md @@ -0,0 +1,348 @@ +# ๐Ÿš€ NLWebNet Deployment Guide + +This guide provides comprehensive instructions for deploying NLWebNet across various platforms and environments. + +## ๐Ÿ“‹ Table of Contents + +- [Quick Start](#quick-start) +- [Docker Deployment](#docker-deployment) +- [Kubernetes Deployment](#kubernetes-deployment) +- [Azure Deployment](#azure-deployment) +- [Production Considerations](#production-considerations) +- [Monitoring and Health Checks](#monitoring-and-health-checks) +- [Troubleshooting](#troubleshooting) + +## Quick Start + +### Prerequisites + +- **.NET 9 SDK** (for building from source) +- **Docker** (for containerized deployment) +- **Azure CLI** (for Azure deployments) +- **kubectl** (for Kubernetes deployments) + +### Local Development + +```bash +# Clone the repository +git clone https://github.com/jongalloway/NLWebNet.git +cd NLWebNet + +# Run with Docker Compose +docker-compose up --build + +# Or run locally (requires .NET 9) +cd samples/Demo +dotnet run +``` + +Access the application at `http://localhost:8080` + +## Docker Deployment + +### Building the Container + +Use the provided build script for easy Docker image creation: + +```bash +# Build with default settings +./scripts/deploy/build-docker.sh + +# Build with specific tag +./scripts/deploy/build-docker.sh -t v1.0.0 + +# Build and push to registry +./scripts/deploy/build-docker.sh -t v1.0.0 -r myregistry.azurecr.io -p +``` + +### Manual Docker Build + +```bash +# Build the image +docker build -t nlwebnet-demo:latest . + +# Run the container +docker run -p 8080:8080 \ + -e ASPNETCORE_ENVIRONMENT=Production \ + -e NLWebNet__DefaultMode=List \ + -e NLWebNet__EnableStreaming=true \ + nlwebnet-demo:latest +``` + +### Docker Compose + +For local development with dependencies: + +```bash +# Start all services +docker-compose up -d + +# View logs +docker-compose logs -f nlwebnet-demo + +# Stop all services +docker-compose down +``` + +### Environment Variables + +Key environment variables for Docker deployment: + +| Variable | Default | Description | +|----------|---------|-------------| +| `ASPNETCORE_ENVIRONMENT` | `Production` | ASP.NET Core environment | +| `ASPNETCORE_URLS` | `http://+:8080` | Listening URLs | +| `NLWebNet__DefaultMode` | `List` | Default query mode | +| `NLWebNet__EnableStreaming` | `true` | Enable streaming responses | +| `AzureOpenAI__Endpoint` | - | Azure OpenAI service endpoint | +| `AzureOpenAI__ApiKey` | - | Azure OpenAI API key | +| `AzureSearch__ServiceName` | - | Azure Search service name | +| `AzureSearch__ApiKey` | - | Azure Search API key | + +## Kubernetes Deployment + +### Quick Deploy + +```bash +# Deploy all resources +kubectl apply -f k8s/ + +# Check deployment status +kubectl get pods -l app=nlwebnet-demo +kubectl get services +kubectl get ingress +``` + +### Step-by-Step Deployment + +1. **Create namespace** (optional): + ```bash + kubectl create namespace nlwebnet + kubectl config set-context --current --namespace=nlwebnet + ``` + +2. **Deploy configuration**: + ```bash + # Update secrets with your API keys + kubectl create secret generic nlwebnet-secrets \ + --from-literal=azure-openai-api-key="your-key" \ + --from-literal=azure-search-api-key="your-key" + + # Apply configuration + kubectl apply -f k8s/configmap.yaml + ``` + +3. **Deploy application**: + ```bash + kubectl apply -f k8s/deployment.yaml + kubectl apply -f k8s/service.yaml + kubectl apply -f k8s/ingress.yaml + ``` + +4. **Verify deployment**: + ```bash + kubectl get pods + kubectl get services + kubectl logs -l app=nlwebnet-demo + ``` + +### Scaling + +```bash +# Manual scaling +kubectl scale deployment nlwebnet-demo --replicas=5 + +# Auto-scaling (HPA already configured) +kubectl get hpa +``` + +### Access the Application + +```bash +# Port forward for testing +kubectl port-forward service/nlwebnet-demo-service 8080:80 + +# Or via LoadBalancer +kubectl get service nlwebnet-demo-loadbalancer +``` + +## Azure Deployment + +### Prerequisites + +```bash +# Install Azure CLI +curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash + +# Login to Azure +az login + +# Set subscription (if needed) +az account set --subscription "your-subscription-id" +``` + +### Container Apps Deployment + +```bash +# Deploy using script +./scripts/deploy/deploy-azure.sh -g myResourceGroup -t container-apps + +# Or manual deployment +az deployment group create \ + --resource-group myResourceGroup \ + --template-file deploy/azure/container-apps.bicep \ + --parameters @deploy/azure/container-apps.parameters.json +``` + +### App Service Deployment + +```bash +# Deploy to App Service +./scripts/deploy/deploy-azure.sh -g myResourceGroup -t app-service -e prod + +# Manual deployment +az deployment group create \ + --resource-group myResourceGroup \ + --template-file deploy/azure/app-service.bicep \ + --parameters appName=nlwebnet environment=prod +``` + +### Azure Kubernetes Service (AKS) + +1. **Create AKS cluster**: + ```bash + az aks create \ + --resource-group myResourceGroup \ + --name nlwebnet-aks \ + --node-count 3 \ + --enable-addons http_application_routing + ``` + +2. **Get credentials**: + ```bash + az aks get-credentials --resource-group myResourceGroup --name nlwebnet-aks + ``` + +3. **Deploy application**: + ```bash + kubectl apply -f k8s/ + ``` + +## Production Considerations + +### Security + +- **Use HTTPS**: Configure TLS certificates +- **Secrets Management**: Use Azure Key Vault or Kubernetes secrets +- **Network Policies**: Implement network segmentation +- **RBAC**: Configure proper access controls +- **Container Security**: Scan images for vulnerabilities + +### Performance + +- **Resource Limits**: Set appropriate CPU/memory limits +- **Auto-scaling**: Configure HPA for Kubernetes or scale rules for Azure +- **Caching**: Implement Redis caching if needed +- **CDN**: Use Azure Front Door or similar for static assets + +### Monitoring + +- **Application Insights**: Enabled by default in Azure deployments +- **Health Checks**: Available at `/health` and `/health/detailed` +- **Logging**: Structured logging with correlation IDs +- **Metrics**: OpenTelemetry integration available + +### Backup and Recovery + +- **Configuration Backup**: Store configuration in version control +- **Database Backup**: If using external databases +- **Disaster Recovery**: Multi-region deployment for critical applications + +## Monitoring and Health Checks + +### Health Check Endpoints + +| Endpoint | Description | +|----------|-------------| +| `/health` | Basic health status | +| `/health/detailed` | Detailed component health | + +### Example Health Check + +```bash +# Basic health check +curl http://localhost:8080/health + +# Detailed health check +curl http://localhost:8080/health/detailed +``` + +### Monitoring Integration + +- **Azure Monitor**: Automatic integration in Azure deployments +- **Prometheus**: Metrics endpoint available for Kubernetes +- **Application Insights**: Telemetry and performance monitoring + +## Troubleshooting + +### Common Issues + +1. **Container won't start**: + ```bash + # Check logs + docker logs nlwebnet-demo + kubectl logs -l app=nlwebnet-demo + ``` + +2. **Health check failing**: + ```bash + # Test health endpoint + curl -v http://localhost:8080/health + ``` + +3. **Performance issues**: + ```bash + # Check resource usage + kubectl top pods + kubectl describe pod + ``` + +### Debug Commands + +```bash +# Docker debugging +docker exec -it nlwebnet-demo /bin/bash + +# Kubernetes debugging +kubectl exec -it -- /bin/bash +kubectl describe pod +kubectl get events --sort-by=.metadata.creationTimestamp +``` + +### Logs + +```bash +# Docker logs +docker-compose logs -f nlwebnet-demo + +# Kubernetes logs +kubectl logs -f -l app=nlwebnet-demo +kubectl logs -f deployment/nlwebnet-demo + +# Azure Container Apps logs +az containerapp logs show --name nlwebnet-dev --resource-group myResourceGroup +``` + +## Support + +For deployment issues: + +1. Check the [GitHub Issues](https://github.com/jongalloway/NLWebNet/issues) +2. Review the application logs +3. Verify configuration settings +4. Test health endpoints +5. Check resource quotas and limits + +--- + +**Note**: This is experimental software and not recommended for production use without thorough testing and evaluation. \ No newline at end of file diff --git a/doc/todo.md b/doc/todo.md index 02a461c..fd85321 100644 --- a/doc/todo.md +++ b/doc/todo.md @@ -547,11 +547,16 @@ 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] Kubernetes manifests (deployment, service, ingress, configmap) + - [x] Environment-specific configurations + - [x] Health check integration (already implemented) + - [x] Container registry setup documentation + - [x] Deployment automation scripts +- [x] **DEPLOYMENT INFRASTRUCTURE COMPLETED**: Comprehensive deployment strategy implemented with Docker, Kubernetes, and Azure support ### Open Questions to Resolve diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..568a446 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,86 @@ +version: '3.8' + +services: + # NLWebNet Demo Application + nlwebnet-demo: + build: + context: . + dockerfile: Dockerfile + target: final + container_name: nlwebnet-demo + ports: + - "8080:8080" + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=http://+:8080 + - NLWebNet__DefaultMode=List + - NLWebNet__EnableStreaming=true + - NLWebNet__DefaultTimeoutSeconds=30 + - NLWebNet__MaxResultsPerQuery=50 + # Override CORS for containerized development + - CORS__AllowedOrigins__0=http://localhost:8080 + - CORS__AllowedOrigins__1=http://localhost:3000 + - CORS__AllowedOrigins__2=http://localhost:5173 + - CORS__AllowCredentials=true + # Optional: Add AI service configuration via environment variables + # - AzureOpenAI__Endpoint=https://your-resource.openai.azure.com/ + # - AzureOpenAI__DeploymentName=gpt-4 + # - AzureOpenAI__ApiKey=your-api-key + # - AzureSearch__ServiceName=your-search-service + # - AzureSearch__IndexName=nlweb-index + # - AzureSearch__ApiKey=your-search-api-key + volumes: + # Mount logs directory for persistent logs + - nlwebnet-logs:/app/logs + # Optional: Override appsettings for development + # - ./samples/Demo/appsettings.Development.json:/app/appsettings.Development.json:ro + networks: + - nlwebnet-network + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + + # Optional: Redis for caching (uncomment if needed) + # redis: + # image: redis:7-alpine + # container_name: nlwebnet-redis + # ports: + # - "6379:6379" + # command: redis-server --appendonly yes + # volumes: + # - redis-data:/data + # networks: + # - nlwebnet-network + # restart: unless-stopped + + # Optional: PostgreSQL for data storage (uncomment if needed) + # postgres: + # image: postgres:15-alpine + # container_name: nlwebnet-postgres + # environment: + # POSTGRES_DB: nlwebnet + # POSTGRES_USER: nlwebnet + # POSTGRES_PASSWORD: nlwebnet123 + # ports: + # - "5432:5432" + # volumes: + # - postgres-data:/var/lib/postgresql/data + # networks: + # - nlwebnet-network + # restart: unless-stopped + +volumes: + nlwebnet-logs: + driver: local + # redis-data: + # driver: local + # postgres-data: + # driver: local + +networks: + nlwebnet-network: + driver: bridge \ No newline at end of file diff --git a/helm/nlwebnet/Chart.yaml b/helm/nlwebnet/Chart.yaml new file mode 100644 index 0000000..344d640 --- /dev/null +++ b/helm/nlwebnet/Chart.yaml @@ -0,0 +1,21 @@ +apiVersion: v2 +name: nlwebnet +description: A Helm chart for NLWebNet - .NET NLWeb Protocol Implementation +type: application +version: 0.1.0 +appVersion: "latest" +home: https://github.com/jongalloway/NLWebNet +sources: + - https://github.com/jongalloway/NLWebNet +maintainers: + - name: Jon Galloway + email: jongalloway@outlook.com +keywords: + - nlweb + - ai + - dotnet + - aspnetcore + - blazor + - mcp +annotations: + category: AI/ML \ No newline at end of file diff --git a/helm/nlwebnet/README.md b/helm/nlwebnet/README.md new file mode 100644 index 0000000..42b3d7a --- /dev/null +++ b/helm/nlwebnet/README.md @@ -0,0 +1,227 @@ +# NLWebNet Helm Chart + +This Helm chart deploys NLWebNet to a Kubernetes cluster. + +## Prerequisites + +- Kubernetes 1.19+ +- Helm 3.0+ + +## Installing the Chart + +```bash +# Add the chart repository (when available) +# helm repo add nlwebnet https://charts.nlwebnet.io +# helm repo update + +# Install from local files +cd helm +helm install nlwebnet ./nlwebnet + +# Install with custom values +helm install nlwebnet ./nlwebnet -f my-values.yaml + +# Install with inline values +helm install nlwebnet ./nlwebnet \ + --set image.repository=myregistry.azurecr.io/nlwebnet-demo \ + --set image.tag=v1.0.0 \ + --set ingress.enabled=true \ + --set ingress.hosts[0].host=nlwebnet.example.com +``` + +## Configuration + +### Basic Configuration + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `image.repository` | Container image repository | `nlwebnet-demo` | +| `image.tag` | Container image tag | `latest` | +| `image.pullPolicy` | Image pull policy | `IfNotPresent` | +| `replicaCount` | Number of replicas | `2` | +| `service.type` | Kubernetes service type | `ClusterIP` | +| `service.port` | Service port | `80` | + +### Application Configuration + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `app.environment` | ASP.NET Core environment | `Production` | +| `app.nlwebnet.defaultMode` | Default query mode | `List` | +| `app.nlwebnet.enableStreaming` | Enable streaming responses | `true` | +| `app.azureOpenAI.endpoint` | Azure OpenAI endpoint | `""` | +| `app.azureOpenAI.deploymentName` | Azure OpenAI deployment | `gpt-4` | + +### Secrets Configuration + +Create secrets separately for security: + +```bash +# Create secrets manually +kubectl create secret generic nlwebnet-secrets \ + --from-literal=azure-openai-api-key="your-azure-openai-key" \ + --from-literal=azure-search-api-key="your-azure-search-key" \ + --from-literal=openai-api-key="your-openai-key" + +# Use existing secrets +helm install nlwebnet ./nlwebnet \ + --set secrets.useExisting=true \ + --set secrets.existingSecretName=nlwebnet-secrets +``` + +### Ingress Configuration + +```bash +# Enable ingress with NGINX +helm install nlwebnet ./nlwebnet \ + --set ingress.enabled=true \ + --set ingress.className=nginx \ + --set ingress.hosts[0].host=nlwebnet.example.com \ + --set ingress.hosts[0].paths[0].path=/ \ + --set ingress.hosts[0].paths[0].pathType=Prefix +``` + +### Auto-scaling Configuration + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `autoscaling.enabled` | Enable horizontal pod autoscaler | `true` | +| `autoscaling.minReplicas` | Minimum number of replicas | `2` | +| `autoscaling.maxReplicas` | Maximum number of replicas | `10` | +| `autoscaling.targetCPUUtilizationPercentage` | Target CPU utilization | `70` | +| `autoscaling.targetMemoryUtilizationPercentage` | Target memory utilization | `80` | + +## Examples + +### Development Environment + +```yaml +# dev-values.yaml +app: + environment: Development + nlwebnet: + defaultMode: List + enableStreaming: true + +ingress: + enabled: true + hosts: + - host: nlwebnet-dev.local + paths: + - path: / + pathType: Prefix + +autoscaling: + enabled: false + +replicaCount: 1 + +resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 200m + memory: 256Mi +``` + +```bash +helm install nlwebnet-dev ./nlwebnet -f dev-values.yaml +``` + +### Production Environment + +```yaml +# prod-values.yaml +app: + environment: Production + azureOpenAI: + endpoint: "https://your-resource.openai.azure.com/" + deploymentName: "gpt-4" + azureSearch: + serviceName: "your-search-service" + indexName: "nlweb-index" + +ingress: + enabled: true + className: nginx + annotations: + cert-manager.io/cluster-issuer: "letsencrypt-prod" + nginx.ingress.kubernetes.io/ssl-redirect: "true" + hosts: + - host: nlwebnet.example.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: nlwebnet-tls + hosts: + - nlwebnet.example.com + +secrets: + useExisting: true + existingSecretName: nlwebnet-secrets + +autoscaling: + enabled: true + minReplicas: 3 + maxReplicas: 20 + +resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi +``` + +```bash +helm install nlwebnet-prod ./nlwebnet -f prod-values.yaml +``` + +## Uninstalling the Chart + +```bash +helm uninstall nlwebnet +``` + +## Upgrading the Chart + +```bash +# Upgrade with new values +helm upgrade nlwebnet ./nlwebnet -f new-values.yaml + +# Upgrade to new chart version +helm upgrade nlwebnet ./nlwebnet --version 0.2.0 +``` + +## Health Checks + +The chart includes health checks that monitor: +- Application health at `/health` +- Detailed component health at `/health/detailed` + +## Monitoring + +Integration with monitoring systems: +- Prometheus metrics (when enabled) +- Application Insights (for Azure deployments) +- OpenTelemetry support + +## Troubleshooting + +```bash +# Check pod status +kubectl get pods -l app.kubernetes.io/name=nlwebnet + +# View pod logs +kubectl logs -l app.kubernetes.io/name=nlwebnet + +# Check service +kubectl get svc nlwebnet + +# Test health endpoint +kubectl port-forward svc/nlwebnet 8080:80 +curl http://localhost:8080/health +``` \ No newline at end of file diff --git a/helm/nlwebnet/templates/_helpers.tpl b/helm/nlwebnet/templates/_helpers.tpl new file mode 100644 index 0000000..16bc164 --- /dev/null +++ b/helm/nlwebnet/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "nlwebnet.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.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.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "nlwebnet.labels" -}} +helm.sh/chart: {{ include "nlwebnet.chart" . }} +{{ include "nlwebnet.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "nlwebnet.selectorLabels" -}} +app.kubernetes.io/name: {{ include "nlwebnet.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "nlwebnet.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "nlwebnet.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/nlwebnet/templates/deployment.yaml b/helm/nlwebnet/templates/deployment.yaml new file mode 100644 index 0000000..ef1f99f --- /dev/null +++ b/helm/nlwebnet/templates/deployment.yaml @@ -0,0 +1,132 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "nlwebnet.fullname" . }} + labels: + {{- include "nlwebnet.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "nlwebnet.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "nlwebnet.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "nlwebnet.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.app.environment | quote }} + - name: ASPNETCORE_URLS + value: "http://+:{{ .Values.service.targetPort }}" + - name: NLWebNet__DefaultMode + value: {{ .Values.app.nlwebnet.defaultMode | quote }} + - name: NLWebNet__EnableStreaming + value: {{ .Values.app.nlwebnet.enableStreaming | quote }} + - name: NLWebNet__DefaultTimeoutSeconds + value: {{ .Values.app.nlwebnet.defaultTimeoutSeconds | quote }} + - name: NLWebNet__MaxResultsPerQuery + value: {{ .Values.app.nlwebnet.maxResultsPerQuery | quote }} + {{- if .Values.app.azureOpenAI.endpoint }} + - name: AzureOpenAI__Endpoint + value: {{ .Values.app.azureOpenAI.endpoint | quote }} + - name: AzureOpenAI__DeploymentName + value: {{ .Values.app.azureOpenAI.deploymentName | quote }} + - name: AzureOpenAI__ApiVersion + value: {{ .Values.app.azureOpenAI.apiVersion | quote }} + {{- end }} + {{- if .Values.app.azureSearch.serviceName }} + - name: AzureSearch__ServiceName + value: {{ .Values.app.azureSearch.serviceName | quote }} + - name: AzureSearch__IndexName + value: {{ .Values.app.azureSearch.indexName | quote }} + {{- end }} + {{- if .Values.app.cors.allowedOrigins }} + {{- range $index, $origin := .Values.app.cors.allowedOrigins }} + - name: CORS__AllowedOrigins__{{ $index }} + value: {{ $origin | quote }} + {{- end }} + {{- end }} + - name: CORS__AllowCredentials + value: {{ .Values.app.cors.allowCredentials | quote }} + {{- if or .Values.secrets.useExisting (not (empty .Values.secrets.azureOpenAIApiKey)) }} + - name: AzureOpenAI__ApiKey + valueFrom: + secretKeyRef: + name: {{ if .Values.secrets.useExisting }}{{ .Values.secrets.existingSecretName }}{{ else }}{{ include "nlwebnet.fullname" . }}-secrets{{ end }} + key: azure-openai-api-key + optional: true + {{- end }} + {{- if or .Values.secrets.useExisting (not (empty .Values.secrets.azureSearchApiKey)) }} + - name: AzureSearch__ApiKey + valueFrom: + secretKeyRef: + name: {{ if .Values.secrets.useExisting }}{{ .Values.secrets.existingSecretName }}{{ else }}{{ include "nlwebnet.fullname" . }}-secrets{{ end }} + key: azure-search-api-key + optional: true + {{- end }} + {{- if or .Values.secrets.useExisting (not (empty .Values.secrets.openAIApiKey)) }} + - name: OpenAI__ApiKey + valueFrom: + secretKeyRef: + name: {{ if .Values.secrets.useExisting }}{{ .Values.secrets.existingSecretName }}{{ else }}{{ include "nlwebnet.fullname" . }}-secrets{{ end }} + key: openai-api-key + optional: true + {{- end }} + {{- if .Values.healthCheck.enabled }} + livenessProbe: + httpGet: + path: {{ .Values.healthCheck.path }} + port: http + initialDelaySeconds: {{ .Values.healthCheck.initialDelaySeconds }} + periodSeconds: {{ .Values.healthCheck.periodSeconds }} + timeoutSeconds: {{ .Values.healthCheck.timeoutSeconds }} + failureThreshold: {{ .Values.healthCheck.failureThreshold }} + {{- end }} + {{- if .Values.readinessProbe.enabled }} + readinessProbe: + httpGet: + path: {{ .Values.readinessProbe.path }} + port: http + initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.readinessProbe.periodSeconds }} + timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }} + failureThreshold: {{ .Values.readinessProbe.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/helm/nlwebnet/templates/hpa.yaml b/helm/nlwebnet/templates/hpa.yaml new file mode 100644 index 0000000..c93ab9e --- /dev/null +++ b/helm/nlwebnet/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "nlwebnet.fullname" . }} + labels: + {{- include "nlwebnet.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "nlwebnet.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/helm/nlwebnet/templates/ingress.yaml b/helm/nlwebnet/templates/ingress.yaml new file mode 100644 index 0000000..e186f7d --- /dev/null +++ b/helm/nlwebnet/templates/ingress.yaml @@ -0,0 +1,59 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "nlwebnet.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.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/helm/nlwebnet/templates/secret.yaml b/helm/nlwebnet/templates/secret.yaml new file mode 100644 index 0000000..f724a67 --- /dev/null +++ b/helm/nlwebnet/templates/secret.yaml @@ -0,0 +1,21 @@ +{{- if not .Values.secrets.useExisting }} +{{- if or (not (empty .Values.secrets.azureOpenAIApiKey)) (not (empty .Values.secrets.azureSearchApiKey)) (not (empty .Values.secrets.openAIApiKey)) }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "nlwebnet.fullname" . }}-secrets + labels: + {{- include "nlwebnet.labels" . | nindent 4 }} +type: Opaque +data: + {{- if not (empty .Values.secrets.azureOpenAIApiKey) }} + azure-openai-api-key: {{ .Values.secrets.azureOpenAIApiKey | b64enc }} + {{- end }} + {{- if not (empty .Values.secrets.azureSearchApiKey) }} + azure-search-api-key: {{ .Values.secrets.azureSearchApiKey | b64enc }} + {{- end }} + {{- if not (empty .Values.secrets.openAIApiKey) }} + openai-api-key: {{ .Values.secrets.openAIApiKey | b64enc }} + {{- end }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/nlwebnet/templates/service.yaml b/helm/nlwebnet/templates/service.yaml new file mode 100644 index 0000000..25f3e6f --- /dev/null +++ b/helm/nlwebnet/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "nlwebnet.fullname" . }} + labels: + {{- include "nlwebnet.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "nlwebnet.selectorLabels" . | nindent 4 }} \ No newline at end of file diff --git a/helm/nlwebnet/templates/serviceaccount.yaml b/helm/nlwebnet/templates/serviceaccount.yaml new file mode 100644 index 0000000..48721f3 --- /dev/null +++ b/helm/nlwebnet/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "nlwebnet.serviceAccountName" . }} + labels: + {{- include "nlwebnet.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/nlwebnet/values.yaml b/helm/nlwebnet/values.yaml new file mode 100644 index 0000000..196b88e --- /dev/null +++ b/helm/nlwebnet/values.yaml @@ -0,0 +1,155 @@ +# Default values for nlwebnet. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 2 + +image: + repository: nlwebnet-demo + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + 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: 1000 + runAsNonRoot: true + +securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + runAsNonRoot: true + runAsUser: 1000 + capabilities: + drop: + - ALL + +service: + type: ClusterIP + port: 80 + targetPort: 8080 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: nlwebnet.local + paths: + - path: / + pathType: Prefix + tls: [] + # - secretName: nlwebnet-tls + # hosts: + # - nlwebnet.local + +resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 100m + memory: 256Mi + +autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 10 + targetCPUUtilizationPercentage: 70 + targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +# Application configuration +app: + environment: Production + nlwebnet: + defaultMode: List + enableStreaming: true + defaultTimeoutSeconds: 30 + maxResultsPerQuery: 50 + + # Azure OpenAI configuration + azureOpenAI: + endpoint: "" + deploymentName: "gpt-4" + apiVersion: "2024-02-01" + # API key should be provided via secret + + # Azure Search configuration + azureSearch: + serviceName: "" + indexName: "nlweb-index" + # API key should be provided via secret + + # CORS configuration + cors: + allowedOrigins: + - "https://your-domain.com" + allowedMethods: + - "GET" + - "POST" + - "OPTIONS" + allowedHeaders: + - "Content-Type" + - "Authorization" + - "Accept" + allowCredentials: true + +# External secrets (create these separately) +secrets: + # Set to true if you want to use existing secrets + useExisting: false + # Name of existing secret containing API keys + existingSecretName: "nlwebnet-secrets" + + # Or provide values here (not recommended for production) + azureOpenAIApiKey: "" + azureSearchApiKey: "" + openAIApiKey: "" + +# Health checks +healthCheck: + enabled: true + path: /health + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + +readinessProbe: + enabled: true + path: /health + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + +# Monitoring +monitoring: + enabled: false + serviceMonitor: + enabled: false + namespace: monitoring + interval: 30s + path: /metrics \ No newline at end of file diff --git a/k8s/configmap.yaml b/k8s/configmap.yaml new file mode 100644 index 0000000..51abf0a --- /dev/null +++ b/k8s/configmap.yaml @@ -0,0 +1,99 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: nlwebnet-config + labels: + app: nlwebnet-demo +data: + # NLWebNet Configuration + nlwebnet-default-mode: "List" + nlwebnet-enable-streaming: "true" + nlwebnet-default-timeout: "30" + nlwebnet-max-results: "50" + + # Azure OpenAI Configuration (non-sensitive) + azure-openai-endpoint: "https://your-resource.openai.azure.com/" + azure-openai-deployment: "gpt-4" + azure-openai-api-version: "2024-02-01" + + # Azure Search Configuration (non-sensitive) + azure-search-service: "your-search-service" + azure-search-index: "nlweb-index" + + # CORS Configuration + cors-allowed-origins: "https://your-domain.com,https://api.your-domain.com" + cors-allowed-methods: "GET,POST,OPTIONS" + cors-allowed-headers: "Content-Type,Authorization,Accept" + cors-allow-credentials: "true" + + # Logging Configuration + logging-level: "Information" + aspnetcore-logging-level: "Warning" + nlwebnet-logging-level: "Information" + +--- +apiVersion: v1 +kind: Secret +metadata: + name: nlwebnet-secrets + labels: + app: nlwebnet-demo +type: Opaque +data: + # API Keys (base64 encoded) + # To encode: echo -n "your-api-key" | base64 + # To create this secret, run: + # kubectl create secret generic nlwebnet-secrets \ + # --from-literal=azure-openai-api-key="your-azure-openai-key" \ + # --from-literal=azure-search-api-key="your-azure-search-key" \ + # --from-literal=openai-api-key="your-openai-key" + + # Placeholder values (replace with actual base64-encoded keys) + azure-openai-api-key: "eW91ci1henVyZS1vcGVuYWkta2V5" # "your-azure-openai-key" + azure-search-api-key: "eW91ci1henVyZS1zZWFyY2gta2V5" # "your-azure-search-key" + openai-api-key: "eW91ci1vcGVuYWkta2V5" # "your-openai-key" + +--- +# Optional: HorizontalPodAutoscaler for automatic scaling +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: nlwebnet-demo-hpa + labels: + app: nlwebnet-demo +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: nlwebnet-demo + minReplicas: 2 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 80 + behavior: + scaleDown: + stabilizationWindowSeconds: 300 + policies: + - type: Percentage + value: 50 + periodSeconds: 60 + scaleUp: + stabilizationWindowSeconds: 60 + policies: + - type: Percentage + value: 100 + periodSeconds: 60 + - type: Pods + value: 2 + periodSeconds: 60 \ No newline at end of file diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml new file mode 100644 index 0000000..1132d8c --- /dev/null +++ b/k8s/deployment.yaml @@ -0,0 +1,115 @@ +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 + protocol: TCP + 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" + # AI Service Configuration (use ConfigMap or Secret) + - name: AzureOpenAI__Endpoint + valueFrom: + configMapKeyRef: + name: nlwebnet-config + key: azure-openai-endpoint + optional: true + - name: AzureOpenAI__DeploymentName + valueFrom: + configMapKeyRef: + name: nlwebnet-config + key: azure-openai-deployment + optional: true + - name: AzureOpenAI__ApiKey + valueFrom: + secretKeyRef: + name: nlwebnet-secrets + key: azure-openai-api-key + optional: true + - name: AzureSearch__ServiceName + valueFrom: + configMapKeyRef: + name: nlwebnet-config + key: azure-search-service + optional: true + - name: AzureSearch__IndexName + valueFrom: + configMapKeyRef: + name: nlwebnet-config + key: azure-search-index + optional: true + - name: AzureSearch__ApiKey + valueFrom: + secretKeyRef: + name: nlwebnet-secrets + key: azure-search-api-key + optional: true + # CORS Configuration + - name: CORS__AllowedOrigins__0 + value: "https://your-domain.com" + - name: CORS__AllowCredentials + value: "true" + resources: + requests: + memory: "256Mi" + cpu: "100m" + 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: 1000 + capabilities: + drop: + - ALL + restartPolicy: Always + securityContext: + fsGroup: 1000 + runAsNonRoot: true \ No newline at end of file diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml new file mode 100644 index 0000000..92a3b32 --- /dev/null +++ b/k8s/ingress.yaml @@ -0,0 +1,84 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: nlwebnet-demo-ingress + labels: + app: nlwebnet-demo + annotations: + # Configure for specific ingress controller (examples for common ones) + + # For NGINX Ingress Controller + kubernetes.io/ingress.class: "nginx" + nginx.ingress.kubernetes.io/ssl-redirect: "true" + nginx.ingress.kubernetes.io/force-ssl-redirect: "true" + nginx.ingress.kubernetes.io/backend-protocol: "HTTP" + + # For Azure Application Gateway Ingress Controller (AGIC) + # kubernetes.io/ingress.class: "azure/application-gateway" + # appgw.ingress.kubernetes.io/backend-protocol: "http" + # appgw.ingress.kubernetes.io/ssl-redirect: "true" + + # For AWS ALB Ingress Controller + # kubernetes.io/ingress.class: "alb" + # alb.ingress.kubernetes.io/scheme: "internet-facing" + # alb.ingress.kubernetes.io/target-type: "ip" + # alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]' + # alb.ingress.kubernetes.io/ssl-redirect: "443" + + # For Google GKE Ingress + # kubernetes.io/ingress.class: "gce" + # kubernetes.io/ingress.global-static-ip-name: "nlwebnet-ip" + + # CORS headers for browser compatibility + nginx.ingress.kubernetes.io/cors-allow-origin: "https://your-domain.com" + nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST, OPTIONS" + nginx.ingress.kubernetes.io/cors-allow-headers: "Content-Type, Authorization, Accept" + nginx.ingress.kubernetes.io/cors-allow-credentials: "true" + + # Rate limiting (adjust as needed) + nginx.ingress.kubernetes.io/rate-limit-connections: "20" + nginx.ingress.kubernetes.io/rate-limit-rps: "10" + + # Enable gzip compression + nginx.ingress.kubernetes.io/enable-compression: "true" + + # Cert-manager for automatic TLS certificates (uncomment if using cert-manager) + # cert-manager.io/cluster-issuer: "letsencrypt-prod" + +spec: + # TLS configuration (uncomment and configure for HTTPS) + # tls: + # - hosts: + # - nlwebnet.your-domain.com + # secretName: nlwebnet-tls-secret + + rules: + - host: nlwebnet.your-domain.com # Replace with your actual domain + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: nlwebnet-demo-service + port: + number: 80 + + # Additional rules for multiple domains or subdomains + # - host: api.your-domain.com + # http: + # paths: + # - path: /ask + # pathType: Prefix + # backend: + # service: + # name: nlwebnet-demo-service + # port: + # number: 80 + # - path: /mcp + # pathType: Prefix + # backend: + # service: + # name: nlwebnet-demo-service + # port: + # number: 80 \ No newline at end of file diff --git a/k8s/service.yaml b/k8s/service.yaml new file mode 100644 index 0000000..23ccab4 --- /dev/null +++ b/k8s/service.yaml @@ -0,0 +1,31 @@ +apiVersion: v1 +kind: Service +metadata: + name: nlwebnet-demo-service + labels: + app: nlwebnet-demo +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 8080 + protocol: TCP + name: http + selector: + app: nlwebnet-demo +--- +apiVersion: v1 +kind: Service +metadata: + name: nlwebnet-demo-loadbalancer + labels: + app: nlwebnet-demo +spec: + type: LoadBalancer + ports: + - port: 80 + targetPort: 8080 + protocol: TCP + name: http + selector: + app: nlwebnet-demo \ No newline at end of file diff --git a/scripts/deploy/build-docker.sh b/scripts/deploy/build-docker.sh new file mode 100755 index 0000000..24ef4b8 --- /dev/null +++ b/scripts/deploy/build-docker.sh @@ -0,0 +1,156 @@ +#!/bin/bash + +# NLWebNet Docker Build and Deploy Script +# This script builds the Docker image and optionally pushes it to a registry + +set -e # Exit on any error + +# Configuration +IMAGE_NAME="nlwebnet-demo" +DEFAULT_TAG="latest" +REGISTRY="" +DOCKERFILE="Dockerfile" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Function to show usage +show_usage() { + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " -t, --tag TAG Docker image tag (default: $DEFAULT_TAG)" + echo " -r, --registry REG Container registry URL (e.g., myregistry.azurecr.io)" + echo " -p, --push Push image to registry after building" + echo " -n, --no-cache Build without using cache" + echo " -h, --help Show this help message" + echo "" + echo "Examples:" + echo " $0 # Build with default tag" + echo " $0 -t v1.0.0 # Build with specific tag" + echo " $0 -r myregistry.azurecr.io -p # Build and push to registry" + echo " $0 -t v1.0.0 -r myregistry.azurecr.io -p # Build specific version and push" +} + +# Parse command line arguments +TAG="$DEFAULT_TAG" +PUSH=false +NO_CACHE=false + +while [[ $# -gt 0 ]]; do + case $1 in + -t|--tag) + TAG="$2" + shift 2 + ;; + -r|--registry) + REGISTRY="$2" + shift 2 + ;; + -p|--push) + PUSH=true + shift + ;; + -n|--no-cache) + NO_CACHE=true + shift + ;; + -h|--help) + show_usage + exit 0 + ;; + *) + print_error "Unknown option: $1" + show_usage + exit 1 + ;; + esac +done + +# Build the full image name +if [[ -n "$REGISTRY" ]]; then + FULL_IMAGE_NAME="$REGISTRY/$IMAGE_NAME:$TAG" +else + FULL_IMAGE_NAME="$IMAGE_NAME:$TAG" +fi + +print_status "Starting Docker build process..." +print_status "Image name: $FULL_IMAGE_NAME" +print_status "Dockerfile: $DOCKERFILE" + +# Check if Dockerfile exists +if [[ ! -f "$DOCKERFILE" ]]; then + print_error "Dockerfile not found: $DOCKERFILE" + exit 1 +fi + +# Build Docker build command +BUILD_CMD="docker build" + +if [[ "$NO_CACHE" == true ]]; then + BUILD_CMD="$BUILD_CMD --no-cache" +fi + +BUILD_CMD="$BUILD_CMD -t $FULL_IMAGE_NAME -f $DOCKERFILE ." + +print_status "Running: $BUILD_CMD" + +# Execute the build +if eval "$BUILD_CMD"; then + print_status "โœ… Docker image built successfully: $FULL_IMAGE_NAME" +else + print_error "โŒ Docker build failed" + exit 1 +fi + +# Push to registry if requested +if [[ "$PUSH" == true ]]; then + if [[ -z "$REGISTRY" ]]; then + print_error "Cannot push: no registry specified. Use -r/--registry option." + exit 1 + fi + + print_status "Pushing image to registry..." + + if docker push "$FULL_IMAGE_NAME"; then + print_status "โœ… Image pushed successfully to $REGISTRY" + else + print_error "โŒ Failed to push image to registry" + exit 1 + fi +fi + +# Show final summary +echo "" +print_status "๐ŸŽ‰ Build process completed successfully!" +print_status "Built image: $FULL_IMAGE_NAME" + +if [[ "$PUSH" == true ]]; then + print_status "Image available at: $REGISTRY/$IMAGE_NAME:$TAG" +fi + +echo "" +print_status "Next steps:" +echo " โ€ข Run locally: docker run -p 8080:8080 $FULL_IMAGE_NAME" +echo " โ€ข Check health: curl http://localhost:8080/health" +echo " โ€ข View app: http://localhost:8080" + +if [[ "$PUSH" == false && -n "$REGISTRY" ]]; then + echo " โ€ข Push to registry: $0 -t $TAG -r $REGISTRY -p" +fi \ No newline at end of file diff --git a/scripts/deploy/deploy-azure.sh b/scripts/deploy/deploy-azure.sh new file mode 100755 index 0000000..b5e15df --- /dev/null +++ b/scripts/deploy/deploy-azure.sh @@ -0,0 +1,221 @@ +#!/bin/bash + +# NLWebNet Azure Deployment Script +# This script deploys NLWebNet to Azure using Bicep templates + +set -e # Exit on any error + +# Configuration +RESOURCE_GROUP="" +LOCATION="eastus" +APP_NAME="nlwebnet" +ENVIRONMENT="dev" +TEMPLATE_FILE="" +PARAMETERS_FILE="" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +print_header() { + echo -e "${BLUE}[DEPLOY]${NC} $1" +} + +# Function to show usage +show_usage() { + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Required Options:" + echo " -g, --resource-group NAME Azure resource group name" + echo " -t, --template TEMPLATE Deployment template (container-apps|app-service)" + echo "" + echo "Optional Options:" + echo " -l, --location LOCATION Azure location (default: $LOCATION)" + echo " -a, --app-name NAME Application name (default: $APP_NAME)" + echo " -e, --environment ENV Environment (dev|staging|prod) (default: $ENVIRONMENT)" + echo " -p, --parameters FILE Parameters file path" + echo " -h, --help Show this help message" + echo "" + echo "Examples:" + echo " $0 -g myResourceGroup -t container-apps" + echo " $0 -g myResourceGroup -t app-service -e prod" + echo " $0 -g myResourceGroup -t container-apps -p my-params.json" +} + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -g|--resource-group) + RESOURCE_GROUP="$2" + shift 2 + ;; + -l|--location) + LOCATION="$2" + shift 2 + ;; + -a|--app-name) + APP_NAME="$2" + shift 2 + ;; + -e|--environment) + ENVIRONMENT="$2" + shift 2 + ;; + -t|--template) + case "$2" in + container-apps) + TEMPLATE_FILE="deploy/azure/container-apps.bicep" + PARAMETERS_FILE="deploy/azure/container-apps.parameters.json" + ;; + app-service) + TEMPLATE_FILE="deploy/azure/app-service.bicep" + ;; + *) + print_error "Invalid template: $2. Use 'container-apps' or 'app-service'" + exit 1 + ;; + esac + shift 2 + ;; + -p|--parameters) + PARAMETERS_FILE="$2" + shift 2 + ;; + -h|--help) + show_usage + exit 0 + ;; + *) + print_error "Unknown option: $1" + show_usage + exit 1 + ;; + esac +done + +# Validate required parameters +if [[ -z "$RESOURCE_GROUP" ]]; then + print_error "Resource group is required. Use -g/--resource-group option." + show_usage + exit 1 +fi + +if [[ -z "$TEMPLATE_FILE" ]]; then + print_error "Template is required. Use -t/--template option." + show_usage + exit 1 +fi + +# Check if template file exists +if [[ ! -f "$TEMPLATE_FILE" ]]; then + print_error "Template file not found: $TEMPLATE_FILE" + exit 1 +fi + +# Check if parameters file exists (if specified) +if [[ -n "$PARAMETERS_FILE" && ! -f "$PARAMETERS_FILE" ]]; then + print_error "Parameters file not found: $PARAMETERS_FILE" + exit 1 +fi + +print_header "๐Ÿš€ Starting Azure deployment for NLWebNet" +print_status "Resource Group: $RESOURCE_GROUP" +print_status "Location: $LOCATION" +print_status "App Name: $APP_NAME" +print_status "Environment: $ENVIRONMENT" +print_status "Template: $TEMPLATE_FILE" + +if [[ -n "$PARAMETERS_FILE" ]]; then + print_status "Parameters: $PARAMETERS_FILE" +fi + +# Check if Azure CLI is installed and user is logged in +if ! command -v az &> /dev/null; then + print_error "Azure CLI is not installed. Please install it first." + print_status "Installation: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli" + exit 1 +fi + +# Check if user is logged in +if ! az account show &> /dev/null; then + print_error "Not logged in to Azure. Please run 'az login' first." + exit 1 +fi + +print_status "โœ… Azure CLI is available and user is authenticated" + +# Check if resource group exists, create if it doesn't +print_status "Checking resource group: $RESOURCE_GROUP" +if ! az group show --name "$RESOURCE_GROUP" &> /dev/null; then + print_warning "Resource group '$RESOURCE_GROUP' does not exist. Creating it..." + if az group create --name "$RESOURCE_GROUP" --location "$LOCATION"; then + print_status "โœ… Resource group created successfully" + else + print_error "โŒ Failed to create resource group" + exit 1 + fi +else + print_status "โœ… Resource group exists" +fi + +# Build deployment command +DEPLOYMENT_NAME="${APP_NAME}-deployment-$(date +%Y%m%d-%H%M%S)" +DEPLOY_CMD="az deployment group create" +DEPLOY_CMD="$DEPLOY_CMD --resource-group $RESOURCE_GROUP" +DEPLOY_CMD="$DEPLOY_CMD --name $DEPLOYMENT_NAME" +DEPLOY_CMD="$DEPLOY_CMD --template-file $TEMPLATE_FILE" + +# Add inline parameters +DEPLOY_CMD="$DEPLOY_CMD --parameters appName=$APP_NAME" +DEPLOY_CMD="$DEPLOY_CMD --parameters location=$LOCATION" +DEPLOY_CMD="$DEPLOY_CMD --parameters environment=$ENVIRONMENT" + +# Add parameters file if specified +if [[ -n "$PARAMETERS_FILE" ]]; then + DEPLOY_CMD="$DEPLOY_CMD --parameters @$PARAMETERS_FILE" +fi + +print_status "Executing deployment..." +print_status "Deployment name: $DEPLOYMENT_NAME" + +# Execute the deployment +if eval "$DEPLOY_CMD"; then + print_status "โœ… Deployment completed successfully!" + + # Get deployment outputs + print_status "Retrieving deployment outputs..." + if az deployment group show --resource-group "$RESOURCE_GROUP" --name "$DEPLOYMENT_NAME" --query properties.outputs &> /dev/null; then + OUTPUTS=$(az deployment group show --resource-group "$RESOURCE_GROUP" --name "$DEPLOYMENT_NAME" --query properties.outputs -o json) + echo "$OUTPUTS" | jq -r 'to_entries[] | " \(.key): \(.value.value)"' 2>/dev/null || echo "$OUTPUTS" + fi +else + print_error "โŒ Deployment failed" + exit 1 +fi + +print_header "๐ŸŽ‰ Deployment completed successfully!" +print_status "You can monitor your deployment in the Azure Portal:" +print_status "https://portal.azure.com/#@/resource/subscriptions/*/resourceGroups/$RESOURCE_GROUP" + +echo "" +print_status "Next steps:" +echo " โ€ข Check application health endpoint" +echo " โ€ข Configure custom domain (if needed)" +echo " โ€ข Set up monitoring and alerts" +echo " โ€ข Configure CI/CD pipeline" \ No newline at end of file