CLI Reference
Basic Usage
tfplan2md [options] [input-file]
terraform show -json plan.tfplan | tfplan2md
Arguments:
input-file— Path to the Terraform plan JSON file. If omitted, reads from stdin.
Options
Usage Examples
terraform show -json plan.tfplan | tfplan2md
tfplan2md plan.json
tfplan2md --principal-mapping principals.json plan.json
tfplan2md plan.json --report-title "Drift Detection - Production"
tfplan2md plan.json --render-target github
tfplan2md plan.json --output plan.md --template my-template.sbn
Template Customization
tfplan2md uses Scriban templates to generate Markdown reports. You can create custom templates to match your team's reporting needs.
Built-in Templates
default
Full report with summary, resource changes grouped by module, and attribute details. Shows exactly what will change.
--template default
summary
Compact overview showing only action counts and resource type breakdown. Perfect for PR titles or large plans.
--template summary
Custom Templates
Create a custom Scriban template (.sbn file) and pass it with the --template option:
tfplan2md plan.json --template path/to/custom-template.sbn
Template Data Model: Your template receives a plan object with the following structure:
{{ plan.terraform_version }} # Terraform version (string)
{{ plan.format_version }} # Plan format version (string)
{{ plan.resource_changes }} # Array of resource change objects
{{ plan.output_changes }} # Array of output change objects
# Resource change object properties
{{ change.address }} # Full resource address (string)
{{ change.module_address }} # Module path (string or null)
{{ change.type }} # Resource type (string)
{{ change.name }} # Resource name (string)
{{ change.action }} # "create", "update", "delete", "replace"
{{ change.before }} # Before values (object or null)
{{ change.after }} # After values (object or null)
{{ change.after_unknown }} # Unknown attributes (object)
{{ change.before_sensitive }} # Sensitive markers (object)
{{ change.after_sensitive }} # Sensitive markers (object)
Scriban Helper Functions
tfplan2md provides custom Scriban functions to help with common formatting tasks:
inline_diff
Generate inline diffs showing added (+), removed (-), and unchanged items in collections.
{{ inline_diff before.rules after.rules "name" }}
format_azure_id
Format long Azure resource IDs into readable, multi-line scoped paths.
{{ format_azure_id change.after.id }}
format_bool
Format boolean values as ✅ (true) or ❌ (false) icons.
{{ format_bool change.after.enabled }}
format_large_value
Format large values (>1000 chars) with inline diff or collapsible details.
{{ format_large_value before.policy after.policy }}
icon_* functions
Add semantic icons for common value types: icon_ip, icon_port, icon_protocol, icon_principal.
{{ icon_ip rule.source_address }}
get_principal_name
Resolve Azure principal IDs to friendly names (requires mapping file).
{{ get_principal_name change.after.principal_id }}
📚 Scriban Reference
For the complete Scriban language syntax (loops, conditionals, filters, built-in functions), see the official documentation:
Scriban Language ReferenceReference the built-in templates in the repository for examples.
Principal Mapping File
Map Azure principal IDs to human-readable names for better role assignment reports. Create a JSON file with this structure:
{
"users": {
"12345678-1234-1234-1234-123456789012": "jane.doe@contoso.com",
"87654321-4321-4321-4321-210987654321": "john.smith@contoso.com"
},
"groups": {
"abcdef12-3456-7890-abcd-ef1234567890": "Platform Team",
"fedcba98-7654-3210-fedc-ba9876543210": "Security Team"
},
"servicePrincipals": {
"11111111-2222-3333-4444-555555555555": "terraform-spn",
"66666666-7777-8888-9999-000000000000": "github-actions-spn"
}
}
Generating Mapping Files with Azure CLI
Use Azure CLI to automatically generate principal mapping files from your Azure AD tenant:
Bash Script
#!/bin/bash
# Generate principal mapping file for tfplan2md
echo '{'
echo ' "users": {'
# Fetch users
az ad user list --query '[].{id:id,upn:userPrincipalName}' -o json | \
jq -r '.[] | " \"" + .id + "\": \"" + .upn + "\""' | \
paste -sd ',' -
echo ' },'
echo ' "groups": {'
# Fetch groups
az ad group list --query '[].{id:id,name:displayName}' -o json | \
jq -r '.[] | " \"" + .id + "\": \"" + .name + "\""' | \
paste -sd ',' -
echo ' },'
echo ' "servicePrincipals": {'
# Fetch service principals
az ad sp list --all --query '[].{id:id,name:displayName}' -o json | \
jq -r '.[] | " \"" + .id + "\": \"" + .name + "\""' | \
paste -sd ',' -
echo ' }'
echo '}'
Run: bash generate-principals.sh > principals.json
PowerShell Script
# Generate principal mapping file for tfplan2md
$mapping = @{
users = @{}
groups = @{}
servicePrincipals = @{}
}
# Fetch users
Write-Host "Fetching users..." -ForegroundColor Cyan
$users = az ad user list --query '[].{id:id,upn:userPrincipalName}' | ConvertFrom-Json
foreach ($user in $users) {
$mapping.users[$user.id] = $user.upn
}
# Fetch groups
Write-Host "Fetching groups..." -ForegroundColor Cyan
$groups = az ad group list --query '[].{id:id,name:displayName}' | ConvertFrom-Json
foreach ($group in $groups) {
$mapping.groups[$group.id] = $group.name
}
# Fetch service principals
Write-Host "Fetching service principals..." -ForegroundColor Cyan
$sps = az ad sp list --all --query '[].{id:id,name:displayName}' | ConvertFrom-Json
foreach ($sp in $sps) {
$mapping.servicePrincipals[$sp.id] = $sp.name
}
# Output JSON
$mapping | ConvertTo-Json -Depth 3
Run: .\Generate-Principals.ps1 > principals.json
Filtering for Specific Principals
For large tenants, you may want to filter for specific principals instead of fetching all:
# Extract principal IDs from Terraform plan
PRINCIPAL_IDS=$(terraform show -json plan.tfplan | \
jq -r '.resource_changes[]? |
select(.type == "azurerm_role_assignment") |
.change.after.principal_id' | sort -u)
# Create mapping for only those principals
echo '{"users":{},"groups":{},"servicePrincipals":{}}' > principals.json
for id in $PRINCIPAL_IDS; do
# Try as user
az ad user show --id "$id" 2>/dev/null | \
jq -r '{users: {($id): .userPrincipalName}}' || \
# Try as group
az ad group show --group "$id" 2>/dev/null | \
jq -r '{groups: {($id): .displayName}}' || \
# Try as service principal
az ad sp show --id "$id" 2>/dev/null | \
jq -r '{servicePrincipals: {($id): .displayName}}'
done | jq -s 'reduce .[] as $item ({}; . * $item)' > principals.json
Use with the --principal-mapping option to replace GUIDs with names in role assignment reports.
tfplan2md plan.json --principal-mapping principals.json
Using Principal Mapping with Docker
When running tfplan2md in a Docker container, you need to mount the principals.json file into the container:
docker run -v $(pwd):/data oocx/tfplan2md \
--principal-mapping /data/principals.json \
/data/plan.json --output /data/plan.md
docker run \
-v $(pwd)/plan.json:/data/plan.json:ro \
-v $(pwd)/principals.json:/app/principals.json:ro \
oocx/tfplan2md --principal-mapping /app/principals.json /data/plan.json
docker run -v $(pwd):/data oocx/tfplan2md --debug \
--principal-mapping /data/principals.json \
/data/plan.json --output /data/plan.md
Render Targets
The --render-target flag controls platform-specific rendering behavior. Attributes with newlines or over 100 characters are automatically moved to collapsible sections below the main table.
azuredevops (default)
Styled HTML with line-by-line and character-level diff highlighting. Optimized for Azure DevOps PR comments. GitHub strips styles but content remains readable.
<pre style="font-family: monospace; line-height: 1.5;"><code>#!/bin/bash
<span style="background-color: #fff5f5; color: #24292e;">echo "v1.0"</span>
<span style="background-color: #f0fff4; color: #24292e;">echo "v2.0"</span>
apt-get update
</code></pre>
--render-target azuredevops or --render-target azdo
github
Traditional diff format with +/- markers. Fully portable and works on both GitHub and Azure DevOps.
```diff
#!/bin/bash
- echo "v1.0"
+ echo "v2.0"
apt-get update
```
--render-target github
Migration note: The --large-value-format flag has been deprecated and replaced by --render-target.
Use --render-target azuredevops for inline-diff behavior or --render-target github for standard-diff behavior.
Troubleshooting
❌ "No valid JSON detected in input"
This error occurs when tfplan2md receives input that is not valid Terraform JSON.
Solution:
- Ensure you're using
terraform show -json plan.tfplan, notterraform plan - Verify the plan file exists and is a valid Terraform plan
- Check that stdin is not empty when piping from terraform
# Generate plan file
terraform plan -out=plan.tfplan
# Convert to JSON and pipe to tfplan2md
terraform show -json plan.tfplan | tfplan2md
❌ "Template file not found"
This error occurs when using --template with a custom template file that doesn't exist.
Solution:
- Verify the template file path is correct (relative or absolute)
- Check file permissions
- For built-in templates, use
defaultorsummary(no file extension)
⚠️ Sensitive values still visible
Sensitive values are masked by default unless --show-sensitive is used.
Solution:
- Remove
--show-sensitiveflag from your command - Mark attributes as
sensitive = truein Terraform to enable masking - Review your CI/CD configuration to ensure sensitive flags aren't accidentally enabled
⚠️ Docker permission denied
Docker commands fail with permission errors.
Solution:
- Add your user to the docker group:
sudo usermod -aG docker $USER - Log out and back in for group changes to take effect
- Or run with
sudo(not recommended for CI/CD)
💡 Need more help?
- Open an issue on GitHub with your error message and command
- Check GitHub Discussions for community help
- Review examples for working configurations