Skip to main content
COSMICBYTEZLABS
NewsSecurityHOWTOsToolsStudyTraining
ProjectsChecklistsAI RankingsNewsletterStatusTagsAbout
Subscribe

Press Enter to search or Esc to close

News
Security
HOWTOs
Tools
Study
Training
Projects
Checklists
AI Rankings
Newsletter
Status
Tags
About
RSS Feed
Reading List
Subscribe

Stay in the Loop

Get the latest security alerts, tutorials, and tech insights delivered to your inbox.

Subscribe NowFree forever. No spam.
COSMICBYTEZLABS

Your trusted source for IT intelligence, cybersecurity insights, and hands-on technical guides.

429+ Articles
114+ Guides

CONTENT

  • Latest News
  • Security Alerts
  • HOWTOs
  • Projects
  • Exam Prep

RESOURCES

  • Search
  • Browse Tags
  • Newsletter Archive
  • Reading List
  • RSS Feed

COMPANY

  • About Us
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CosmicBytez Labs. All rights reserved.

System Status: Operational
  1. Home
  2. Projects
  3. CI/CD Pipeline with GitHub Actions and Azure
CI/CD Pipeline with GitHub Actions and Azure
PROJECTIntermediate

CI/CD Pipeline with GitHub Actions and Azure

Build a secure CI/CD pipeline with GitHub Actions deploying to Azure. Covers build, test, security scanning (SAST/DAST), and deployment with OIDC...

Dylan H.

DevOps Engineering

February 3, 2026
11 min read
4-6 hours

Tools & Technologies

GitHub ActionsAzure CLIDockerTerraform

CI/CD Pipeline with GitHub Actions and Azure

Build an enterprise-grade CI/CD pipeline using GitHub Actions with secure deployment to Azure. This project implements security scanning, automated testing, and OIDC-based authentication for zero-secret deployments.

Project Overview

What We're Building

┌─────────────────────────────────────────────────────────────────────┐
│                    CI/CD Pipeline Architecture                       │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Developer                                                          │
│      │                                                              │
│      ▼                                                              │
│  ┌──────────────┐                                                   │
│  │   GitHub     │                                                   │
│  │  Repository  │                                                   │
│  └──────┬───────┘                                                   │
│         │ Push/PR                                                   │
│         ▼                                                           │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │                    GitHub Actions                             │  │
│  │  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐         │  │
│  │  │  Build  │─▶│  Test   │─▶│Security │─▶│ Deploy  │         │  │
│  │  │         │  │         │  │ Scan    │  │         │         │  │
│  │  └─────────┘  └─────────┘  └─────────┘  └────┬────┘         │  │
│  └──────────────────────────────────────────────┼───────────────┘  │
│                                                 │                   │
│                               ┌─────────────────┼─────────────────┐│
│                               │                 ▼                  ││
│                               │    ┌─────────────────────┐        ││
│                               │    │   Azure (OIDC)      │        ││
│                               │    │   No Secrets!       │        ││
│                               │    └──────────┬──────────┘        ││
│                               │               │                    ││
│                               │    ┌──────────┼──────────┐        ││
│                               │    ▼          ▼          ▼        ││
│                               │ ┌─────┐   ┌─────┐   ┌─────────┐  ││
│                               │ │ ACR │   │ AKS │   │App Svc  │  ││
│                               │ └─────┘   └─────┘   └─────────┘  ││
│                               └────────────────────────────────────┘│
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Prerequisites

  • GitHub repository with application code
  • Azure subscription
  • Azure CLI installed locally
  • Docker installed locally
  • Basic understanding of GitHub Actions

Part 1: Azure Infrastructure Setup

Step 1: Create Azure Resources

# Variables
RESOURCE_GROUP="rg-webapp-prod"
LOCATION="eastus"
ACR_NAME="acrwebappprod"
APP_SERVICE_PLAN="asp-webapp-prod"
WEB_APP_NAME="webapp-prod-$(openssl rand -hex 4)"
 
# Create resource group
az group create --name $RESOURCE_GROUP --location $LOCATION
 
# Create Azure Container Registry
az acr create \
  --resource-group $RESOURCE_GROUP \
  --name $ACR_NAME \
  --sku Standard \
  --admin-enabled false
 
# Create App Service Plan
az appservice plan create \
  --resource-group $RESOURCE_GROUP \
  --name $APP_SERVICE_PLAN \
  --is-linux \
  --sku P1v3
 
# Create Web App
az webapp create \
  --resource-group $RESOURCE_GROUP \
  --plan $APP_SERVICE_PLAN \
  --name $WEB_APP_NAME \
  --deployment-container-image-name mcr.microsoft.com/appsvc/staticsite:latest
 
# Configure Web App to use ACR
az webapp config container set \
  --resource-group $RESOURCE_GROUP \
  --name $WEB_APP_NAME \
  --docker-custom-image-name "$ACR_NAME.azurecr.io/webapp:latest" \
  --docker-registry-server-url "https://$ACR_NAME.azurecr.io"

Step 2: Configure OIDC Authentication

Create Azure AD Application:

# Create app registration
APP_ID=$(az ad app create \
  --display-name "GitHub-Actions-OIDC" \
  --query appId -o tsv)
 
# Create service principal
az ad sp create --id $APP_ID
 
# Get subscription and tenant IDs
SUBSCRIPTION_ID=$(az account show --query id -o tsv)
TENANT_ID=$(az account show --query tenantId -o tsv)
 
# Configure federated credential for GitHub
GITHUB_ORG="your-github-org"
GITHUB_REPO="your-repo-name"
 
az ad app federated-credential create \
  --id $APP_ID \
  --parameters "{
    \"name\": \"github-main-branch\",
    \"issuer\": \"https://token.actions.githubusercontent.com\",
    \"subject\": \"repo:${GITHUB_ORG}/${GITHUB_REPO}:ref:refs/heads/main\",
    \"audiences\": [\"api://AzureADTokenExchange\"]
  }"
 
# Add credential for pull requests (optional)
az ad app federated-credential create \
  --id $APP_ID \
  --parameters "{
    \"name\": \"github-pull-request\",
    \"issuer\": \"https://token.actions.githubusercontent.com\",
    \"subject\": \"repo:${GITHUB_ORG}/${GITHUB_REPO}:pull_request\",
    \"audiences\": [\"api://AzureADTokenExchange\"]
  }"
 
# Grant permissions to resource group
az role assignment create \
  --assignee $APP_ID \
  --role Contributor \
  --scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP"
 
# Grant ACR push permission
ACR_ID=$(az acr show --name $ACR_NAME --query id -o tsv)
az role assignment create \
  --assignee $APP_ID \
  --role AcrPush \
  --scope $ACR_ID
 
# Output values for GitHub secrets
echo "AZURE_CLIENT_ID: $APP_ID"
echo "AZURE_TENANT_ID: $TENANT_ID"
echo "AZURE_SUBSCRIPTION_ID: $SUBSCRIPTION_ID"

Step 3: Configure GitHub Secrets

Navigate to: Repository → Settings → Secrets and variables → Actions

Add these secrets:

Secret NameValue
AZURE_CLIENT_IDApp registration Application ID
AZURE_TENANT_IDAzure AD Tenant ID
AZURE_SUBSCRIPTION_IDAzure Subscription ID
ACR_NAMEAzure Container Registry name
WEBAPP_NAMEAzure Web App name

Part 2: GitHub Actions Workflow

Step 4: Create Main CI/CD Workflow

# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
 
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
 
env:
  ACR_NAME: ${{ secrets.ACR_NAME }}
  WEBAPP_NAME: ${{ secrets.WEBAPP_NAME }}
  IMAGE_NAME: webapp
 
permissions:
  id-token: write
  contents: read
  security-events: write
  pull-requests: write
 
jobs:
  # ===================
  # BUILD JOB
  # ===================
  build:
    name: Build Application
    runs-on: ubuntu-latest
 
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
 
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
 
      - name: Install dependencies
        run: npm ci
 
      - name: Build application
        run: npm run build
 
      - name: Upload build artifact
        uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: dist/
          retention-days: 7
 
  # ===================
  # TEST JOB
  # ===================
  test:
    name: Run Tests
    runs-on: ubuntu-latest
    needs: build
 
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
 
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
 
      - name: Install dependencies
        run: npm ci
 
      - name: Run unit tests
        run: npm test -- --coverage
 
      - name: Upload coverage report
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          fail_ci_if_error: false
 
  # ===================
  # SECURITY SCANNING
  # ===================
  security-scan:
    name: Security Scanning
    runs-on: ubuntu-latest
    needs: build
 
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
 
      # SAST - Static Application Security Testing
      - name: Run CodeQL Analysis
        uses: github/codeql-action/init@v3
        with:
          languages: javascript
 
      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@v3
 
      # Dependency Scanning
      - name: Run npm audit
        run: npm audit --audit-level=high
        continue-on-error: true
 
      - name: Run Snyk security scan
        uses: snyk/actions/node@master
        continue-on-error: true
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          args: --severity-threshold=high
 
      # Secret Scanning
      - name: Scan for secrets
        uses: trufflesecurity/trufflehog@main
        with:
          path: ./
          base: ${{ github.event.repository.default_branch }}
          head: HEAD
 
  # ===================
  # DOCKER BUILD
  # ===================
  docker-build:
    name: Build Docker Image
    runs-on: ubuntu-latest
    needs: [test, security-scan]
    if: github.event_name == 'push'
 
    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
 
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
 
      - name: Download build artifact
        uses: actions/download-artifact@v4
        with:
          name: build-output
          path: dist/
 
      # Login to Azure
      - name: Azure Login (OIDC)
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
 
      # Login to ACR
      - name: Login to ACR
        run: az acr login --name ${{ env.ACR_NAME }}
 
      # Generate Docker metadata
      - name: Docker meta
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix=
            type=ref,event=branch
            type=raw,value=latest,enable={{is_default_branch}}
 
      # Build and push
      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
 
      # Scan container image
      - name: Scan container image
        uses: azure/container-scan@v0
        with:
          image-name: ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ github.sha }}
          severity-threshold: HIGH
 
  # ===================
  # DEPLOY TO STAGING
  # ===================
  deploy-staging:
    name: Deploy to Staging
    runs-on: ubuntu-latest
    needs: docker-build
    if: github.ref == 'refs/heads/develop'
    environment:
      name: staging
      url: https://${{ secrets.STAGING_WEBAPP_NAME }}.azurewebsites.net
 
    steps:
      - name: Azure Login (OIDC)
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
 
      - name: Deploy to Azure Web App
        uses: azure/webapps-deploy@v3
        with:
          app-name: ${{ secrets.STAGING_WEBAPP_NAME }}
          images: ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ github.sha }}
 
      - name: Health check
        run: |
          sleep 30
          curl -f https://${{ secrets.STAGING_WEBAPP_NAME }}.azurewebsites.net/health || exit 1
 
  # ===================
  # DEPLOY TO PRODUCTION
  # ===================
  deploy-production:
    name: Deploy to Production
    runs-on: ubuntu-latest
    needs: docker-build
    if: github.ref == 'refs/heads/main'
    environment:
      name: production
      url: https://${{ secrets.WEBAPP_NAME }}.azurewebsites.net
 
    steps:
      - name: Azure Login (OIDC)
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
 
      - name: Deploy to Azure Web App
        uses: azure/webapps-deploy@v3
        with:
          app-name: ${{ secrets.WEBAPP_NAME }}
          images: ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ github.sha }}
 
      - name: Health check
        run: |
          sleep 30
          curl -f https://${{ secrets.WEBAPP_NAME }}.azurewebsites.net/health || exit 1
 
      - name: Create deployment summary
        run: |
          echo "## Deployment Summary" >> $GITHUB_STEP_SUMMARY
          echo "- **Environment:** Production" >> $GITHUB_STEP_SUMMARY
          echo "- **Image:** ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
          echo "- **URL:** https://${{ secrets.WEBAPP_NAME }}.azurewebsites.net" >> $GITHUB_STEP_SUMMARY

Step 5: Create Dockerfile

# Dockerfile
# Build stage
FROM node:20-alpine AS builder
 
WORKDIR /app
 
COPY package*.json ./
RUN npm ci --only=production
 
COPY . .
RUN npm run build
 
# Production stage
FROM node:20-alpine AS production
 
WORKDIR /app
 
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001
 
# Copy built assets
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package*.json ./
 
USER nodejs
 
EXPOSE 3000
 
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
 
CMD ["node", "dist/server.js"]

Part 3: Security Scanning Deep Dive

Step 6: Configure SAST (Static Analysis)

# .github/workflows/security.yml
name: Security Scanning
 
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 6 * * 1'  # Weekly on Monday
 
permissions:
  security-events: write
  contents: read
 
jobs:
  codeql:
    name: CodeQL Analysis
    runs-on: ubuntu-latest
 
    strategy:
      fail-fast: false
      matrix:
        language: ['javascript']
 
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
 
      - name: Initialize CodeQL
        uses: github/codeql-action/init@v3
        with:
          languages: ${{ matrix.language }}
          queries: security-extended
 
      - name: Autobuild
        uses: github/codeql-action/autobuild@v3
 
      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@v3
        with:
          category: "/language:${{ matrix.language }}"
 
  dependency-review:
    name: Dependency Review
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
 
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4
 
      - name: Dependency Review
        uses: actions/dependency-review-action@v4
        with:
          fail-on-severity: high
          deny-licenses: GPL-3.0, AGPL-3.0

Step 7: Configure DAST (Dynamic Analysis)

# .github/workflows/dast.yml
name: DAST Scanning
 
on:
  workflow_run:
    workflows: ["CI/CD Pipeline"]
    types:
      - completed
    branches: [develop]
 
jobs:
  dast-scan:
    name: OWASP ZAP Scan
    runs-on: ubuntu-latest
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
 
    steps:
      - name: Run OWASP ZAP Baseline Scan
        uses: zaproxy/action-baseline@v0.12.0
        with:
          target: 'https://${{ secrets.STAGING_WEBAPP_NAME }}.azurewebsites.net'
          rules_file_name: '.zap/rules.tsv'
          cmd_options: '-a'
 
      - name: Upload ZAP Report
        uses: actions/upload-artifact@v4
        with:
          name: zap-report
          path: report_html.html

Part 4: Infrastructure as Code

Step 8: Terraform for Azure Infrastructure

# terraform/main.tf
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
    }
  }
 
  backend "azurerm" {
    resource_group_name  = "rg-terraform-state"
    storage_account_name = "stterraformstate"
    container_name       = "tfstate"
    key                  = "webapp.tfstate"
  }
}
 
provider "azurerm" {
  features {}
}
 
variable "environment" {
  type    = string
  default = "prod"
}
 
variable "location" {
  type    = string
  default = "eastus"
}
 
resource "azurerm_resource_group" "main" {
  name     = "rg-webapp-${var.environment}"
  location = var.location
}
 
resource "azurerm_container_registry" "main" {
  name                = "acrwebapp${var.environment}"
  resource_group_name = azurerm_resource_group.main.name
  location            = azurerm_resource_group.main.location
  sku                 = "Standard"
  admin_enabled       = false
}
 
resource "azurerm_service_plan" "main" {
  name                = "asp-webapp-${var.environment}"
  resource_group_name = azurerm_resource_group.main.name
  location            = azurerm_resource_group.main.location
  os_type             = "Linux"
  sku_name            = "P1v3"
}
 
resource "azurerm_linux_web_app" "main" {
  name                = "webapp-${var.environment}-${random_id.suffix.hex}"
  resource_group_name = azurerm_resource_group.main.name
  location            = azurerm_resource_group.main.location
  service_plan_id     = azurerm_service_plan.main.id
 
  site_config {
    always_on = true
 
    application_stack {
      docker_image_name   = "${azurerm_container_registry.main.login_server}/webapp:latest"
      docker_registry_url = "https://${azurerm_container_registry.main.login_server}"
    }
  }
 
  identity {
    type = "SystemAssigned"
  }
}
 
resource "random_id" "suffix" {
  byte_length = 4
}
 
output "webapp_url" {
  value = "https://${azurerm_linux_web_app.main.default_hostname}"
}

Step 9: Terraform Workflow

# .github/workflows/terraform.yml
name: Terraform Infrastructure
 
on:
  push:
    branches: [main]
    paths:
      - 'terraform/**'
  pull_request:
    branches: [main]
    paths:
      - 'terraform/**'
 
permissions:
  id-token: write
  contents: read
  pull-requests: write
 
jobs:
  terraform:
    name: Terraform
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: terraform
 
    steps:
      - name: Checkout
        uses: actions/checkout@v4
 
      - name: Azure Login (OIDC)
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
 
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.7.0
 
      - name: Terraform Init
        run: terraform init
 
      - name: Terraform Format
        run: terraform fmt -check
 
      - name: Terraform Validate
        run: terraform validate
 
      - name: Terraform Plan
        id: plan
        run: terraform plan -no-color -out=tfplan
        continue-on-error: true
 
      - name: Comment PR with Plan
        uses: actions/github-script@v7
        if: github.event_name == 'pull_request'
        with:
          script: |
            const output = `#### Terraform Plan 📖
            \`\`\`
            ${{ steps.plan.outputs.stdout }}
            \`\`\`
            `;
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            })
 
      - name: Terraform Apply
        if: github.ref == 'refs/heads/main' && github.event_name == 'push'
        run: terraform apply -auto-approve tfplan

Verification Checklist

Azure Setup:

  • Resource group created
  • ACR created and configured
  • App Service Plan created
  • Web App configured
  • OIDC authentication working

GitHub Configuration:

  • Secrets configured
  • Workflow files created
  • Branch protection enabled
  • Environment approvals configured

Pipeline Testing:

  • Build job succeeds
  • Tests pass
  • Security scans complete
  • Docker image pushed
  • Deployment succeeds

Resources

  • GitHub Actions Documentation
  • Azure OIDC Authentication
  • Azure App Service Documentation

Questions? Reach out in our community Discord!

Related Reading

  • Claude Code for IT Operations: Building a Multi-Project
  • How to Secure GitHub Actions Workflows with OIDC, SHA
  • Azure Landing Zone with Terraform
#CI/CD#GitHub Actions#Azure#DevOps#automation#Security

Related Articles

Claude Code for IT Operations: Building a Multi-Project

Transform Claude Code from a chatbot into a DevOps co-pilot. Set up CLAUDE.md templates, custom hooks, reusable agents, deployment skills, and MCP server...

12 min read

Azure Landing Zone with Terraform

Deploy enterprise-ready Azure environment with hub-spoke network, Azure Firewall, Log Analytics, Defender for Cloud following Microsoft CAF best practices.

11 min read

Keycloak SSO: Self-Hosted Identity Provider for Your Homelab

Deploy Keycloak with Docker Compose and PostgreSQL to build a centralised single sign-on platform for your homelab services, with OIDC integration for...

11 min read
Back to all Projects