@azure-samples/apim-samples
GitHub Copilot Instructions for this Repository
Install
agr install @azure-samples/apim-samples --target copilotWrites 1 file into .github/copilot-instructions.md, pinned to git-26d1ea4b.
- .github/copilot-instructions.md
Document
applyTo: "**"
GitHub Copilot Instructions for this Repository
Purpose
This instructions file is designed to guide GitHub Copilot's behavior specifically for this repository. It is intended to provide clear, general, and maintainable guidelines for code generation, style, and collaboration.
In case of any conflict, instructions from other individualized or project-specific files (such as my-copilot.instructions.md) take precedence over this file.
Repository Context
- This repository provides a playground to safely experiment with and learn Azure API Management (APIM) policies in various architectures.
- The primary technologies are Python, Bicep, Jupyter notebooks, Azure CLI, APIM policy XML, and Markdown.
- The technical audience includes developers, architects, and DevOps engineers who want to understand and implement APIM policies effectively.
- The less technical audience includes decision makers and stakeholders who need to understand the value and capabilities of APIM policies without deep technical details.
Instruction Hierarchy
- When the user asks about Python or a Python file is referenced in the chat context, prefer guidance and examples from
./python.instructions.md. - When the user asks about Bicep or a Bicep file is referenced in the chat context, prefer guidance and examples from
./bicep.instructions.md. - When the user asks about JSON or a JSON file is referenced in the chat context, prefer guidance and examples from
./json.instructions.md. - When the user asks about GitHub Workflows or workflow files (
.github/workflows/*.yml) are referenced in the chat context, prefer guidance and examples from./github-workflows.instructions.md. - When other languages are used, look for a relevant instructions file to be included. The format is
./[language].instructions.mdwhere[language]acts as a placeholder. Also consider synonyms such asJavaScript,JScript, etc.
In case of any conflicting instructions, the following hierarchy shall apply. If a conflict cannot be resolved by this hierarchy, please prompt the user and ask for their situational preference.
- Individualized instructions (e.g. a developer's or an organization's instruction file(s)), if present
- This repository's
.github/.copilot-instructions.md - General best practices and guidelines from sources such as Microsoft Learn This includes the Microsoft Cloud Adoption Framework.
- Official GitHub Copilot best practices documentation
Copilot Personality Behavior
- Never be rude, dismissive, condescending, threatening, aggressive, or otherwise negative.
- Emphasise friendly, supportive, and collaborative interactions.
- Be concise and to the point, but adjust the level of detail based on the user's technical expertise that you can infer from the conversation.
General Principles
- Write concise, efficient, and well-documented code for a global audience.
- Consider non-native English speakers in code comments and documentation, using clear and simple language.
- Treat accessibility as a default quality requirement across the entire repository, not only for presentations.
- New or updated user-facing experiences (docs, webpages, notebooks, dashboards/workbooks, and slide content) must target WCAG 2.0 AA contrast and non-color-only communication as the baseline.
- Continuously refine repository instructions, skills, and agents when a surfaced problem reveals an addressable gap. Update the narrowest authoritative customization in the same change when practical, so future work benefits without duplicating or overfitting guidance.
Consistency & Uniformity
Uniformity, clarity, and ease of use are paramount across all infrastructures and samples. Every infrastructure and every sample should look and feel as alike as possible so that users maintain familiarity as they move between them. A user who has completed one sample should never feel like they are viewing something entirely new when they open the next.
- Follow the established templates. New infrastructures must follow the structure of existing infrastructures. New samples must follow
samples/_TEMPLATE. Deviations are permitted only when a sample has genuinely unique requirements, and those deviations should be minimal. - Use consistent naming, headings, and cell order. Markdown headings, variable names, section labels (e.g.
USER CONFIGURATION,SYSTEM CONFIGURATION), emoji usage, and code cell ordering must match the patterns established by the template and existing artefacts. - Keep README structure uniform. Infrastructure READMEs and sample READMEs each follow their own standard layout (see the guidelines below). Readers should be able to predict where to find objectives, configuration steps, and execution instructions.
- Reuse established utilities at their existing boundary. Compose
NotebookHelper,InfrastructureNotebookHelper,ApimRequests,ApimTesting,azure_resources, and shared Bicep modules rather than inventing parallel abstractions. Do not add a sample-local wrapper that merely forwards to a shared helper. - Keep notebooks educational. Leave configuration, scenario declarations, APIM concepts, expected outcomes, and assertions visible. Move incidental mechanics such as parsing, retries, polling, persistence, command composition, response normalization, repeated request setup, and resource cleanup into testable Python helpers.
- Choose the narrowest real owner. New one-sample behavior starts in a descriptive
samples/<sample>/<domain>_helpers.pymodule. Promote it to a focusedshared/python/module only when at least two active consumers need the same stable contract; anticipated reuse is not sufficient. - Mirror tone and depth. Similar sections across artefacts should use similar levels of detail. If one sample's README explains configuration in three sentences, another sample of comparable complexity should do the same.
- Sort samples alphabetically. Wherever samples are listed (README tables, landing page cards, JSON-LD structured data, diagrams, AGENTS.md), they must appear in alphabetical order by their display name. Infrastructures keep their current deliberate ordering.
- Use consistent sample display names. The display name used for a sample in README tables, landing page cards, JSON-LD, and compatibility diagrams must be identical. The canonical name is the one shown in the compatibility-matrix SVG diagram (e.g. "Costing", not "Costing & Showback"; "OAuth 3rd-Party", not "Credential Manager (with Spotify)"). Longer descriptions belong in the Description column or card body text, not in the name.
- Validate against peers. Before finalising a new infrastructure or sample, compare it side-by-side with at least one existing peer to identify structural or stylistic drift.
General Coding Guidelines
- Enforce a seven-day post-release waiting period for every package ecosystem. Python resolution must use the
tool.uv.exclude-newer = "7 days"setting, locked installs must runpython setup/verify_dependency_age.py --scope pythonbeforeuv sync --locked, and GitHub Actions must pass the repository-wide dependency-age workflow. - Do not bypass the waiting period with direct
pip install,uv pip install,npm install, floating GitHub Action tags, or ad hoc installer commands. Update the lockfile or pin only a release that has completed the waiting period. - Keep Dependabot
cooldown.default-daysat seven or greater for every configured ecosystem. Security updates remain subject to the same repository verifier even when the hosting service bypasses cooldown for advisory PRs. - All code, scripts, and configuration must be cross-platform compatible, supporting Windows, Linux, and macOS. If any special adjustments are to be made, please clearly indicate so in comments.
- Prioritize clarity, maintainability, and readability in all generated code.
- Focus on achieving a Minimal Viable Product (MVP) first, then iterate.
- Follow language-specific conventions and style guides (e.g., PEP 8 for Python).
- Use idiomatic code and language-specific best practices.
- Write clear and concise comments for each function and class.
- Use descriptive names for variables, functions, and classes.
- Handle edge cases and errors gracefully.
- Break down complex logic into smaller, manageable functions or classes.
- Use type annotations and docstrings where appropriate.
- Prefer standard libraries and well-maintained dependencies.
- Use
samples/_TEMPLATEas the baseline for every new sample. The template provides the canonical structure, cell order, and format. New samples must not deviate from this structure unless the sample has genuinely unique requirements.
Linting and Style
- Ruff is the Python linter; follow
pyproject.tomlfor line length and rule selection. - Prefer explicit imports over
from module import *to avoidF403/F405. - Wrap long strings or function calls to stay within the configured line length.
Repository Structure
/: Root directory containing the main files and folders. Bicep configuration is stored inbicepconfig.json.assets/: Draw.io diagrams, SVG exports, and images. Static assets such as these should be placed here. Architecture diagrams should be placed in the /diagrams subfolder.docs/: Source for the public GitHub Pages landing page. See the GitHub Pages Site section below for upkeep rules.infrastructure/: Contains Jupyter notebooks for setting up various API Management infrastructures. When modifying samples, these notebooks should not need to be modified.samples/: Various policy and scenario samples that can be applied to the infrastructures.setup/: General setup scripts and configurations for the repository and dev environment setup.shared/: Shared resources, such as Bicep modules, Python libraries, and other reusable components.tests/: Contains unit tests for Python code and Bicep modules. This folder should contain all tests for all code in the repository.
Infrastructure Development Guidelines
Infrastructures live in infrastructure/[infra-name]/ and provide the foundational Azure environment that samples deploy onto. All infrastructures must follow the same structure and patterns so that users experience a consistent workflow regardless of which architecture they choose.
Infrastructure File Structure
Each infrastructure in infrastructure/[infra-name]/ must contain:
create.ipynb- Jupyter notebook that deploys the infrastructurecreate_infrastructure.py- Python helper script for infrastructure creation logicmain.bicep- Bicep template for deploying the infrastructure resourcesparams.json- Bicep parameter fileclean-up.ipynb- Jupyter notebook for tearing down the infrastructureREADME.md- Documentation explaining the architecture, objectives, and execution steps
Infrastructure Jupyter Notebook (create.ipynb) Structure
All infrastructure notebooks must follow this exact cell pattern:
Cell 1: Configure & Create (Markdown)
- Heading:
### ๐ ๏ธ Configure Infrastructure Parameters & Create the Infrastructure - One-sentence description naming the specific infrastructure
- Bold reminder:
โ๏ธ **Modify entries under _User-defined parameters_**. - Optional: a short note if the infrastructure has unique deployment phases (e.g. private link approval)
Cell 2: Configure & Create (Python Code)
- Import only
APIM_SKU,INFRASTRUCTUREfromapimtypes,InfrastructureNotebookHelperfromutils, andprint_okfromconsole USER CONFIGURATIONsection withrg_location,index, andapim_sku(comment each with inline description)SYSTEM CONFIGURATIONsection: instantiateInfrastructureNotebookHelperand callcreate_infrastructure()- Final line:
print_ok('All done!')
Cell 3: Clean Up (Markdown)
- Heading:
### ๐๏ธ Clean up resources - Standard text: "When you're finished experimenting, it's advisable to remove all associated resources from Azure to avoid unnecessary cost. Use the clean-up notebook for that."
Infrastructure README.md
Use this consistent layout:
- Title - Name of the architecture (e.g. "Simple API Management Infrastructure")
- Description - One to two sentences summarising the architecture and its value
- Architecture diagram -
<img>tag referencing the SVG in the infrastructure folder - ๐ฏ Objectives - Numbered list of what the infrastructure provides
- โ๏ธ Configuration - One-sentence reference to the notebook's initialise-variables section
- โถ๏ธ Execution - Expected runtime badge and numbered steps to run the notebook
- Reference links - Markdown reference-style links at the bottom
Sample Development Guidelines
Sample File Structure
Each sample in samples/[sample-name]/ must contain:
create.ipynb- Jupyter notebook that deploys and demonstrates the samplemain.bicep- Bicep template for deploying sample resourcesREADME.md- Documentation explaining the sample, use cases, and concepts<domain>_helpers.py- Optional sample-local Python mechanics when the notebook would otherwise own parsing, retries, sessions, persistence, or multi-step orchestrationapim-policies/*.xml- Sample-owned APIM policy files (if applicable to the sample)queries/*.kql- KQL (Kusto Query Language) files (if applicable to the sample)
New sample-owned APIM policy XML and KQL files must not be placed at the sample root. Existing root-level files may remain until their sample is migrated.
New Sample Sync Checklist
Whenever a new sample is added:
- Ask for the sample name if it has not been provided. Do not invent it.
- Ask for supported infrastructures if they have not been provided. Do not assume "All infrastructures".
- Create the sample under
samples/[sample-name]/unless the user explicitly requests another location. - Use
samples/_TEMPLATEas the baseline and suggest updating the template if the improvement should apply to future samples. - Update the root
README.mdsample table in alphabetical order. - Update
docs/index.html, including the matching sample card and JSON-LDItemListentry. - Update
assets/APIM-Samples-Slide-Deck.htmlwhen the sample catalog, counts, or descriptions are surfaced in the deck. - Update
tests/Test-Matrix.mdandassets/diagrams/Infrastructure-Sample-Compatibility.svg(add a new row for the sample in alphabetical order, marking each infrastructure as compatible or not). - Keep the canonical sample display name identical across README tables, website cards, slide deck content, and compatibility diagrams.
Jupyter Notebook (create.ipynb) Structure
Follow this pattern for all sample create.ipynb files. Consistency here is critical - users should recognise the layout immediately from having used any other sample:
Cell 1: Title & Overview (Markdown)
- Notebook title and brief description
- Reference to README.md for detailed information
Cell 2: What This Sample Does (Markdown)
- Bullet list of key actions/demonstrations
- Keep focused on user-facing outcomes
Cell 3: Initialize Notebook Variables (Markdown)
- Heading with note that only USER CONFIGURATION should be modified
Cell 4: Initialize Notebook Variables (Python Code)
This cell should be straightforward configuration only. No Azure SDK calls here.
Structure:
- Import statements at the top:
- Standard library imports (time, json, tempfile, requests, pathlib, datetime)
utils,apimtypes,console,azure_resources(includingaz,get_infra_rg_name,get_account_info)
- USER CONFIGURATION section:
rg_location: Azure region (default:Region.EAST_US_2)index: Deployment index for resource naming (default: 1)deployment: Selected infrastructure type (reference INFRASTRUCTURE enum options)api_prefix: Prefix for APIs to avoid naming collisionstags: List of descriptive tags- Sample-specific configuration (e.g., SKU, feature flags, thresholds)
- SYSTEM CONFIGURATION section:
sample_folder: Folder name matching the sample directoryrg_name: Computed usingget_infra_rg_name(deployment, index)supported_infras: List of compatible infrastructure typesnb_helper: Instance ofutils.NotebookHelper(...)- Do NOT check if resource group exists here
- Get account info:
- Call
get_account_info()to retrieve subscription ID and user info
- Call
- Final line:
print_ok('Notebook initialized')
Important: Do NOT call az commands in this cell. Do NOT create a config dictionary. Do NOT initialize deployment outputs. All Azure operations and variable definitions should happen in subsequent operation cells.
Cell 5+: Functional Cells (Markdown + Code pairs)
- Each logical operation gets a markdown heading cell followed by one or more code cells
- Keep educational configuration, scenario steps, and key APIM concepts visible in the notebook. Extract non-educational Python mechanics, such as reusable orchestration, parsing, retries, polling, data transformation, and repeated request setup, into existing shared helpers or focused sample-local Python helper modules. Compose established helpers first; extend one only when the behavior belongs to its existing responsibility, state, and lifecycle.
- Follow
shared/python/README.mdfor the complete helper and supporting-class strategy, including ownership, dependency direction, state, lifecycle, selective autoreload, promotion, and testing rules. - Before finalizing a notebook, explicitly review each code cell for incidental mechanics. Line count is only a signal; extract based on responsibility and testability, not size alone.
- Sample-local helpers must use explicit inputs and typed outputs, must not read notebook globals, and must own every session, temporary file, or other closeable resource they create. Prefer context managers for lifecycles that span multiple calls.
- Inject remote, sleep, clock, session-factory, or command-runner boundaries when needed to test success, failure, retry, and cleanup paths without live Azure access or real delays.
- Add
tests/python/test_<sample>_helpers.pyfor each extracted sample-local module. Target meaningful success, failure, malformed-input, and cleanup coverage, not line-count padding. - Load actively edited sample-local modules through a module-qualified import and register selective autoreload with
utils.enable_module_autoreload('<module_name>'). Do not use broad autoreload that can discard notebook state.
First operation cell (typically deployment):
โ ๏ธ CRITICAL: Use nb_helper.deploy_sample() for all sample deployments. This method:
- Automatically validates the infrastructure exists (checks resource group)
- Prompts user to select or create infrastructure if needed
- Handles all Azure availability checks internally
- Returns deployment outputs including the APIM service name
Process:
- Print configuration summary using variables from init cell
- Build
bicep_parametersdict with sample-specific parameters (e.g.,location,costExportFrequency)- DO NOT manually query for APIM services
- DO NOT pass
apimServiceNametobicep_parametersif the infrastructure already provides it
- Call
nb_helper.deploy_sample(bicep_parameters)to deploy Bicep template - Call
nb_helper.get_deployment_context(output)to validate common outputs, then store its fields as individual variables (not in a dictionary)
- Example:
apim_name = deployment_context.apim_name,apim_gateway_url = deployment_context.apim_gateway_url - Continue to use
output.get(...)oroutput.getJson(...)for sample-specific outputs that are not part ofSampleDeploymentContext
Invalid approach (do NOT do this):
# โ WRONG - Manual APIM service queries
apim_list_result = az.run(f'az apim list --resource-group {rg_name}...')
apim_name = apim_list_result.json_data[0]['name'] # WRONG!
# โ WRONG - Passing APIM name in bicep parameters when it should come from output
bicep_parameters = {'apimServiceName': {'value': apim_name}}
Valid approach (do this):
# โ
CORRECT - Let deploy_sample() handle infrastructure validation
bicep_parameters = {
'location': {'value': rg_location},
'costExportFrequency': {'value': cost_export_frequency}
}
output = nb_helper.deploy_sample(bicep_parameters)
deployment_context = nb_helper.get_deployment_context(output)
apim_name = deployment_context.apim_name
apim_gateway_url = deployment_context.apim_gateway_url
apim_apis = deployment_context.apis
Subsequent cells:
- Check prerequisites with
if 'variable_name' not in locals(): raise SystemExit(1) - Use variables directly in code (e.g.,
rg_name,subscription_id,apim_name) - Do NOT recreate or duplicate variables from previous cells
- Follow pattern: Markdown description โ Code implementation โ Output validation
Variable Management
Do NOT use a config dictionary. Use individual variables that flow naturally through cells:
- Init cell defines user and system configuration variables
- Deployment cell creates new variables for deployment outputs (e.g.,
apim_name,app_insights_name) - Subsequent cells reference these variables directly
- Check prerequisites using
if 'variable_name' not in locals():pattern - Variables created in one cell are automatically available in all subsequent cells
Example:
# Init cell
apim_sku = APIM_SKU.BASICV2
deployment = INFRASTRUCTURE.SIMPLE_APIM
subscription_id = get_account_info()[2]
# Deployment cell
deployment_context = nb_helper.get_deployment_context(output)
apim_name = deployment_context.apim_name
app_insights_name = output.get('applicationInsightsName')
# Cost export cell
if 'app_insights_name' not in locals():
raise SystemExit(1)
storage_account_id = f'/subscriptions/{subscription_id}/...'
NotebookHelper Usage
What NotebookHelper does:
__init__(): Initializes with sample folder, resource group name, location, infrastructure type, and supported infrastructure listdeploy_sample(bicep_parameters): Orchestrates the complete deployment process:- Checks if the desired resource group/infrastructure exists
- If not found, queries all available infrastructures and prompts user to select or create new
- Executes the Bicep deployment with provided parameters
- Returns
Outputobject containing deployment results (resource names, IDs, connection strings, endpoints)
get_deployment_context(output=None): Validates and returns the common APIM service name, gateway URL, and API outputs. Whenoutputis omitted, it uses the successful output cached bydeploy_sample().create_apim_requests(apim_gateway_url, subscription_key=None, headers=None): Resolves the selected infrastructure endpoint and creates a configuredApimRequestsclient.
How to use:
-
Initialize in the configuration cell (Cell 4):
nb_helper = utils.NotebookHelper( sample_folder, rg_name, rg_location, deployment, supported_infras, index=index, apim_sku=APIM_SKU.BASICV2 # Optional: default is BASICV2 ) -
Call in the deployment cell (Cell 5+):
bicep_parameters = { 'location': {'value': rg_location}, # ... other sample-specific parameters } output = nb_helper.deploy_sample(bicep_parameters)
Extract common and sample-specific outputs:
deployment_context = nb_helper.get_deployment_context(output)
apim_name = deployment_context.apim_name
apim_gateway_url = deployment_context.apim_gateway_url
apim_apis = deployment_context.apis
app_insights_name = output.get('applicationInsightsName')
CRITICAL: Do not bypass NotebookHelper!
- โ Do NOT manually check
az group exists - โ Do NOT manually query
az apim listto find APIM services - โ Do NOT check if resources exist before deployment
- โ
Let
deploy_sample()handle all infrastructure validation, selection, and existence checking
Bicep Template (main.bicep)
- Deploy only resources specific to the sample (don't re-deploy APIM infrastructure)
- Accept parameters for APIM service name, location, sample-specific config
- Use
shared/bicep/modules where available for reusable components - Return outputs for all created resources (names, IDs, connection strings, etc.)
Always reuse infrastructure-provided resources
Samples must default to reusing the resources already created by the infrastructure deployment. Do not redeploy resources the infrastructure already provides โ this creates duplicate Azure resources, costs extra money, can misconfigure APIM logger wiring, and confuses users about which resource is "the real one".
The simple-apim infrastructure (and equivalents) already provides:
- An APIM service (with
apim-loggeralready attached) - A Log Analytics workspace (already wired to APIM diagnostics)
- An Application Insights component (already wired as the APIM logger)
Sample main.bicep files must consume these via existing resource references rather than module ... = { ... } deployments. The sample's create.ipynb should read the infrastructure's resource names from nb_helper.deployment_outputs (populated by deploy_sample()) โ these include applicationInsightsName, logAnalyticsWorkspaceName, apimServiceName, etc.
A sample may deploy its own Application Insights, Log Analytics, or other shared monitoring infrastructure only when one of these conditions holds:
- The sample needs isolation from other samples (e.g. a dedicated workspace for cost data that must not mix with other samples' telemetry).
- The sample's resource has sample-specific configuration that cannot be applied to the shared infrastructure resource without affecting other samples (e.g. a custom retention policy, a different region for compliance).
- The sample exercises a scenario where having a separate resource is the point of the sample (e.g. a sample demonstrating multi-workspace federation).
When a sample does deploy its own version of an infrastructure-provided resource, the sample README must include a short section explaining why the duplicate is necessary and how it differs from the infrastructure-provided one.
When wiring APIM API-level diagnostics (apimLoggerName parameter on shared/bicep/modules/apim/v1/api.bicep), omit the parameter so the API inherits the infrastructure's apim-logger. Only set apimLoggerName when the sample has a justified reason (per the rules above) to point a specific API at a different logger.
Sample README.md
Every sample README must follow this standard layout to maintain uniformity across the repository. Users should be able to predict where to find each piece of information:
- Title -
# Samples: [Sample Name] - Description - One to two sentences summarising the sample
- Supported infrastructures badge -
โ๏ธ **Supported infrastructures**: ... - Expected runtime badge -
๐ **Expected *Run All* runtime (excl. infrastructure prerequisite): ~N minutes** - ๐ฏ Objectives - Numbered list of learning or experimentation goals
- โ Prerequisites (if applicable) - Sample-specific prerequisites only; see rules below
- ๐ Scenario (if applicable) - Use case or scenario context; omit if not relevant
- ๐ฉ๏ธ Lab Components - What the lab deploys and how it benefits the user
- โ๏ธ Configuration - How to choose an infrastructure and run the notebook
- ๐ผ๏ธ Expected Results (if applicable) - Screenshots and descriptions of what the user should see after running the sample
- ๐งน Clean Up (if applicable) - Reference to a clean-up notebook or manual steps
- ๐ Additional Resources (if applicable) - Links to relevant documentation
Match the heading emojis, heading levels, and section ordering exactly. If a section is not applicable, omit it entirely rather than leaving it empty.
Prerequisites rules
-
Do NOT repeat general prerequisites (Azure subscription, Azure CLI, Python environment, APIM instance). These are documented once in the root README's Getting Started section and apply to all samples. The APIM Samples Developer CLI (
start.ps1/start.sh) handles environment setup. -
Only add
## โ Prerequisiteswhen a sample has genuinely unique requirements that go beyond the root README, such as:- Additional Azure RBAC role assignments beyond Contributor
- External service accounts (e.g., a Spotify developer account)
- Special tooling or configuration not covered by the Developer CLI
-
When a Prerequisites section is needed, open with a one-line reference to the root README for general prerequisites, then list only the sample-specific requirements. Example:
## โ Prerequisites Beyond the [general prerequisites](../../README.md#-getting-started) (Azure subscription, CLI, Python environment), this sample requires ... -
The
oauth-3rd-partysample is the canonical example of sample-specific prerequisites (external service accounts). Thecostingsample is the canonical example of additional RBAC requirements.
Testing and Traffic Generation
-
Use the
ApimRequestsandApimTestingclasses fromapimrequests.pyandapimtesting.pyfor structured API testing with verbose logging and response formatting. -
Favour
requests.Session()for high-volume loops and multi-caller traffic that does not need per-request logging. Creating a barerequests.get/postinside a loop opens a fresh TCP+TLS connection on every iteration, adding 200-500 ms per request. Create one session at the top of the section, route all calls through it, and close it in afinallyblock. Import asimport requests as http_requestsfor clarity when therequestsname would shadow other uses. -
ApimRequestsuses one reusable session for single, multiple, and asynchronous requests. Create it throughnb_helper.create_apim_requests(...)and use it as a context manager so endpoint routing, infrastructure headers, TLS behavior, and cleanup remain consistent. -
One session per cell is fine. Each notebook cell should be independently runnable, so create and close a session within the same cell rather than sharing one across cells.
-
For
ApimRequests, usenb_helper.create_apim_requests(...)to resolve the correct endpoint URL, headers, and TLS behavior. Useutils.get_endpoint(...)directly only when constructing a raw session for high-volume traffic. -
Session pattern example (preferred for loops):
import requests as http_requests endpoint_url, request_headers, allow_insecure_tls = utils.get_endpoint(deployment, rg_name, apim_gateway_url) session = http_requests.Session() session.verify = not allow_insecure_tls if request_headers: session.headers.update(request_headers) session.headers['Ocp-Apim-Subscription-Key'] = subscription_key url = f'{endpoint_url}/api-path' try: for item in items: session.get(url, headers={'Authorization': f'Bearer {item["token"]}'}, timeout=30) finally: session.close() -
ApimRequests example (for structured test verification with logging):
from apimtesting import ApimTesting tests = ApimTesting("Sample Tests", sample_folder, nb_helper.deployment) with nb_helper.create_apim_requests(apim_gateway_url, subscription_key) as reqs: output = reqs.singleGet('/api-path', msg='Calling API') tests.verify('Expected String' in output, True)
Notebook Cell Ordering: Batch Traffic, Then Verify
Telemetry pipelines (Log Analytics ingestion, Application Insights custom metrics, ApiManagementGatewayLlmLog, Cost Management exports) all have ingestion latency measured in minutes, not seconds. A naive notebook structure that alternates generate traffic โ wait/verify โ generate more traffic โ wait/verify pays this latency tax repeatedly and stretches a sample's Run All time well beyond what it needs to be.
When designing or restructuring a sample notebook:
- Batch all traffic generation first, then verify once at the end. Group every traffic-generating cell into a contiguous block, followed by a contiguous block of verification cells. By the time the verification cells run, ingestion pipelines have already had several minutes of warm-up across the preceding traffic cells, so polling typically returns on the first attempt instead of on a cold-start retry schedule.
- Avoid inline verification inside traffic cells. A cell that sends traffic and polls for that traffic's metrics in the same cell forces every subsequent step to wait for that poll. If verification is genuinely useful, factor it out into a dedicated verification cell at the end of the notebook.
- Group related cells with a shared prefix in the markdown heading. When the notebook has multiple traffic cells and multiple verification cells, prefix each heading with a short tag so users immediately see which cells belong together. The
costingsample uses[Traffic]and[Verify]prefixes (e.g.### 7/14: [Traffic] Generate Sample API Traffic,### 12/14: [Verify] Verify Log Ingestion). Keep prefixes short, bracketed, and consistent within a notebook. - Place setup-time work (alerts, exports, dashboards) before traffic cells, so the demo traffic exercises them end-to-end. For example, budget alerts should be created before traffic is generated, not after.
- Place context cells (pricing tables, cost models, reference data) before action cells. A pricing-and-cost-analysis cell that does no Azure calls should run early so users see the cost framing before they generate billable traffic, not as a postscript.
- Reuse polling output across verifications when possible. If two verification cells query the same workspace or component, consolidate them into a single cell or schedule their polls so the second one benefits from the warm pipeline established by the first.
Language-specific Instructions
- Python: see
.github/python.instructions.md - Bicep: see
.github/bicep.instructions.md - JSON: see
.github/json.instructions.md - Markdown: see
.github/markdown.instructions.md
Formatting and Style
- Maintain consistent indentation and whitespace but consider Editor Config settings, etc, for the repository.
- Use only LF, never CRLF for line endings.
- Use blank lines to separate logical sections of code. Whitespace is encouraged for readability.
- Organize code into logical sections (constants, variables, private/public methods, etc.).
- Prefer single over double quotes, avoiding typographic quotes.
- Only use apostrophe (U+0027) and quotes (U+0022), not left or right single or double quotation marks.
- Use only ASCII punctuation in text content. Replace em-dashes (U+2014) and en-dashes (U+2013) with a plain hyphen-minus (
-), the minus sign (U+2212) with-, the horizontal ellipsis (U+2026) with..., and math comparison glyphs (U+2264, U+2265, U+2260) with<=,>=,!=. These typographic characters do not always render correctly in downstream surfaces such as Azure Monitor Workbook markdown tiles, slide-deck HTML, and some terminal fonts. This rule applies to every text-bearing file in the repository, including Markdown, Python docstrings/comments, Bicep@descriptionstrings, XML policy comments, and JSON string values that contain user-facing text (notably Azure Monitor Workbook*.workbook.jsonfiles where markdown is embedded incontent.jsonstrings). - Do not localize URLs (e.g. no "en-us" in links).
For Markdown-specific formatting guidelines (including critical rules about emoji variation selectors and table alignment), see .github/markdown.instructions.md.
Testing and Edge Cases
- Include test cases for critical paths and edge cases.
- Include negative tests to ensure robustness.
- Document expected behavior for edge cases and error handling.
- Write unit tests for functions and classes, following language-specific testing frameworks.
GitHub Pages Site
The public landing page at https://azure-samples.github.io/Apim-Samples/ is built from docs/index.html by .github/workflows/github-pages.yml on every push to main. The page intentionally mirrors a subset of the root README.md (infrastructure cards, sample cards, quick-start steps) so that visitors get a polished overview without cloning the repo.
Treat docs/index.html as a downstream consumer of the README. When you change any of the following, update the landing page in the same PR:
| Change | Update required in docs/index.html | Also update |
|---|---|---|
| Add / remove / rename an infrastructure | Add / remove / rename the matching .infra-card and the matching ListItem in the JSON-LD ItemList (in <head>). Update the infrastructure count in the first .value-card if it still says "Five". | Add / remove the SVG copy line in .github/workflows/github-pages.yml. |
| Add / remove / rename a sample | Add / remove / rename the matching .sample-card and the matching ListItem in the JSON-LD ItemList. | โ |
| Change a sample's supported infrastructures | Update the .infra-tag text on that sample's card. | โ |
| Change the quick-start flow in the root README | Update the four .step items. | โ |
Rename or replace an architecture SVG in assets/diagrams/ | โ | Update the matching cp line in .github/workflows/github-pages.yml. |
Check docs/README.md for local preview instructions and styling notes. The page is deliberately plain static HTML + an external stylesheet (docs/styles.css), with no executable JavaScript and no build tooling, so that it cannot rot due to a transitive npm dependency. The only <script> tag is the JSON-LD structured-data block, which must stay inline because search-engine crawlers do not reliably follow external JSON-LD references. Keep it that way unless there is a compelling reason to add a build step.
Required before each commit
- Ensure all code is well-documented and follows the guidelines in this file.
- Generate every Markdown file, including instructions, skills, and agents, against
.markdownlint.jsonand.github/markdown.instructions.mdfrom the start. - After any Markdown change, run
npx --no-install markdownlint-cli2 "**/*.md" "#**/.venv/**" "#**/node_modules/**"from the repository root and require zero violations. - Preserve document hierarchy while fixing lint issues. Do not flatten nested lists or collapse structured content into prose solely to satisfy markdownlint.
- Ensure that Jupyter notebooks do not contain any cell output.
- Ensure that Jupyter notebooks have
indexassigned to1in the first cell. - If the change touches the infrastructure list, sample list, quick-start steps, or architecture SVGs, ensure
docs/index.html(and the asset copy step in.github/workflows/github-pages.ymlwhere relevant) has been updated to match.
Jupyter Notebook Instructions
- Use these configuration settings as a reference for the VS Code Jupyter extension configuration.
- When generating or editing notebook files as JSON, structure the document with a top-level
cellsarray. - Each cell must be a valid JSON object with
cell_type,metadata.language, andsource. - Existing cells must keep a unique
metadata.idvalue. - New cells do not need a
metadata.idvalue unless an editor or tool assigns one. - Keep notebook JSON logically structured and valid. Do not emit partial notebook fragments when a full notebook document is required.
- Place all
importstatements at the top of every code cell, before any other code. Never nest imports insideif/else/tryblocks within a cell. Ruff'sPLC0415does not flag imports inside module-level conditionals, so this must be enforced manually. - When describing notebook changes to users, refer to cells by visible cell number (Cell 1, Cell 2, etc.), not by internal cell IDs.
Presentation Instructions
- Presentation source files should remain accessible to a broad audience and should target WCAG 2.0 AA color contrast as the default baseline.
- Do not rely on color alone to communicate meaning. If a color distinguishes state, priority, or emphasis, pair it with text, icons, labels, or structure.
- Body text and other normal-sized text on slides should meet at least
4.5:1contrast against their background. Large text (roughly24pxregular or18.66pxbold and above) and essential UI indicators should meet at least3:1. - Pay special attention to helper text, footer text, captions, badge text, and text placed on gradients or translucent overlays. These are the most common places where contrast regressions appear.
- When using muted text on light backgrounds, prefer dark neutral tones over mid-gray decorative values. When using text on dark or gradient backgrounds, prefer near-white text unless the accent color has been checked for sufficient contrast.
- Preserve meaningful
alttext for presentation images and diagrams, and avoid conveying critical information only inside images when the same message can be stated in text on the slide. - If you introduce a new presentation theme or palette, validate the shared color tokens first so accessibility is enforced consistently across all slides.
Diagram Instructions
- Architecture diagrams are maintained as Draw.io (
.drawio) files inassets/diagrams/. SVG exports are co-located alongside the.drawiosource files. - The Draw.io diagrams were created with the Azure Draw.io MCP Server.
- Keep diagrams simple. For Azure, include major components, not individual aspects of components. For example, there is no need for individual policies in WAFs or APIs in API Management, Smart Detector Alert Rules, etc.
- Less is more. Don't be too verbose in the diagrams.
- Sample names in compatibility-matrix diagrams are the canonical display names. README tables, landing page cards, and JSON-LD entries must use the same names. When adding or renaming a sample, update the diagram and all listings together.
- Samples in compatibility-matrix diagrams must be listed in alphabetical order by display name.
- Never include subscription IDs, resource group names, or any other sensitive information in the diagrams. That data is not relevant.
KQL (Kusto Query Language) Instructions
-
Store every sample-owned KQL query in a dedicated
.kqlfile undersamples/<sample-name>/queries/rather than at the sample root or embedded inline in Python code. This keeps notebooks readable and lets users copy-paste the query directly into a Log Analytics or Azure Data Explorer query editor. -
Resolve
.kqlfiles from the sample'squeries/directory and load them withPath.read_text():from pathlib import Path kql_path = Path(utils.get_project_root()) / 'samples' / sample_folder / 'queries' / 'my-query.kql' kql_query = Path(kql_path).read_text(encoding='utf-8') -
Parameterise KQL queries using native
letbindings. Define parameters asletstatements prepended to the query body at runtime, keeping the.kqlfile free of Python string interpolation:kusto_query = f"let buName = '{bu_name}';\nlet threshold = {alert_threshold};\n{kql_template}" -
In the
.kqlfile, document available parameters in a comment header so users know whichletbindings to supply:// Parameters (prepend as KQL 'let' bindings before running): // let buName = 'bu-hr'; // Business unit subscription ID // let threshold = 1000; // Request count threshold ApiManagementGatewayLogs | where ApimSubscriptionId == buName | summarize RequestCount = count() | where RequestCount > threshold -
When executing KQL via
az restoraz monitor log-analytics query, write the query body to a temporary JSON file and pass it with--body @tempfile.jsonto avoid shell pipe-character interpretation issues on Windows.
Azure Monitor Workbook File Convention
Azure Monitor Workbook definitions stored in this repository must follow the <name>.workbook.json suffix convention. The double extension makes the file's purpose obvious at a glance, prevents collisions with other JSON artefacts in the same folder (Bicep parameter files, schemas, configuration), and groups all workbook source files under a single, greppable pattern.
- Naming: Use kebab-case for
<name>and match the sample folder where reasonable (e.g.costing.workbook.jsoninsamples/costing/,latency.workbook.jsonif a future sample adds one). One file per workbook. - Location: Place the file in the sample folder that owns the workbook (e.g.
samples/<sample-name>/<sample-name>.workbook.json). Workbooks shared across samples belong undershared/with a descriptive<name>prefix. - Schema: The first property must be
"$schema": "https://raw.githubusercontent.com/Microsoft/Application-Insights-Workbooks/refs/heads/master/schema/workbook.json". Follow.github/json.instructions.mdfor$schemaURL formatting. - Bicep loading: When embedding the workbook in a Bicep template, load it via
loadJsonContent('<name>.workbook.json')and serialise it to a string forproperties.serializedData. Match the filename exactly โ do not rename toworkbook.jsoninside Bicep. - Push script: A sample that ships a workbook should provide an
update-workbook.ps1(or equivalent) helper that reads<name>.workbook.jsonand updates the deployed workbook resource viaaz rest. The script should accept a mandatory-rgparameter and preserve any user-edited workbook parameter values (e.g. cost rates) from the live resource so that re-pushing source-controlled changes does not clobber portal edits. - Tests: Workbook JSON files should be parsed and structurally validated by a unit test (see
tests/python/test_costing_workbook.pyfor the canonical pattern: schema check, parameter presence, KQL query well-formedness).
Azure Monitor Workbook Query Optimization
Azure Monitor Workbook query items execute independently โ there is no native mechanism to share a materialized table across query items. Apply the following patterns to minimise data scanned and improve workbook responsiveness:
-
materialize()for multi-referenceletbindings. When aletbinding is referenced more than once in the same query (e.g. once for atoscalar(count)and once for the mainsummarize), wrap it inmaterialize()so Log Analytics computes the base set once per query execution rather than scanning the underlying table twice.let logs = materialize(ApiManagementGatewayLogs | where TimeGenerated {TimeRange} and ApimSubscriptionId startswith 'bu-'); let totalRequests = toscalar(logs | summarize count()); logs | summarize RequestCount = count() by ApimSubscriptionId | extend UsageShare = round(RequestCount * 100.0 / totalRequests, 2) -
Column-project before joins. When joining two tables, add an explicit
| projecton both sides to carry only the columns that the downstreamsummarize/extend/projectactually needs. Wide diagnostic tables likeApiManagementGatewayLlmLogcontain many columns; projecting only the needed ones before the join reduces memory and network cost.ApiManagementGatewayLlmLog | where TimeGenerated {TimeRange} and TotalTokens > 0 | project CorrelationId, TotalTokens // only what this query needs | join kind=inner ( ApiManagementGatewayLogs | where TimeGenerated {TimeRange} and ApimSubscriptionId startswith 'bu-' | project CorrelationId, BusinessUnit = ApimSubscriptionId ) on CorrelationId | summarize TotalTokens = sum(TotalTokens) by BusinessUnit -
Avoid duplicate queries across items. If two workbook visualisations require identical data, consider whether they can share a single query item with different chart/table renderings, or whether the layout can be restructured to avoid scanning the same data twice. Workbook Merge items can combine two previously-computed result sets but cannot perform arbitrary re-aggregation.
-
Keep the Workbook
timeContexton each query item rather than relying solely on the global parameter. This ensures Log Analytics can push down the time filter to the storage layer even when the parameter is complex. -
Prefer
summarizeclose to the source. Pushsummarizeas early as possible in the pipeline to reduce the volume of rows flowing through subsequent operators.
Admin APIs (/admin/) Convention
Samples that require administrative or operational endpoints (cache loading, configuration reloads, health checks, etc.) must place them under an /admin/ API path. This establishes a consistent, recognisable pattern across all APIM Samples.
- API path:
{api_prefix}admin(e.g.cors-admin,lb-admin). The sample'sapi_prefixkeeps admin APIs namespaced per sample. - Subscription required: Always
True. Admin APIs must never be publicly accessible without a subscription key. - Production security: Subscription keys are a baseline gate but are shared secrets, not identity-based auth. Production deployments should layer JWT validation (
validate-azure-ad-tokenorvalidate-jwt) on top of subscription keys. See theauthXandauthX-prosamples for implementation patterns. - Naming: Use kebab-case operation paths that describe the action (e.g.
/load-cache,/clear-cache,/refresh-config). - Tags: Include the sample's tags so the admin API is grouped with its sibling APIs in the APIM portal.
- Documentation: The admin API's display name should start with the option or sample context (e.g.
Option 3 Admin) so its purpose is clear in the APIM portal.
API Management Policy XML Instructions
- Store every sample-owned APIM policy XML file under
samples/<sample-name>/apim-policies/. Keep reusable, cross-sample policy assets undershared/apim-policies/. - When migrating existing samples, update policy path helpers to check
samples/<sample-name>/apim-policies/first and the sample root second as a temporary backwards-compatible fallback. New files must useapim-policies/; do not rely on the fallback for newly created policies. - After moving KQL or policy XML files, check and update every notebook, Python helper, Bicep
loadTextContent()call, test, script, and documentation reference that consumes them. Add or update tests for canonical-directory resolution, root-level fallback, explicit paths, auto-detected sample names, and missing files before considering the migration complete. - Policies should use camelCase for all variable names.
- In policy expressions inside double-quoted XML attributes, escape embedded double quotes as
"(for example,value="@((string)context.Variables["callerId"])"). Unescaped quotes make the XML malformed. Expressions in element text, such as<value>@("text")</value>, do not require this escaping. - Policy expressions (
@(...)and@{...}) may only reference .NET types and members on APIM's allow-list. Using anything outside the list (e.g.System.Globalization.*,DateTime.TryParse,DateTime.ToUniversalTime,System.Text.Json) causes a deploy-timeValidationError: One or more fields contain incorrect valueswith no further detail. Verify each type/member against the allow-list before writing the expression. See.github/skills/apim-policies/SKILL.mdfor common pitfalls and allowed replacements.
Trust
Not scanned yet. Artifacts are graded after they are crawled, so a recently discovered one may have no result for a while.
Versions
git-26d1ea4b10502026-08-04