--- name: iot-edge-module description: 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: ```bash 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: • Project namespace: • Container registry: • Contracts project: () • Deployment manifests: found • NuGet feed: ``` 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":** ```bash 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: • Module ID: • Directory: // ``` **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: ``` // ``` Check if directory already exists (MUST use this exact bash syntax): ```bash test -d "/" && 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: `//.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: ```xml ``` If NOT using shared contracts: ```xml ``` **B. Contracts Dockerfile COPY (`{{CONTRACTS_CSPROJ_COPY}}`)** If using shared contracts: ```dockerfile COPY /*.csproj ./src/ ``` If NOT using shared contracts: ``` (empty - no COPY line) ``` **C. NuGet Configuration (`{{NUGET_CONFIG_SECTION}}`)** If using private NuGet feed: ```dockerfile ENV VSS_NUGET_EXTERNAL_FEED_ENDPOINTS="{\"endpointCredentials\": [{\"endpoint\":\"\", \"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` | `.csproj` | Rename to match ModuleName | | `template-module.json` | `module.json` | - | | `template-Program.cs` | `Program.cs` | - | | `template-Service.cs` | `Service.cs` | Rename to match ModuleName | | `template-GlobalUsings.cs` | `GlobalUsings.cs` | - | | `template-ServiceLoggerMessages.cs` | `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:** `//` **File:** `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:** `//Contracts/` **File:** `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 `/Extensions/LoggingBuilderExtensions.cs` exists: - If file exists: Skip this step (already created by previous module) - If file does NOT exist: Create it **Directory:** `/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:** `//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: ```bash 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 ? (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: `/{name}.deployment.manifest.json` - If `manifests_base_path` not detected, use `/../{name}.deployment.manifest.json` - Write base manifest to file 3. **Add the new module to the base manifest:** - Run update script: ```bash python scripts/update_deployment_manifest.py \ "" \ "" \ --registry "" ``` - This adds the newly scaffolded module as the first custom module 4. **Report to user:** ``` ✓ Created base deployment manifest: ✓ Added to manifest (startup order: 1) ``` **Continue to Step 8 (or Step 9 if no updates needed)** **Multi-manifest selection prompt:** ``` Found deployment manifests: 1. ( modules) Path: 2. ( modules) 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: ```bash python scripts/update_deployment_manifest.py \ "" \ "" \ --registry "" ``` **Process the output:** 1. Check for `"success": true` in JSON output 2. Report to user: "✓ Added to (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 : Add to $edgeAgent.properties.desired.modules: { "": { "version": "1.0", "type": "docker", "status": "running", "restartPolicy": "always", "startupOrder": , "settings": { "image": "${MODULES.}", "createOptions": { "HostConfig": { "LogConfig": { "Type": "json-file", "Config": { "max-size": "10m", "max-file": "10" } }, "Mounts": [{ "Type": "volume", "Target": "/app/data/", "Source": "" }] } } } } } Add to $edgeHub.properties.desired.routes: { "ToIoTHub": { "route": "FROM /messages/modules//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: `- **** (\`\`) - ` - 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: ```bash python scripts/manage_solution.py --root . --detect ``` **Process detection results:** **If .slnx file found:** Automatically add module to solution: ```bash python scripts/manage_solution.py \ --root . \ --add-module "" \ --module-name "" ``` - Parse JSON output - If `action: "added"`: Report "✓ Added to solution at position " - If `action: "already_exists"`: Report "Module already in solution" - If `action: "error"`: Show error and continue **If .sln file found:** - Run manual instruction generator: ```bash python scripts/manage_solution.py \ --root . \ --add-module "" \ --module-name "" ``` - Parse JSON output and display `instructions` field to user - Recommend using: `dotnet sln add ""` **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: / (11 files) • Constants file: • LoggingBuilderExtensions: <"Created" or "Already exists - skipped"> • Updated manifests: manifest(s) [or "Created base manifest" if first module] • Solution integration: <"Added to .slnx" or "Manual instructions provided" or "Skipped"> Configuration: • Container registry: • Project namespace: • NuGet feed: • Shared contracts: <"Yes" or "No"> ``` **Next steps for the user:** 1. **Implement module logic:** - Edit `Service.cs` - Add business logic in `ExecuteAsync()` - Register direct method handlers if needed 2. **Add dependencies (if needed):** - Update `.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: ```json "env": { "MyOptions__Setting": { "value": "value" } } ``` 4. **Test locally:** - Use `Properties/launchSettings.json` for standalone mode - Run: `dotnet run --project ` - Module runs with mock IoT Hub client 5. **Add direct methods (if needed):** - Add method names to `Constants.cs` - Register handlers in service: ```csharp 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: `/` - Constants: `` - Deployment manifest(s): `` ## 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(hostContext.Configuration)` - Set via env vars in deployment manifest **3. Add module-to-module routing:** - Update route in deployment manifest: ```json "route": "FROM /messages/modules/source/* INTO BrokeredEndpoint(\"/modules/target/inputs/input1\")" ``` **4. Add host binds (replace volume mounts):** - Update deployment manifest `createOptions`: ```json "Binds": ["/host/path/:/container/path/"] ``` **5. Add privileged access (for device access like TPM):** - Update deployment manifest `createOptions.HostConfig`: ```json "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`): ```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)