feat: complete MLOps pipeline with working Azure CI/CD #1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Secure MLOps CI/CD Pipeline | ||
|
Check failure on line 1 in .github/workflows/secure-azure-deploy.yml
|
||
| on: | ||
| push: | ||
| branches: [ main ] | ||
| pull_request: | ||
| branches: [ main ] | ||
| workflow_dispatch: | ||
| inputs: | ||
| environment: | ||
| description: 'Environment to deploy to' | ||
| required: true | ||
| default: 'staging' | ||
| type: choice | ||
| options: | ||
| - staging | ||
| - production | ||
| # Security: Minimal required permissions | ||
| permissions: | ||
| id-token: write # Required for OIDC | ||
| contents: read # Required to checkout code | ||
| security-events: write # Required for security scanning | ||
| actions: read # Required for dependency graph | ||
| env: | ||
| PYTHON_VERSION: '3.11' | ||
| IMAGE_NAME: iris-mlops-pipeline | ||
| jobs: | ||
| # Job 1: Security Scanning | ||
| security-scan: | ||
| name: 🔒 Security Scanning | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: 📥 Checkout code | ||
| uses: actions/checkout@v4 | ||
| - name: 🔍 Run GitLeaks | ||
| uses: gitleaks/gitleaks-action@v2 | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| - name: 🔐 Scan for secrets | ||
| run: | | ||
| echo "Checking for hardcoded credentials..." | ||
| if grep -r -E "(password|secret|key|token).*[=:]\s*['\"][^'\"]{20,}['\"]" --include="*.py" --include="*.yml" --exclude-dir=".git" .; then | ||
| echo "❌ Potential credentials found in code!" | ||
| exit 1 | ||
| else | ||
| echo "✅ No hardcoded credentials detected" | ||
| fi | ||
| # Job 2: Code Quality & Testing | ||
| code-quality: | ||
| name: 🔍 Code Quality & Testing | ||
| runs-on: ubuntu-latest | ||
| needs: security-scan | ||
| steps: | ||
| - name: 📥 Checkout code | ||
| uses: actions/checkout@v4 | ||
| - name: 🐍 Set up Python ${{ env.PYTHON_VERSION }} | ||
| uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: ${{ env.PYTHON_VERSION }} | ||
| cache: 'pip' | ||
| - name: 📦 Install dependencies | ||
| run: | | ||
| python -m pip install --upgrade pip | ||
| pip install -r requirements.txt | ||
| pip install bandit safety pytest | ||
| - name: 🔒 Security linting with Bandit | ||
| run: | | ||
| bandit -r src/ -ll -i || true | ||
| - name: 🛡️ Check dependencies for vulnerabilities | ||
| run: | | ||
| safety check || true | ||
| - name: 🧪 Run tests | ||
| run: | | ||
| pytest tests/ -v | ||
| # Job 3: Build with Security Scanning | ||
| build-secure: | ||
| name: 🐳 Secure Build & Scan | ||
| runs-on: ubuntu-latest | ||
| needs: [security-scan, code-quality] | ||
| if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' | ||
| outputs: | ||
| image-digest: ${{ steps.build.outputs.digest }} | ||
| steps: | ||
| - name: 📥 Checkout code | ||
| uses: actions/checkout@v4 | ||
| - name: 🔧 Set up Docker Buildx | ||
| uses: docker/setup-buildx-action@v3 | ||
| - name: 🔑 Log in to Azure Container Registry | ||
| uses: azure/docker-login@v1 | ||
| with: | ||
| login-server: ${{ secrets.CONTAINER_REGISTRY }} | ||
| username: ${{ secrets.ACR_USERNAME }} | ||
| password: ${{ secrets.ACR_PASSWORD }} | ||
| - name: 🔨 Build Docker image | ||
| id: build | ||
| uses: docker/build-push-action@v5 | ||
| with: | ||
| context: . | ||
| file: ./Dockerfile.azure | ||
| push: true | ||
| tags: | | ||
| ${{ secrets.CONTAINER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest | ||
| ${{ secrets.CONTAINER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} | ||
| cache-from: type=gha | ||
| cache-to: type=gha,mode=max | ||
| platforms: linux/amd64 | ||
| - name: 🔍 Scan image with Trivy | ||
| uses: aquasecurity/trivy-action@master | ||
| with: | ||
| image-ref: ${{ secrets.CONTAINER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} | ||
| format: 'sarif' | ||
| output: 'trivy-results.sarif' | ||
| - name: 📊 Upload Trivy scan results | ||
| uses: github/codeql-action/upload-sarif@v3 | ||
| if: always() | ||
| with: | ||
| sarif_file: 'trivy-results.sarif' | ||
| # Job 4: Secure Azure Deployment | ||
| deploy-secure: | ||
| name: 🚀 Secure Azure Deployment | ||
| runs-on: ubuntu-latest | ||
| needs: build-secure | ||
| if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' | ||
| environment: | ||
| name: ${{ github.event.inputs.environment || 'staging' }} | ||
| url: https://${{ secrets.AZURE_WEBAPP_NAME }}.azurewebsites.net | ||
| steps: | ||
| - name: 📥 Checkout code | ||
| uses: actions/checkout@v4 | ||
| - name: 🔑 Azure Login | ||
| uses: azure/login@v1 | ||
| with: | ||
| creds: ${{ secrets.AZURE_CREDENTIALS }} | ||
| - name: 🔒 Validate Azure connection | ||
| run: | | ||
| # Verify we can access the resource group | ||
| az group show --name ${{ secrets.AZURE_RESOURCE_GROUP }} --query "name" -o tsv | ||
| # Verify we can access the web app | ||
| az webapp show --name ${{ secrets.AZURE_WEBAPP_NAME }} --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} --query "name" -o tsv | ||
| - name: 🚀 Deploy to Azure Web App | ||
| uses: azure/webapps-deploy@v2 | ||
| with: | ||
| app-name: ${{ secrets.AZURE_WEBAPP_NAME }} | ||
| images: ${{ secrets.CONTAINER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} | ||
| - name: ⚙️ Configure secure app settings | ||
| run: | | ||
| # Use Azure CLI to set environment variables securely | ||
| az webapp config appsettings set \ | ||
| --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \ | ||
| --name ${{ secrets.AZURE_WEBAPP_NAME }} \ | ||
| --settings \ | ||
| WEBSITES_PORT="8000" \ | ||
| ENVIRONMENT="${{ github.event.inputs.environment || 'staging' }}" \ | ||
| COMMIT_SHA="${{ github.sha }}" \ | ||
| BUILD_NUMBER="${{ github.run_number }}" \ | ||
| DEPLOYMENT_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" | ||
| # Set secrets from GitHub secrets (without exposing values) | ||
| az webapp config appsettings set \ | ||
| --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \ | ||
| --name ${{ secrets.AZURE_WEBAPP_NAME }} \ | ||
| --settings \ | ||
| AZURE_STORAGE_ACCOUNT="${{ secrets.AZURE_STORAGE_ACCOUNT }}" \ | ||
| APPINSIGHTS_INSTRUMENTATION_KEY="${{ secrets.APPINSIGHTS_INSTRUMENTATION_KEY }}" | ||
| # Only set storage key if provided | ||
| if [ ! -z "${{ secrets.AZURE_STORAGE_KEY }}" ]; then | ||
| az webapp config appsettings set \ | ||
| --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \ | ||
| --name ${{ secrets.AZURE_WEBAPP_NAME }} \ | ||
| --settings \ | ||
| AZURE_STORAGE_KEY="${{ secrets.AZURE_STORAGE_KEY }}" | ||
| fi | ||
| - name: 🔄 Restart Web App | ||
| run: | | ||
| az webapp restart \ | ||
| --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \ | ||
| --name ${{ secrets.AZURE_WEBAPP_NAME }} | ||
| - name: ⏳ Wait for deployment | ||
| run: sleep 60 | ||
| - name: 🏥 Security-aware health check | ||
| run: | | ||
| echo "Performing security-aware health check..." | ||
| # Check if the app is responding | ||
| max_attempts=10 | ||
| attempt=1 | ||
| while [ $attempt -le $max_attempts ]; do | ||
| echo "Health check attempt $attempt of $max_attempts" | ||
| # Construct URL using secrets | ||
| app_name="${{ secrets.AZURE_WEBAPP_NAME }}" | ||
| health_url="https://${app_name}.azurewebsites.net/health" | ||
| response=$(curl -s -w "%{http_code}" -H "User-Agent: GitHub-Actions-Security-Check" \ | ||
| --max-time 30 \ | ||
| "$health_url") | ||
| http_code="${response: -3}" | ||
| if [ "$http_code" = "200" ]; then | ||
| echo "✅ Health check passed!" | ||
| # Verify no sensitive information is exposed | ||
| echo "Checking for information disclosure..." | ||
| response_body="${response%???}" # Remove HTTP code from end | ||
| if echo "$response_body" | grep -i "password\|secret\|key\|token"; then | ||
| echo "⚠️ Warning: Potential information disclosure detected" | ||
| else | ||
| echo "✅ No sensitive information exposed" | ||
| fi | ||
| break | ||
| else | ||
| echo "❌ Health check failed with code: $http_code" | ||
| if [ $attempt -eq $max_attempts ]; then | ||
| echo "🚨 Health check failed after $max_attempts attempts" | ||
| exit 1 | ||
| fi | ||
| sleep 30 | ||
| attempt=$((attempt + 1)) | ||
| fi | ||
| done | ||
| - name: 🧪 Security validation tests | ||
| run: | | ||
| echo "Running security validation tests..." | ||
| app_name="${{ secrets.AZURE_WEBAPP_NAME }}" | ||
| base_url="https://${app_name}.azurewebsites.net" | ||
| # Test that admin endpoints are not accessible | ||
| admin_response=$(curl -s -w "%{http_code}" --max-time 10 \ | ||
| "${base_url}/admin" || echo "000") | ||
| if [ "${admin_response: -3}" != "404" ] && [ "${admin_response: -3}" != "403" ]; then | ||
| echo "⚠️ Warning: Admin endpoint may be accessible" | ||
| else | ||
| echo "✅ Admin endpoints properly secured" | ||
| fi | ||
| # Test API with valid payload | ||
| echo "Testing API security..." | ||
| curl -f -X POST "${base_url}/predict" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d '{ | ||
| "features": [ | ||
| {"sepal_length": 5.1, "sepal_width": 3.5, "petal_length": 1.4, "petal_width": 0.2} | ||
| ] | ||
| }' || echo "⚠️ API test failed" | ||
| # Job 5: Cleanup & Audit | ||
| cleanup-audit: | ||
| name: 🧹 Cleanup & Audit | ||
| runs-on: ubuntu-latest | ||
| needs: [deploy-secure] | ||
| if: always() | ||
| steps: | ||
| - name: 📋 Audit deployment | ||
| run: | | ||
| echo "=== DEPLOYMENT AUDIT SUMMARY ===" | ||
| echo "Environment: ${{ github.event.inputs.environment || 'staging' }}" | ||
| echo "Commit SHA: ${{ github.sha }}" | ||
| echo "Build Number: ${{ github.run_number }}" | ||
| echo "Triggered by: ${{ github.actor }}" | ||
| echo "Event: ${{ github.event_name }}" | ||
| echo "Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)" | ||
| if [ "${{ needs.deploy-secure.result }}" == "success" ]; then | ||
| echo "✅ Deployment: SUCCESS" | ||
| else | ||
| echo "❌ Deployment: FAILED" | ||
| fi | ||
| - name: 🔒 Security audit log | ||
| run: | | ||
| echo "=== SECURITY AUDIT ===" | ||
| echo "Security scan: ${{ needs.security-scan.result }}" | ||
| echo "Code quality: ${{ needs.code-quality.result }}" | ||
| echo "Secure build: ${{ needs.build-secure.result }}" | ||
| echo "Secure deploy: ${{ needs.deploy-secure.result }}" | ||
| echo "Deployment completed with security validation" | ||