This repository contains reusable Terraform modules for deploying APIs to existing Azure API Management services. These modules are designed to be consumed by developers in their own repositories through Azure DevOps pipelines.
Developer Repository:
βββ azure-pipelines.yml # Multi-environment pipeline
βββ terraform/
β βββ main.tf # References our modules
β βββ variables.tf # Variable definitions
β βββ terraform.tfvars # Common configuration
β βββ dev.tfvars # DEV subscription config
β βββ test.tfvars # TEST subscription config
βββ README.md
Centralized Module Repository (this repo):
βββ modules/
β βββ apim_api/ # API deployment module
β βββ apim_subscription/ # Subscription module
βββ examples/
βββ petstore/ # Reference implementation
- Centralized Module Repository: Single source of truth for APIM deployments
- Developer Self-Service: Developers reference modules in their own repositories
- Multi-Environment Pipeline: Single pipeline with multiple stages for different environments
- Hybrid Configuration: Common config in one file, user-specific subscriptions in separate files
- Variable Interpolation: Azure DevOps variables properly passed to Terraform
- API Deployment: Deploy APIs to existing APIM services
- API Versioning: Support for multiple API versions with version sets
- Policy Templates: Pre-built policy templates for different scenarios
- Subscription Management: Automatic subscription key generation
- Backend Integration: Support for external API backends
- Product Association: Automatic product creation and API linking
- Azure CLI installed and authenticated
- Terraform >= 1.0
- Existing Azure API Management service
- Azure subscription with appropriate permissions
- Azure DevOps pipeline access
Creates APIs within existing APIM services with:
- Version sets for API versioning
- Backend configuration
- Product association
- Policy application
- OpenAPI/Swagger import
Creates subscription keys for products with:
- Automatic key generation
- User association
- Active state management
Developers create a terraform/main.tf file in their repository:
terraform {
required_version = ">= 1.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
}
# Reference the centralized API module
module "my_api" {
source = "git::https://dev.azure.com/your-org/your-project/_git/apim-modules//modules/apim_api?ref=v1.0.0"
api_name = var.api_name
api_display_name = var.api_display_name
api_path = var.api_path
api_spec_url = var.api_spec_url
api_version = var.api_version
api_version_set_name = var.api_version_set_name
api_versioning_scheme = var.api_versioning_scheme
backend_name = var.backend_name
backend_url = var.backend_url
product_name = var.product_name
product_display = var.product_display
policy_template = var.policy_template
resource_group_name = var.resource_group_name
service_name = var.service_name
}
# Reference the centralized subscription module
module "my_subscription" {
source = "git::https://dev.azure.com/your-org/your-project/_git/apim-modules//modules/apim_subscription?ref=v1.0.0"
resource_group_name = var.resource_group_name
service_name = var.service_name
subscription_name = var.subscription_name
product_id = module.my_api.product_id
}Create terraform/variables.tf:
# API Configuration
variable "api_name" {
description = "Terraform resource name for the API"
type = string
}
variable "api_display_name" {
description = "Display name shown in Azure Portal"
type = string
}
variable "api_path" {
description = "Path under the APIM gateway"
type = string
}
variable "api_spec_url" {
description = "URL to the OpenAPI/Swagger spec file"
type = string
}
# Versioning Configuration
variable "api_version" {
description = "Version label for the API"
type = string
}
variable "api_version_set_name" {
description = "Logical group name linking multiple API versions"
type = string
}
variable "api_versioning_scheme" {
description = "How versioning is exposed to clients"
type = string
}
# Backend Configuration
variable "backend_name" {
description = "Name of the backend service in APIM"
type = string
}
variable "backend_url" {
description = "URL of the actual backend API/service"
type = string
}
# Product Configuration
variable "product_name" {
description = "Internal product ID"
type = string
}
variable "product_display" {
description = "Display name of the product"
type = string
}
# Policy Configuration
variable "policy_template" {
description = "Name of the policy template to apply"
type = string
}
# APIM Service Configuration
variable "resource_group_name" {
description = "Name of the existing resource group containing the APIM service"
type = string
}
variable "service_name" {
description = "Name of the existing APIM service"
type = string
}
# Subscription Configuration
variable "subscription_name" {
description = "Name of the subscription"
type = string
}Create terraform/terraform.tfvars with common configuration for all environments:
# Common Configuration (identical across all environments)
api_name = "my-api-v1"
api_display_name = "My API"
api_path = "myapi"
api_spec_url = "https://myapi.com/openapi.json"
# Versioning
api_version = "v1"
api_version_set_name = "myapi"
api_versioning_scheme = "Segment"
# Backend
backend_name = "my-backend"
backend_url = "https://myapi.com"
# Product
product_name = "my-product"
product_display = "My Product"
# Policy
policy_template = "external-inbound-base"
# Note: subscription_name is environment-specific and defined in:
# - dev.tfvars
# - test.tfvarsCreate environment-specific .tfvars files for subscription names:
terraform/dev.tfvars:
# DEV Environment - Subscription Configuration
subscription_name = "my-subscription-dev"terraform/test.tfvars:
# TEST Environment - Subscription Configuration
subscription_name = "my-subscription-test"Create azure-pipelines.yml:
trigger:
- main
- develop
pool:
vmImage: 'ubuntu-latest'
stages:
- stage: DeployDev
displayName: 'Deploy to DEV'
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop'))
variables:
- group: apim-variables-dev
jobs:
- deployment: DeployToDev
displayName: 'Deploy to DEV'
environment: 'dev'
strategy:
runOnce:
deploy:
steps:
- task: TerraformInstaller@0
displayName: 'Install Terraform'
inputs:
terraformVersion: 'latest'
- task: TerraformTaskV4@4
displayName: 'Terraform Init'
inputs:
provider: 'azurerm'
command: 'init'
workingDirectory: '$(System.DefaultWorkingDirectory)/terraform'
backendServiceArm: 'Azure-Service-Connection'
backendAzureRmResourceGroupName: '$(TF_STATE_RG)'
backendAzureRmStorageAccountName: '$(TF_STATE_SA)'
backendAzureRmContainerName: '$(TF_STATE_CONTAINER)'
backendAzureRmKey: '$(TF_STATE_KEY)'
- task: TerraformTaskV4@4
displayName: 'Terraform Plan'
inputs:
provider: 'azurerm'
command: 'plan'
workingDirectory: '$(System.DefaultWorkingDirectory)/terraform'
environmentServiceNameAzureRM: 'Azure-Service-Connection'
commandOptions: '-var-file=terraform.tfvars -var-file=dev.tfvars -var="resource_group_name=$(RESOURCE_GROUP_NAME)" -var="service_name=$(SERVICE_NAME)" -out=$(Build.ArtifactStagingDirectory)/tfplan'
- task: TerraformTaskV4@4
displayName: 'Terraform Apply'
inputs:
provider: 'azurerm'
command: 'apply'
workingDirectory: '$(System.DefaultWorkingDirectory)/terraform'
environmentServiceNameAzureRM: 'Azure-Service-Connection'
commandOptions: '$(Build.ArtifactStagingDirectory)/tfplan'
- stage: DeployTest
displayName: 'Deploy to TEST'
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
variables:
- group: apim-variables-test
jobs:
- deployment: DeployToTest
displayName: 'Deploy to TEST'
environment: 'test'
strategy:
runOnce:
deploy:
steps:
# Similar steps but with test variable group and test.tfvars
- task: TerraformTaskV4@4
displayName: 'Terraform Plan'
inputs:
provider: 'azurerm'
command: 'plan'
workingDirectory: '$(System.DefaultWorkingDirectory)/terraform'
environmentServiceNameAzureRM: 'Azure-Service-Connection'
commandOptions: '-var-file=terraform.tfvars -var-file=test.tfvars -var="resource_group_name=$(RESOURCE_GROUP_NAME)" -var="service_name=$(SERVICE_NAME)" -out=$(Build.ArtifactStagingDirectory)/tfplan'Each environment has its own variable group with:
RESOURCE_GROUP_NAME- Environment-specific resource groupSERVICE_NAME- Environment-specific APIM service nameTF_STATE_RG- Terraform state resource groupTF_STATE_SA- Terraform state storage accountTF_STATE_CONTAINER- Terraform state containerTF_STATE_KEY- Terraform state key
Single .tfvars file with common configuration for all environments:
- API Configuration (name, display name, path, spec URL)
- Versioning Configuration
- Backend Configuration
- Product Configuration
- Policy Configuration
Each environment has its own .tfvars file with:
subscription_name- User-specific subscription name for that environment
The key mechanism is variable interpolation from Azure DevOps to Terraform:
- Azure DevOps Variable Groups β Pipeline Variables β Terraform Variables
# Pipeline stage with variable group
variables:
- group: apim-variables-dev # Contains RESOURCE_GROUP_NAME, SERVICE_NAME
# Terraform command with variable interpolation
commandOptions: '-var-file=terraform.tfvars -var-file=dev.tfvars -var="resource_group_name=$(RESOURCE_GROUP_NAME)" -var="service_name=$(SERVICE_NAME)"'- Variable Resolution Order (highest priority first):
-var="resource_group_name=$(RESOURCE_GROUP_NAME)"(from Azure DevOps)dev.tfvars(subscription names)terraform.tfvars(common config)- Default values in
variables.tf
For DEV environment:
# Pipeline command
terraform plan \
-var-file=terraform.tfvars \
-var-file=dev.tfvars \
-var="resource_group_name=rg-apim-dev" \
-var="service_name=apim-dev"Variable Resolution:
resource_group_name="rg-apim-dev"(from Azure DevOps)service_name="apim-dev"(from Azure DevOps)subscription_name="my-subscription-dev"(from dev.tfvars)api_name="my-api-v1"(from terraform.tfvars)product_name="my-product"(from terraform.tfvars)
- Azure DevOps project with appropriate permissions
- Azure Service Connection configured
- Access to create variable groups and environments
Create two separate variable groups for each environment. Only resource group and service names are environment-specific and stored in variable groups:
RESOURCE_GROUP_NAME = "rg-apim-dev"
SERVICE_NAME = "apim-dev"
TF_STATE_RG = "rg-terraform-state-dev"
TF_STATE_SA = "tfstatedev"
TF_STATE_CONTAINER = "tfstate"
TF_STATE_KEY = "dev.tfstate"
RESOURCE_GROUP_NAME = "rg-apim-test"
SERVICE_NAME = "apim-test"
TF_STATE_RG = "rg-terraform-state-test"
TF_STATE_SA = "tfstatetest"
TF_STATE_CONTAINER = "tfstate"
TF_STATE_KEY = "test.tfstate"
Create two environments in Azure DevOps:
- dev - For DEV deployments
- test - For TEST deployments
Create an Azure Resource Manager service connection named Azure-Service-Connection with appropriate permissions for all environments.
developbranch: Triggers DEV deployment onlymainbranch: Triggers TEST deployment
The pipeline has two stages, each representing an environment:
- DeployDev Stage: Uses
apim-variables-devvariable group - DeployTest Stage: Uses
apim-variables-testvariable group
- From Variable Group:
rg-apim-dev,apim-dev - From terraform.tfvars: Common API, product configuration
- From dev.tfvars:
subscription_name = "my-subscription-dev" - API URL:
https://apim-dev.azure-api.net/myapi
- From Variable Group:
rg-apim-test,apim-test - From terraform.tfvars: Common API, product configuration
- From test.tfvars:
subscription_name = "my-subscription-test" - API URL:
https://apim-test.azure-api.net/myapi
| Variable | DEV | TEST |
|---|---|---|
| RESOURCE_GROUP_NAME | rg-apim-dev | rg-apim-test |
| SERVICE_NAME | apim-dev | apim-test |
| TF_STATE_RG | rg-terraform-state-dev | rg-terraform-state-test |
| TF_STATE_SA | tfstatedev | tfstatetest |
| TF_STATE_CONTAINER | tfstate | tfstate |
| TF_STATE_KEY | dev.tfstate | test.tfstate |
- Single file: Contains all API, product configuration
- Version controlled: Part of the code repository
- Environment agnostic: Same configuration deployed to all environments
| File | Environment | Subscription Name |
|---|---|---|
dev.tfvars |
DEV | my-subscription-dev |
test.tfvars |
TEST | my-subscription-test |
- Environment Isolation: Each environment has separate resource groups and APIM services
- State Isolation: Separate Terraform state files prevent cross-environment conflicts
- Variable Group Isolation: Each environment has its own variable group
- Approval Gates: Configure approval gates on environments as needed
- Variable Security: Mark sensitive variables as secret in variable groups
- Development: Push to
developbranch to deploy to DEV - Testing: Push to
mainbranch to deploy to TEST - Monitoring: Check pipeline logs for deployment status and API URLs
Test the pipeline by:
- Creating a feature branch from
develop - Making changes to the
terraform.tfvarsfile - Pushing to
developto trigger DEV deployment - Merging to
mainto trigger TEST deployment
external-inbound-base: Basic external API policyinternal-inbound-base: Internal API with rate limiting and quotasexternal-outbound-base: External API with request forwarding
- Rate limiting
- Quota management
- API key validation
- Error handling
- Response headers
- Request tracking
Each module provides useful outputs for pipeline consumption:
- API Module: API ID, Product ID, Version Set ID
- Subscription Module: Subscription ID, Primary/Secondary Keys
- Subscription keys are marked as sensitive
- Policies include proper error handling
- Backend SSL validation enabled
- Secure default configurations
- Module versioning for stability
- Environment isolation with separate resource groups
To destroy all resources:
terraform destroyThe examples/petstore/ directory contains a complete reference implementation showing:
- Multi-environment pipeline configuration
- Common
terraform.tfvarsfile with shared configuration - Environment-specific
.tfvarsfiles for subscription names - Azure DevOps setup documentation
- Petstore API with versioning
- Product with subscription
- Policy application
Note: This example assumes existing APIM services in different resource groups for each environment.
- Follow the modular structure
- Add proper variable validation
- Include comprehensive outputs
- Document new policy templates
- Update examples as needed
- Version modules for pipeline stability
This project is licensed under the MIT License.