Skip to content

feat: complete MLOps pipeline with working Azure CI/CD #1

feat: complete MLOps pipeline with working Azure CI/CD

feat: complete MLOps pipeline with working Azure CI/CD #1

name: Secure MLOps CI/CD Pipeline

Check failure on line 1 in .github/workflows/secure-azure-deploy.yml

View workflow run for this annotation

GitHub Actions / .github/workflows/secure-azure-deploy.yml

Invalid workflow file

(Line: 148, Col: 12): Unrecognized named-value: 'secrets'. Located at position 1 within expression: secrets.AZURE_WEBAPP_NAME
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"