Seamlessly deploy and manage APIs in Azure API Management directly from Kubernetes
The Azure APIM Operator is a Kubernetes operator built with Kubebuilder and Go that automates the registration and management of APIs in Azure API Management (APIM). It provides a declarative, GitOps-friendly way to manage your API lifecycle by leveraging Kubernetes Custom Resource Definitions (CRDs).
- ✅ Automated API Registration - APIs are automatically registered in Azure APIM when deployed to Kubernetes
- ✅ OpenAPI Integration - Automatically fetches and imports OpenAPI/Swagger specifications
- ✅ Declarative Management - Manage APIs, Products, and Tags using Kubernetes resources
- ✅ GitOps Ready - Works seamlessly with GitOps workflows and CI/CD pipelines
- ✅ Production Ready - Built with enterprise-grade features including RBAC, metrics, and health checks
- Automatic API Registration - Detects new deployments and automatically registers them in Azure APIM
- OpenAPI/Swagger Integration - Fetches OpenAPI definitions from your deployed services
- Product Management - Automatically assigns APIs to APIM Products
- Tag Management - Organize APIs with tags for better categorization
- Service URL Updates - Automatically updates backend service URLs in APIM
- Revision Support - Manages API revisions in Azure APIM
- Azure Workload Identity - Secure authentication using Azure Workload Identity
- Helm Deployment - Easy installation and lifecycle management via Helm charts
- Multi-Namespace Support - Deploy APIs across multiple Kubernetes namespaces
- Observability - Built-in metrics, health checks, and structured logging
- Kubernetes cluster (1.21+) - AKS, EKS, GKE, or any compatible distribution
- kubectl - Configured to access your cluster
- Helm (3.8+) - For operator installation
- Azure CLI - For Azure resource management (optional)
- Azure Subscription with an Azure API Management instance
- Azure Workload Identity configured (or Service Principal credentials)
- RBAC Permissions - The identity needs permissions to manage APIM resources:
Microsoft.ApiManagement/service/apis/*Microsoft.ApiManagement/service/products/*Microsoft.ApiManagement/service/tags/*
- Go (1.21+)
- Kubebuilder (
go install sigs.k8s.io/kubebuilder/...) - Docker - For building container images
Deploy the operator using Helm:
helm upgrade --install azure-apim-operator ./charts/azure-apim-operator \
--namespace apim-operator \
--create-namespaceCreate an APIMService resource to define your Azure APIM instance:
apiVersion: apim.operator.io/v1
kind: APIMService
metadata:
name: my-apim-instance
namespace: apim-operator
spec:
name: my-apim-service
resourceGroup: my-resource-group
subscription: <your-azure-subscription-id>Set up Azure Workload Identity by configuring environment variables in the operator deployment:
env:
- name: AZURE_CLIENT_ID
value: "<your-managed-identity-client-id>"
- name: AZURE_TENANT_ID
value: "<your-azure-tenant-id>"Or use a Service Account with Workload Identity annotations:
apiVersion: v1
kind: ServiceAccount
metadata:
name: azure-apim-operator
namespace: apim-operator
annotations:
azure.workload.identity/client-id: "<your-managed-identity-client-id>"Create an APIMAPI resource for your application:
apiVersion: apim.operator.io/v1
kind: APIMAPI
metadata:
name: my-api
namespace: default
spec:
APIID: my-api
serviceUrl: https://my-api.example.com
routePrefix: /api/v1
openApiDefinitionUrl: https://my-api.example.com/swagger/v1/swagger.json
apimService: my-apim-instance
productIds:
- my-product
tagIds:
- backend
- v1Apply the configuration:
kubectl apply -f my-api.yamlDeploy your application with the correct labels:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-api
labels:
app.kubernetes.io/name: my-api
spec:
replicas: 2
selector:
matchLabels:
app.kubernetes.io/name: my-api
template:
metadata:
labels:
app.kubernetes.io/name: my-api
spec:
containers:
- name: api
image: my-api:latest
ports:
- containerPort: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-api
spec:
rules:
- host: my-api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-api
port:
number: 80The operator will automatically:
- Detect the ReplicaSet when pods are ready
- Create an
APIMAPIDeploymentresource - Fetch the OpenAPI specification
- Register the API in Azure APIM
- Poll APIM async import status when Azure returns
202 Accepted - Assign products and tags
- Clean up intermediate resources
Check that CRDs are installed:
kubectl get crds | grep apim.operator.ioCheck operator status:
kubectl get pods -n apim-operator
kubectl logs -l app.kubernetes.io/name=azure-apim-operator -n apim-operatorCheck API registration status:
kubectl get apimapi -A
kubectl describe apimapi my-apiThe operator consists of multiple controllers that work together to automate API management:
graph TB
subgraph K8s["Kubernetes Cluster"]
subgraph AppNS["Application Namespace"]
RS[ReplicaSet<br/>app.kubernetes.io/name: my-api]
Pod1[Pod 1<br/>Ready]
Pod2[Pod 2<br/>Ready]
Ingress[Ingress<br/>example.com]
APIMAPI[APIMAPI CR<br/>Defines API config]
RS --> Pod1
RS --> Pod2
Pod1 --> Ingress
Pod2 --> Ingress
end
subgraph OperatorNS["apim-operator Namespace"]
Operator[Azure APIM Operator<br/>Controller Manager]
APIMService[APIMService CR<br/>Azure APIM Instance Config]
end
subgraph CRDs["Custom Resources"]
APIMAPIDeploy[APIMAPIDeployment CR<br/>Intermediate Resource]
APIMProduct[APIMProduct CR]
APIMTag[APIMTag CR]
end
end
subgraph Azure["Azure Cloud"]
APIM[Azure API Management<br/>Service Instance]
AAD[Azure Active Directory<br/>Authentication]
end
subgraph Controllers["Operator Controllers"]
RSWatcher[ReplicaSetWatcher<br/>Controller]
DeployController[APIMAPIDeployment<br/>Controller]
APIController[APIMAPI<br/>Controller]
ServiceController[APIMService<br/>Controller]
ProductController[APIMProduct<br/>Controller]
TagController[APIMTag<br/>Controller]
end
%% Watch Flow
RS -.watches.-> RSWatcher
RSWatcher -.checks.-> Pod1
RSWatcher -.checks.-> Pod2
RSWatcher -.checks.-> Ingress
RSWatcher -.reads.-> APIMAPI
RSWatcher -.reads.-> APIMService
%% Creation Flow
RSWatcher -.creates.-> APIMAPIDeploy
%% Deployment Flow
APIMAPIDeploy -.watches.-> DeployController
DeployController -.fetches.-> Ingress
DeployController -.fetches.-> OpenAPI[OpenAPI Spec<br/>/swagger.json]
DeployController -.authenticates.-> AAD
DeployController -.registers.-> APIM
%% Configuration Flow
APIMAPI -.managed by.-> APIController
APIMService -.managed by.-> ServiceController
APIMProduct -.managed by.-> ProductController
APIMTag -.managed by.-> TagController
%% Azure Integration
AAD -.provides token.-> DeployController
DeployController -.API Management REST API.-> APIM
%% Styling
classDef k8sResource fill:#326ce5,stroke:#fff,color:#fff
classDef crd fill:#ff6b6b,stroke:#fff,color:#fff
classDef controller fill:#4ecdc4,stroke:#fff,color:#fff
classDef azure fill:#0078d4,stroke:#fff,color:#fff
classDef operator fill:#ffa500,stroke:#fff,color:#fff
class RS,Pod1,Pod2,Ingress k8sResource
class APIMAPI,APIMAPIDeploy,APIMService,APIMProduct,APIMTag crd
class RSWatcher,DeployController,APIController,ServiceController,ProductController,TagController controller
class APIM,AAD azure
class Operator operator
The following diagram illustrates the complete workflow from application deployment to API registration:
sequenceDiagram
participant Dev as Developer
participant K8s as Kubernetes API
participant RS as ReplicaSet
participant RSWatcher as ReplicaSetWatcher<br/>Controller
participant DeployCtrl as APIMAPIDeployment<br/>Controller
participant App as Application Pod
participant AzureAPIM as Azure APIM
participant AAD as Azure AD
Dev->>K8s: Deploy Application<br/>with APIMAPI CR
K8s->>RS: Create ReplicaSet
RS->>App: Create Pods
Note over RS,App: Wait for Pods to be Ready
RSWatcher->>RS: Watch ReplicaSet changes
RSWatcher->>App: Check Pod readiness
RSWatcher->>K8s: Check Ingress exists
alt All conditions met
RSWatcher->>K8s: Create APIMAPIDeployment CR
end
DeployCtrl->>K8s: Watch APIMAPIDeployment
DeployCtrl->>App: Fetch OpenAPI Spec<br/>(/swagger.json)
App-->>DeployCtrl: Return OpenAPI JSON
DeployCtrl->>AAD: Authenticate<br/>(Managed Identity/Service Principal)
AAD-->>DeployCtrl: Bearer Token
DeployCtrl->>AzureAPIM: PUT /apis/{apiId}<br/>Import OpenAPI Definition
AzureAPIM-->>DeployCtrl: API Created/Updated
DeployCtrl->>AzureAPIM: PATCH /apis/{apiId}<br/>Set Service URL
AzureAPIM-->>DeployCtrl: Service URL Updated
DeployCtrl->>AzureAPIM: PUT /products/{productId}/apis/{apiId}<br/>Assign Products
AzureAPIM-->>DeployCtrl: Products Assigned
DeployCtrl->>AzureAPIM: PUT /apis/{apiId}/tags/{tagId}<br/>Assign Tags
AzureAPIM-->>DeployCtrl: Tags Assigned
DeployCtrl->>K8s: Update APIMAPIDeployment Status
DeployCtrl->>K8s: Delete APIMAPIDeployment CR<br/>(Cleanup)
Note over AzureAPIM: API is now available<br/>through APIM Gateway
graph LR
subgraph "Kubernetes Resources"
A[Deployment]
B[ReplicaSet]
C[Pods]
D[Ingress]
E[Service]
end
subgraph "Operator CRDs"
F[APIMAPI]
G[APIMAPIDeployment]
H[APIMService]
I[APIMProduct]
J[APIMTag]
end
subgraph "Operator Controllers"
K[ReplicaSetWatcher]
L[APIMAPIDeployment]
M[APIMAPI]
N[APIMService]
O[APIMProduct]
P[APIMTag]
end
subgraph "Azure Services"
Q[Azure APIM]
R[Azure AD]
end
A --> B
B --> C
C --> D
C --> E
B -->|Watches| K
F -->|References| H
K -->|Creates| G
G -->|Triggers| L
L -->|Fetches OpenAPI| E
L -->|Authenticates| R
L -->|Registers API| Q
L -->|Assigns| I
L -->|Assigns| J
H -->|Manages| N
F -->|Manages| M
I -->|Manages| O
J -->|Manages| P
style F fill:#ff6b6b
style G fill:#ff6b6b
style H fill:#ff6b6b
style I fill:#ff6b6b
style J fill:#ff6b6b
style K fill:#4ecdc4
style L fill:#4ecdc4
style M fill:#4ecdc4
style N fill:#4ecdc4
style O fill:#4ecdc4
style P fill:#4ecdc4
style Q fill:#0078d4
style R fill:#0078d4
flowchart TD
Start([Application Deployment]) --> Deploy[Deploy to Kubernetes]
Deploy --> RS[ReplicaSet Created]
RS --> Ready{Pods Ready?}
Ready -->|No| Wait[Wait for Pods]
Wait --> Ready
Ready -->|Yes| IngressCheck{Ingress Exists?}
IngressCheck -->|No| WaitIngress[Wait for Ingress]
WaitIngress --> IngressCheck
IngressCheck -->|Yes| CreateCR[Create APIMAPIDeployment CR]
CreateCR --> FetchOpenAPI[Fetch OpenAPI Spec<br/>from Application]
FetchOpenAPI --> Auth[Authenticate with Azure AD]
Auth --> ImportAPI[Import API to Azure APIM]
ImportAPI --> SetServiceURL[Set Service URL]
SetServiceURL --> AssignProducts{Products<br/>Configured?}
AssignProducts -->|Yes| AssignProd[Assign to Products]
AssignProducts -->|No| AssignTags
AssignProd --> AssignTags{Tags<br/>Configured?}
AssignTags -->|Yes| AssignTag[Assign Tags]
AssignTags -->|No| UpdateStatus
AssignTag --> UpdateStatus[Update Status]
UpdateStatus --> Cleanup[Delete APIMAPIDeployment CR]
Cleanup --> End([API Available in APIM])
style Start fill:#90EE90
style End fill:#90EE90
style Ready fill:#FFD700
style IngressCheck fill:#FFD700
style AssignProducts fill:#FFD700
style AssignTags fill:#FFD700
The operator consists of several specialized controllers:
- Purpose: Monitors Kubernetes ReplicaSets and triggers API registration
- Behavior:
- Watches for ReplicaSet changes
- Matches ReplicaSets to
APIMAPIresources usingapp.kubernetes.io/namelabels - Verifies that pods are ready and running
- Creates an intermediate
APIMAPIDeploymentCR when all conditions are met
- Purpose: Handles the actual API registration in Azure APIM
- Behavior:
- Watches for
APIMAPIDeploymentresources - Fetches OpenAPI/Swagger specification from the application endpoint
- Logs the fetched Swagger/OpenAPI payload for debugging
- Authenticates with Azure AD using Workload Identity
- Registers or updates the API in Azure APIM via Azure Management API
- Polls APIM long-running operation status when import returns
202 Accepted - Updates service URLs to point to the Kubernetes service
- Assigns products and tags if configured
- Cleans up the intermediate CR after successful deployment
- Watches for
- Purpose: Manages the lifecycle of
APIMAPIresources - Behavior: Handles updates and status tracking
- Purpose: Manages Azure APIM service configuration
- Behavior: Retrieves and stores APIM service details (hostnames, etc.)
- Purpose: Creates and manages APIM Products
- Behavior: Synchronizes Product definitions with Azure APIM
- Purpose: Creates and manages APIM Tags
- Behavior: Synchronizes Tag definitions with Azure APIM
Defines an Azure API Management service instance:
apiVersion: apim.operator.io/v1
kind: APIMService
metadata:
name: my-apim-service
namespace: apim-operator
spec:
name: my-apim-service-name # APIM service name in Azure
resourceGroup: my-resource-group # Azure resource group
subscription: <subscription-id> # Azure subscription IDDefines an API to be registered in Azure APIM:
apiVersion: apim.operator.io/v1
kind: APIMAPI
metadata:
name: my-api
namespace: default
spec:
APIID: my-api # Unique API identifier in APIM
serviceUrl: https://api.example.com # Backend service URL
routePrefix: /api/v1 # Route prefix in APIM
openApiDefinitionUrl: https://api.example.com/swagger.json # OpenAPI spec URL
apimService: my-apim-service # Reference to APIMService
productIds: # Optional: Product IDs to assign
- my-product
tagIds: # Optional: Tag IDs to assign
- backend
- v1Defines an APIM Product:
apiVersion: apim.operator.io/v1
kind: APIMProduct
metadata:
name: my-product
namespace: default
spec:
productID: my-product
displayName: My Product
description: Product description
apimService: my-apim-service
published: trueDefines an APIM Tag:
apiVersion: apim.operator.io/v1
kind: APIMTag
metadata:
name: my-tag
namespace: default
spec:
tagID: my-tag
displayName: My Tag
apimService: my-apim-serviceThe operator supports Azure Workload Identity for secure, passwordless authentication.
- Create a Managed Identity in Azure:
az identity create --name apim-operator-identity --resource-group my-resource-group- Grant Permissions to the Managed Identity:
# Get the principal ID
PRINCIPAL_ID=$(az identity show --name apim-operator-identity --resource-group my-resource-group --query principalId -o tsv)
# Grant API Management Contributor role
az role assignment create \
--assignee $PRINCIPAL_ID \
--role "API Management Service Contributor" \
--scope "/subscriptions/<subscription-id>/resourceGroups/<resource-group>/providers/Microsoft.ApiManagement/service/<apim-service-name>"- Configure the Service Account with Workload Identity:
apiVersion: v1
kind: ServiceAccount
metadata:
name: azure-apim-operator
namespace: apim-operator
annotations:
azure.workload.identity/client-id: "<managed-identity-client-id>"- Set Environment Variables in the operator deployment:
env:
- name: AZURE_CLIENT_ID
value: "<managed-identity-client-id>"
- name: AZURE_TENANT_ID
value: "<azure-tenant-id>"If you prefer using a Service Principal, you can set the following environment variables:
env:
- name: AZURE_CLIENT_ID
value: "<service-principal-client-id>"
- name: AZURE_CLIENT_SECRET
value: "<service-principal-secret>"
- name: AZURE_TENANT_ID
value: "<azure-tenant-id>"Build and push the Docker image using the provided script:
./scripts/docker.sh v1.0.0This script:
- Builds the Go application
- Creates a Docker image
- Tags it with the version
- Pushes to Azure Container Registry (if configured)
Package and deploy the Helm chart:
# Package the chart
./scripts/helm.sh 1.0.0
# Or install directly
helm upgrade --install azure-apim-operator ./charts/azure-apim-operator \
--namespace apim-operator \
--create-namespace \
--set image.tag=v1.0.0You can customize the installation using Helm values:
# values.yaml
replicaCount: 2
image:
repository: myregistry.azurecr.io/azure-apim-operator
tag: v1.0.0
pullPolicy: IfNotPresent
env:
- name: AZURE_CLIENT_ID
value: "<your-client-id>"
- name: AZURE_TENANT_ID
value: "<your-tenant-id>"
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 128MiThe operator requires specific RBAC permissions to function. These are automatically created by the Helm chart.
- Watch ReplicaSets - To detect application deployments
- Watch Pods - To check pod readiness
- Watch Ingress - To verify ingress existence
- Manage CRDs - To create and manage custom resources
- Update Status - To update resource status
RBAC permissions can be customized by editing:
config/rbac/role.yaml- For CRDs and Kubernetes built-in resourcescharts/azure-apim-operator/templates/clusterrole.yaml- Helm chart RBAC
After changes, regenerate manifests:
make manifestsThe operator exposes health check endpoints:
- Liveness Probe:
/healthz - Readiness Probe:
/readyz
The operator exposes Prometheus metrics on the metrics endpoint (default: port 8443).
View operator logs:
# All pods
kubectl logs -l app.kubernetes.io/name=azure-apim-operator -n apim-operator
# Specific pod
kubectl logs -n apim-operator <pod-name>
# Follow logs
kubectl logs -f -l app.kubernetes.io/name=azure-apim-operator -n apim-operatorCheck the status of your APIs:
# List all APIs
kubectl get apimapi -A
# Detailed status
kubectl describe apimapi <api-name> -n <namespace>
# Check deployment status
kubectl get apimapideployment -ASymptoms: Custom resources not recognized
Solution:
# Check if CRDs exist
kubectl get crds | grep apim.operator.io
# If missing, install them
kubectl apply -f config/crd/bases/Symptoms: Failed to get Azure token errors
Solution:
- Verify
AZURE_CLIENT_IDandAZURE_TENANT_IDare set - Check Workload Identity configuration
- Verify Managed Identity has correct permissions
- Check Service Account annotations
Symptoms: API not appearing in Azure APIM
Solution:
- Check ReplicaSet Watcher logs
- Verify
APIMAPIresource exists and is correct - Ensure pods are ready
- Check
APIMAPIDeploymentstatus
Symptoms:
- Logs show
✅ API imported to APIM, but new endpoints are not visible in APIM - Azure import request returns
202 Accepted
What it means:
- Azure APIM import is asynchronous and may still be processing (or may fail later)
What to check in logs:
📄 Swagger content- confirms exact document fetched by operator⏳ Polling APIM async import status- confirms async polling started⌛ APIM async import still in progress- operation still running✅ APIM async import completed- operation reached terminal success❌ APIM async import did not complete successfully- operation failed
Solution:
- Ensure operator version includes async polling for
202 Acceptedimports - Verify the logged Swagger content contains expected new endpoints
- If async operation fails, inspect the error body in operator logs for APIM validation details
- Re-run deployment only after fixing the reported APIM validation error
Symptoms: ValidationError stating an operation already exists when the operator re-imports an API
Cause: APIM uses the OpenAPI operationId as the internal resource name. If your spec omits operationId, APIM auto-generates resource names on first import and generates different names on subsequent imports, so it tries to create duplicates instead of updating.
Solution:
- Add a stable, unique
operationIdto every operation in your OpenAPI spec and re-deploy - See the Best Practices section for framework-specific examples
Symptoms: Cannot fetch OpenAPI specification
Solution:
- Verify the OpenAPI URL is accessible from the operator pod
- Check network policies
- Verify the endpoint returns valid OpenAPI JSON
- Check application logs
Symptoms: API registered but products/tags missing
Solution:
- Ensure
APIMProductandAPIMTagresources exist - Verify product/tag IDs match
- Check operator logs for assignment errors
# Check operator pod status
kubectl get pods -n apim-operator
# View detailed pod information
kubectl describe pod -n apim-operator <pod-name>
# Check events
kubectl get events -n apim-operator --sort-by='.lastTimestamp'
# Check CRD status
kubectl get apimapi -A -o yaml
kubectl get apimapideployment -A -o yaml
# Check RBAC
kubectl get clusterrole azure-apim-operator-manager-role -o yaml
kubectl get clusterrolebinding azure-apim-operator-manager-rolebinding -o yaml
# Tail operator logs and focus on OpenAPI import lifecycle
kubectl logs -f -l app.kubernetes.io/name=azure-apim-operator -n apim-operator \
| egrep "Swagger content|Polling APIM async import status|async import|API imported to APIM"-
Label Your Resources: Always use
app.kubernetes.io/namelabel on Deployments and ReplicaSetsmetadata: labels: app.kubernetes.io/name: my-api
-
Create Ingress Early: Ensure Ingress resources are created before or alongside Deployments
-
Health Checks: Implement proper readiness and liveness probes in your applications
-
OpenAPI Endpoint: Ensure your OpenAPI/Swagger endpoint is accessible and returns valid JSON
-
Set
operationIdon Every Operation: Your OpenAPI spec must include a stable, uniqueoperationIdfor each operation. APIM uses theoperationIdas the internal resource name to match incoming operations against existing ones during re-imports. Without it, APIM auto-generates resource names on first import and generates different names on subsequent imports, causingValidationError: Operation already existsfailures.Common framework examples:
ASP.NET Minimal API — use
.WithName():app.MapGet("/pets", GetPets).WithName("GetPets"); app.MapPost("/pets", CreatePet).WithName("CreatePet");
ASP.NET Controllers — the method name is used automatically, or set it explicitly:
[HttpGet("pets", Name = "GetPets")] public IActionResult GetPets() { ... }
Raw OpenAPI YAML/JSON:
paths: /pets: get: operationId: GetPets
-
Unique API IDs: Use unique, descriptive API IDs across all namespaces
-
Route Prefixes: Use consistent route prefix conventions (e.g.,
/api/v1,/api/v2) -
Service URLs: Point to stable service endpoints (use Ingress hostnames or Service FQDNs)
-
Products & Tags: Organize APIs using products and tags for better management
-
Least Privilege: Grant only necessary permissions to the Managed Identity
-
Network Policies: Implement network policies to restrict operator access if needed
-
Secrets Management: Use Azure Key Vault or Kubernetes secrets for sensitive configuration
-
RBAC: Regularly audit RBAC permissions and remove unnecessary access
-
Alerting: Set up alerts for operator failures and API registration issues
-
Logging: Centralize logs for better observability
-
Metrics: Monitor operator metrics and API registration success rates
-
Status Checks: Regularly check API status in both Kubernetes and Azure APIM
- Kubebuilder Documentation
- Azure API Management Documentation
- Azure Workload Identity
- Kubernetes Operators
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
- Built with Kubebuilder
- Uses controller-runtime
- Azure SDK for Go
Copyright 2025 Hedin IT
Licensed under the Apache License, Version 2.0