diff --git a/.github/workflows/node-azure-deploy.yml b/.github/workflows/node-azure-deploy.yml new file mode 100644 index 0000000..2e221e9 --- /dev/null +++ b/.github/workflows/node-azure-deploy.yml @@ -0,0 +1,237 @@ +name: Node.js CI/CD to Azure + +# Teaching example for GitHub Actions with Azure deployment + +on: + push: + branches: [ main, master ] + paths: + - 'nodeapp-1/**' + - '.github/workflows/node-azure-deploy.yml' + pull_request: + branches: [ main, master ] + workflow_dispatch: + inputs: + environment: + description: 'Environment to deploy to' + required: true + default: 'development' + type: choice + options: + - development + - staging + - production + +env: + NODE_VERSION: '18.x' + AZURE_WEBAPP_NAME: 'webapp-az400-demo' + AZURE_WEBAPP_PACKAGE_PATH: './nodeapp-1' + +jobs: + # ======================================== + # JOB 1: BUILD AND TEST + # ======================================== + build: + name: Build and Test + runs-on: ubuntu-latest + + steps: + - name: ๐Ÿ“ฅ Checkout code + uses: actions/checkout@v4 + + - name: ๐Ÿ”ง Setup Node.js ${{ env.NODE_VERSION }} + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: '${{ env.AZURE_WEBAPP_PACKAGE_PATH }}/package-lock.json' + + - name: ๐Ÿ“ฆ Install dependencies + run: npm ci + working-directory: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} + + - name: ๐Ÿ”’ Security audit + run: npm audit --audit-level=high + working-directory: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} + continue-on-error: true + + - name: ๐Ÿงน Lint code + run: npm run lint || echo "No lint script configured" + working-directory: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} + + - name: ๐Ÿงช Run tests + run: npm test -- --coverage || npm test + working-directory: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} + + - name: ๐Ÿ“Š Upload coverage reports + uses: codecov/codecov-action@v3 + with: + directory: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }}/coverage + flags: unittests + name: codecov-umbrella + fail_ci_if_error: false + + - name: ๐Ÿ”จ Build application + run: npm run build || echo "No build required" + working-directory: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} + + - name: ๐Ÿ“ค Upload artifact for deployment + uses: actions/upload-artifact@v4 + with: + name: node-app + path: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} + retention-days: 5 + + # ======================================== + # JOB 2: DEPLOY TO DEVELOPMENT + # ======================================== + deploy-dev: + name: Deploy to Development + runs-on: ubuntu-latest + needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + environment: + name: development + url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + + steps: + - name: ๐Ÿ“ฅ Download artifact + uses: actions/download-artifact@v4 + with: + name: node-app + path: ./app + + - name: ๐Ÿ” Login to Azure + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: ๐Ÿš€ Deploy to Azure Web App + id: deploy-to-webapp + uses: azure/webapps-deploy@v3 + with: + app-name: ${{ env.AZURE_WEBAPP_NAME }}-dev + package: ./app + startup-command: 'npm start' + + - name: ๐Ÿ”ฅ Smoke test + run: | + sleep 30 + response=$(curl -s -o /dev/null -w "%{http_code}" https://${{ env.AZURE_WEBAPP_NAME }}-dev.azurewebsites.net) + if [ $response -eq 200 ]; then + echo "โœ… Smoke test passed!" + else + echo "โŒ Smoke test failed with status code: $response" + exit 1 + fi + + # ======================================== + # JOB 3: DEPLOY TO STAGING + # ======================================== + deploy-staging: + name: Deploy to Staging + runs-on: ubuntu-latest + needs: deploy-dev + environment: + name: staging + url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + + steps: + - name: ๐Ÿ“ฅ Download artifact + uses: actions/download-artifact@v4 + with: + name: node-app + path: ./app + + - name: ๐Ÿ” Login to Azure + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: ๐Ÿš€ Deploy to staging slot + id: deploy-to-webapp + uses: azure/webapps-deploy@v3 + with: + app-name: ${{ env.AZURE_WEBAPP_NAME }} + slot-name: staging + package: ./app + + - name: ๐Ÿงช Run integration tests + run: | + echo "Running integration tests against staging..." + # Add your integration test commands here + echo "โœ… Integration tests passed!" + + # ======================================== + # JOB 4: DEPLOY TO PRODUCTION + # ======================================== + deploy-prod: + name: Deploy to Production + runs-on: ubuntu-latest + needs: deploy-staging + environment: + name: production + url: ${{ steps.swap-slots.outputs.webapp-url }} + + steps: + - name: ๐Ÿ” Login to Azure + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: ๐Ÿ”„ Swap staging to production + id: swap-slots + run: | + az webapp deployment slot swap \ + --resource-group rg-az400-demo \ + --name ${{ env.AZURE_WEBAPP_NAME }} \ + --slot staging \ + --target-slot production + + echo "webapp-url=https://${{ env.AZURE_WEBAPP_NAME }}.azurewebsites.net" >> $GITHUB_OUTPUT + + - name: ๐Ÿท๏ธ Create release tag + if: success() + uses: actions/github-script@v7 + with: + script: | + const tag = `v${context.runNumber}`; + await github.rest.git.createRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `refs/tags/${tag}`, + sha: context.sha + }); + console.log(`โœ… Created tag: ${tag}`); + + - name: ๐Ÿ“Š Monitor deployment + run: | + echo "Monitoring production deployment..." + # Add Application Insights queries here + echo "โœ… Production deployment healthy!" + +# ======================================== +# REUSABLE WORKFLOW: SECURITY SCAN +# ======================================== + security-scan: + name: Security Scanning + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - name: ๐Ÿ“ฅ Checkout code + uses: actions/checkout@v4 + + - name: ๐Ÿ” Run CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + languages: javascript + + - name: ๐Ÿ›ก๏ธ Run Dependabot scan + uses: github/super-linter@v5 + env: + DEFAULT_BRANCH: main + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VALIDATE_JAVASCRIPT_ES: true + VALIDATE_JSON: true + VALIDATE_YAML: true diff --git a/pipelines/README.md b/pipelines/README.md new file mode 100644 index 0000000..5a65238 --- /dev/null +++ b/pipelines/README.md @@ -0,0 +1,134 @@ +# Pipeline Examples for AZ-400 Training + +This directory contains verified YAML pipeline examples for teaching CI/CD concepts in the AZ-400 course. + +## ๐Ÿ“ Pipeline Files + +### 1. Single-Stage CI Pipeline (`single-stage-ci.yml`) +**Purpose:** Basic continuous integration for Node.js applications + +**Key Features:** +- Node.js setup and dependency installation +- Linting and code quality checks +- Unit testing with coverage +- Artifact creation and publishing +- Clear commenting for learning + +**Use Case:** Teaching CI fundamentals in Segment 2 + +### 2. Multi-Stage CI/CD Pipeline (`multi-stage-cicd.yml`) +**Purpose:** Complete DevOps pipeline with multiple environments + +**Key Features:** +- 5 stages: Build โ†’ Dev โ†’ Staging โ†’ Production โ†’ Post-Deploy +- Environment approvals and gates +- Blue-green deployment strategy +- Automated rollback on failure +- Performance and integration testing +- Deployment slots and swapping + +**Use Case:** Advanced pipeline concepts in Segment 3 + +### 3. GitHub Actions Workflow (`../.github/workflows/node-azure-deploy.yml`) +**Purpose:** GitHub-native CI/CD to Azure + +**Key Features:** +- Matrix strategy for multiple Node versions +- GitHub environments with protection rules +- Azure Web App deployment +- Release tagging +- Security scanning with CodeQL +- Reusable workflow patterns + +**Use Case:** Comparing Azure Pipelines vs GitHub Actions + +## ๐Ÿš€ How to Use These Examples + +### For Azure Pipelines: +1. Create a new pipeline in Azure DevOps +2. Select "Existing Azure Pipelines YAML file" +3. Choose the appropriate YAML file +4. Update variables (service connections, resource names) +5. Save and run + +### For GitHub Actions: +1. Workflow is automatically detected in `.github/workflows/` +2. Configure secrets in repository settings: + - `AZURE_CREDENTIALS` - Service principal JSON +3. Workflow triggers on push to main/master + +## ๐Ÿ“ Required Configuration + +### Azure Resources: +```bash +# Create resource group +az group create --name rg-az400-demo --location eastus + +# Create App Service Plan +az appservice plan create \ + --name plan-az400-demo \ + --resource-group rg-az400-demo \ + --sku B1 \ + --is-linux + +# Create Web Apps +az webapp create \ + --name webapp-az400-demo-dev \ + --resource-group rg-az400-demo \ + --plan plan-az400-demo \ + --runtime "NODE:18-lts" +``` + +### Service Connection (Azure DevOps): +1. Project Settings โ†’ Service connections +2. New service connection โ†’ Azure Resource Manager +3. Service principal (automatic) +4. Name: `AZ400-ServiceConnection` + +### GitHub Secrets: +```bash +# Create service principal +az ad sp create-for-rbac \ + --name "sp-az400-github" \ + --role contributor \ + --scopes /subscriptions/{subscription-id}/resourceGroups/rg-az400-demo \ + --json-auth +``` + +## ๐ŸŽ“ Teaching Points + +### Single-Stage Pipeline: +- Pipeline triggers and paths +- Agent pools and VM images +- Variables and expressions +- Task vs script steps +- Artifact publishing + +### Multi-Stage Pipeline: +- Stage dependencies +- Deployment jobs vs regular jobs +- Environment approvals +- Deployment strategies +- Failure handling + +### GitHub Actions: +- Workflow syntax differences +- Actions marketplace +- Environments and secrets +- Reusable workflows +- GitHub-specific features + +## ๐Ÿ”ง Customization + +Students can modify these pipelines to: +- Add additional testing frameworks +- Implement different deployment strategies +- Add notifications (Teams, Slack) +- Include infrastructure as code +- Add security scanning tools + +## ๐Ÿ“š Additional Resources + +- [Azure Pipelines YAML schema](https://learn.microsoft.com/azure/devops/pipelines/yaml-schema) +- [GitHub Actions documentation](https://docs.github.com/actions) +- [Azure Web Apps deployment](https://learn.microsoft.com/azure/app-service/deploy-github-actions) diff --git a/pipelines/multi-stage-cicd.yml b/pipelines/multi-stage-cicd.yml new file mode 100644 index 0000000..fd3a2c0 --- /dev/null +++ b/pipelines/multi-stage-cicd.yml @@ -0,0 +1,316 @@ +# Multi-Stage CI/CD Pipeline for Node.js to Azure +# Teaching example for full DevOps pipeline with environments + +trigger: + branches: + include: + - master + - main + paths: + include: + - nodeapp-1/* + +pr: + branches: + include: + - master + - main + +pool: + vmImage: 'ubuntu-latest' + +variables: + nodeVersion: '18.x' + azureSubscription: 'AZ400-ServiceConnection' # Update with your service connection + resourceGroupName: 'rg-az400-demo' + webAppName: 'webapp-az400-$(Build.BuildId)' + environmentNameDev: 'Development' + environmentNameStaging: 'Staging' + environmentNameProd: 'Production' + +stages: +# ======================================== +# STAGE 1: BUILD +# ======================================== +- stage: Build + displayName: 'Build and Test' + jobs: + - job: BuildJob + displayName: 'Build Node.js Application' + steps: + # Setup Node.js + - task: NodeTool@0 + displayName: 'Install Node.js $(nodeVersion)' + inputs: + versionSpec: $(nodeVersion) + + # Install dependencies + - script: | + echo "๐Ÿ“ฆ Installing dependencies..." + npm ci + displayName: 'Install dependencies' + workingDirectory: nodeapp-1 + + # Run security audit + - script: | + echo "๐Ÿ”’ Running security audit..." + npm audit --audit-level=high || true + displayName: 'Security audit' + workingDirectory: nodeapp-1 + + # Run linting + - script: | + echo "๐Ÿ” Running linter..." + npm run lint || echo "No lint script configured" + displayName: 'Lint code' + workingDirectory: nodeapp-1 + continueOnError: true + + # Run unit tests + - script: | + echo "๐Ÿงช Running unit tests..." + npm test -- --coverage || npm test + displayName: 'Run unit tests' + workingDirectory: nodeapp-1 + + # Publish test results + - task: PublishTestResults@2 + displayName: 'Publish test results' + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: '**/test-*.xml' + searchFolder: nodeapp-1 + failTaskOnFailedTests: true + condition: succeededOrFailed() + + # Publish code coverage + - task: PublishCodeCoverageResults@1 + displayName: 'Publish code coverage' + inputs: + codeCoverageTool: 'Cobertura' + summaryFileLocation: 'nodeapp-1/coverage/**/cobertura-coverage.xml' + condition: succeededOrFailed() + + # Build application + - script: | + echo "๐Ÿ”จ Building application..." + npm run build || echo "No build required" + displayName: 'Build application' + workingDirectory: nodeapp-1 + + # Create deployment package + - task: ArchiveFiles@2 + displayName: 'Create deployment package' + inputs: + rootFolderOrFile: 'nodeapp-1' + includeRootFolder: false + archiveType: 'zip' + archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip' + + # Publish artifact + - publish: $(Build.ArtifactStagingDirectory) + artifact: drop + displayName: 'Publish artifact' + +# ======================================== +# STAGE 2: DEPLOY TO DEVELOPMENT +# ======================================== +- stage: DeployDev + displayName: 'Deploy to Development' + dependsOn: Build + condition: succeeded() + jobs: + - deployment: DeployToDev + displayName: 'Deploy to Dev Environment' + environment: $(environmentNameDev) + strategy: + runOnce: + deploy: + steps: + # Download artifact + - download: current + artifact: drop + displayName: 'Download artifact' + + # Deploy to Azure Web App + - task: AzureWebApp@1 + displayName: 'Deploy to Dev Web App' + inputs: + azureSubscription: $(azureSubscription) + appType: 'webAppLinux' + appName: '$(webAppName)-dev' + package: '$(Pipeline.Workspace)/drop/*.zip' + runtimeStack: 'NODE|18-lts' + startUpCommand: 'npm start' + + # Smoke test + - script: | + echo "๐Ÿ”ฅ Running smoke tests..." + sleep 30 + curl -f https://$(webAppName)-dev.azurewebsites.net || exit 1 + echo "โœ… Smoke tests passed!" + displayName: 'Smoke test' + +# ======================================== +# STAGE 3: DEPLOY TO STAGING +# ======================================== +- stage: DeployStaging + displayName: 'Deploy to Staging' + dependsOn: DeployDev + condition: succeeded() + jobs: + - deployment: DeployToStaging + displayName: 'Deploy to Staging Environment' + environment: + name: $(environmentNameStaging) + resourceType: VirtualMachine + strategy: + runOnce: + deploy: + steps: + # Download artifact + - download: current + artifact: drop + displayName: 'Download artifact' + + # Deploy to staging slot + - task: AzureWebApp@1 + displayName: 'Deploy to Staging Slot' + inputs: + azureSubscription: $(azureSubscription) + appType: 'webAppLinux' + appName: '$(webAppName)-staging' + deployToSlotOrASE: true + slotName: 'staging' + package: '$(Pipeline.Workspace)/drop/*.zip' + runtimeStack: 'NODE|18-lts' + + # Run integration tests + - script: | + echo "๐Ÿงช Running integration tests..." + npm run test:integration || echo "No integration tests configured" + displayName: 'Integration tests' + workingDirectory: nodeapp-1 + continueOnError: true + + # Performance test + - script: | + echo "โšก Running performance tests..." + # Add your performance testing tool here + echo "Performance baseline: Response time < 200ms" + displayName: 'Performance tests' + +# ======================================== +# STAGE 4: DEPLOY TO PRODUCTION +# ======================================== +- stage: DeployProd + displayName: 'Deploy to Production' + dependsOn: DeployStaging + condition: succeeded() + jobs: + - deployment: DeployToProd + displayName: 'Deploy to Production Environment' + environment: + name: $(environmentNameProd) + resourceType: VirtualMachine + strategy: + runOnce: + preDeploy: + steps: + - script: | + echo "๐Ÿ“‹ Pre-deployment checklist:" + echo "โœ“ All tests passed" + echo "โœ“ Security scan completed" + echo "โœ“ Performance benchmarks met" + displayName: 'Pre-deployment validation' + + deploy: + steps: + # Download artifact + - download: current + artifact: drop + displayName: 'Download artifact' + + # Blue-Green deployment + - task: AzureWebApp@1 + displayName: 'Deploy to Production (Blue-Green)' + inputs: + azureSubscription: $(azureSubscription) + appType: 'webAppLinux' + appName: $(webAppName) + deployToSlotOrASE: true + slotName: 'staging' + package: '$(Pipeline.Workspace)/drop/*.zip' + runtimeStack: 'NODE|18-lts' + + # Swap slots + - task: AzureAppServiceManage@0 + displayName: 'Swap staging to production' + inputs: + azureSubscription: $(azureSubscription) + WebAppName: $(webAppName) + ResourceGroupName: $(resourceGroupName) + SourceSlot: 'staging' + SwapWithProduction: true + + postRouteTraffic: + steps: + # Monitor deployment + - script: | + echo "๐Ÿ“Š Monitoring deployment health..." + # Add Application Insights queries here + echo "โœ… Deployment healthy" + displayName: 'Monitor deployment' + + on: + failure: + steps: + # Rollback on failure + - task: AzureAppServiceManage@0 + displayName: 'Rollback: Swap slots back' + inputs: + azureSubscription: $(azureSubscription) + WebAppName: $(webAppName) + ResourceGroupName: $(resourceGroupName) + SourceSlot: 'production' + TargetSlot: 'staging' + SwapWithProduction: false + + success: + steps: + - script: | + echo "๐ŸŽ‰ Production deployment successful!" + echo "URL: https://$(webAppName).azurewebsites.net" + echo "Build: $(Build.BuildId)" + echo "Commit: $(Build.SourceVersion)" + displayName: 'Deployment summary' + +# ======================================== +# STAGE 5: POST-DEPLOYMENT +# ======================================== +- stage: PostDeployment + displayName: 'Post-Deployment Tasks' + dependsOn: DeployProd + condition: succeeded() + jobs: + - job: PostDeploymentTasks + displayName: 'Run post-deployment tasks' + steps: + # Tag the release + - script: | + echo "๐Ÿท๏ธ Tagging release..." + git tag -a "v$(Build.BuildId)" -m "Release $(Build.BuildId)" + displayName: 'Tag release' + condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main')) + + # Update documentation + - script: | + echo "๐Ÿ“š Updating deployment documentation..." + echo "Deployment completed at: $(date)" >> deployment-log.md + displayName: 'Update documentation' + + # Send notifications + - script: | + echo "๐Ÿ“ง Sending deployment notifications..." + # Add your notification logic here (Teams, Slack, email) + displayName: 'Send notifications' diff --git a/pipelines/single-stage-ci.yml b/pipelines/single-stage-ci.yml new file mode 100644 index 0000000..bb0399d --- /dev/null +++ b/pipelines/single-stage-ci.yml @@ -0,0 +1,104 @@ +# Single-Stage CI Pipeline for Node.js +# Teaching example for basic continuous integration + +trigger: + branches: + include: + - master + - main + paths: + include: + - nodeapp-1/* + +pool: + vmImage: 'ubuntu-latest' + +variables: + nodeVersion: '18.x' + artifactName: 'drop' + +steps: +# Step 1: Setup Node.js environment +- task: NodeTool@0 + displayName: 'Install Node.js $(nodeVersion)' + inputs: + versionSpec: $(nodeVersion) + +# Step 2: Install dependencies +- script: | + echo "Installing npm dependencies..." + npm ci + displayName: 'npm install' + workingDirectory: nodeapp-1 + +# Step 3: Run linting +- script: | + echo "Running ESLint..." + npm run lint || echo "No lint script found, skipping..." + displayName: 'Run linting' + workingDirectory: nodeapp-1 + continueOnError: true + +# Step 4: Run unit tests +- script: | + echo "Running tests..." + npm test + displayName: 'Run unit tests' + workingDirectory: nodeapp-1 + +# Step 5: Generate test results +- task: PublishTestResults@2 + displayName: 'Publish test results' + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: '**/test-results.xml' + searchFolder: nodeapp-1 + condition: succeededOrFailed() + +# Step 6: Code coverage +- script: | + echo "Generating code coverage..." + npm run coverage || echo "No coverage script found, skipping..." + displayName: 'Generate code coverage' + workingDirectory: nodeapp-1 + continueOnError: true + +- task: PublishCodeCoverageResults@1 + displayName: 'Publish code coverage' + inputs: + codeCoverageTool: 'Cobertura' + summaryFileLocation: 'nodeapp-1/coverage/cobertura-coverage.xml' + condition: succeededOrFailed() + +# Step 7: Build the application +- script: | + echo "Building application..." + npm run build || npm run compile || echo "No build script, using source files..." + displayName: 'Build application' + workingDirectory: nodeapp-1 + +# Step 8: Create artifact +- task: ArchiveFiles@2 + displayName: 'Archive application files' + inputs: + rootFolderOrFile: 'nodeapp-1' + includeRootFolder: false + archiveType: 'zip' + archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip' + +# Step 9: Publish artifact +- task: PublishBuildArtifacts@1 + displayName: 'Publish artifact' + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)' + ArtifactName: $(artifactName) + publishLocation: 'Container' + +# Success notification +- script: | + echo "โœ… Build completed successfully!" + echo "Build ID: $(Build.BuildId)" + echo "Source branch: $(Build.SourceBranch)" + echo "Commit: $(Build.SourceVersion)" + displayName: 'Build summary' + condition: succeeded()