Skip to content

06 - CI/CD (Dedicated)

This tutorial sets up CI/CD for Dedicated with standard zip deployment. Dedicated supports Kudu/SCM and zipdeploy workflows, which makes GitHub Actions integration straightforward.

Prerequisites

export RG="rg-func-dedicated-dev"
export APP_NAME="func-dedi-<unique-suffix>"
export PLAN_NAME="asp-dedi-b1-dev"
export STORAGE_NAME="stdedidev<unique>"
export LOCATION="koreacentral"

What You'll Build

You will package the Python Function App from apps/python, deploy it with Zip Deploy and remote build settings, and implement an automated GitHub Actions deployment workflow.

Infrastructure Context

Plan: Dedicated (B1) | Network: Public internet in this tutorial | VNet: Supported by platform, not configured here

The app runs on a fixed App Service Plan (always on, no scale-to-zero). Basic B1 supports App Service VNet integration and private endpoints, but this guide uses Standard (S1+) for private networking scenarios to provide scale headroom, deployment slots, and a production-oriented validation path.

flowchart TD
    INET[Internet] -->|HTTPS| FA[Function App\nDedicated B1-P3v3\nLinux Python 3.11]

    subgraph VNET["VNet 10.0.0.0/16"]
        subgraph INT_SUB["Integration Subnet 10.0.1.0/24\nDelegation: Microsoft.Web/serverFarms"]
            FA
        end
        subgraph PE_SUB["Private Endpoint Subnet 10.0.2.0/24"]
            PE_BLOB[PE: blob]
            PE_QUEUE[PE: queue]
            PE_TABLE[PE: table]
            PE_FILE[PE: file]
        end
    end

    PE_BLOB --> ST["Storage Account"]
    PE_QUEUE --> ST
    PE_TABLE --> ST
    PE_FILE --> ST

    subgraph DNS[Private DNS Zones]
        DNS_BLOB[privatelink.blob.core.windows.net]
        DNS_QUEUE[privatelink.queue.core.windows.net]
        DNS_TABLE[privatelink.table.core.windows.net]
        DNS_FILE[privatelink.file.core.windows.net]
    end

    PE_BLOB -.-> DNS_BLOB
    PE_QUEUE -.-> DNS_QUEUE
    PE_TABLE -.-> DNS_TABLE
    PE_FILE -.-> DNS_FILE

    FA -.->|System-Assigned MI| ENTRA[Microsoft Entra ID]
    FA --> AI[Application Insights]

    RFP["📦 WEBSITE_RUN_FROM_PACKAGE=1\nNo content share required"] -.- FA
    ALWAYS_ON["⚙️ Always On: true\nFixed capacity"] -.- FA

    style FA fill:#5c2d91,color:#fff
    style VNET fill:#E8F5E9,stroke:#4CAF50
    style ST fill:#FFF3E0
    style DNS fill:#E3F2FD
flowchart TD
    A[GitHub push] --> B[GitHub Actions workflow]
    B --> C["Build and deploy apps/python"]
    C --> D[Dedicated Function App]

Steps

Step 1 - Build a deployable zip package

python -m venv .venv
source .venv/bin/activate
pip install --requirement apps/python/requirements.txt

cd apps/python
zip --recurse-paths ../functionapp.zip .
cd ..

Step 2 - Deploy with zipdeploy

az functionapp config appsettings set \
  --name $APP_NAME \
  --resource-group $RG \
  --settings \
    SCM_DO_BUILD_DURING_DEPLOYMENT=true \
    ENABLE_ORYX_BUILD=true

az functionapp deployment source config-zip \
  --name $APP_NAME \
  --resource-group $RG \
  --src functionapp.zip
CLI element Explanation
Command(s) az functionapp config appsettings set, az functionapp deployment source config-zip
Key flags --name, --resource-group, --settings, --src
Variables $APP_NAME, $RG
Expected result Azure CLI returns provisioning details; confirm the resource name and successful provisioning state before continuing.

CLI overrides SCM_DO_BUILD_DURING_DEPLOYMENT

The az functionapp deployment source config-zip command automatically sets SCM_DO_BUILD_DURING_DEPLOYMENT=false, which prevents pip install from running during deployment. For Python apps on Dedicated, prefer func azure functionapp publish $APP_NAME --python instead of manual zip deploy — it handles the remote build correctly.

Step 3 - Verify deployment and endpoint health

az functionapp deployment list-publishing-profiles \
  --name $APP_NAME \
  --resource-group $RG \
  --output table

curl --request GET "https://$APP_NAME.azurewebsites.net/api/health"
CLI element Explanation
Command(s) az functionapp deployment list-publishing-profiles
Key flags --name, --resource-group, --output, --request
Variables $APP_NAME, $RG
Expected result Azure CLI returns provisioning details; confirm the resource name and successful provisioning state before continuing.

Create .github/workflows/deploy-dedicated.yml:

name: deploy-dedicated

on:
  push:
    branches: [ main ]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Deploy Azure Functions app
        uses: Azure/functions-action@v1
        with:
          app-name: ${{ vars.AZURE_FUNCTIONAPP_NAME }}
          package: apps/python
          publish-profile: ${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
          scm-do-build-during-deployment: true
          enable-oryx-build: true

Step 5 - Deployment slots (S1+ only)

Deployment slots are unavailable on B1. Upgrade to S1 or P1v2, then create and use a staging slot:

az appservice plan update \
  --name $PLAN_NAME \
  --resource-group $RG \
  --sku S1

az functionapp deployment slot create \
  --name $APP_NAME \
  --resource-group $RG \
  --slot staging

az functionapp deployment source config-zip \
  --name $APP_NAME \
  --resource-group $RG \
  --slot staging \
  --src functionapp.zip

az functionapp deployment slot swap \
  --name $APP_NAME \
  --resource-group $RG \
  --slot staging \
  --target-slot production
CLI element Explanation
Command(s) az appservice plan update, az functionapp deployment slot create, az functionapp deployment source config-zip, az functionapp deployment slot swap
Key flags --name, --resource-group, --sku, --slot, --src, --target-slot
Variables $PLAN_NAME, $RG, $APP_NAME
Expected result Azure CLI returns provisioning details; confirm the resource name and successful provisioning state before continuing.

Requires Standard tier or higher

Deployment slots are not available on Basic (B1) tier. Upgrade to Standard (S1) or Premium (P1v2) before using slots.

Verification

az functionapp deployment source config-zip ...:

{
  "active": true,
  "author": "N/A",
  "complete": true,
  "deployer": "ZipDeploy",
  "id": "xxxxxxxxxxxxxxxx",
  "message": "Created via a push deployment",
  "status": 4,
  "url": "https://func-dedi-<unique-suffix>.scm.azurewebsites.net/api/deployments/xxxxxxxxxxxxxxxx"
}

curl --request GET "https://$APP_NAME.azurewebsites.net/api/health":

{
  "status": "healthy",
  "timestamp": "2026-04-03T11:20:00Z",
  "version": "1.0.0"
}

az functionapp deployment slot create ... (after S1 upgrade):

{
  "hostNames": [
    "func-dedi-<unique-suffix>-staging.azurewebsites.net"
  ],
  "name": "func-dedi-<unique-suffix>/slots/staging",
  "state": "Running"
}

Next Steps

You now have a repeatable Dedicated deployment pipeline with optional slot-based release flow on S1/P1v2.

Next: 07 - Extending with Triggers

See Also

Sources