Building a Complete Azure DevOps Pipeline for APIM with Endpoint-Based Policies

Overview

In this tutorial, I'll walk you through creating a comprehensive Azure DevOps pipeline that deploys infrastructure, containerized applications, and API Management (APIM) with endpoint-specific policies. This approach provides a developer-friendly workflow where each API endpoint has its own configuration and security policy.

The Challenge

Modern cloud applications often require:
Infrastructure as Code (IaC) deployments
Containerized microservices
API Management with granular security policies
Automated CI/CD workflows

The challenge is orchestrating all these components in a way that's maintainable, scalable, and easy for developers to work with.

The Solution: Modular Endpoint-Based Architecture

1. Structuring Endpoints

We organized the project so that each API endpoint lives in its own folder within the webapp directory:

webapp/
├── endpoints/
│   ├── getProducts/
│   │   ├── config.json        # Endpoint metadata
│   │   └── policy.xml         # APIM security policy
│   ├── getUsers/
│   │   ├── config.json
│   │   └── policy.xml
│   └── healthCheck/
│       ├── config.json
│       └── policy.xml


Why this matters: Developers working on the webapp can see API contracts, security policies, and implementation code all in one place. No need to hunt through separate infrastructure repos.

2. Endpoint Configuration Files

Each endpoint has a simple JSON configuration:

{
  "operationId": "getProducts",
  "displayName": "Get Products",
  "method": "GET",
  "urlTemplate": "/products",
  "description": "Retrieves all products",
  "requiredRoles": ["User", "Admin"],
  "policyFile": "policy.xml"
}

3. Security Policies Per Endpoint Each endpoint has its own APIM policy file with role-based access control:

<policies>
  <inbound>
    <validate-azure-ad-token tenant-id="{{TENANT_ID}}">
      <client-application-ids>
        <application-id>{{CLIENT_ID}}</application-id>
      </client-application-ids>
      <required-claims>
        <claim name="roles" match="any">
          <value>User</value>
          <value>Admin</value>
        </claim>
      </required-claims>
    </validate-azure-ad-token>
  </inbound>
</policies>
Key benefit: Different endpoints can have different security requirements without complex conditionals.

Building the Azure DevOps Pipeline

Stage 1: Build Container Images

The pipeline starts by building Docker images for the web application and Azure Functions: Uses az acr build for cloud-based builds Tags images with build ID and 'latest' No local Docker daemon required Pro tip: Cloud-based ACR builds are faster and don't require configuring Docker on build agents.

Stage 2: Deploy Infrastructure

Using Bicep templates, we deploy: Azure Container Registry Container Apps for webapp and functions API Management service Storage accounts Log Analytics workspace Virtual networks Important: The deployment captures outputs (ACR name, APIM name, container app names) that later stages need.

Stage 3: Push Container Images

Images are pushed to Azure Container Registry using the cloud build approach:

- script: |
    az acr build --registry $(acrName) \
      --image webapp:$(imageTag) \
      --image webapp:latest \
      --file webapp/Dockerfile webapp/


Stage 4: Deploy Containers

Container Apps are updated with the newly built images:

  - script: |
    az containerapp update \
      --name $(containerAppWebappName) \
      --resource-group $(resourceGroupName) \
      --image $(acrLoginServer)/webapp:$(imageTag)

Challenge solved: We ensured proper stage dependencies so that infrastructure outputs are available when needed.

Stage 5: Configure Entra ID (Optional)

This stage can create app registrations and roles in Azure AD, but: Requires special Azure AD permissions Often better to configure manually or skip if already set up Made optional with condition: false if pre-configured Lesson learned: Service principals need different permissions for Azure resources vs. Azure AD. Don't assume Contributor role gives AD access.

Stage 6: Deploy APIM Endpoints

The heart of the pipeline - automatically discovers and deploys all endpoints:

# Auto-discovers all endpoint folders
Get-ChildItem -Directory ./webapp/endpoints/ | ForEach-Object {
    $config = Get-Content "$_/config.json" | ConvertFrom-Json
    $policy = Get-Content "$_/policy.xml"
    
    # Deploy operation to APIM
    # Apply policy with variable substitution
}

Key features:

Template variable replacement (tenant ID, client ID, backend URL) Validates JSON and XML syntax Individual endpoint deployment (fails one, others still deploy) Detailed logging for troubleshooting

Stage 7: Post-Deployment Tests

Validates the deployment with health checks and connectivity tests:

- script: |
    $apimGateway = az apim show --name $(apimName) \
      --resource-group $(resourceGroupName) \
      --query gatewayUrl -o tsv
    
    $response = Invoke-WebRequest "$apimGateway/health"



Important:

Made tests informative rather than blocking, since some endpoints require authentication.

Key Takeaways

1. Co-location is King

Keeping endpoint configs with application code makes it easier for developers to maintain consistency between implementation and API contracts.

2. Idempotent Deployments

Azure Resource Manager deployments are declarative - they create what doesn't exist and update what changed. Don't delete resources unnecessarily.

3. Handle Immutable Properties

Some Azure resource properties can't be changed after creation (like requireInfrastructureEncryption on storage accounts). Remove them from templates for update scenarios.

4. Stage Dependencies Matter

Carefully manage dependsOn relationships between pipeline stages. Later stages need access to earlier stage outputs through proper variable passing.

5. Service Principal Permissions

Understand the difference between Azure Resource Manager permissions (Contributor, Owner) and Azure Active Directory permissions (Application.Read.All, Directory.Read.All).

6. Cloud-Based Builds

Using az acr build instead of local Docker builds simplifies the pipeline and makes it faster.

7. Fail Gracefully

Not every stage needs to succeed. Optional configurations (like AD role setup) can use continueOnError: true or be disabled entirely.

8. Template Variables

Use placeholders like {{TENANT_ID}} in policy files and replace them during deployment for environment-specific configurations.

Adding New Endpoints

The beauty of this architecture is its simplicity. To add a new endpoint: Create folder: webapp/endpoints/myNewEndpoint/ Add config: config.json with endpoint metadata Add policy: policy.xml with security rules Commit and push: The pipeline auto-discovers and deploys it No pipeline modifications needed. No manual APIM configuration. Just add the files and go.

Best Practices Implemented

Infrastructure as Code: Everything is version-controlled
GitOps workflow: Changes go through pull requests
Automated validation: JSON/XML syntax checking
Environment separation: Template variables for different environments
Detailed logging: Clear feedback at each stage
Atomic operations: Individual endpoint failures don't block others
Documentation as code: Configs serve as API documentation

Conclusion

This pipeline architecture provides a robust, scalable foundation for API-driven applications. By treating each endpoint as a first-class citizen with its own configuration and policies, we create a system that's both powerful for DevOps and intuitive for developers.
The modular approach means teams can work independently on different endpoints without conflicts, while the automated pipeline ensures consistent deployment and security across the entire API surface.
Whether you're building a new API or modernizing an existing one, this pattern provides a solid starting point that grows with your needs.

Technologies Used:

  • Azure DevOps Pipelines
  • Azure Bicep (IaC)
  • Azure Container Apps
  • Azure API Management
  • Azure Container Registry
  • Azure Active Directory / Entra ID
  • PowerShell Core
  • Popular posts from this blog

    Kerstin's Fate developer diary

    Exploring LLMs with Ollama and Llama3

    Containers & Kubernetes in Windows Server 2025 or RedHat EL(RHEL)