Files
gh-atc-net-atc-agentic-tool…/skills/iot-edge-module/SKILL.md
2025-11-29 17:58:35 +08:00

23 KiB

name, description
name description
iot-edge-module This skill should be used when creating a new Azure IoT Edge module. Use when the user requests to scaffold, create, or set up a new edge module. The skill automates module scaffolding, manifest configuration, and shared contract generation with intelligent project structure detection.

IoT Edge Module Scaffolding Skill

This skill automates the creation of new Azure IoT Edge modules. Use this skill to scaffold a complete module structure including source code, Docker configuration, deployment manifests, and shared contracts.

Execution Environment

IMPORTANT: All bash commands MUST use Unix syntax regardless of platform.

  • Always use Unix bash syntax (e.g., test -d, [ -f ], etc.)
  • NEVER use Windows CMD syntax (e.g., if exist, dir, etc.)
  • NEVER pipe commands to null (2>/dev/null, 2>nul, etc.) - let all output and errors show naturally
  • Bash is available on all platforms: native on Linux/macOS, via WSL/Git Bash on Windows
  • Python scripts are cross-platform and handle path normalization internally

Communication Guidelines

Be concise and avoid stating obvious or expected behavior:

  • Don't explain why assets are in the plugin directory (this is expected)
  • Don't describe where script files "should" be located (this is obvious)
  • Focus output on actionable information, progress, and actual errors only
  • Avoid verbose explanations of normal/expected conditions

When to Use This Skill

Trigger this skill when the user requests to:

  • Create a new IoT Edge module
  • Scaffold an edge module
  • Set up a new module for IoT Edge
  • Add a new module to the edge deployment

Prerequisites

Before scaffolding a module, gather the following information from the user:

  1. Module name (PascalCase, e.g., "DataProcessorModule")
  2. Module description (brief description of module purpose)

Scaffolding Process

Follow these steps in order to scaffold a new IoT Edge module:

Step 1: Detect or Load Project Structure

Run the project structure detection script to identify existing patterns:

python scripts/detect_project_structure.py --root .

Step 1.5: Verify IoTEdgeModules Folder Structure

Check if the IoTEdgeModules folder exists:

If the detection script couldn't find modules_base_path or it doesn't exist:

Prompt user:

IoTEdgeModules folder not found. Where should modules be created?

1. Create at default location: src/IoTEdgeModules/modules/
2. Specify custom path
3. Cancel scaffolding

Choose option (1/2/3):

If option 1 selected:

  • Create directory structure:
    src/IoTEdgeModules/
    ├── modules/
    └── config/
    
  • Update detected configuration with this path

If option 2 selected:

  • Prompt: "Enter custom IoTEdgeModules path (e.g., edge/modules/):"
  • Validate path is reasonable
  • Create directory structure at custom location
  • Update detected configuration

If option 3 selected:

  • Exit scaffolding with message: "Scaffolding cancelled by user"

What this detects:

  • Modules base path (e.g., src/IoTEdgeModules/modules)
  • Contracts project path and name
  • Deployment manifests location
  • Project namespace
  • Container registry URL
  • NuGet feed URL (if configured)

Processing the output:

  1. Parse the JSON output
  2. If config_source is "saved", use the saved configuration silently
  3. If config_source is "detected", present findings to user for confirmation
  4. If detection fails or user rejects, prompt for each value manually

Confirmation prompt (if detected, not saved):

Display the detected configuration to the user in a clear, readable format:

Detected project structure:
• Modules location: <modules_base_path>
• Project namespace: <project_namespace>
• Container registry: <container_registry>
• Contracts project: <contracts_project_name> (<contracts_project_path>)
• Deployment manifests: <manifests_found count> found
• NuGet feed: <nuget_feed_url or "Not configured">

Then use AskUserQuestion tool to ask for confirmation:

  • Question: "Use this detected configuration?"
  • Header: "Config"
  • Options:
    1. "Yes, use it" - "Proceed with the detected configuration"
    2. "Save and use" - "Save this configuration for future modules and use it now"
    3. "No, customize" - "Manually specify configuration values instead"

If user selects "Save and use":

python scripts/detect_project_structure.py --root . --save

If user selects "No, customize" or if detection fails, prompt for:

  • Project namespace (e.g., "Company.IoT.EdgeAPI")
  • Container registry URL (e.g., "myregistry.azurecr.io")
  • Modules base path (default: "src/IoTEdgeModules/modules")
  • Contracts project name and path (or "none" if not using shared contracts)

Step 2: Gather Module-Specific Information

Ask the user for:

Required:

  1. Module name (PascalCase)
  2. Module description

Optional Features:

A. Private NuGet Feed

  • Ask: "Does this project require a private NuGet feed? (Yes/No)"
  • If Yes and not detected: Prompt for NuGet feed URL
  • If Yes and detected: Confirm detected URL or allow override

B. Shared Contracts Project

  • If contracts project was detected: Automatically use it for module constants (no prompt needed)
  • If not detected: Ask "Do you have a shared contracts project? (Yes/No/Create standalone)"

Step 3: Validate and Normalize Module Names

Convert the user-provided module name to required formats:

ModuleName (PascalCase):

  • Remove "Module" suffix if present, then add it back
  • Example: "DataProcessor" → "DataProcessorModule"
  • Example: "DataProcessorModule" → "DataProcessorModule"

modulename (lowercase):

  • Convert ModuleName to lowercase
  • Example: "DataProcessorModule" → "dataprocessormodule"

Confirm with user using AskUserQuestion tool:

Present the module details and ask for confirmation:

  • Question: "Proceed with creating this module?"
  • Header: "Confirm Module"
  • Options:
    • "Yes, create module" → Continue to Step 4
    • "No, use different name" → Go back to Step 2 (gather module name)

Display in the question description:

Module will be created as:
• C# class name: <ModuleName>
• Module ID: <modulename>
• Directory: <modules_base_path>/<modulename>/

Do NOT assume "Yes" or proceed without using AskUserQuestion tool and getting explicit user confirmation

Step 4: Create Module Directory Structure

Create the module directory:

<modules_base_path>/<modulename>/

Check if directory already exists (MUST use this exact bash syntax):

test -d "<modules_base_path>/<modulename>" && echo "EXISTS" || echo "NOT_EXISTS"

Note: Do NOT use Windows CMD syntax like if exist. Always use Unix bash syntax as shown above.

  • If EXISTS: Ask user "Module directory exists. Overwrite? (Yes/Rename/Cancel)"
  • If Rename: Prompt for new name and restart from Step 3

Step 5: Generate Module Files from Templates

Use the template files in assets/ to generate module files with runtime substitutions. The skill generates 11 files total.

Placeholder substitutions:

Placeholder Value Example
{{ModuleName}} PascalCase module name DataProcessorModule
{{modulename}} Lowercase module name dataprocessormodule
{{ModuleDescription}} User-provided description Processes sensor data
{{CONTAINER_REGISTRY}} Detected or provided registry myregistry.azurecr.io
{{PROJECT_NAMESPACE}} Detected or provided namespace Company.IoT.EdgeAPI
{{MODULE_CSPROJ_PATH}} Calculated module csproj path: <modules_base_path>/<modulename>/<ModuleName>.csproj src/IoTEdgeModules/modules/dataprocessormodule/DataProcessorModule.csproj
{{MODULE_PUBLISH_PATH}} Calculated publish path src/IoTEdgeModules/modules/dataprocessormodule
{{CONTRACTS_PROJECT_REFERENCE}} Conditional contracts reference See below
{{CONTRACTS_CSPROJ_COPY}} Conditional Dockerfile COPY See below
{{NUGET_CONFIG_SECTION}} Conditional NuGet configuration See below

Conditional placeholder handling:

A. Contracts Project Reference ({{CONTRACTS_PROJECT_REFERENCE}})

Calculate relative path from module directory to contracts directory:

Example:

  • Module at: src/IoTEdgeModules/modules/mydemomodule/
  • Contracts at: src/Company.IoT.Modules.Contracts/
  • Relative path: ../../../Company.IoT.Modules.Contracts
    • Go up 3 levels: mydemomodule/modules/IoTEdgeModules/src/
    • Then down to: Company.IoT.Modules.Contracts/

If using shared contracts:

  <ItemGroup>
    <ProjectReference Include="<relative-path-to-contracts>/<contracts_project_name>.csproj" />
  </ItemGroup>

If NOT using shared contracts:

  <!-- No shared contracts project -->

B. Contracts Dockerfile COPY ({{CONTRACTS_CSPROJ_COPY}})

If using shared contracts:

COPY <contracts_project_path>/*.csproj ./src/

If NOT using shared contracts:

(empty - no COPY line)

C. NuGet Configuration ({{NUGET_CONFIG_SECTION}})

If using private NuGet feed:

ENV VSS_NUGET_EXTERNAL_FEED_ENDPOINTS="{\"endpointCredentials\": [{\"endpoint\":\"<nuget_feed_url>\", \"username\":\"docker\", \"password\":\"${FEED_ACCESSTOKEN}\"}]}"

If NOT using private NuGet feed:

(empty - no ENV line)

Template file mappings:

Template File Target File Notes
template.csproj <ModuleName>.csproj Rename to match ModuleName
template-module.json module.json -
template-Program.cs Program.cs -
template-Service.cs <ModuleName>Service.cs Rename to match ModuleName
template-GlobalUsings.cs GlobalUsings.cs -
template-ServiceLoggerMessages.cs <ModuleName>ServiceLoggerMessages.cs Rename to match ModuleName
template-Dockerfile.amd64 Dockerfile.amd64 -
template-Dockerfile.amd64.debug Dockerfile.amd64.debug -
template-.dockerignore .dockerignore -
template-.gitignore .gitignore -
template-launchSettings.json Properties/launchSettings.json Create Properties/ first

Processing workflow:

For each template file listed in the table above, process sequentially:

  1. Read the template file from assets/
  2. Replace all placeholders with calculated values
  3. Write to target location in module directory using the target filename from the table
  4. Report progress: "✓ Created "

Process all 11 files one at a time before proceeding to Step 6.

Step 6: Create Shared Contract Constants (Conditional)

If using shared contracts project:

Directory: <contracts_project_path>/<ModuleName>/

File: <ModuleName>Constants.cs

Process:

  1. Create directory if it doesn't exist
  2. Read template-ModuleConstants.cs
  3. Replace placeholders
  4. Write to contracts project location

If NOT using shared contracts:

Directory: <modules_base_path>/<modulename>/Contracts/

File: <ModuleName>Constants.cs

Process:

  1. Create Contracts/ folder in module directory
  2. Read template-ModuleConstants.cs
  3. Replace {{PROJECT_NAMESPACE}}.Modules.Contracts with just {{ModuleName}}.Contracts
  4. Write to module's Contracts folder

Step 6.5: Create LoggingBuilderExtensions (First Module Only)

This extension method is required for AddModuleConsoleLogging() in Program.cs.

If using shared contracts project:

Check if <contracts_project_path>/Extensions/LoggingBuilderExtensions.cs exists:

  • If file exists: Skip this step (already created by previous module)
  • If file does NOT exist: Create it

Directory: <contracts_project_path>/Extensions/

File: LoggingBuilderExtensions.cs

Process:

  1. Create Extensions/ directory if it doesn't exist
  2. Read template-LoggingBuilderExtensions.cs
  3. Replace {{PROJECT_NAMESPACE}} placeholder
  4. Write to contracts project Extensions folder
  5. Report to user: "✓ Created LoggingBuilderExtensions.cs in shared contracts project"

If NOT using shared contracts:

Directory: <modules_base_path>/<modulename>/Extensions/

File: LoggingBuilderExtensions.cs

Process:

  1. Create Extensions/ folder in module directory
  2. Read template-LoggingBuilderExtensions.cs
  3. Replace {{PROJECT_NAMESPACE}}.Modules.Contracts with {{ModuleName}}
  4. Write to module's Extensions folder

Step 7: Scan and Select Deployment Manifests

Run the manifest scanning script:

python scripts/scan_manifests.py --root .

Process the output:

  1. Parse JSON to get list of manifest files
  2. If 0 manifests found: Go to Step 7.5 (create base manifest)
  3. If 1 manifest found: Ask "Add module to <manifest_name>? (Yes/No)"
  4. If multiple manifests found: Present selection list

Step 7.5: Handle "No Manifests Found" Scenario

If scan_manifests.py returns 0 manifests:

Prompt user:

No deployment manifests found. This appears to be the first module in the project.

Create a base deployment manifest with this module? (Yes/No)

If user selects No:

  • Skip to Step 9 (README update)
  • Inform user: "Module created without deployment manifest. You'll need to create a manifest manually."

If user selects Yes:

  1. Prompt for manifest name:

    Manifest name (default: base): _
    
    • Accept user input or use "base" as default
    • Validate name (alphanumeric, dashes, underscores only)
  2. Create base deployment manifest:

    • Read template: assets/template-base.deployment.manifest.json
    • Determine manifest path: <manifests_base_path>/{name}.deployment.manifest.json
    • If manifests_base_path not detected, use <modules_base_path>/../{name}.deployment.manifest.json
    • Write base manifest to file
  3. Add the new module to the base manifest:

    • Run update script:
      python scripts/update_deployment_manifest.py \
        "<manifest_path>" \
        "<modulename>" \
        --registry "<container_registry>"
      
    • This adds the newly scaffolded module as the first custom module
  4. Report to user:

    ✓ Created base deployment manifest: <manifest_path>
    ✓ Added <modulename> to manifest (startup order: 1)
    

Continue to Step 8 (or Step 9 if no updates needed)

Multi-manifest selection prompt:

Found <count> deployment manifests:

1. <manifest_basename> (<modules_count> modules)
   Path: <manifest_path>

2. <manifest_basename> (<modules_count> modules)
   Path: <manifest_path>

Which manifest(s) should include this module?
(Enter numbers separated by commas, or 'all', or 'none')

Step 8: Update Deployment Manifests (Automated)

For each selected manifest, run the update script:

python scripts/update_deployment_manifest.py \
  "<manifest_path>" \
  "<modulename>" \
  --registry "<container_registry>"

Process the output:

  1. Check for "success": true in JSON output
  2. Report to user: "✓ Added to <manifest_name> (startup order: <startup_order>)"
  3. If error: Report error and provide manual fallback instructions

Error handling:

If the script fails (e.g., module already exists, invalid JSON):

  1. Show error message from script
  2. Provide manual instructions:
Manual update required for <manifest_path>:

Add to $edgeAgent.properties.desired.modules:
{
  "<modulename>": {
    "version": "1.0",
    "type": "docker",
    "status": "running",
    "restartPolicy": "always",
    "startupOrder": <next-order>,
    "settings": {
      "image": "${MODULES.<modulename>}",
      "createOptions": {
        "HostConfig": {
          "LogConfig": {
            "Type": "json-file",
            "Config": {
              "max-size": "10m",
              "max-file": "10"
            }
          },
          "Mounts": [{
            "Type": "volume",
            "Target": "/app/data/",
            "Source": "<modulename>"
          }]
        }
      }
    }
  }
}

Add to $edgeHub.properties.desired.routes:
{
  "<modulename>ToIoTHub": {
    "route": "FROM /messages/modules/<modulename>/outputs/* INTO $upstream",
    "priority": 0,
    "timeToLiveSecs": 86400
  }
}

Step 9: Update README.md (Optional)

Search for "Solution project overview" or "IoTEdge modules" section in README.md:

If section exists:

  • Add entry: - **<modulename>** (\<module_path>`) - `
  • Insert alphabetically

If section doesn't exist:

  • Ask user: "README.md section not found. Create it? (Yes/No)"

Step 9.5: Add Module to Solution File

Detect solution file:

Run the solution detection script:

python scripts/manage_solution.py --root . --detect

Process detection results:

If .slnx file found:

Automatically add module to solution:

python scripts/manage_solution.py \
  --root . \
  --add-module "<module_csproj_path>" \
  --module-name "<ModuleName>"
  • Parse JSON output
  • If action: "added": Report "✓ Added to solution at position <insertion_index>"
  • If action: "already_exists": Report "Module already in solution"
  • If action: "error": Show error and continue

If .sln file found:

  • Run manual instruction generator:
    python scripts/manage_solution.py \
      --root . \
      --add-module "<module_csproj_path>" \
      --module-name "<ModuleName>"
    
  • Parse JSON output and display instructions field to user
  • Recommend using: dotnet sln add "<module_csproj_path>"

If no solution file found:

  • Skip this step
  • Inform user: "No solution file found. Module created successfully without solution integration."

Step 10: Provide Summary and Next Steps

Summary of created files:

✓ Module scaffolding complete!

Created:
• Module directory: <module_full_path>/ (11 files)
• Constants file: <constants_full_path>
• LoggingBuilderExtensions: <"Created" or "Already exists - skipped">
• Updated manifests: <manifest_count> manifest(s) [or "Created base manifest" if first module]
• Solution integration: <"Added to .slnx" or "Manual instructions provided" or "Skipped">

Configuration:
• Container registry: <container_registry>
• Project namespace: <project_namespace>
• NuGet feed: <nuget_feed_url or "None">
• Shared contracts: <"Yes" or "No">

Next steps for the user:

  1. Implement module logic:

    • Edit <ModuleName>Service.cs
    • Add business logic in ExecuteAsync()
    • Register direct method handlers if needed
  2. Add dependencies (if needed):

    • Update <ModuleName>.csproj with additional NuGet packages
    • Add service registrations in Program.cs
  3. Configure module (if needed):

    • Create options classes in Options/ folder
    • Add environment variables to deployment manifest:
      "env": {
        "MyOptions__Setting": { "value": "value" }
      }
      
  4. Test locally:

    • Use Properties/launchSettings.json for standalone mode
    • Run: dotnet run --project <module_path>
    • Module runs with mock IoT Hub client
  5. Add direct methods (if needed):

    • Add method names to <ModuleName>Constants.cs
    • Register handlers in service:
      await moduleClient.RegisterMethodHandlerAsync(
          YourModuleConstants.DirectMethodName,
          HandleMethodAsync,
          stoppingToken);
      
  6. Customize routing (if needed):

    • Default route: Module → IoT Hub ($upstream)
    • For module-to-module: Update route to use BrokeredEndpoint
    • Edit in deployment manifest
  7. Build and deploy:

    • Build module Docker image
    • Push to container registry
    • Deploy manifest to IoT Hub

File paths for quick reference:

  • Module source: <module_full_path>/
  • Constants: <constants_full_path>
  • Deployment manifest(s): <manifest_paths>

Reference Documentation

For detailed information, see reference files:

  • references/module-structure.md - Complete module structure reference
  • references/deployment-manifests.md - Deployment manifest reference

Load these when:

  • User asks about module structure details
  • User needs help with deployment manifest configuration
  • Debugging scaffolding issues
  • Understanding naming conventions

Common Customizations

After scaffolding, users may want to:

1. Add Quartz scheduler support:

  • Add NuGet package Quartz
  • Register: services.AddQuartz()
  • Create Jobs/ folder with IJob implementations

2. Add configuration options:

  • Create Options/ folder
  • Define option classes
  • Register: services.Configure<MyOptions>(hostContext.Configuration)
  • Set via env vars in deployment manifest

3. Add module-to-module routing:

  • Update route in deployment manifest:
    "route": "FROM /messages/modules/source/* INTO BrokeredEndpoint(\"/modules/target/inputs/input1\")"
    

4. Add host binds (replace volume mounts):

  • Update deployment manifest createOptions:
    "Binds": ["/host/path/:/container/path/"]
    

5. Add privileged access (for device access like TPM):

  • Update deployment manifest createOptions.HostConfig:
    "Privileged": true
    

6. Remove volume mount (for stateless modules):

  • Delete Mounts section from deployment manifest

Error Handling

Module directory exists:

  • Prompt: "Overwrite/Rename/Cancel"
  • If Overwrite: Delete existing directory first
  • If Rename: Go back to Step 3 with new name

Manifest update fails:

  • Show error from Python script
  • Provide manual update instructions
  • Continue with other manifests

Detection fails:

  • Fall back to manual prompts for each value
  • Offer to save configuration for future runs

Missing Python:

  • If Python not available, provide manual instructions for all steps
  • Skip automated manifest updates, provide JSON templates

Advanced: Configuration File Format

Saved configuration (.claude/.iot-edge-module-config.json):

{
  "config_source": "detected",
  "modules_base_path": "src/IoTEdgeModules/modules",
  "contracts_project_path": "src/Company.Modules.Contracts",
  "contracts_project_name": "Company.Modules.Contracts",
  "manifests_base_path": "src/IoTEdgeModules",
  "project_namespace": "Company.IoT.EdgeAPI",
  "container_registry": "myregistry.azurecr.io",
  "nuget_feed_url": null,
  "has_contracts_project": true,
  "has_nuget_feed": false
}

Users can manually edit this file to override auto-detection.

Notes

  • Module directory names: lowercase with "module" suffix (e.g., dataprocessormodule)
  • C# class names: PascalCase with "Module" suffix (e.g., DataProcessorModule)
  • Namespaces: PascalCase, no "module" suffix (e.g., namespace DataProcessorModule;)
  • All modules use non-root user (moduleuser, UID 2000) for security
  • Build context is repo root (contextPath: "../../../")
  • Log rotation: 10MB max size, 10 files
  • Default route: Module outputs → $upstream (IoT Hub)