10 - Deployment Strategies

Module
PostgreSQL
Progress
84%

Deployment Strategies

🎯 What This Lab Covers

This lab provides comprehensive guidance on deploying your MCP retail server to production environments using modern containerization and cloud-native approaches.

You'll learn to deploy scalable, secure, and monitored MCP servers that can handle enterprise workloads.

Overview

Production deployment of MCP servers requires careful consideration of containerization, orchestration, security, scalability, and monitoring.

This lab covers deploying to Azure Container Apps with PostgreSQL Flexible Server, implementing CI/CD pipelines, and configuring auto-scaling for variable workloads.

The deployment strategies range from simple single-container deployments for development to sophisticated multi-region, auto-scaling production environments with comprehensive monitoring and security features.

Learning Objectives

By the end of this lab, you will be able to:

  • Containerize MCP servers using Docker with multi-stage builds
  • Deploy to Azure Container Apps with secure networking
  • Configure production-grade PostgreSQL with high availability
  • Implement CI/CD pipelines for automated deployment
  • Scale applications automatically based on demand
  • Monitor production deployments with comprehensive observability
  • 🐳 Docker Containerization

    Multi-Stage Dockerfile

    
    # Dockerfile - Production-ready multi-stage build
    
    FROM python:3.11-slim AS builder
    
    
    
    # Set build environment
    
    ENV PYTHONDONTWRITEBYTECODE=1 \
    
        PYTHONUNBUFFERED=1 \
    
        PIP_NO_CACHE_DIR=1 \
    
        PIP_DISABLE_PIP_VERSION_CHECK=1
    
    
    
    # Install build dependencies
    
    RUN apt-get update && apt-get install -y \
    
        build-essential \
    
        libpq-dev \
    
        curl \
    
        && rm -rf /var/lib/apt/lists/*
    
    
    
    # Create virtual environment
    
    RUN python -m venv /opt/venv
    
    ENV PATH="/opt/venv/bin:$PATH"
    
    
    
    # Copy requirements and install dependencies
    
    COPY requirements.lock.txt /tmp/
    
    RUN pip install --no-cache-dir -r /tmp/requirements.lock.txt
    
    
    
    # Production stage
    
    FROM python:3.11-slim AS production
    
    
    
    # Set production environment
    
    ENV PYTHONDONTWRITEBYTECODE=1 \
    
        PYTHONUNBUFFERED=1 \
    
        PATH="/opt/venv/bin:$PATH" \
    
        PYTHONPATH="/app"
    
    
    
    # Install runtime dependencies
    
    RUN apt-get update && apt-get install -y \
    
        libpq5 \
    
        curl \
    
        && rm -rf /var/lib/apt/lists/* \
    
        && groupadd -r mcp \
    
        && useradd -r -g mcp -d /app -s /bin/bash mcp
    
    
    
    # Copy virtual environment from builder
    
    COPY --from=builder /opt/venv /opt/venv
    
    
    
    # Set working directory and copy application
    
    WORKDIR /app
    
    COPY --chown=mcp:mcp . .
    
    
    
    # Create necessary directories with proper permissions
    
    RUN mkdir -p /app/logs /app/data /tmp/mcp \
    
        && chown -R mcp:mcp /app /tmp/mcp \
    
        && chmod -R 755 /app
    
    
    
    # Health check
    
    HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
    
        CMD python -m mcp_server.health_check || exit 1
    
    
    
    # Switch to non-root user
    
    USER mcp
    
    
    
    # Expose port
    
    EXPOSE 8000
    
    
    
    # Default command
    
    CMD ["python", "-m", "mcp_server.main"]
    
    

    Docker Compose for Development

    
    # docker-compose.yml - Development environment
    
    version: '3.8'
    
    
    
    services:
    
      mcp-server:
    
        build:
    
          context: .
    
          dockerfile: Dockerfile
    
          target: production
    
        ports:
    
          - "8000:8000"
    
        environment:
    
          - POSTGRES_HOST=postgres
    
          - POSTGRES_PORT=5432
    
          - POSTGRES_DB=retail_db
    
          - POSTGRES_USER=mcp_user
    
          - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    
          - PROJECT_ENDPOINT=${PROJECT_ENDPOINT}
    
          - AZURE_CLIENT_ID=${AZURE_CLIENT_ID}
    
          - AZURE_CLIENT_SECRET=${AZURE_CLIENT_SECRET}
    
          - AZURE_TENANT_ID=${AZURE_TENANT_ID}
    
          - LOG_LEVEL=INFO
    
          - ENVIRONMENT=development
    
        depends_on:
    
          postgres:
    
            condition: service_healthy
    
        volumes:
    
          - ./logs:/app/logs
    
        networks:
    
          - mcp-network
    
        restart: unless-stopped
    
        healthcheck:
    
          test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
    
          interval: 30s
    
          timeout: 10s
    
          retries: 3
    
          start_period: 60s
    
    
    
      postgres:
    
        image: pgvector/pgvector:pg16
    
        environment:
    
          - POSTGRES_DB=retail_db
    
          - POSTGRES_USER=postgres
    
          - POSTGRES_PASSWORD=${POSTGRES_ADMIN_PASSWORD}
    
        ports:
    
          - "5432:5432"
    
        volumes:
    
          - postgres_data:/var/lib/postgresql/data
    
          - ./docker-init:/docker-entrypoint-initdb.d
    
          - ./data:/backup
    
        networks:
    
          - mcp-network
    
        restart: unless-stopped
    
        healthcheck:
    
          test: ["CMD-SHELL", "pg_isready -U postgres -d retail_db"]
    
          interval: 30s
    
          timeout: 10s
    
          retries: 3
    
          start_period: 60s
    
    
    
      redis:
    
        image: redis:7-alpine
    
        ports:
    
          - "6379:6379"
    
        command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
    
        volumes:
    
          - redis_data:/data
    
        networks:
    
          - mcp-network
    
        restart: unless-stopped
    
        healthcheck:
    
          test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
    
          interval: 30s
    
          timeout: 10s
    
          retries: 3
    
    
    
    volumes:
    
      postgres_data:
    
        driver: local
    
      redis_data:
    
        driver: local
    
    
    
    networks:
    
      mcp-network:
    
        driver: bridge
    
    

    Production Docker Compose

    
    # docker-compose.prod.yml - Production environment
    
    version: '3.8'
    
    
    
    services:
    
      mcp-server:
    
        image: ${CONTAINER_REGISTRY}/mcp-retail-server:${IMAGE_TAG}
    
        ports:
    
          - "8000:8000"
    
        environment:
    
          - POSTGRES_HOST=${POSTGRES_HOST}
    
          - POSTGRES_PORT=${POSTGRES_PORT}
    
          - POSTGRES_DB=${POSTGRES_DB}
    
          - POSTGRES_USER=${POSTGRES_USER}
    
          - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    
          - PROJECT_ENDPOINT=${PROJECT_ENDPOINT}
    
          - AZURE_CLIENT_ID=${AZURE_CLIENT_ID}
    
          - AZURE_CLIENT_SECRET=${AZURE_CLIENT_SECRET}
    
          - AZURE_TENANT_ID=${AZURE_TENANT_ID}
    
          - APPLICATIONINSIGHTS_CONNECTION_STRING=${APPLICATIONINSIGHTS_CONNECTION_STRING}
    
          - LOG_LEVEL=INFO
    
          - ENVIRONMENT=production
    
          - REDIS_URL=${REDIS_URL}
    
        deploy:
    
          replicas: 3
    
          resources:
    
            limits:
    
              cpus: '2.0'
    
              memory: 2G
    
            reservations:
    
              cpus: '0.5'
    
              memory: 512M
    
          restart_policy:
    
            condition: on-failure
    
            delay: 5s
    
            max_attempts: 3
    
          update_config:
    
            parallelism: 1
    
            delay: 10s
    
            failure_action: rollback
    
        networks:
    
          - mcp-network
    
        healthcheck:
    
          test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
    
          interval: 30s
    
          timeout: 10s
    
          retries: 3
    
          start_period: 60s
    
    
    
    networks:
    
      mcp-network:
    
        external: true
    
    

    ☁️ Azure Container Apps Deployment

    Infrastructure as Code with Bicep

    
    // infra/container-apps.bicep - Azure Container Apps deployment
    
    @description('Location for all resources')
    
    param location string = resourceGroup().location
    
    
    
    @description('Environment name')
    
    param environmentName string
    
    
    
    @description('Container App name')
    
    param containerAppName string
    
    
    
    @description('Container registry details')
    
    param containerRegistry object
    
    
    
    @description('Database connection details')
    
    @secure()
    
    param databaseConnectionString string
    
    
    
    @description('Azure OpenAI configuration')
    
    param azureOpenAI object
    
    
    
    @description('Application Insights workspace ID')
    
    param workspaceId string
    
    
    
    // Container Apps Environment
    
    resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' = {
    
      name: '${environmentName}-env'
    
      location: location
    
      properties: {
    
        appLogsConfiguration: {
    
          destination: 'log-analytics'
    
          logAnalyticsConfiguration: {
    
            customerId: workspaceId
    
          }
    
        }
    
        infrastructureResourceGroup: '${environmentName}-infra-rg'
    
      }
    
    }
    
    
    
    // Container App
    
    resource mcp_retail_server 'Microsoft.App/containerApps@2023-05-01' = {
    
      name: containerAppName
    
      location: location
    
      properties: {
    
        managedEnvironmentId: containerAppsEnvironment.id
    
        configuration: {
    
          activeRevisionsMode: 'Single'
    
          ingress: {
    
            external: false
    
            targetPort: 8000
    
            allowInsecure: false
    
            traffic: [
    
              {
    
                weight: 100
    
                latestRevision: true
    
              }
    
            ]
    
          }
    
          registries: [
    
            {
    
              server: containerRegistry.server
    
              identity: containerRegistry.identity
    
            }
    
          ]
    
          secrets: [
    
            {
    
              name: 'database-connection-string'
    
              value: databaseConnectionString
    
            }
    
            {
    
              name: 'azure-openai-key'
    
              value: azureOpenAI.apiKey
    
            }
    
          ]
    
        }
    
        template: {
    
          containers: [
    
            {
    
              name: 'mcp-retail-server'
    
              image: '${containerRegistry.server}/mcp-retail-server:latest'
    
              resources: {
    
                cpu: json('1.0')
    
                memory: '2Gi'
    
              }
    
              env: [
    
                {
    
                  name: 'POSTGRES_CONNECTION_STRING'
    
                  secretRef: 'database-connection-string'
    
                }
    
                {
    
                  name: 'PROJECT_ENDPOINT'
    
                  value: azureOpenAI.endpoint
    
                }
    
                {
    
                  name: 'AZURE_OPENAI_API_KEY'
    
                  secretRef: 'azure-openai-key'
    
                }
    
                {
    
                  name: 'LOG_LEVEL'
    
                  value: 'INFO'
    
                }
    
                {
    
                  name: 'ENVIRONMENT'
    
                  value: 'production'
    
                }
    
              ]
    
              probes: [
    
                {
    
                  type: 'Liveness'
    
                  httpGet: {
    
                    path: '/health'
    
                    port: 8000
    
                    scheme: 'HTTP'
    
                  }
    
                  initialDelaySeconds: 60
    
                  periodSeconds: 30
    
                  timeoutSeconds: 10
    
                  failureThreshold: 3
    
                }
    
                {
    
                  type: 'Readiness'
    
                  httpGet: {
    
                    path: '/ready'
    
                    port: 8000
    
                    scheme: 'HTTP'
    
                  }
    
                  initialDelaySeconds: 30
    
                  periodSeconds: 10
    
                  timeoutSeconds: 5
    
                  failureThreshold: 3
    
                }
    
              ]
    
            }
    
          ]
    
          scale: {
    
            minReplicas: 2
    
            maxReplicas: 20
    
            rules: [
    
              {
    
                name: 'http-scaling'
    
                http: {
    
                  metadata: {
    
                    concurrentRequests: '10'
    
                  }
    
                }
    
              }
    
              {
    
                name: 'cpu-scaling'
    
                custom: {
    
                  type: 'cpu'
    
                  metadata: {
    
                    type: 'Utilization'
    
                    value: '70'
    
                  }
    
                }
    
              }
    
            ]
    
          }
    
        }
    
      }
    
    }
    
    
    
    // Output the FQDN
    
    output containerAppFQDN string = mcp_retail_server.properties.configuration.ingress.fqdn
    
    output containerAppId string = mcp_retail_server.id
    
    

    PostgreSQL Flexible Server

    
    // infra/database.bicep - PostgreSQL Flexible Server
    
    @description('Location for all resources')
    
    param location string = resourceGroup().location
    
    
    
    @description('PostgreSQL server name')
    
    param serverName string
    
    
    
    @description('Database administrator login')
    
    param administratorLogin string
    
    
    
    @description('Database administrator password')
    
    @secure()
    
    param administratorPassword string
    
    
    
    @description('Virtual network subnet ID')
    
    param subnetId string
    
    
    
    @description('Private DNS zone ID')
    
    param privateDnsZoneId string
    
    
    
    // PostgreSQL Flexible Server
    
    resource postgresqlServer 'Microsoft.DBforPostgreSQL/flexibleServers@2023-03-01-preview' = {
    
      name: serverName
    
      location: location
    
      sku: {
    
        name: 'Standard_D4s_v3'
    
        tier: 'GeneralPurpose'
    
      }
    
      properties: {
    
        administratorLogin: administratorLogin
    
        administratorLoginPassword: administratorPassword
    
        version: '16'
    
        storage: {
    
          storageSizeGB: 128
    
          autoGrow: 'Enabled'
    
          type: 'PremiumSSD'
    
        }
    
        backup: {
    
          backupRetentionDays: 35
    
          geoRedundantBackup: 'Enabled'
    
        }
    
        highAvailability: {
    
          mode: 'ZoneRedundant'
    
        }
    
        network: {
    
          delegatedSubnetResourceId: subnetId
    
          privateDnsZoneArmResourceId: privateDnsZoneId
    
        }
    
        maintenanceWindow: {
    
          dayOfWeek: 0
    
          startHour: 2
    
          startMinute: 0
    
        }
    
      }
    
    }
    
    
    
    // Database
    
    resource retailDatabase 'Microsoft.DBforPostgreSQL/flexibleServers/databases@2023-03-01-preview' = {
    
      parent: postgresqlServer
    
      name: 'retail_db'
    
      properties: {
    
        charset: 'UTF8'
    
        collation: 'en_US.utf8'
    
      }
    
    }
    
    
    
    // PostgreSQL extensions
    
    resource pgvectorExtension 'Microsoft.DBforPostgreSQL/flexibleServers/configurations@2023-03-01-preview' = {
    
      parent: postgresqlServer
    
      name: 'shared_preload_libraries'
    
      properties: {
    
        value: 'pg_stat_statements,pgaudit,vector'
    
        source: 'user-override'
    
      }
    
    }
    
    
    
    // Output connection details
    
    output serverFQDN string = postgresqlServer.properties.fullyQualifiedDomainName
    
    output serverId string = postgresqlServer.id
    
    output databaseName string = retailDatabase.name
    
    

    🚀 CI/CD Pipeline Configuration

    GitHub Actions Workflow

    
    # .github/workflows/deploy.yml - CI/CD pipeline
    
    name: Deploy MCP Retail Server
    
    
    
    on:
    
      push:
    
        branches: [main]
    
      pull_request:
    
        branches: [main]
    
      workflow_dispatch:
    
        inputs:
    
          environment:
    
            description: 'Deployment environment'
    
            required: true
    
            default: 'development'
    
            type: choice
    
            options:
    
              - development
    
              - staging
    
              - production
    
    
    
    env:
    
      CONTAINER_REGISTRY: mcpretailregistry.azurecr.io
    
      IMAGE_NAME: mcp-retail-server
    
      AZURE_RESOURCE_GROUP: mcp-retail-rg
    
    
    
    jobs:
    
      test:
    
        runs-on: ubuntu-latest
    
        services:
    
          postgres:
    
            image: pgvector/pgvector:pg16
    
            env:
    
              POSTGRES_PASSWORD: postgres
    
              POSTGRES_DB: retail_test
    
            options: >-
    
              --health-cmd pg_isready
    
              --health-interval 10s
    
              --health-timeout 5s
    
              --health-retries 5
    
            ports:
    
              - 5432:5432
    
    
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Set up Python
    
            uses: actions/setup-python@v4
    
            with:
    
              python-version: '3.11'
    
              cache: 'pip'
    
    
    
          - name: Install dependencies
    
            run: |
    
              python -m pip install --upgrade pip
    
              pip install -r requirements.lock.txt
    
              pip install pytest pytest-cov pytest-asyncio
    
    
    
          - name: Set up test database
    
            run: |
    
              PGPASSWORD=postgres psql -h localhost -U postgres -d retail_test -f scripts/create_schema.sql
    
              python scripts/generate_sample_data.py --test
    
            env:
    
              POSTGRES_HOST: localhost
    
              POSTGRES_PORT: 5432
    
              POSTGRES_DB: retail_test
    
              POSTGRES_USER: postgres
    
              POSTGRES_PASSWORD: postgres
    
    
    
          - name: Run tests
    
            run: |
    
              pytest tests/ -v --cov=mcp_server --cov-report=xml --cov-report=html
    
            env:
    
              POSTGRES_HOST: localhost
    
              POSTGRES_PORT: 5432
    
              POSTGRES_DB: retail_test
    
              POSTGRES_USER: postgres
    
              POSTGRES_PASSWORD: postgres
    
              PROJECT_ENDPOINT: ${{ secrets.TEST_PROJECT_ENDPOINT }}
    
              AZURE_CLIENT_ID: ${{ secrets.TEST_AZURE_CLIENT_ID }}
    
              AZURE_CLIENT_SECRET: ${{ secrets.TEST_AZURE_CLIENT_SECRET }}
    
              AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
    
    
    
          - name: Upload coverage reports
    
            uses: codecov/codecov-action@v3
    
            with:
    
              file: ./coverage.xml
    
              flags: unittests
    
    
    
      security-scan:
    
        runs-on: ubuntu-latest
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Run Trivy vulnerability scanner
    
            uses: aquasecurity/trivy-action@master
    
            with:
    
              scan-type: 'fs'
    
              scan-ref: '.'
    
              format: 'sarif'
    
              output: 'trivy-results.sarif'
    
    
    
          - name: Upload Trivy scan results
    
            uses: github/codeql-action/upload-sarif@v2
    
            with:
    
              sarif_file: 'trivy-results.sarif'
    
    
    
          - name: Run Bandit security linter
    
            run: |
    
              pip install bandit[toml]
    
              bandit -r mcp_server/ -f json -o bandit-report.json
    
    
    
      build:
    
        runs-on: ubuntu-latest
    
        needs: [test, security-scan]
    
        if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
    
        
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Azure Login
    
            uses: azure/login@v1
    
            with:
    
              creds: ${{ secrets.AZURE_CREDENTIALS }}
    
    
    
          - name: Build and push Docker image
    
            uses: azure/docker-login@v1
    
            with:
    
              login-server: ${{ env.CONTAINER_REGISTRY }}
    
              username: ${{ secrets.REGISTRY_USERNAME }}
    
              password: ${{ secrets.REGISTRY_PASSWORD }}
    
    
    
          - name: Build, tag, and push image
    
            run: |
    
              # Generate unique tag
    
              IMAGE_TAG="${GITHUB_SHA::8}-$(date +%s)"
    
              
    
              # Build image
    
              docker build \
    
                --target production \
    
                --tag $CONTAINER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG \
    
                --tag $CONTAINER_REGISTRY/$IMAGE_NAME:latest \
    
                .
    
              
    
              # Push images
    
              docker push $CONTAINER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG
    
              docker push $CONTAINER_REGISTRY/$IMAGE_NAME:latest
    
              
    
              # Save tag for deployment
    
              echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
    
    
    
          - name: Output image details
    
            run: |
    
              echo "Built and pushed image: $CONTAINER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG"
    
    
    
      deploy-staging:
    
        runs-on: ubuntu-latest
    
        needs: build
    
        if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    
        environment: staging
    
    
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Azure Login
    
            uses: azure/login@v1
    
            with:
    
              creds: ${{ secrets.AZURE_CREDENTIALS }}
    
    
    
          - name: Deploy to staging
    
            uses: azure/CLI@v1
    
            with:
    
              azcliversion: latest
    
              inlineScript: |
    
                # Deploy infrastructure
    
                az deployment group create \
    
                  --resource-group $AZURE_RESOURCE_GROUP-staging \
    
                  --template-file infra/main.bicep \
    
                  --parameters infra/main.parameters.staging.json \
    
                  --parameters containerImageTag=$IMAGE_TAG
    
    
    
                # Update container app
    
                az containerapp update \
    
                  --name mcp-retail-server-staging \
    
                  --resource-group $AZURE_RESOURCE_GROUP-staging \
    
                  --image $CONTAINER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG
    
    
    
          - name: Run integration tests
    
            run: |
    
              # Wait for deployment to be ready
    
              sleep 60
    
              
    
              # Run integration tests against staging
    
              pytest tests/integration/ \
    
                --endpoint https://mcp-retail-server-staging.azurecontainerapps.io \
    
                --timeout 300
    
    
    
      deploy-production:
    
        runs-on: ubuntu-latest
    
        needs: [build, deploy-staging]
    
        if: github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'production'
    
        environment: production
    
    
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Azure Login
    
            uses: azure/login@v1
    
            with:
    
              creds: ${{ secrets.AZURE_CREDENTIALS }}
    
    
    
          - name: Deploy to production
    
            uses: azure/CLI@v1
    
            with:
    
              azcliversion: latest
    
              inlineScript: |
    
                # Deploy with blue-green strategy
    
                az deployment group create \
    
                  --resource-group $AZURE_RESOURCE_GROUP-prod \
    
                  --template-file infra/main.bicep \
    
                  --parameters infra/main.parameters.prod.json \
    
                  --parameters containerImageTag=$IMAGE_TAG \
    
                  --parameters deploymentSlot=green
    
    
    
                # Health check
    
                az containerapp show \
    
                  --name mcp-retail-server-prod-green \
    
                  --resource-group $AZURE_RESOURCE_GROUP-prod
    
    
    
                # Switch traffic (blue-green deployment)
    
                az containerapp ingress traffic set \
    
                  --name mcp-retail-server-prod \
    
                  --resource-group $AZURE_RESOURCE_GROUP-prod \
    
                  --revision-weight latest=100
    
    

    Azure DevOps Pipeline

    
    # azure-pipelines.yml - Azure DevOps pipeline
    
    trigger:
    
      branches:
    
        include:
    
          - main
    
          - develop
    
      paths:
    
        exclude:
    
          - docs/*
    
          - README.md
    
    
    
    variables:
    
      containerRegistry: 'mcpretailregistry.azurecr.io'
    
      imageName: 'mcp-retail-server'
    
      imageTag: '$(Build.BuildId)'
    
      azureServiceConnection: 'azure-service-connection'
    
    
    
    stages:
    
      - stage: Build
    
        displayName: 'Build and Test'
    
        jobs:
    
          - job: Test
    
            displayName: 'Run Tests'
    
            pool:
    
              vmImage: 'ubuntu-latest'
    
            
    
            services:
    
              postgres:
    
                image: pgvector/pgvector:pg16
    
                env:
    
                  POSTGRES_PASSWORD: postgres
    
                  POSTGRES_DB: retail_test
    
                ports:
    
                  5432:5432
    
    
    
            steps:
    
              - task: UsePythonVersion@0
    
                inputs:
    
                  versionSpec: '3.11'
    
                  displayName: 'Use Python 3.11'
    
    
    
              - script: |
    
                  python -m pip install --upgrade pip
    
                  pip install -r requirements.lock.txt
    
                  pip install pytest pytest-cov pytest-asyncio
    
                displayName: 'Install dependencies'
    
    
    
              - script: |
    
                  PGPASSWORD=postgres psql -h localhost -U postgres -d retail_test -f scripts/create_schema.sql
    
                  python scripts/generate_sample_data.py --test
    
                displayName: 'Set up test database'
    
                env:
    
                  POSTGRES_HOST: localhost
    
                  POSTGRES_PORT: 5432
    
                  POSTGRES_DB: retail_test
    
                  POSTGRES_USER: postgres
    
                  POSTGRES_PASSWORD: postgres
    
    
    
              - script: |
    
                  pytest tests/ -v --cov=mcp_server --cov-report=xml --junitxml=test-results.xml
    
                displayName: 'Run tests'
    
                env:
    
                  POSTGRES_HOST: localhost
    
                  POSTGRES_PORT: 5432
    
                  POSTGRES_DB: retail_test
    
                  POSTGRES_USER: postgres
    
                  POSTGRES_PASSWORD: postgres
    
    
    
              - task: PublishTestResults@2
    
                condition: succeededOrFailed()
    
                inputs:
    
                  testResultsFiles: 'test-results.xml'
    
                  testRunTitle: 'Python Tests'
    
    
    
              - task: PublishCodeCoverageResults@1
    
                inputs:
    
                  codeCoverageTool: 'Cobertura'
    
                  summaryFileLocation: 'coverage.xml'
    
    
    
          - job: Build
    
            displayName: 'Build Docker Image'
    
            dependsOn: Test
    
            pool:
    
              vmImage: 'ubuntu-latest'
    
    
    
            steps:
    
              - task: AzureCLI@2
    
                displayName: 'Build and push Docker image'
    
                inputs:
    
                  azureSubscription: $(azureServiceConnection)
    
                  scriptType: 'bash'
    
                  scriptLocation: 'inlineScript'
    
                  inlineScript: |
    
                    # Login to container registry
    
                    az acr login --name $(containerRegistry)
    
                    
    
                    # Build and push image
    
                    docker build \
    
                      --target production \
    
                      --tag $(containerRegistry)/$(imageName):$(imageTag) \
    
                      --tag $(containerRegistry)/$(imageName):latest \
    
                      .
    
                    
    
                    docker push $(containerRegistry)/$(imageName):$(imageTag)
    
                    docker push $(containerRegistry)/$(imageName):latest
    
    
    
      - stage: Deploy_Staging
    
        displayName: 'Deploy to Staging'
    
        dependsOn: Build
    
        condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    
        
    
        jobs:
    
          - deployment: DeployStaging
    
            displayName: 'Deploy to Staging Environment'
    
            pool:
    
              vmImage: 'ubuntu-latest'
    
            environment: 'staging'
    
            
    
            strategy:
    
              runOnce:
    
                deploy:
    
                  steps:
    
                    - task: AzureCLI@2
    
                      displayName: 'Deploy infrastructure'
    
                      inputs:
    
                        azureSubscription: $(azureServiceConnection)
    
                        scriptType: 'bash'
    
                        scriptLocation: 'inlineScript'
    
                        inlineScript: |
    
                          az deployment group create \
    
                            --resource-group mcp-retail-staging-rg \
    
                            --template-file infra/main.bicep \
    
                            --parameters infra/main.parameters.staging.json \
    
                            --parameters containerImageTag=$(imageTag)
    
    
    
      - stage: Deploy_Production
    
        displayName: 'Deploy to Production'
    
        dependsOn: Deploy_Staging
    
        condition: and(succeeded(), eq(variables['Build.Reason'], 'Manual'))
    
        
    
        jobs:
    
          - deployment: DeployProduction
    
            displayName: 'Deploy to Production Environment'
    
            pool:
    
              vmImage: 'ubuntu-latest'
    
            environment: 'production'
    
            
    
            strategy:
    
              runOnce:
    
                deploy:
    
                  steps:
    
                    - task: AzureCLI@2
    
                      displayName: 'Deploy to production'
    
                      inputs:
    
                        azureSubscription: $(azureServiceConnection)
    
                        scriptType: 'bash'
    
                        scriptLocation: 'inlineScript'
    
                        inlineScript: |
    
                          az deployment group create \
    
                            --resource-group mcp-retail-prod-rg \
    
                            --template-file infra/main.bicep \
    
                            --parameters infra/main.parameters.prod.json \
    
                            --parameters containerImageTag=$(imageTag)
    
    

    📊 Scaling and Performance

    Auto-scaling Configuration

    
    # k8s/hpa.yaml - Horizontal Pod Autoscaler for Kubernetes
    
    apiVersion: autoscaling/v2
    
    kind: HorizontalPodAutoscaler
    
    metadata:
    
      name: mcp-retail-server-hpa
    
      namespace: mcp-retail
    
    spec:
    
      scaleTargetRef:
    
        apiVersion: apps/v1
    
        kind: Deployment
    
        name: mcp-retail-server
    
      minReplicas: 3
    
      maxReplicas: 50
    
      metrics:
    
        - type: Resource
    
          resource:
    
            name: cpu
    
            target:
    
              type: Utilization
    
              averageUtilization: 70
    
        - type: Resource
    
          resource:
    
            name: memory
    
            target:
    
              type: Utilization
    
              averageUtilization: 80
    
        - type: Pods
    
          pods:
    
            metric:
    
              name: http_requests_per_second
    
            target:
    
              type: AverageValue
    
              averageValue: 100
    
      behavior:
    
        scaleDown:
    
          stabilizationWindowSeconds: 300
    
          policies:
    
            - type: Percent
    
              value: 50
    
              periodSeconds: 60
    
        scaleUp:
    
          stabilizationWindowSeconds: 60
    
          policies:
    
            - type: Percent
    
              value: 100
    
              periodSeconds: 30
    
            - type: Pods
    
              value: 5
    
              periodSeconds: 30
    
          selectPolicy: Max
    
    

    Performance Monitoring

    
    # mcp_server/monitoring/performance.py
    
    """
    
    Performance monitoring and metrics collection for production deployment.
    
    """
    
    import asyncio
    
    import time
    
    import psutil
    
    from typing import Dict, Any
    
    from dataclasses import dataclass
    
    from datetime import datetime, timedelta
    
    import logging
    
    
    
    @dataclass
    
    class PerformanceMetrics:
    
        """Performance metrics data structure."""
    
        timestamp: datetime
    
        cpu_percent: float
    
        memory_percent: float
    
        memory_used_mb: float
    
        active_connections: int
    
        request_rate: float
    
        avg_response_time: float
    
        error_rate: float
    
        database_connections: int
    
    
    
    class PerformanceMonitor:
    
        """Monitor and collect performance metrics."""
    
        
    
        def __init__(self, config):
    
            self.config = config
    
            self.logger = logging.getLogger(__name__)
    
            
    
            # Metrics collection
    
            self.metrics_history = []
    
            self.request_times = []
    
            self.error_count = 0
    
            self.request_count = 0
    
            
    
            # Database monitoring
    
            self.db_pool = None
    
            
    
        async def start_monitoring(self):
    
            """Start continuous performance monitoring."""
    
            
    
            self.logger.info("Starting performance monitoring")
    
            
    
            # Start metrics collection task
    
            asyncio.create_task(self._collect_metrics_loop())
    
            asyncio.create_task(self._cleanup_old_metrics())
    
            
    
        async def _collect_metrics_loop(self):
    
            """Continuously collect performance metrics."""
    
            
    
            while True:
    
                try:
    
                    metrics = await self._collect_current_metrics()
    
                    self.metrics_history.append(metrics)
    
                    
    
                    # Log critical metrics
    
                    if metrics.cpu_percent > 90:
    
                        self.logger.warning(f"High CPU usage: {metrics.cpu_percent:.1f}%")
    
                    
    
                    if metrics.memory_percent > 90:
    
                        self.logger.warning(f"High memory usage: {metrics.memory_percent:.1f}%")
    
                    
    
                    if metrics.error_rate > 0.05:  # 5% error rate
    
                        self.logger.warning(f"High error rate: {metrics.error_rate:.2%}")
    
                    
    
                    await asyncio.sleep(30)  # Collect every 30 seconds
    
                    
    
                except Exception as e:
    
                    self.logger.error(f"Error collecting metrics: {e}")
    
                    await asyncio.sleep(60)
    
        
    
        async def _collect_current_metrics(self) -> PerformanceMetrics:
    
            """Collect current system metrics."""
    
            
    
            # System metrics
    
            cpu_percent = psutil.cpu_percent(interval=1)
    
            memory = psutil.virtual_memory()
    
            
    
            # Application metrics
    
            current_time = datetime.utcnow()
    
            recent_requests = [
    
                req_time for req_time in self.request_times
    
                if current_time - req_time < timedelta(minutes=1)
    
            ]
    
            
    
            request_rate = len(recent_requests) / 60.0  # requests per second
    
            
    
            # Calculate average response time
    
            avg_response_time = 0.0
    
            if hasattr(self, '_recent_response_times'):
    
                recent_response_times = [
    
                    rt for rt in self._recent_response_times
    
                    if current_time - rt['timestamp'] < timedelta(minutes=5)
    
                ]
    
                if recent_response_times:
    
                    avg_response_time = sum(rt['time'] for rt in recent_response_times) / len(recent_response_times)
    
            
    
            # Error rate calculation
    
            error_rate = 0.0
    
            if self.request_count > 0:
    
                error_rate = self.error_count / self.request_count
    
            
    
            # Database connections
    
            db_connections = 0
    
            if self.db_pool:
    
                db_connections = len(self.db_pool._holders)
    
            
    
            return PerformanceMetrics(
    
                timestamp=current_time,
    
                cpu_percent=cpu_percent,
    
                memory_percent=memory.percent,
    
                memory_used_mb=memory.used / (1024 * 1024),
    
                active_connections=0,  # To be implemented with connection tracking
    
                request_rate=request_rate,
    
                avg_response_time=avg_response_time,
    
                error_rate=error_rate,
    
                database_connections=db_connections
    
            )
    
        
    
        async def _cleanup_old_metrics(self):
    
            """Clean up old metrics to prevent memory leaks."""
    
            
    
            while True:
    
                try:
    
                    cutoff_time = datetime.utcnow() - timedelta(hours=24)
    
                    
    
                    # Clean up metrics history
    
                    self.metrics_history = [
    
                        m for m in self.metrics_history
    
                        if m.timestamp > cutoff_time
    
                    ]
    
                    
    
                    # Clean up request times
    
                    self.request_times = [
    
                        rt for rt in self.request_times
    
                        if rt > cutoff_time
    
                    ]
    
                    
    
                    # Reset counters periodically
    
                    if datetime.utcnow().minute == 0:  # Every hour
    
                        self.error_count = 0
    
                        self.request_count = 0
    
                    
    
                    await asyncio.sleep(3600)  # Run every hour
    
                    
    
                except Exception as e:
    
                    self.logger.error(f"Error cleaning up metrics: {e}")
    
                    await asyncio.sleep(3600)
    
        
    
        def record_request(self, response_time: float, success: bool = True):
    
            """Record a request for metrics."""
    
            
    
            current_time = datetime.utcnow()
    
            self.request_times.append(current_time)
    
            self.request_count += 1
    
            
    
            if not success:
    
                self.error_count += 1
    
            
    
            # Record response time
    
            if not hasattr(self, '_recent_response_times'):
    
                self._recent_response_times = []
    
            
    
            self._recent_response_times.append({
    
                'timestamp': current_time,
    
                'time': response_time
    
            })
    
        
    
        def get_current_metrics(self) -> Dict[str, Any]:
    
            """Get current performance metrics."""
    
            
    
            if not self.metrics_history:
    
                return {}
    
            
    
            latest_metrics = self.metrics_history[-1]
    
            
    
            return {
    
                'timestamp': latest_metrics.timestamp.isoformat(),
    
                'system': {
    
                    'cpu_percent': latest_metrics.cpu_percent,
    
                    'memory_percent': latest_metrics.memory_percent,
    
                    'memory_used_mb': latest_metrics.memory_used_mb
    
                },
    
                'application': {
    
                    'active_connections': latest_metrics.active_connections,
    
                    'request_rate': latest_metrics.request_rate,
    
                    'avg_response_time': latest_metrics.avg_response_time,
    
                    'error_rate': latest_metrics.error_rate
    
                },
    
                'database': {
    
                    'connections': latest_metrics.database_connections
    
                }
    
            }
    
        
    
        def get_metrics_summary(self, hours: int = 24) -> Dict[str, Any]:
    
            """Get performance metrics summary for the specified hours."""
    
            
    
            cutoff_time = datetime.utcnow() - timedelta(hours=hours)
    
            recent_metrics = [
    
                m for m in self.metrics_history
    
                if m.timestamp > cutoff_time
    
            ]
    
            
    
            if not recent_metrics:
    
                return {}
    
            
    
            # Calculate averages
    
            avg_cpu = sum(m.cpu_percent for m in recent_metrics) / len(recent_metrics)
    
            avg_memory = sum(m.memory_percent for m in recent_metrics) / len(recent_metrics)
    
            avg_response_time = sum(m.avg_response_time for m in recent_metrics) / len(recent_metrics)
    
            
    
            # Calculate peaks
    
            max_cpu = max(m.cpu_percent for m in recent_metrics)
    
            max_memory = max(m.memory_percent for m in recent_metrics)
    
            max_response_time = max(m.avg_response_time for m in recent_metrics)
    
            
    
            return {
    
                'period_hours': hours,
    
                'averages': {
    
                    'cpu_percent': round(avg_cpu, 2),
    
                    'memory_percent': round(avg_memory, 2),
    
                    'response_time': round(avg_response_time, 3)
    
                },
    
                'peaks': {
    
                    'cpu_percent': round(max_cpu, 2),
    
                    'memory_percent': round(max_memory, 2),
    
                    'response_time': round(max_response_time, 3)
    
                },
    
                'data_points': len(recent_metrics)
    
            }
    
    

    🔐 Production Security Configuration

    Security Hardening

    
    # k8s/security-policy.yaml - Kubernetes security policies
    
    apiVersion: v1
    
    kind: SecurityContext
    
    metadata:
    
      name: mcp-retail-security-context
    
    spec:
    
      runAsNonRoot: true
    
      runAsUser: 1000
    
      runAsGroup: 1000
    
      fsGroup: 1000
    
      seccompProfile:
    
        type: RuntimeDefault
    
      capabilities:
    
        drop:
    
          - ALL
    
      readOnlyRootFilesystem: true
    
      allowPrivilegeEscalation: false
    
    
    
    ---
    
    apiVersion: networking.k8s.io/v1
    
    kind: NetworkPolicy
    
    metadata:
    
      name: mcp-retail-network-policy
    
      namespace: mcp-retail
    
    spec:
    
      podSelector:
    
        matchLabels:
    
          app: mcp-retail-server
    
      policyTypes:
    
        - Ingress
    
        - Egress
    
      ingress:
    
        - from:
    
            - namespaceSelector:
    
                matchLabels:
    
                  name: ingress-nginx
    
          ports:
    
            - protocol: TCP
    
              port: 8000
    
      egress:
    
        - to:
    
            - namespaceSelector:
    
                matchLabels:
    
                  name: database
    
          ports:
    
            - protocol: TCP
    
              port: 5432
    
        - to: []
    
          ports:
    
            - protocol: TCP
    
              port: 443  # HTTPS for Azure OpenAI
    
            - protocol: TCP
    
              port: 53   # DNS
    
            - protocol: UDP
    
              port: 53   # DNS
    
    

    Environment Configuration

    
    # scripts/setup-production-env.sh
    
    #!/bin/bash
    
    
    
    # Production environment setup script
    
    set -euo pipefail
    
    
    
    echo "🔧 Setting up production environment..."
    
    
    
    # Create resource groups
    
    az group create --name "mcp-retail-prod-rg" --location "East US"
    
    az group create --name "mcp-retail-shared-rg" --location "East US"
    
    
    
    # Create Key Vault
    
    echo "🔐 Creating Azure Key Vault..."
    
    az keyvault create \
    
      --name "mcp-retail-kv-prod" \
    
      --resource-group "mcp-retail-shared-rg" \
    
      --location "East US" \
    
      --enable-rbac-authorization true
    
    
    
    # Set secrets
    
    echo "🔑 Setting up secrets..."
    
    az keyvault secret set \
    
      --vault-name "mcp-retail-kv-prod" \
    
      --name "postgres-password" \
    
      --value "${POSTGRES_PASSWORD}"
    
    
    
    az keyvault secret set \
    
      --vault-name "mcp-retail-kv-prod" \
    
      --name "azure-openai-key" \
    
      --value "${AZURE_OPENAI_KEY}"
    
    
    
    # Create container registry
    
    echo "📦 Creating container registry..."
    
    az acr create \
    
      --name "mcpretailregistry" \
    
      --resource-group "mcp-retail-shared-rg" \
    
      --sku Premium \
    
      --admin-enabled false
    
    
    
    # Create virtual network
    
    echo "🌐 Creating virtual network..."
    
    az network vnet create \
    
      --name "mcp-retail-vnet" \
    
      --resource-group "mcp-retail-shared-rg" \
    
      --address-prefix "10.0.0.0/16" \
    
      --subnet-name "container-apps" \
    
      --subnet-prefix "10.0.1.0/24"
    
    
    
    az network vnet subnet create \
    
      --name "database" \
    
      --resource-group "mcp-retail-shared-rg" \
    
      --vnet-name "mcp-retail-vnet" \
    
      --address-prefix "10.0.2.0/24" \
    
      --delegations Microsoft.DBforPostgreSQL/flexibleServers
    
    
    
    # Deploy infrastructure
    
    echo "🏗️  Deploying infrastructure..."
    
    az deployment group create \
    
      --resource-group "mcp-retail-prod-rg" \
    
      --template-file "infra/main.bicep" \
    
      --parameters "infra/main.parameters.prod.json"
    
    
    
    echo "✅ Production environment setup complete!"
    
    

    🎯 Key Takeaways

    After completing this lab, you should have:

    Container Strategy: Production-ready Docker containers with security hardening

    Cloud Deployment: Azure Container Apps with auto-scaling and monitoring

    Database Deployment: PostgreSQL Flexible Server with high availability

    CI/CD Pipelines: Automated testing, building, and deployment workflows

    Performance Monitoring: Comprehensive metrics collection and alerting

    Security Configuration: Production-grade security policies and network isolation

    🚀 What's Next

    Continue with Lab 11: Monitoring and Observability to:

  • Set up comprehensive monitoring with Application Insights
  • Configure structured logging and distributed tracing
  • Implement alerting and automated response systems
  • Monitor business metrics and performance KPIs
  • 📚 Additional Resources

    Container Technologies

  • Docker Best Practices - Official Docker best practices
  • Azure Container Apps - Azure Container Apps documentation
  • Kubernetes Documentation - Kubernetes official documentation
  • CI/CD and DevOps

  • GitHub Actions - GitHub Actions documentation
  • Azure DevOps - Azure DevOps services
  • Infrastructure as Code - Azure Bicep documentation
  • Security and Monitoring

  • Azure Security Center - Azure security recommendations
  • Container Security - Kubernetes security concepts
  • Application Insights - Azure Application Insights
  • ---

    Previous: Lab 09: VS Code Integration

    Next: Lab 11: Monitoring and Observability

    배포 전략

    🎯 이 실습에서 다루는 내용

    이 실습은 현대적인 컨테이너화 및 클라우드 네이티브 접근 방식을 사용하여 MCP 리테일 서버를 프로덕션 환경에 배포하는 포괄적인 가이드를 제공합니다. 엔터프라이즈 워크로드를 처리할 수 있는 확장 가능하고 안전하며 모니터링 가능한 MCP 서버를 배포하는 방법을 배우게 됩니다.

    개요

    MCP 서버의 프로덕션 배포는 컨테이너화, 오케스트레이션, 보안, 확장성 및 모니터링을 신중히 고려해야 합니다. 이 실습에서는 Azure Container Apps와 PostgreSQL Flexible Server를 사용한 배포, CI/CD 파이프라인 구현, 가변 워크로드를 위한 자동 확장 구성에 대해 다룹니다.

    배포 전략은 개발을 위한 간단한 단일 컨테이너 배포부터 다중 지역, 자동 확장 프로덕션 환경에 이르기까지 포괄적인 모니터링 및 보안 기능을 갖춘 복잡한 배포까지 다양합니다.

    학습 목표

    이 실습을 완료하면 다음을 수행할 수 있습니다:

  • 컨테이너화: Docker를 사용하여 MCP 서버를 멀티 스테이지 빌드로 컨테이너화
  • 배포: 안전한 네트워킹을 통해 Azure Container Apps에 배포
  • 구성: 고가용성을 갖춘 프로덕션급 PostgreSQL 구성
  • 구현: 자동 배포를 위한 CI/CD 파이프라인
  • 확장: 수요에 따라 애플리케이션 자동 확장
  • 모니터링: 포괄적인 관측 기능을 통해 프로덕션 배포 모니터링
  • 🐳 Docker 컨테이너화

    멀티 스테이지 Dockerfile

    
    # Dockerfile - Production-ready multi-stage build
    
    FROM python:3.11-slim AS builder
    
    
    
    # Set build environment
    
    ENV PYTHONDONTWRITEBYTECODE=1 \
    
        PYTHONUNBUFFERED=1 \
    
        PIP_NO_CACHE_DIR=1 \
    
        PIP_DISABLE_PIP_VERSION_CHECK=1
    
    
    
    # Install build dependencies
    
    RUN apt-get update && apt-get install -y \
    
        build-essential \
    
        libpq-dev \
    
        curl \
    
        && rm -rf /var/lib/apt/lists/*
    
    
    
    # Create virtual environment
    
    RUN python -m venv /opt/venv
    
    ENV PATH="/opt/venv/bin:$PATH"
    
    
    
    # Copy requirements and install dependencies
    
    COPY requirements.lock.txt /tmp/
    
    RUN pip install --no-cache-dir -r /tmp/requirements.lock.txt
    
    
    
    # Production stage
    
    FROM python:3.11-slim AS production
    
    
    
    # Set production environment
    
    ENV PYTHONDONTWRITEBYTECODE=1 \
    
        PYTHONUNBUFFERED=1 \
    
        PATH="/opt/venv/bin:$PATH" \
    
        PYTHONPATH="/app"
    
    
    
    # Install runtime dependencies
    
    RUN apt-get update && apt-get install -y \
    
        libpq5 \
    
        curl \
    
        && rm -rf /var/lib/apt/lists/* \
    
        && groupadd -r mcp \
    
        && useradd -r -g mcp -d /app -s /bin/bash mcp
    
    
    
    # Copy virtual environment from builder
    
    COPY --from=builder /opt/venv /opt/venv
    
    
    
    # Set working directory and copy application
    
    WORKDIR /app
    
    COPY --chown=mcp:mcp . .
    
    
    
    # Create necessary directories with proper permissions
    
    RUN mkdir -p /app/logs /app/data /tmp/mcp \
    
        && chown -R mcp:mcp /app /tmp/mcp \
    
        && chmod -R 755 /app
    
    
    
    # Health check
    
    HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
    
        CMD python -m mcp_server.health_check || exit 1
    
    
    
    # Switch to non-root user
    
    USER mcp
    
    
    
    # Expose port
    
    EXPOSE 8000
    
    
    
    # Default command
    
    CMD ["python", "-m", "mcp_server.main"]
    
    

    개발용 Docker Compose

    
    # docker-compose.yml - Development environment
    
    version: '3.8'
    
    
    
    services:
    
      mcp-server:
    
        build:
    
          context: .
    
          dockerfile: Dockerfile
    
          target: production
    
        ports:
    
          - "8000:8000"
    
        environment:
    
          - POSTGRES_HOST=postgres
    
          - POSTGRES_PORT=5432
    
          - POSTGRES_DB=retail_db
    
          - POSTGRES_USER=mcp_user
    
          - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    
          - PROJECT_ENDPOINT=${PROJECT_ENDPOINT}
    
          - AZURE_CLIENT_ID=${AZURE_CLIENT_ID}
    
          - AZURE_CLIENT_SECRET=${AZURE_CLIENT_SECRET}
    
          - AZURE_TENANT_ID=${AZURE_TENANT_ID}
    
          - LOG_LEVEL=INFO
    
          - ENVIRONMENT=development
    
        depends_on:
    
          postgres:
    
            condition: service_healthy
    
        volumes:
    
          - ./logs:/app/logs
    
        networks:
    
          - mcp-network
    
        restart: unless-stopped
    
        healthcheck:
    
          test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
    
          interval: 30s
    
          timeout: 10s
    
          retries: 3
    
          start_period: 60s
    
    
    
      postgres:
    
        image: pgvector/pgvector:pg16
    
        environment:
    
          - POSTGRES_DB=retail_db
    
          - POSTGRES_USER=postgres
    
          - POSTGRES_PASSWORD=${POSTGRES_ADMIN_PASSWORD}
    
        ports:
    
          - "5432:5432"
    
        volumes:
    
          - postgres_data:/var/lib/postgresql/data
    
          - ./docker-init:/docker-entrypoint-initdb.d
    
          - ./data:/backup
    
        networks:
    
          - mcp-network
    
        restart: unless-stopped
    
        healthcheck:
    
          test: ["CMD-SHELL", "pg_isready -U postgres -d retail_db"]
    
          interval: 30s
    
          timeout: 10s
    
          retries: 3
    
          start_period: 60s
    
    
    
      redis:
    
        image: redis:7-alpine
    
        ports:
    
          - "6379:6379"
    
        command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
    
        volumes:
    
          - redis_data:/data
    
        networks:
    
          - mcp-network
    
        restart: unless-stopped
    
        healthcheck:
    
          test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
    
          interval: 30s
    
          timeout: 10s
    
          retries: 3
    
    
    
    volumes:
    
      postgres_data:
    
        driver: local
    
      redis_data:
    
        driver: local
    
    
    
    networks:
    
      mcp-network:
    
        driver: bridge
    
    

    프로덕션 Docker Compose

    
    # docker-compose.prod.yml - Production environment
    
    version: '3.8'
    
    
    
    services:
    
      mcp-server:
    
        image: ${CONTAINER_REGISTRY}/mcp-retail-server:${IMAGE_TAG}
    
        ports:
    
          - "8000:8000"
    
        environment:
    
          - POSTGRES_HOST=${POSTGRES_HOST}
    
          - POSTGRES_PORT=${POSTGRES_PORT}
    
          - POSTGRES_DB=${POSTGRES_DB}
    
          - POSTGRES_USER=${POSTGRES_USER}
    
          - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    
          - PROJECT_ENDPOINT=${PROJECT_ENDPOINT}
    
          - AZURE_CLIENT_ID=${AZURE_CLIENT_ID}
    
          - AZURE_CLIENT_SECRET=${AZURE_CLIENT_SECRET}
    
          - AZURE_TENANT_ID=${AZURE_TENANT_ID}
    
          - APPLICATIONINSIGHTS_CONNECTION_STRING=${APPLICATIONINSIGHTS_CONNECTION_STRING}
    
          - LOG_LEVEL=INFO
    
          - ENVIRONMENT=production
    
          - REDIS_URL=${REDIS_URL}
    
        deploy:
    
          replicas: 3
    
          resources:
    
            limits:
    
              cpus: '2.0'
    
              memory: 2G
    
            reservations:
    
              cpus: '0.5'
    
              memory: 512M
    
          restart_policy:
    
            condition: on-failure
    
            delay: 5s
    
            max_attempts: 3
    
          update_config:
    
            parallelism: 1
    
            delay: 10s
    
            failure_action: rollback
    
        networks:
    
          - mcp-network
    
        healthcheck:
    
          test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
    
          interval: 30s
    
          timeout: 10s
    
          retries: 3
    
          start_period: 60s
    
    
    
    networks:
    
      mcp-network:
    
        external: true
    
    

    ☁️ Azure Container Apps 배포

    Bicep을 사용한 코드형 인프라

    
    // infra/container-apps.bicep - Azure Container Apps deployment
    
    @description('Location for all resources')
    
    param location string = resourceGroup().location
    
    
    
    @description('Environment name')
    
    param environmentName string
    
    
    
    @description('Container App name')
    
    param containerAppName string
    
    
    
    @description('Container registry details')
    
    param containerRegistry object
    
    
    
    @description('Database connection details')
    
    @secure()
    
    param databaseConnectionString string
    
    
    
    @description('Azure OpenAI configuration')
    
    param azureOpenAI object
    
    
    
    @description('Application Insights workspace ID')
    
    param workspaceId string
    
    
    
    // Container Apps Environment
    
    resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' = {
    
      name: '${environmentName}-env'
    
      location: location
    
      properties: {
    
        appLogsConfiguration: {
    
          destination: 'log-analytics'
    
          logAnalyticsConfiguration: {
    
            customerId: workspaceId
    
          }
    
        }
    
        infrastructureResourceGroup: '${environmentName}-infra-rg'
    
      }
    
    }
    
    
    
    // Container App
    
    resource mcp_retail_server 'Microsoft.App/containerApps@2023-05-01' = {
    
      name: containerAppName
    
      location: location
    
      properties: {
    
        managedEnvironmentId: containerAppsEnvironment.id
    
        configuration: {
    
          activeRevisionsMode: 'Single'
    
          ingress: {
    
            external: false
    
            targetPort: 8000
    
            allowInsecure: false
    
            traffic: [
    
              {
    
                weight: 100
    
                latestRevision: true
    
              }
    
            ]
    
          }
    
          registries: [
    
            {
    
              server: containerRegistry.server
    
              identity: containerRegistry.identity
    
            }
    
          ]
    
          secrets: [
    
            {
    
              name: 'database-connection-string'
    
              value: databaseConnectionString
    
            }
    
            {
    
              name: 'azure-openai-key'
    
              value: azureOpenAI.apiKey
    
            }
    
          ]
    
        }
    
        template: {
    
          containers: [
    
            {
    
              name: 'mcp-retail-server'
    
              image: '${containerRegistry.server}/mcp-retail-server:latest'
    
              resources: {
    
                cpu: json('1.0')
    
                memory: '2Gi'
    
              }
    
              env: [
    
                {
    
                  name: 'POSTGRES_CONNECTION_STRING'
    
                  secretRef: 'database-connection-string'
    
                }
    
                {
    
                  name: 'PROJECT_ENDPOINT'
    
                  value: azureOpenAI.endpoint
    
                }
    
                {
    
                  name: 'AZURE_OPENAI_API_KEY'
    
                  secretRef: 'azure-openai-key'
    
                }
    
                {
    
                  name: 'LOG_LEVEL'
    
                  value: 'INFO'
    
                }
    
                {
    
                  name: 'ENVIRONMENT'
    
                  value: 'production'
    
                }
    
              ]
    
              probes: [
    
                {
    
                  type: 'Liveness'
    
                  httpGet: {
    
                    path: '/health'
    
                    port: 8000
    
                    scheme: 'HTTP'
    
                  }
    
                  initialDelaySeconds: 60
    
                  periodSeconds: 30
    
                  timeoutSeconds: 10
    
                  failureThreshold: 3
    
                }
    
                {
    
                  type: 'Readiness'
    
                  httpGet: {
    
                    path: '/ready'
    
                    port: 8000
    
                    scheme: 'HTTP'
    
                  }
    
                  initialDelaySeconds: 30
    
                  periodSeconds: 10
    
                  timeoutSeconds: 5
    
                  failureThreshold: 3
    
                }
    
              ]
    
            }
    
          ]
    
          scale: {
    
            minReplicas: 2
    
            maxReplicas: 20
    
            rules: [
    
              {
    
                name: 'http-scaling'
    
                http: {
    
                  metadata: {
    
                    concurrentRequests: '10'
    
                  }
    
                }
    
              }
    
              {
    
                name: 'cpu-scaling'
    
                custom: {
    
                  type: 'cpu'
    
                  metadata: {
    
                    type: 'Utilization'
    
                    value: '70'
    
                  }
    
                }
    
              }
    
            ]
    
          }
    
        }
    
      }
    
    }
    
    
    
    // Output the FQDN
    
    output containerAppFQDN string = mcp_retail_server.properties.configuration.ingress.fqdn
    
    output containerAppId string = mcp_retail_server.id
    
    

    PostgreSQL Flexible Server

    
    // infra/database.bicep - PostgreSQL Flexible Server
    
    @description('Location for all resources')
    
    param location string = resourceGroup().location
    
    
    
    @description('PostgreSQL server name')
    
    param serverName string
    
    
    
    @description('Database administrator login')
    
    param administratorLogin string
    
    
    
    @description('Database administrator password')
    
    @secure()
    
    param administratorPassword string
    
    
    
    @description('Virtual network subnet ID')
    
    param subnetId string
    
    
    
    @description('Private DNS zone ID')
    
    param privateDnsZoneId string
    
    
    
    // PostgreSQL Flexible Server
    
    resource postgresqlServer 'Microsoft.DBforPostgreSQL/flexibleServers@2023-03-01-preview' = {
    
      name: serverName
    
      location: location
    
      sku: {
    
        name: 'Standard_D4s_v3'
    
        tier: 'GeneralPurpose'
    
      }
    
      properties: {
    
        administratorLogin: administratorLogin
    
        administratorLoginPassword: administratorPassword
    
        version: '16'
    
        storage: {
    
          storageSizeGB: 128
    
          autoGrow: 'Enabled'
    
          type: 'PremiumSSD'
    
        }
    
        backup: {
    
          backupRetentionDays: 35
    
          geoRedundantBackup: 'Enabled'
    
        }
    
        highAvailability: {
    
          mode: 'ZoneRedundant'
    
        }
    
        network: {
    
          delegatedSubnetResourceId: subnetId
    
          privateDnsZoneArmResourceId: privateDnsZoneId
    
        }
    
        maintenanceWindow: {
    
          dayOfWeek: 0
    
          startHour: 2
    
          startMinute: 0
    
        }
    
      }
    
    }
    
    
    
    // Database
    
    resource retailDatabase 'Microsoft.DBforPostgreSQL/flexibleServers/databases@2023-03-01-preview' = {
    
      parent: postgresqlServer
    
      name: 'retail_db'
    
      properties: {
    
        charset: 'UTF8'
    
        collation: 'en_US.utf8'
    
      }
    
    }
    
    
    
    // PostgreSQL extensions
    
    resource pgvectorExtension 'Microsoft.DBforPostgreSQL/flexibleServers/configurations@2023-03-01-preview' = {
    
      parent: postgresqlServer
    
      name: 'shared_preload_libraries'
    
      properties: {
    
        value: 'pg_stat_statements,pgaudit,vector'
    
        source: 'user-override'
    
      }
    
    }
    
    
    
    // Output connection details
    
    output serverFQDN string = postgresqlServer.properties.fullyQualifiedDomainName
    
    output serverId string = postgresqlServer.id
    
    output databaseName string = retailDatabase.name
    
    

    🚀 CI/CD 파이프라인 구성

    GitHub Actions 워크플로우

    
    # .github/workflows/deploy.yml - CI/CD pipeline
    
    name: Deploy MCP Retail Server
    
    
    
    on:
    
      push:
    
        branches: [main]
    
      pull_request:
    
        branches: [main]
    
      workflow_dispatch:
    
        inputs:
    
          environment:
    
            description: 'Deployment environment'
    
            required: true
    
            default: 'development'
    
            type: choice
    
            options:
    
              - development
    
              - staging
    
              - production
    
    
    
    env:
    
      CONTAINER_REGISTRY: mcpretailregistry.azurecr.io
    
      IMAGE_NAME: mcp-retail-server
    
      AZURE_RESOURCE_GROUP: mcp-retail-rg
    
    
    
    jobs:
    
      test:
    
        runs-on: ubuntu-latest
    
        services:
    
          postgres:
    
            image: pgvector/pgvector:pg16
    
            env:
    
              POSTGRES_PASSWORD: postgres
    
              POSTGRES_DB: retail_test
    
            options: >-
    
              --health-cmd pg_isready
    
              --health-interval 10s
    
              --health-timeout 5s
    
              --health-retries 5
    
            ports:
    
              - 5432:5432
    
    
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Set up Python
    
            uses: actions/setup-python@v4
    
            with:
    
              python-version: '3.11'
    
              cache: 'pip'
    
    
    
          - name: Install dependencies
    
            run: |
    
              python -m pip install --upgrade pip
    
              pip install -r requirements.lock.txt
    
              pip install pytest pytest-cov pytest-asyncio
    
    
    
          - name: Set up test database
    
            run: |
    
              PGPASSWORD=postgres psql -h localhost -U postgres -d retail_test -f scripts/create_schema.sql
    
              python scripts/generate_sample_data.py --test
    
            env:
    
              POSTGRES_HOST: localhost
    
              POSTGRES_PORT: 5432
    
              POSTGRES_DB: retail_test
    
              POSTGRES_USER: postgres
    
              POSTGRES_PASSWORD: postgres
    
    
    
          - name: Run tests
    
            run: |
    
              pytest tests/ -v --cov=mcp_server --cov-report=xml --cov-report=html
    
            env:
    
              POSTGRES_HOST: localhost
    
              POSTGRES_PORT: 5432
    
              POSTGRES_DB: retail_test
    
              POSTGRES_USER: postgres
    
              POSTGRES_PASSWORD: postgres
    
              PROJECT_ENDPOINT: ${{ secrets.TEST_PROJECT_ENDPOINT }}
    
              AZURE_CLIENT_ID: ${{ secrets.TEST_AZURE_CLIENT_ID }}
    
              AZURE_CLIENT_SECRET: ${{ secrets.TEST_AZURE_CLIENT_SECRET }}
    
              AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
    
    
    
          - name: Upload coverage reports
    
            uses: codecov/codecov-action@v3
    
            with:
    
              file: ./coverage.xml
    
              flags: unittests
    
    
    
      security-scan:
    
        runs-on: ubuntu-latest
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Run Trivy vulnerability scanner
    
            uses: aquasecurity/trivy-action@master
    
            with:
    
              scan-type: 'fs'
    
              scan-ref: '.'
    
              format: 'sarif'
    
              output: 'trivy-results.sarif'
    
    
    
          - name: Upload Trivy scan results
    
            uses: github/codeql-action/upload-sarif@v2
    
            with:
    
              sarif_file: 'trivy-results.sarif'
    
    
    
          - name: Run Bandit security linter
    
            run: |
    
              pip install bandit[toml]
    
              bandit -r mcp_server/ -f json -o bandit-report.json
    
    
    
      build:
    
        runs-on: ubuntu-latest
    
        needs: [test, security-scan]
    
        if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
    
        
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Azure Login
    
            uses: azure/login@v1
    
            with:
    
              creds: ${{ secrets.AZURE_CREDENTIALS }}
    
    
    
          - name: Build and push Docker image
    
            uses: azure/docker-login@v1
    
            with:
    
              login-server: ${{ env.CONTAINER_REGISTRY }}
    
              username: ${{ secrets.REGISTRY_USERNAME }}
    
              password: ${{ secrets.REGISTRY_PASSWORD }}
    
    
    
          - name: Build, tag, and push image
    
            run: |
    
              # Generate unique tag
    
              IMAGE_TAG="${GITHUB_SHA::8}-$(date +%s)"
    
              
    
              # Build image
    
              docker build \
    
                --target production \
    
                --tag $CONTAINER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG \
    
                --tag $CONTAINER_REGISTRY/$IMAGE_NAME:latest \
    
                .
    
              
    
              # Push images
    
              docker push $CONTAINER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG
    
              docker push $CONTAINER_REGISTRY/$IMAGE_NAME:latest
    
              
    
              # Save tag for deployment
    
              echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
    
    
    
          - name: Output image details
    
            run: |
    
              echo "Built and pushed image: $CONTAINER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG"
    
    
    
      deploy-staging:
    
        runs-on: ubuntu-latest
    
        needs: build
    
        if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    
        environment: staging
    
    
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Azure Login
    
            uses: azure/login@v1
    
            with:
    
              creds: ${{ secrets.AZURE_CREDENTIALS }}
    
    
    
          - name: Deploy to staging
    
            uses: azure/CLI@v1
    
            with:
    
              azcliversion: latest
    
              inlineScript: |
    
                # Deploy infrastructure
    
                az deployment group create \
    
                  --resource-group $AZURE_RESOURCE_GROUP-staging \
    
                  --template-file infra/main.bicep \
    
                  --parameters infra/main.parameters.staging.json \
    
                  --parameters containerImageTag=$IMAGE_TAG
    
    
    
                # Update container app
    
                az containerapp update \
    
                  --name mcp-retail-server-staging \
    
                  --resource-group $AZURE_RESOURCE_GROUP-staging \
    
                  --image $CONTAINER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG
    
    
    
          - name: Run integration tests
    
            run: |
    
              # Wait for deployment to be ready
    
              sleep 60
    
              
    
              # Run integration tests against staging
    
              pytest tests/integration/ \
    
                --endpoint https://mcp-retail-server-staging.azurecontainerapps.io \
    
                --timeout 300
    
    
    
      deploy-production:
    
        runs-on: ubuntu-latest
    
        needs: [build, deploy-staging]
    
        if: github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'production'
    
        environment: production
    
    
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Azure Login
    
            uses: azure/login@v1
    
            with:
    
              creds: ${{ secrets.AZURE_CREDENTIALS }}
    
    
    
          - name: Deploy to production
    
            uses: azure/CLI@v1
    
            with:
    
              azcliversion: latest
    
              inlineScript: |
    
                # Deploy with blue-green strategy
    
                az deployment group create \
    
                  --resource-group $AZURE_RESOURCE_GROUP-prod \
    
                  --template-file infra/main.bicep \
    
                  --parameters infra/main.parameters.prod.json \
    
                  --parameters containerImageTag=$IMAGE_TAG \
    
                  --parameters deploymentSlot=green
    
    
    
                # Health check
    
                az containerapp show \
    
                  --name mcp-retail-server-prod-green \
    
                  --resource-group $AZURE_RESOURCE_GROUP-prod
    
    
    
                # Switch traffic (blue-green deployment)
    
                az containerapp ingress traffic set \
    
                  --name mcp-retail-server-prod \
    
                  --resource-group $AZURE_RESOURCE_GROUP-prod \
    
                  --revision-weight latest=100
    
    

    Azure DevOps 파이프라인

    
    # azure-pipelines.yml - Azure DevOps pipeline
    
    trigger:
    
      branches:
    
        include:
    
          - main
    
          - develop
    
      paths:
    
        exclude:
    
          - docs/*
    
          - README.md
    
    
    
    variables:
    
      containerRegistry: 'mcpretailregistry.azurecr.io'
    
      imageName: 'mcp-retail-server'
    
      imageTag: '$(Build.BuildId)'
    
      azureServiceConnection: 'azure-service-connection'
    
    
    
    stages:
    
      - stage: Build
    
        displayName: 'Build and Test'
    
        jobs:
    
          - job: Test
    
            displayName: 'Run Tests'
    
            pool:
    
              vmImage: 'ubuntu-latest'
    
            
    
            services:
    
              postgres:
    
                image: pgvector/pgvector:pg16
    
                env:
    
                  POSTGRES_PASSWORD: postgres
    
                  POSTGRES_DB: retail_test
    
                ports:
    
                  5432:5432
    
    
    
            steps:
    
              - task: UsePythonVersion@0
    
                inputs:
    
                  versionSpec: '3.11'
    
                  displayName: 'Use Python 3.11'
    
    
    
              - script: |
    
                  python -m pip install --upgrade pip
    
                  pip install -r requirements.lock.txt
    
                  pip install pytest pytest-cov pytest-asyncio
    
                displayName: 'Install dependencies'
    
    
    
              - script: |
    
                  PGPASSWORD=postgres psql -h localhost -U postgres -d retail_test -f scripts/create_schema.sql
    
                  python scripts/generate_sample_data.py --test
    
                displayName: 'Set up test database'
    
                env:
    
                  POSTGRES_HOST: localhost
    
                  POSTGRES_PORT: 5432
    
                  POSTGRES_DB: retail_test
    
                  POSTGRES_USER: postgres
    
                  POSTGRES_PASSWORD: postgres
    
    
    
              - script: |
    
                  pytest tests/ -v --cov=mcp_server --cov-report=xml --junitxml=test-results.xml
    
                displayName: 'Run tests'
    
                env:
    
                  POSTGRES_HOST: localhost
    
                  POSTGRES_PORT: 5432
    
                  POSTGRES_DB: retail_test
    
                  POSTGRES_USER: postgres
    
                  POSTGRES_PASSWORD: postgres
    
    
    
              - task: PublishTestResults@2
    
                condition: succeededOrFailed()
    
                inputs:
    
                  testResultsFiles: 'test-results.xml'
    
                  testRunTitle: 'Python Tests'
    
    
    
              - task: PublishCodeCoverageResults@1
    
                inputs:
    
                  codeCoverageTool: 'Cobertura'
    
                  summaryFileLocation: 'coverage.xml'
    
    
    
          - job: Build
    
            displayName: 'Build Docker Image'
    
            dependsOn: Test
    
            pool:
    
              vmImage: 'ubuntu-latest'
    
    
    
            steps:
    
              - task: AzureCLI@2
    
                displayName: 'Build and push Docker image'
    
                inputs:
    
                  azureSubscription: $(azureServiceConnection)
    
                  scriptType: 'bash'
    
                  scriptLocation: 'inlineScript'
    
                  inlineScript: |
    
                    # Login to container registry
    
                    az acr login --name $(containerRegistry)
    
                    
    
                    # Build and push image
    
                    docker build \
    
                      --target production \
    
                      --tag $(containerRegistry)/$(imageName):$(imageTag) \
    
                      --tag $(containerRegistry)/$(imageName):latest \
    
                      .
    
                    
    
                    docker push $(containerRegistry)/$(imageName):$(imageTag)
    
                    docker push $(containerRegistry)/$(imageName):latest
    
    
    
      - stage: Deploy_Staging
    
        displayName: 'Deploy to Staging'
    
        dependsOn: Build
    
        condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    
        
    
        jobs:
    
          - deployment: DeployStaging
    
            displayName: 'Deploy to Staging Environment'
    
            pool:
    
              vmImage: 'ubuntu-latest'
    
            environment: 'staging'
    
            
    
            strategy:
    
              runOnce:
    
                deploy:
    
                  steps:
    
                    - task: AzureCLI@2
    
                      displayName: 'Deploy infrastructure'
    
                      inputs:
    
                        azureSubscription: $(azureServiceConnection)
    
                        scriptType: 'bash'
    
                        scriptLocation: 'inlineScript'
    
                        inlineScript: |
    
                          az deployment group create \
    
                            --resource-group mcp-retail-staging-rg \
    
                            --template-file infra/main.bicep \
    
                            --parameters infra/main.parameters.staging.json \
    
                            --parameters containerImageTag=$(imageTag)
    
    
    
      - stage: Deploy_Production
    
        displayName: 'Deploy to Production'
    
        dependsOn: Deploy_Staging
    
        condition: and(succeeded(), eq(variables['Build.Reason'], 'Manual'))
    
        
    
        jobs:
    
          - deployment: DeployProduction
    
            displayName: 'Deploy to Production Environment'
    
            pool:
    
              vmImage: 'ubuntu-latest'
    
            environment: 'production'
    
            
    
            strategy:
    
              runOnce:
    
                deploy:
    
                  steps:
    
                    - task: AzureCLI@2
    
                      displayName: 'Deploy to production'
    
                      inputs:
    
                        azureSubscription: $(azureServiceConnection)
    
                        scriptType: 'bash'
    
                        scriptLocation: 'inlineScript'
    
                        inlineScript: |
    
                          az deployment group create \
    
                            --resource-group mcp-retail-prod-rg \
    
                            --template-file infra/main.bicep \
    
                            --parameters infra/main.parameters.prod.json \
    
                            --parameters containerImageTag=$(imageTag)
    
    

    📊 확장 및 성능

    자동 확장 구성

    
    # k8s/hpa.yaml - Horizontal Pod Autoscaler for Kubernetes
    
    apiVersion: autoscaling/v2
    
    kind: HorizontalPodAutoscaler
    
    metadata:
    
      name: mcp-retail-server-hpa
    
      namespace: mcp-retail
    
    spec:
    
      scaleTargetRef:
    
        apiVersion: apps/v1
    
        kind: Deployment
    
        name: mcp-retail-server
    
      minReplicas: 3
    
      maxReplicas: 50
    
      metrics:
    
        - type: Resource
    
          resource:
    
            name: cpu
    
            target:
    
              type: Utilization
    
              averageUtilization: 70
    
        - type: Resource
    
          resource:
    
            name: memory
    
            target:
    
              type: Utilization
    
              averageUtilization: 80
    
        - type: Pods
    
          pods:
    
            metric:
    
              name: http_requests_per_second
    
            target:
    
              type: AverageValue
    
              averageValue: 100
    
      behavior:
    
        scaleDown:
    
          stabilizationWindowSeconds: 300
    
          policies:
    
            - type: Percent
    
              value: 50
    
              periodSeconds: 60
    
        scaleUp:
    
          stabilizationWindowSeconds: 60
    
          policies:
    
            - type: Percent
    
              value: 100
    
              periodSeconds: 30
    
            - type: Pods
    
              value: 5
    
              periodSeconds: 30
    
          selectPolicy: Max
    
    

    성능 모니터링

    
    # mcp_server/monitoring/performance.py
    
    """
    
    Performance monitoring and metrics collection for production deployment.
    
    """
    
    import asyncio
    
    import time
    
    import psutil
    
    from typing import Dict, Any
    
    from dataclasses import dataclass
    
    from datetime import datetime, timedelta
    
    import logging
    
    
    
    @dataclass
    
    class PerformanceMetrics:
    
        """Performance metrics data structure."""
    
        timestamp: datetime
    
        cpu_percent: float
    
        memory_percent: float
    
        memory_used_mb: float
    
        active_connections: int
    
        request_rate: float
    
        avg_response_time: float
    
        error_rate: float
    
        database_connections: int
    
    
    
    class PerformanceMonitor:
    
        """Monitor and collect performance metrics."""
    
        
    
        def __init__(self, config):
    
            self.config = config
    
            self.logger = logging.getLogger(__name__)
    
            
    
            # Metrics collection
    
            self.metrics_history = []
    
            self.request_times = []
    
            self.error_count = 0
    
            self.request_count = 0
    
            
    
            # Database monitoring
    
            self.db_pool = None
    
            
    
        async def start_monitoring(self):
    
            """Start continuous performance monitoring."""
    
            
    
            self.logger.info("Starting performance monitoring")
    
            
    
            # Start metrics collection task
    
            asyncio.create_task(self._collect_metrics_loop())
    
            asyncio.create_task(self._cleanup_old_metrics())
    
            
    
        async def _collect_metrics_loop(self):
    
            """Continuously collect performance metrics."""
    
            
    
            while True:
    
                try:
    
                    metrics = await self._collect_current_metrics()
    
                    self.metrics_history.append(metrics)
    
                    
    
                    # Log critical metrics
    
                    if metrics.cpu_percent > 90:
    
                        self.logger.warning(f"High CPU usage: {metrics.cpu_percent:.1f}%")
    
                    
    
                    if metrics.memory_percent > 90:
    
                        self.logger.warning(f"High memory usage: {metrics.memory_percent:.1f}%")
    
                    
    
                    if metrics.error_rate > 0.05:  # 5% error rate
    
                        self.logger.warning(f"High error rate: {metrics.error_rate:.2%}")
    
                    
    
                    await asyncio.sleep(30)  # Collect every 30 seconds
    
                    
    
                except Exception as e:
    
                    self.logger.error(f"Error collecting metrics: {e}")
    
                    await asyncio.sleep(60)
    
        
    
        async def _collect_current_metrics(self) -> PerformanceMetrics:
    
            """Collect current system metrics."""
    
            
    
            # System metrics
    
            cpu_percent = psutil.cpu_percent(interval=1)
    
            memory = psutil.virtual_memory()
    
            
    
            # Application metrics
    
            current_time = datetime.utcnow()
    
            recent_requests = [
    
                req_time for req_time in self.request_times
    
                if current_time - req_time < timedelta(minutes=1)
    
            ]
    
            
    
            request_rate = len(recent_requests) / 60.0  # requests per second
    
            
    
            # Calculate average response time
    
            avg_response_time = 0.0
    
            if hasattr(self, '_recent_response_times'):
    
                recent_response_times = [
    
                    rt for rt in self._recent_response_times
    
                    if current_time - rt['timestamp'] < timedelta(minutes=5)
    
                ]
    
                if recent_response_times:
    
                    avg_response_time = sum(rt['time'] for rt in recent_response_times) / len(recent_response_times)
    
            
    
            # Error rate calculation
    
            error_rate = 0.0
    
            if self.request_count > 0:
    
                error_rate = self.error_count / self.request_count
    
            
    
            # Database connections
    
            db_connections = 0
    
            if self.db_pool:
    
                db_connections = len(self.db_pool._holders)
    
            
    
            return PerformanceMetrics(
    
                timestamp=current_time,
    
                cpu_percent=cpu_percent,
    
                memory_percent=memory.percent,
    
                memory_used_mb=memory.used / (1024 * 1024),
    
                active_connections=0,  # To be implemented with connection tracking
    
                request_rate=request_rate,
    
                avg_response_time=avg_response_time,
    
                error_rate=error_rate,
    
                database_connections=db_connections
    
            )
    
        
    
        async def _cleanup_old_metrics(self):
    
            """Clean up old metrics to prevent memory leaks."""
    
            
    
            while True:
    
                try:
    
                    cutoff_time = datetime.utcnow() - timedelta(hours=24)
    
                    
    
                    # Clean up metrics history
    
                    self.metrics_history = [
    
                        m for m in self.metrics_history
    
                        if m.timestamp > cutoff_time
    
                    ]
    
                    
    
                    # Clean up request times
    
                    self.request_times = [
    
                        rt for rt in self.request_times
    
                        if rt > cutoff_time
    
                    ]
    
                    
    
                    # Reset counters periodically
    
                    if datetime.utcnow().minute == 0:  # Every hour
    
                        self.error_count = 0
    
                        self.request_count = 0
    
                    
    
                    await asyncio.sleep(3600)  # Run every hour
    
                    
    
                except Exception as e:
    
                    self.logger.error(f"Error cleaning up metrics: {e}")
    
                    await asyncio.sleep(3600)
    
        
    
        def record_request(self, response_time: float, success: bool = True):
    
            """Record a request for metrics."""
    
            
    
            current_time = datetime.utcnow()
    
            self.request_times.append(current_time)
    
            self.request_count += 1
    
            
    
            if not success:
    
                self.error_count += 1
    
            
    
            # Record response time
    
            if not hasattr(self, '_recent_response_times'):
    
                self._recent_response_times = []
    
            
    
            self._recent_response_times.append({
    
                'timestamp': current_time,
    
                'time': response_time
    
            })
    
        
    
        def get_current_metrics(self) -> Dict[str, Any]:
    
            """Get current performance metrics."""
    
            
    
            if not self.metrics_history:
    
                return {}
    
            
    
            latest_metrics = self.metrics_history[-1]
    
            
    
            return {
    
                'timestamp': latest_metrics.timestamp.isoformat(),
    
                'system': {
    
                    'cpu_percent': latest_metrics.cpu_percent,
    
                    'memory_percent': latest_metrics.memory_percent,
    
                    'memory_used_mb': latest_metrics.memory_used_mb
    
                },
    
                'application': {
    
                    'active_connections': latest_metrics.active_connections,
    
                    'request_rate': latest_metrics.request_rate,
    
                    'avg_response_time': latest_metrics.avg_response_time,
    
                    'error_rate': latest_metrics.error_rate
    
                },
    
                'database': {
    
                    'connections': latest_metrics.database_connections
    
                }
    
            }
    
        
    
        def get_metrics_summary(self, hours: int = 24) -> Dict[str, Any]:
    
            """Get performance metrics summary for the specified hours."""
    
            
    
            cutoff_time = datetime.utcnow() - timedelta(hours=hours)
    
            recent_metrics = [
    
                m for m in self.metrics_history
    
                if m.timestamp > cutoff_time
    
            ]
    
            
    
            if not recent_metrics:
    
                return {}
    
            
    
            # Calculate averages
    
            avg_cpu = sum(m.cpu_percent for m in recent_metrics) / len(recent_metrics)
    
            avg_memory = sum(m.memory_percent for m in recent_metrics) / len(recent_metrics)
    
            avg_response_time = sum(m.avg_response_time for m in recent_metrics) / len(recent_metrics)
    
            
    
            # Calculate peaks
    
            max_cpu = max(m.cpu_percent for m in recent_metrics)
    
            max_memory = max(m.memory_percent for m in recent_metrics)
    
            max_response_time = max(m.avg_response_time for m in recent_metrics)
    
            
    
            return {
    
                'period_hours': hours,
    
                'averages': {
    
                    'cpu_percent': round(avg_cpu, 2),
    
                    'memory_percent': round(avg_memory, 2),
    
                    'response_time': round(avg_response_time, 3)
    
                },
    
                'peaks': {
    
                    'cpu_percent': round(max_cpu, 2),
    
                    'memory_percent': round(max_memory, 2),
    
                    'response_time': round(max_response_time, 3)
    
                },
    
                'data_points': len(recent_metrics)
    
            }
    
    

    🔐 프로덕션 보안 구성

    보안 강화

    
    # k8s/security-policy.yaml - Kubernetes security policies
    
    apiVersion: v1
    
    kind: SecurityContext
    
    metadata:
    
      name: mcp-retail-security-context
    
    spec:
    
      runAsNonRoot: true
    
      runAsUser: 1000
    
      runAsGroup: 1000
    
      fsGroup: 1000
    
      seccompProfile:
    
        type: RuntimeDefault
    
      capabilities:
    
        drop:
    
          - ALL
    
      readOnlyRootFilesystem: true
    
      allowPrivilegeEscalation: false
    
    
    
    ---
    
    apiVersion: networking.k8s.io/v1
    
    kind: NetworkPolicy
    
    metadata:
    
      name: mcp-retail-network-policy
    
      namespace: mcp-retail
    
    spec:
    
      podSelector:
    
        matchLabels:
    
          app: mcp-retail-server
    
      policyTypes:
    
        - Ingress
    
        - Egress
    
      ingress:
    
        - from:
    
            - namespaceSelector:
    
                matchLabels:
    
                  name: ingress-nginx
    
          ports:
    
            - protocol: TCP
    
              port: 8000
    
      egress:
    
        - to:
    
            - namespaceSelector:
    
                matchLabels:
    
                  name: database
    
          ports:
    
            - protocol: TCP
    
              port: 5432
    
        - to: []
    
          ports:
    
            - protocol: TCP
    
              port: 443  # HTTPS for Azure OpenAI
    
            - protocol: TCP
    
              port: 53   # DNS
    
            - protocol: UDP
    
              port: 53   # DNS
    
    

    환경 구성

    
    # scripts/setup-production-env.sh
    
    #!/bin/bash
    
    
    
    # Production environment setup script
    
    set -euo pipefail
    
    
    
    echo "🔧 Setting up production environment..."
    
    
    
    # Create resource groups
    
    az group create --name "mcp-retail-prod-rg" --location "East US"
    
    az group create --name "mcp-retail-shared-rg" --location "East US"
    
    
    
    # Create Key Vault
    
    echo "🔐 Creating Azure Key Vault..."
    
    az keyvault create \
    
      --name "mcp-retail-kv-prod" \
    
      --resource-group "mcp-retail-shared-rg" \
    
      --location "East US" \
    
      --enable-rbac-authorization true
    
    
    
    # Set secrets
    
    echo "🔑 Setting up secrets..."
    
    az keyvault secret set \
    
      --vault-name "mcp-retail-kv-prod" \
    
      --name "postgres-password" \
    
      --value "${POSTGRES_PASSWORD}"
    
    
    
    az keyvault secret set \
    
      --vault-name "mcp-retail-kv-prod" \
    
      --name "azure-openai-key" \
    
      --value "${AZURE_OPENAI_KEY}"
    
    
    
    # Create container registry
    
    echo "📦 Creating container registry..."
    
    az acr create \
    
      --name "mcpretailregistry" \
    
      --resource-group "mcp-retail-shared-rg" \
    
      --sku Premium \
    
      --admin-enabled false
    
    
    
    # Create virtual network
    
    echo "🌐 Creating virtual network..."
    
    az network vnet create \
    
      --name "mcp-retail-vnet" \
    
      --resource-group "mcp-retail-shared-rg" \
    
      --address-prefix "10.0.0.0/16" \
    
      --subnet-name "container-apps" \
    
      --subnet-prefix "10.0.1.0/24"
    
    
    
    az network vnet subnet create \
    
      --name "database" \
    
      --resource-group "mcp-retail-shared-rg" \
    
      --vnet-name "mcp-retail-vnet" \
    
      --address-prefix "10.0.2.0/24" \
    
      --delegations Microsoft.DBforPostgreSQL/flexibleServers
    
    
    
    # Deploy infrastructure
    
    echo "🏗️  Deploying infrastructure..."
    
    az deployment group create \
    
      --resource-group "mcp-retail-prod-rg" \
    
      --template-file "infra/main.bicep" \
    
      --parameters "infra/main.parameters.prod.json"
    
    
    
    echo "✅ Production environment setup complete!"
    
    

    🎯 주요 요점

    이 실습을 완료한 후, 다음을 갖추게 됩니다:

    컨테이너 전략: 보안이 강화된 프로덕션 준비 Docker 컨테이너

    클라우드 배포: 자동 확장 및 모니터링이 포함된 Azure Container Apps

    데이터베이스 배포: 고가용성을 갖춘 PostgreSQL Flexible Server

    CI/CD 파이프라인: 자동화된 테스트, 빌드 및 배포 워크플로우

    성능 모니터링: 포괄적인 메트릭 수집 및 경고

    보안 구성: 프로덕션급 보안 정책 및 네트워크 격리

    🚀 다음 단계

    Lab 11: 모니터링 및 관측을 계속 진행하여:

  • Application Insights를 사용한 포괄적인 모니터링 설정
  • 구조화된 로깅 및 분산 추적 구성
  • 경고 및 자동 응답 시스템 구현
  • 비즈니스 메트릭 및 성능 KPI 모니터링
  • 📚 추가 자료

    컨테이너 기술

  • Docker Best Practices - Docker 공식 모범 사례
  • Azure Container Apps - Azure Container Apps 문서
  • Kubernetes Documentation - Kubernetes 공식 문서
  • CI/CD 및 DevOps

  • GitHub Actions - GitHub Actions 문서
  • Azure DevOps - Azure DevOps 서비스
  • Infrastructure as Code - Azure Bicep 문서
  • 보안 및 모니터링

  • Azure Security Center - Azure 보안 권장 사항
  • Container Security - Kubernetes 보안 개념
  • Application Insights - Azure Application Insights
  • ---

    이전: Lab 09: VS Code 통합

    다음: Lab 11: 모니터링 및 관측

    ---

    면책 조항:

    이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.

    정확성을 위해 최선을 다하고 있으나, 자동 번역에는 오류나 부정확성이 포함될 수 있습니다.

    원본 문서의 원어 버전을 권위 있는 자료로 간주해야 합니다.

    중요한 정보의 경우, 전문적인 인간 번역을 권장합니다.

    이 번역 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.

    MCP Academy — microsoft/mcp-for-beginners