Documentation

Complete reference for using tfplan2md in your Terraform workflow.

CLI Reference

Basic Usage

Syntax
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

-o, --output <file>
Write output to a file instead of stdout.
-t, --template <name|file>
Use a built-in template by name (default, summary) or a custom Scriban template file path.
--report-title <title>
Override the report title (level-1 heading) with a custom value.
-p, --principal-mapping <file>
Map Azure principal IDs (GUIDs) to human-readable names using a JSON file. Applies to azurerm_role_assignment resources.
--render-target <platform>
Target platform for rendering:
azuredevops (default, alias: azdo) — Styled HTML with line-by-line and character-level diff highlighting
github — Traditional diff format with +/- markers
Note: Replaces deprecated --large-value-format flag.
--show-unchanged-values
Include unchanged attribute values in tables. By default, only changed attributes are shown.
--show-sensitive
Show sensitive values unmasked. By default, sensitive values are masked as 🔒 (sensitive value).
--hide-metadata
Suppress tfplan2md version and generation timestamp from the report header.
--debug
Append diagnostic information to the report for troubleshooting. Includes principal mapping diagnostics (load status, principal counts, failed ID resolutions) and template resolution details.
-h, --help
Display help message.
-v, --version
Display version information.

Usage Examples

From stdin
terraform show -json plan.tfplan | tfplan2md
From file
tfplan2md plan.json
With principal mapping
tfplan2md --principal-mapping principals.json plan.json
With custom report title
tfplan2md plan.json --report-title "Drift Detection - Production"
GitHub-friendly rendering
tfplan2md plan.json --render-target github
With output file and custom template
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:

Usage
tfplan2md plan.json --template path/to/custom-template.sbn

Template Data Model: Your template receives a plan object with the following structure:

Template Variables
{{ 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 Reference

Reference 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:

principals.json
{
  "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
generate-principals.sh
#!/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-Principals.ps1
# 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:

Bash - Filter by IDs
# 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.

Usage
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 - Mount from current directory
docker run -v $(pwd):/data oocx/tfplan2md \
  --principal-mapping /data/principals.json \
  /data/plan.json --output /data/plan.md
Docker - Mount as read-only
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 - With debug output
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.

Output Example
<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.

Output Example
```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, not terraform plan
  • Verify the plan file exists and is a valid Terraform plan
  • Check that stdin is not empty when piping from terraform
Correct usage
# 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 default or summary (no file extension)

⚠️ Sensitive values still visible

Sensitive values are masked by default unless --show-sensitive is used.

Solution:

  • Remove --show-sensitive flag from your command
  • Mark attributes as sensitive = true in 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?