{"id":51088511,"url":"https://github.com/vakaobr/iac-cartographer","last_synced_at":"2026-06-23T23:30:51.671Z","repository":{"id":360296878,"uuid":"1249403430","full_name":"vakaobr/iac-cartographer","owner":"vakaobr","description":"Fleet-level documentation for your Terraform / IaC estate","archived":false,"fork":false,"pushed_at":"2026-06-02T05:25:10.000Z","size":11726,"stargazers_count":2,"open_issues_count":4,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-02T07:21:05.206Z","etag":null,"topics":["claude","documentation","iac","python","terraform"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/vakaobr.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-25T17:00:13.000Z","updated_at":"2026-06-02T05:25:13.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/vakaobr/iac-cartographer","commit_stats":null,"previous_names":["vakaobr/iac-cartographer"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/vakaobr/iac-cartographer","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vakaobr%2Fiac-cartographer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vakaobr%2Fiac-cartographer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vakaobr%2Fiac-cartographer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vakaobr%2Fiac-cartographer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vakaobr","download_url":"https://codeload.github.com/vakaobr/iac-cartographer/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vakaobr%2Fiac-cartographer/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34711176,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-23T02:00:07.161Z","response_time":65,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["claude","documentation","iac","python","terraform"],"created_at":"2026-06-23T23:30:51.284Z","updated_at":"2026-06-23T23:30:51.654Z","avatar_url":"https://github.com/vakaobr.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cpicture\u003e\n    \u003csource srcset=\"banner.webp\" type=\"image/webp\"\u003e\n    \u003cimg src=\"banner.png\" alt=\"iac-cartographer banner\" width=\"100%\"\u003e\n  \u003c/picture\u003e\n\u003c/p\u003e\n\n# iac-cartographer\n\n[![CI](https://github.com/vakaobr/iac-cartographer/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/vakaobr/iac-cartographer/actions/workflows/ci.yml)\n[![coverage](https://raw.githubusercontent.com/vakaobr/iac-cartographer/badges/coverage.svg)](https://github.com/vakaobr/iac-cartographer/actions/workflows/ci.yml)\n[![Dependabot](https://img.shields.io/badge/Dependabot-enabled-025E8C?logo=dependabot\u0026logoColor=white)](https://github.com/vakaobr/iac-cartographer/network/updates)\n[![docs](https://img.shields.io/badge/docs-iac--cartographer.andersonleite.me-blue)](https://iac-cartographer.andersonleite.me/)\n[![Changelog](https://img.shields.io/badge/changelog-Keep_a_Changelog-orange)](CHANGELOG.md)\n\n\u003e Fleet-level documentation for your Terraform / IaC estate.\n\n`iac-cartographer` discovers every Terraform repository across your\nconfigured sources (GitLab groups, GitHub orgs, Bitbucket workspaces,\nself-hosted Gitea / Forgejo orgs, or a curated file), extracts\nstructural facts with [`terraform-docs`](https://terraform-docs.io)\n(plus an HCL parser fallback for fields `terraform-docs` strips),\nasks an LLM to write a short purpose summary for each repo, and\npublishes a parent + child page hierarchy to your chosen output\n(Confluence Cloud, Notion, GitHub Wiki, Markdown, HTML, or JSON).\nOptionally layers live workspace info — current run, last\nsuccessful apply, drift, stale-apply alerts — from Terraform Cloud /\nHCP / TFE or Terrakube on top of each rendered page. Pages\nrepublish only when the underlying content changes (banner-SHA\nshort-circuit), so it's safe to run as often as you like.\n\n```\n┌────────────────────────────────────────────┐\n│ Discovery                                  │   GitLab · GitHub · Bitbucket\n│ (concurrent, deduped, deny-list filtered)  │   Gitea/Forgejo · curated file\n└────────────────────┬───────────────────────┘\n                     ▼\n              clone shallow ──► terraform-docs per .tf dir\n                     │\n        ┌────────────┴────────────┐\n        ▼                         ▼\n  required_providers          ┌────────────────────────────────┐\n  parsed from HCL             │ LLM (narrative summary)        │   Bedrock · Anthropic\n        │                     │                                │   Vertex · Azure OpenAI\n        │                     └────────────────────────────────┘   OpenAI · Ollama\n        │                         │\n        └─────────► aggregate ◄───┘\n                        │\n                        ▼\n        ┌────────────────────────────────┐\n        │ Publisher (banner-SHA          │   Confluence (ADF) · Notion\n        │  idempotent republish)         │   GitHub Wiki\n        │                                │   Markdown · HTML · JSON\n        └───────────────┬────────────────┘\n                        ▼\n        ┌────────────────────────────────┐\n        │ Notifications (info/warn/error)│   Slack · Teams · email · SNS\n        │  multi-channel fanout +        │   PagerDuty · Opsgenie · Discord\n        │  per-level filter              │   Slack-incoming · RocketChat\n        │                                │   Mattermost · generic webhook\n        │                                │   stdout/JSONL\n        └────────────────────────────────┘\n```\n\nEvery component on the right of each box is **pluggable**: pick the\ndiscovery sources, LLM backend, publisher, secrets backend, and\nnotification destinations that fit your environment. Mix and match —\nGitHub + Bitbucket discovery, Vertex AI for narratives, Markdown\noutput to a docs repo, Vault for secrets, Slack info + PagerDuty\nerrors.\n\n## Why\n\n* **Self-onboarding for engineers.** A new hire opens one Confluence page and\n  sees the entire IaC estate — what each repo does, which providers, which\n  modules, last commit and author.\n* **Always current.** Re-runs are idempotent and refresh on a schedule of your\n  choosing. The page never lies for long.\n* **Fix-it signals are visible.** Repos missing a `required_providers` block\n  render with a `(not declared)` marker; repos with unpinned versions get\n  `(unpinned)`. The page surfaces problems instead of hiding them.\n* **Cheap.** Single-shot LLM spend per run is typically well under €1 for\n  a small fleet (30-ish repos against Bedrock + Sonnet 4.5 with prompt\n  caching). Run for free against a local Ollama model — the structural\n  inventory is unaffected by which backend renders the narrative.\n\n## Status\n\n`v0.1.x` on PyPI — extracted from a real production deployment, then\nrebuilt around pluggable backends for the public release. Discovery,\nLLM, publisher, secrets, and notifications are all swappable today (see\n[Shipped](#shipped) below for the full matrix). The API surface is\n\"1.0-track but pre-1.0\": a few config keys and CLI flags are being\nlocked before `v1.0`, each with a backward-compatible alias and a\ndeprecation warning, so upgrades within `0.1.x` won't break you.\n\n## Quick start\n\n**Just want to see what it produces?** Clone this repo and run the\nzero-credentials demo — it shallow-clones three small public Terraform\nrepositories and writes the rendered Markdown inventory under\n`./demo-output/`:\n\n```bash\ngit clone https://github.com/vakaobr/iac-cartographer.git\ncd iac-cartographer\npip install -e .\n./examples/demo/run.sh\n# Open demo-output/index.md\n```\n\nSee [`examples/demo/README.md`](examples/demo/README.md) for the demo\nwalkthrough + variations (swap publisher to HTML, plug in a real LLM, …).\n\n**What it looks like published.** Here's a real overview page rendered to\nConfluence — the auto-generated banner, the per-repo inventory table\n(host, providers, resource counts, last commit), and an AI-written\npurpose for each repo:\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/assets/example-output-confluence.png\" alt=\"iac-cartographer overview page rendered in Confluence: an auto-generated Terraform/IaC inventory table with one row per repository\" width=\"100%\"\u003e\n\u003c/p\u003e\n\n**Prefer to see it without running anything?**\n[`examples/demo/sample-output.md`](examples/demo/sample-output.md) is a\ncommitted sample of the rendered Markdown — the overview index, a\nper-repo deep-dive page, and the inline `(unpinned)` / `(not declared)`\nhygiene markers.\n\n---\n\nThe fastest path from zero to a running scaffold:\n\n```bash\npip install iac-cartographer            # or pip install -e . from a checkout\niac-cartographer --init                 # scaffolds config.yaml + .env\n# edit the two files; replace `REPLACE_ME-...` placeholders\nset -a; . ./iac-cartographer.env; set +a\niac-cartographer --once --dry-run --config ./iac-cartographer.config.yaml\n```\n\n`iac-cartographer --init` accepts flags to scaffold for any backend combination:\n\n```bash\niac-cartographer --init \\\n  --secrets-backend env \\                                # or `aws` | `vault`\n  --publisher markdown \\                                 # or `confluence`\n  --llm anthropic \\                                      # or `bedrock` (the scaffolder covers these two; edit by hand for vertex / azure_openai / openai / ollama)\n  --config-path ./iac-cartographer.config.yaml \\\n  --env-path    ./iac-cartographer.env\n```\n\nThe longer-form quick start below explains each piece — every section maps to one or two flags on `--init`.\n\n### 1. Install\n\n```bash\npip install iac-cartographer            # from PyPI (recommended)\n# or from a checkout, for hacking on the source:\npip install -e .\n\n# or as a container image, no Python install needed:\ndocker pull ghcr.io/vakaobr/iac-cartographer:latest\n```\n\nRequirements:\n* Python 3.12+\n* [`terraform-docs`](https://terraform-docs.io) on your PATH\n* A publishing target — either a Confluence Cloud space, a Notion\n  parent page shared with an internal integration, a GitHub repo\n  with the wiki enabled, or a writable directory if you're using\n  the Markdown / HTML / JSON publishers\n* An LLM backend — pick the one your environment already has credentials for:\n  * **`bedrock`** *(default)* — AWS credentials with `bedrock:InvokeModel` on a Claude model\n  * **`anthropic`** — an Anthropic API key (for deployments without Bedrock access)\n  * **`vertex`** — GCP Application Default Credentials with Vertex AI access *(requires `pip install iac-cartographer[gcp]`)*\n  * **`azure_openai`** — Azure OpenAI resource + API key or AAD identity *(requires `pip install iac-cartographer[azure]`)*\n  * **`openai`** — an OpenAI API key, or any OpenAI-compatible gateway *(requires `pip install iac-cartographer[openai]`)*\n  * **`ollama`** — a reachable Ollama server (`http://localhost:11434` by default) — zero auth, zero outbound traffic, zero API spend\n\n### 2. Pre-create the publishing target\n\nThe shape of this step depends on which publisher you picked:\n\n| Publisher | What to pre-create | Notes |\n|---|---|---|\n| **`confluence`** | Empty parent page in your target space (e.g. `DOCS`). Note the numeric page ID from the URL (`/wiki/spaces/DOCS/pages/123456789/...` → `123456789`). | See [Confluence backend](https://iac-cartographer.andersonleite.me/backends/publishers/#confluence) for token scoping. |\n| **`notion`** | Empty Notion page; share it with your internal integration via the Connections menu so the integration can write children. Note the page UUID from the URL. | See [Notion backend](https://iac-cartographer.andersonleite.me/backends/publishers/#notion). |\n| **`github_wiki`** | A GitHub repo with the wiki enabled. Visit the repo's `/wiki` tab once and create any one page to bootstrap `\u003cowner\u003e/\u003crepo\u003e.wiki.git`. | See [GitHub Wiki backend](https://iac-cartographer.andersonleite.me/backends/publishers/#github-wiki). |\n| **`markdown`** / **`html`** / **`json`** | Any writable directory the process can `os.makedirs(..., exist_ok=True)` into. Nothing else. | See [Markdown / HTML / JSON backends](https://iac-cartographer.andersonleite.me/backends/publishers/). |\n\n### 3. Seed credentials\n\nDefault backend is AWS Secrets Manager — for env-var or HashiCorp Vault deployments see **Secrets backends** further down. Logical secret names (used by every backend):\n\n| Secret name | When required | JSON shape |\n|---|---|---|\n| `iac-cartographer/confluence` | when `publisher.kind == \"confluence\"` | `{\"email\": \"bot@example.com\", \"api_token\": \"ATATT...\"}` |\n| `iac-cartographer/notion` | when `publisher.kind == \"notion\"` | `{\"integration_token\": \"secret_...\"}` *(internal-integration token; share parent page with the integration)* |\n| `iac-cartographer/gitlab` | when `discovery.gitlab_group_ids` is non-empty | `{\"token\": \"glpat-...\"}` |\n| `iac-cartographer/github` | when `discovery.github_orgs` is non-empty | `{\"token\": \"ghp_...\"}` |\n| `iac-cartographer/slack` | always | `{\"bot_token\": \"xoxb-...\"}` |\n| `iac-cartographer/anthropic` | only when `llm.backend == \"anthropic\"` | `{\"api_key\": \"sk-ant-...\"}` |\n| `iac-cartographer/azure_openai` | only when `llm.backend == \"azure_openai\"` and `azure_openai_use_aad` is false | `{\"api_key\": \"...\"}` |\n| `iac-cartographer/openai` | only when `llm.backend == \"openai\"` | `{\"api_key\": \"sk-...\"}` |\n| `iac-cartographer/bitbucket` | only when `discovery.bitbucket_workspaces` is non-empty | `{\"access_token\": \"bbat-...\"}` *(or `{\"username\": \"...\", \"app_password\": \"...\"}` for the legacy form)* |\n| `iac-cartographer/gitea` | only when `discovery.gitea_orgs` is non-empty | `{\"token\": \"...\"}` *(Gitea / Forgejo personal-access token — powers both discovery + clone)* |\n| `iac-cartographer/webhook` | only when any `notifications[].kind == \"webhook\"` | `{\"url\": \"https://...\"}` |\n| `iac-cartographer/slack_webhook` | only when any `notifications[].kind == \"slack_webhook\"` | `{\"url\": \"https://...\"}` *(Slack-incoming / RocketChat / Mattermost URL)* |\n| `iac-cartographer/teams` | only when any `notifications[].kind == \"teams\"` | `{\"url\": \"https://...\"}` *(Teams workflow / Office 365 Connector URL)* |\n| `iac-cartographer/email` | only when any `notifications[].kind == \"email\"` | `{\"username\": \"...\", \"password\": \"...\"}` *(SMTP credentials — see provider quirks in docs)* |\n| `iac-cartographer/pagerduty` | only when any `notifications[].kind == \"pagerduty\"` | `{\"routing_key\": \"...\"}` *(per-Service Events API v2 integration key)* |\n| `iac-cartographer/opsgenie` | only when any `notifications[].kind == \"opsgenie\"` | `{\"api_key\": \"...\"}` *(team / integration API key — region-bound)* |\n| `iac-cartographer/discord` | only when any `notifications[].kind == \"discord\"` | `{\"url\": \"https://discord.com/api/webhooks/...\"}` *(per-channel webhook URL)* |\n\nThe `bedrock`, `vertex`, and `ollama` LLM backends are identity-based\n(IAM, GCP Workload Identity, or no auth at all) and don't need a secret.\nThe `sns` and `stdout` notification channels are similarly credential-free\n— SNS via the AWS credential chain, stdout writes to a process stream.\n\nThe Confluence token must be a **legacy unscoped** API token (the plain\n\"Create API token\" form at id.atlassian.com, not \"Create API token with\nscopes\" — the latter requires an installed OAuth app on the workspace).\n\n### 4. Seed the config in AWS SSM Parameter Store\n\n```yaml\n# Path: /iac-cartographer/config (SecureString)\ndiscovery:\n  gitlab_group_ids: [15]                          # GitLab group IDs to scan\n  gitlab_base_url: \"https://gitlab.example.com\"   # omit for gitlab.com\n  github_orgs: [\"acme-org\"]                       # GitHub orgs to scan\n  bitbucket_workspaces: [\"acme\"]                  # Bitbucket workspaces (optional)\n  gitea_orgs: [\"acme\"]                            # Gitea / Forgejo orgs (optional)\n  gitea_base_url: \"https://gitea.example.com\"     # required when gitea_orgs is non-empty\n  # repos_file: \"./repos.yaml\"                    # extra curated source (optional)\n  deny_repos:                                     # glob patterns to skip\n    - \"acme-org/*-archived\"\n    - \"acme-org/examples-*\"\n\nllm:\n  # backend: bedrock (default), anthropic, vertex, azure_openai, openai, ollama\n  backend: \"bedrock\"\n  # Bedrock: inference-profile ID. Other backends use a model name —\n  # see docs/backends/llm.md for the per-backend convention.\n  model_id: \"eu.anthropic.claude-sonnet-4-5-20250929-v1:0\"\n\npublisher:\n  # \"confluence\" (default), \"notion\", \"github_wiki\", \"markdown\", \"html\", or \"json\"\n  kind: \"confluence\"\n\nconfluence:\n  site: \"acme.atlassian.net\"\n  space_key: \"DOCS\"\n  parent_page_id_ssm_path: \"/iac-cartographer/confluence-parent-id\"\n\n# Only used when publisher.kind == \"markdown\"\nmarkdown:\n  output_dir: \"./iac-inventory\"\n\nslack:\n  channel: \"#alerts\"\n```\n\nSee [`examples/config.example.yaml`](examples/config.example.yaml) for the\nfull set of fields with comments.\n\nAlso seed the parent page ID:\n\n```bash\naws ssm put-parameter \\\n  --name \"/iac-cartographer/confluence-parent-id\" \\\n  --value \"123456789\" --type String\n```\n\n### 5. Run it\n\n```bash\n# Dry-run locally (no Confluence writes, no Slack messages, placeholder narratives)\niac-cartographer --once --dry-run --no-bedrock --config /path/to/config.yaml\n\n# Production single shot (reads config from SSM by default)\niac-cartographer --once\n\n# Restrict to a subset of repos\niac-cartographer --once --repos acme-org/main-cluster,acme-org/auth-service\n\n# Or from a file — newline-delimited, `#` comments and blank lines ignored.\niac-cartographer --once --repos @repos.txt\n\n# Override the local publisher output directory for a one-off run without\n# editing the config (markdown / html / json only — ignored for confluence\n# / notion / github_wiki).\niac-cartographer --once --output-dir ./out\n\n# Use a cheaper model for validation\niac-cartographer --once --model eu.anthropic.claude-haiku-4-5-20251001-v1:0\n\n# Compute a between-run diff against a prior JSON-publisher snapshot\n# (3 new, 1 archived, AWS provider bumped, etc.). Pairs with\n# `publisher.kind: json` on the baseline run; the diff prints Markdown\n# to stdout and rides on the end-of-run Slack post.\niac-cartographer --once --diff ./iac-inventory-json\n\n# Lint a single local repo against IaC-hygiene rules (CI-gating friendly).\n# No discovery, no LLM, no publisher — just the extractor + rules.\niac-cartographer --lint ./infra                       # exit 0/2 on undeclared providers\niac-cartographer --lint ./infra --fail-on=warn        # also fail on unpinned versions\niac-cartographer --lint ./infra --format=github       # GitHub Actions annotations\niac-cartographer --lint ./infra --format=json         # machine-readable for CI dashboards\n\n# Pre-flight self-test of the active config (offline, no API calls, sub-second).\n# Checks terraform-docs version, optional deps, discovery, LLM, publisher, notifications.\niac-cartographer --diagnose --config ./config.yaml          # exit 0 ok / 1 warn / 2 fail\niac-cartographer --diagnose --live --config ./config.yaml   # + live reachability (needs creds; LLM probe is cost-safe)\niac-cartographer --diagnose --live --probe-llm --config ./config.yaml  # + one real 1-token LLM completion (opt-in, ~cents)\n```\n\n## How to run it on a schedule\n\nThe CLI is a one-shot — `iac-cartographer --once` runs the whole pipeline once\nand exits. Drop-in deployment scaffolding for the three most common schedulers\nlives under [`examples/runtime/`](examples/runtime/):\n\n| File | Scheduler | When to use |\n|---|---|---|\n| [Helm chart](charts/iac-cartographer/) | Kubernetes `CronJob` (templated) | The recommended path for k8s. Values for schedule, namespace, image tag, secrets backend, resources, workload-identity binding. |\n| [`kubernetes-cronjob.yaml`](examples/runtime/kubernetes-cronjob.yaml) | Kubernetes `CronJob` (raw manifest) | Read-and-copy reference for the raw shape — useful for learning what the Helm chart renders to, or for clusters where Helm isn't available. |\n| [`aws-ecs-fargate/`](examples/runtime/aws-ecs-fargate/) | AWS ECS Fargate + EventBridge Scheduler (Terraform) | The reference deployment — what the project was extracted from. Managed services, IAM identity, ~€1/month for a 50-repo weekly fleet. |\n| [`gcp-cloud-run-job/`](examples/runtime/gcp-cloud-run-job/) | GCP Cloud Run Jobs + Cloud Scheduler (Terraform) | GCP-native batch path. Workload identity, per-second billing. |\n| [`azure-container-apps-job/`](examples/runtime/azure-container-apps-job/) | Azure Container Apps Jobs (Terraform) | Azure-native batch path. AAD / Managed Identity wiring. |\n| [`github-actions.yml`](examples/runtime/github-actions.yml) | GitHub Actions `schedule` | Lightweight setup with no infrastructure to own; secrets live in the GitHub repo settings. |\n| [`cron.sh`](examples/runtime/cron.sh) | Plain `cron` / `systemd-timer` | A single VM you already own. Docker-based, so no Python install needed on the host. |\n\n## Publishing locally instead of Confluence\n\nSix publisher backends ship today — pick with `publisher.kind`:\n\n| Backend | When to use |\n|---|---|\n| `confluence` *(default)* | You already have Confluence; you want the inventory cross-linked with the rest of your wiki. |\n| `notion` | Your team's docs live in Notion. Each repo becomes a sub-page of a configured parent; an Overview sub-page carries the aggregate summary + cross-links. Requires `pip install iac-cartographer[notion]`. |\n| `github_wiki` | Your team's docs surface lives on GitHub already. The inventory becomes Markdown pages git-pushed to `\u003cowner\u003e/\u003crepo\u003e.wiki.git`, browsable at `github.com/\u003cowner\u003e/\u003crepo\u003e/wiki`. Reuses the existing GitHub token. |\n| `markdown` | You run a static-site generator (mkdocs / Hugo / Docusaurus / Jekyll) and want to feed the rendered Markdown into its build. Or you're committing the output to a docs repo so PRs show diffs. |\n| `html` | You want **self-contained HTML files** with no build step — open them directly in a browser, zip-and-email to a stakeholder, upload to S3 + CloudFront / GitHub Pages, print to PDF for an audit. Embedded CSS, no JS, no external fonts. |\n| `json` | You want a **machine-readable feed** for Backstage catalog imports, internal CMDBs, dashboards, or custom drift-detection tooling. `index.json` carries one row per repo + aggregates; per-repo files carry the full inventory. |\n\n### Markdown layout\n\n```\n\u003cmarkdown.output_dir\u003e/\n├── index.md                              # overview / index page\n└── repos/\n    ├── acme-org__main-cluster.md         # one file per discovered repo\n    ├── acme-org__auth-service.md         # full_name slugged with \"__\"\n    └── ...\n```\n\nEach file's first line is `\u003c!-- iac-cartographer-sha: \u003csha\u003e --\u003e`.\n\n### HTML layout\n\n```\n\u003chtml.output_dir\u003e/\n├── index.html\n└── repos/\n    ├── acme-org__main-cluster.html\n    └── ...\n```\n\nEach file's head contains a `\u003cmeta name=\"iac-cartographer-sha\" content=\"...\"\u003e`\ntag. Dark mode is automatic (CSS `prefers-color-scheme`); a `@media print`\nblock tightens the layout when printed.\n\n### JSON layout\n\n```\n\u003cjson.output_dir\u003e/\n├── index.json                            # overview + aggregates\n└── repos/\n    ├── acme-org__main-cluster.json       # full RepoInventory per repo\n    └── ...\n```\n\n`index.json` is sized for catalog-import use cases — a single fetch returns one row per repo with summary fields (`full_name`, `host`, `providers`, `environments`, `purpose`, `child_document` pointer, …) plus `aggregates.{repo_count,total_resources,top_providers}` for dashboards. Per-repo files carry the full Pydantic-serialised inventory. Top-level `iac_cartographer.sha` field carries the banner SHA.\n\nAll six publishers share the same banner-SHA idempotency contract: on\nthe next run we compare the embedded SHA against the freshly-computed\nvalue and skip the write when they match. Repos that change get\nrewritten; repos that don't, don't. Each publisher carries the SHA in\na backend-native location — HTML comment, ADF version-string, JSON\nfield, Notion callout block — but the comparison logic is shared.\n\n## Discovery sources\n\nEach non-empty field under `discovery:` activates one repository source.\nThey all run concurrently, the orchestrator dedupes by `full_name`\n(first-seen wins), then `deny_repos` glob patterns are applied to the\nmerged result.\n\n| Source | Activates when | What it does |\n|---|---|---|\n| GitLab | `gitlab_group_ids` non-empty | Blob-search `extension:tf` across each group (incl. subgroups). |\n| GitHub | `github_orgs` non-empty | Code-search `extension:tf` across each org. |\n| Bitbucket Cloud | `bitbucket_workspaces` non-empty | Enumerate every repo in each workspace. *(Bitbucket Cloud has no public code-search on free plans — narrow large workspaces with `deny_repos`.)* |\n| Gitea / Forgejo | `gitea_orgs` non-empty | Enumerate every repo in each org via `/api/v1/orgs/{org}/repos`. One source covers both platforms (Forgejo preserves Gitea API compat). `gitea_base_url` is required — every deployment is self-hosted. |\n| Curated file | `repos_file` set | Load a YAML/JSON list of `RepoMetadata` records from disk. Useful for air-gapped runs, self-hosted VCS without a first-party source (Codeberg uses the Gitea API so `gitea_orgs` works too; Sourcehut / others go via file), or to pin a focused subset. See [`examples/repos.example.yaml`](examples/repos.example.yaml) for the schema. |\n\nMix and match: configure GitLab + a curated file, or Bitbucket-only, or all five together. At least one source must be configured (the orchestrator fails loud if none are).\n\n## Secrets backends\n\n`secrets.backend` picks where credentials + opaque parameters (the\nConfluence parent page ID, etc.) come from. Three backends ship today:\n\n| Backend | Secrets from | Parameters from | When to use |\n|---|---|---|---|\n| `aws` *(default)* | AWS Secrets Manager | SSM Parameter Store | Production deployments on AWS — what the original deployment uses. |\n| `env` | env var `IAC_CARTOGRAPHER_SECRET_\u003cNAME\u003e` (JSON) | env var `IAC_CARTOGRAPHER_PARAM_\u003cNAME\u003e` (plain) | CI/GitHub Actions, k8s with the secrets injected as env vars, local dev. Optional `.env` autoload. |\n| `vault` | HashiCorp Vault KV v2 at `{mount}/data/{prefix}{name}` | Same path, payload must contain a `value` field | Multi-cloud / on-prem / regulated environments where Vault is already standard. |\n\nExample `env` backend setup:\n\n```bash\nexport IAC_CARTOGRAPHER_SECRET_CONFLUENCE='{\"email\":\"bot@x.test\",\"api_token\":\"ATATT...\"}'\nexport IAC_CARTOGRAPHER_SECRET_GITLAB='{\"token\":\"glpat-...\"}'\nexport IAC_CARTOGRAPHER_SECRET_GITHUB='{\"token\":\"ghp_...\"}'\nexport IAC_CARTOGRAPHER_SECRET_SLACK='{\"bot_token\":\"xoxb-...\"}'\nexport IAC_CARTOGRAPHER_PARAM_CONFLUENCE_PARENT_ID='123456789'\niac-cartographer --once --config /etc/iac-cartographer/config.yaml\n```\n\n`config.yaml` then declares the backend:\n\n```yaml\nsecrets:\n  backend: \"env\"\n  env_dotenv_path: \"/etc/iac-cartographer/.env\"  # optional\n```\n\nVault example:\n\n```yaml\nsecrets:\n  backend: \"vault\"\n  vault_addr: \"https://vault.example.com\"\n  vault_mount: \"secret\"\n  vault_path_prefix: \"iac-cartographer/\"\n```\n\n```bash\nexport VAULT_TOKEN=\"$(vault login -method=oidc -token-only)\"\nvault kv put secret/iac-cartographer/gitlab token=glpat-...\nvault kv put secret/iac-cartographer/confluence-parent-id value=123456789\niac-cartographer --once --config /etc/iac-cartographer/config.yaml\n```\n\nFor the Confluence parent page ID specifically: when storing a non-secret integer in an external parameter store feels like overkill, set `confluence.parent_page_id` directly in the YAML and the parameter-store lookup is skipped entirely.\n\n## Reading the output\n\nOn the published pages (regardless of publisher) you'll see a few\nplaceholders worth knowing:\n\n| Marker | Meaning |\n|---|---|\n| `\u003ccanonical\u003e (not declared)` in Source | The repo provisions this provider without a matching `terraform { required_providers { ... } }` block. The canonical source is inferred from a curated map. **This is a fix-it signal** — modern Terraform fails `terraform init` for any non-Hashicorp namespace lacking the declaration. |\n| `(not declared — unknown to inventory)` in Source | Same as above, except the provider isn't in our curated map. PRs adding new providers welcome. |\n| `(unpinned)` in Version | No `version = \"...\"` constraint declared. Worth pinning. |\n| `(Narrative summary unavailable for this run...)` in Purpose | The LLM backend returned an error, hit a rate limit, or emitted invalid JSON for this repo. Structural facts (providers, resources, modules) are unaffected. Auto-retries once per run. |\n| `:warning: Narrative review needed (AI-H1...)` on Slack | A repo's narrative contained a prompt-injection trigger phrase. Narrative is dropped from the page; structural facts publish unchanged. Inspect the source repo for unusual README content. |\n\n## Roadmap\n\n### Shipped\n\nThe five pluggable seams:\n\n* **Publishers** — Confluence, Notion, GitHub Wiki, local Markdown, standalone HTML, machine-readable JSON.\n* **LLM** — AWS Bedrock, Anthropic API direct, Vertex AI (Claude on GCP), Azure OpenAI (GPT on Azure), OpenAI direct (GPT via api.openai.com / OpenAI-compatible gateways), Ollama (local LLM).\n* **Discovery** — GitLab groups, GitHub orgs, Bitbucket workspaces, Gitea / Forgejo orgs (self-hosted), curated YAML/JSON file.\n* **Secrets** — AWS Secrets Manager + SSM, process env vars (with `.env` autoload), HashiCorp Vault KV v2.\n* **Notifications** — multi-channel dispatcher with per-level routing (info / warn / error). Ten channels: Slack (bot-token + incoming-webhook), Microsoft Teams (Adaptive Card), RocketChat / Mattermost (Slack-compat webhook), generic JSON webhook, email (SMTP via `aiosmtplib`), AWS SNS, PagerDuty (Events API v2), Opsgenie (Alerts API; US + EU), Discord (Incoming Webhook), and stdout/JSONL (CI + air-gapped).\n\nPlus the Phase 3 distribution + onboarding wins:\n\n* **PyPI release workflow** — OIDC trusted publishing, tag-driven, version-match guard. Cuts a release on `git tag v*`.\n* **Container image** — `ghcr.io/vakaobr/iac-cartographer` with cosign keyless signing + SPDX SBOM on every tag push. Multi-arch (`linux/amd64` + `linux/arm64`).\n* **Helm chart** — [`charts/iac-cartographer/`](charts/iac-cartographer/) for k8s CronJob deployments with workload-identity bindings.\n* **`iac-cartographer --init` scaffolder** — interactive starter `config.yaml` + `.env` for any backend combination.\n* **Zero-credentials demo** — `./examples/demo/run.sh` clones three public Terraform repos and produces real Markdown output without any tokens.\n* **Docs site (versioned)** — mkdocs-material at [iac-cartographer.andersonleite.me](https://iac-cartographer.andersonleite.me/). Versioned via [`mike`](https://github.com/jimporter/mike); the header dropdown lets readers switch between `latest`, `dev`, and any tagged release. See [`docs/operations/docs-deploy.md`](docs/operations/docs-deploy.md).\n* **`--diff \u003cprev-output\u003e` mode** — between-run structural diff against a prior JSON-publisher snapshot. Adds / removes / provider bumps / module bumps / resource-count deltas. Prints Markdown to stdout and rides on the end-of-run Slack post as a one-liner (`3 new, 1 archived, 2 changed; 37 unchanged`). See [`docs/operations/diff.md`](docs/operations/diff.md).\n* **`iac-cartographer --lint \u003cpath\u003e` subcommand** — IaC hygiene linter (undeclared providers, unpinned providers / modules) with text / JSON / GitHub-Actions-annotation output. Ships a `.pre-commit-hooks.yaml` for pre-commit users. CI-gating-friendly exit codes. See [`docs/operations/lint.md`](docs/operations/lint.md).\n* **`iac-cartographer --diagnose` pre-flight self-test** — offline checklist over the active config: `terraform-docs` version, optional-deps for the configured backends, discovery sources, LLM config consistency, publisher write target, notification routing. No live API calls; sub-second; CI-gating exit codes (0 ok / 1 warn / 2 fail). Add `--live` to also verify real reachability (fetch the required secret bundle, authenticate each discovery source, probe the LLM endpoint, reach the publisher target) — needs credentials, and the LLM probe stays cost-safe (never runs a completion). Add `--probe-llm` on top of `--live` for one opt-in bounded `max_tokens=1` completion (true inference confidence, a fraction of a cent of real spend). See [`docs/operations/diagnose.md`](docs/operations/diagnose.md).\n* **Observability for non-AWS deployments** — opt-in structured JSON logging (`IAC_CARTOGRAPHER_LOG_FORMAT=json`) and an optional OpenTelemetry metrics exporter (`pip install iac-cartographer[otel]` + an OTLP endpoint env var) emitting run / per-repo-duration / LLM-token / publish-outcome signals. Both default-off; the existing CloudWatch path is unchanged. See [`docs/operations/observability.md`](docs/operations/observability.md).\n* **Live-state overlay** — read-only `LiveStateOverlay` protocol with two implementations: **Terraform Cloud / HCP / Terraform Enterprise** ([#98](https://github.com/vakaobr/iac-cartographer/issues/98)) and **Terrakube** ([#99](https://github.com/vakaobr/iac-cartographer/issues/99)). When configured, each rendered child page gains a Live-state section (workspace name + URL, current run status, last successful apply, drift, live resource count). Free sub-feature: a `warn`-level **stale failed-apply alert** for any workspace stuck in an errored / failed state past a configurable threshold (default 2 days) — routed through the same `notifications:` dispatcher and dispatched once per run. Default backend is `none` so existing deployments see zero change. See [`docs/reference/configuration.md#live_state`](docs/reference/configuration.md#live_state) and [`docs/backends/terrakube.md`](docs/backends/terrakube.md).\n\n### Coming next\n\nOpen follow-ups, roughly ordered by user-impact / effort ratio. Issues welcome on any of these — pick one and open one to claim it before sending a PR.\n\n* **Lazy secret loading.** The `gitlab` / `github` / `slack` secrets are fetched eagerly on every run regardless of which sources / publishers are active. Make them lazy so a Markdown-only, GitHub-only deployment doesn't need a Slack secret present. (See the note in [`docs/backends/secrets.md`](docs/backends/secrets.md).)\n* **`--diagnose --live` cost-aware LLM probe.** The live LLM check currently stops at client construction (cost-safe but shallow) for the non-Ollama backends. An opt-in `--diagnose --live --probe-llm` that runs a 1-token completion would give true end-to-end confidence at a few cents — gated behind an explicit flag so it never surprises anyone with spend.\n* **1.0 API freeze.** Before tagging `v1.0`, do a deliberate pass over the YAML config schema + CLI surface to lock names that are still \"1.0-track but pre-1.0\".\n\n### Broader IaC support — living up to the name\n\nRight now the tool is named **iac-cartographer** but only really understands Terraform / OpenTofu. The items below trace the path from \"Terraform inventory done well\" to \"actually IaC-shaped\". They're ordered by ROI inside each block; items in the same block can be tackled in parallel.\n\n**Quick wins, no new language to parse:**\n\n* **State backend reporting.** Parse `terraform { backend \"...\" { ... } }` per repo and surface, on the rendered page: backend type (`s3` / `gcs` / `azurerm` / `remote` / `local`), encryption flag, locking config, key path, region. Turns the inventory into a posture tool, not just a catalogue — flagging an S3 backend without encryption, or a DynamoDB-locking gap, is a real security signal infra teams act on. Fits inside the existing extractor; no new dependency.\n* **Mermaid resource-dependency graph.** Generate a Mermaid diagram from the existing `ResourceRef` list + provider dependencies and embed it in the rendered page. Confluence (with the official Mermaid macro) and GitHub markdown both render Mermaid natively, so no headless renderer is needed. Chunk above ~25 nodes — full DAGs for a 200-resource module are unreadable. PNG / SVG output is deliberately out of scope until someone needs it (the dependency cost of headless renderers isn't worth the marginal value over inline Mermaid).\n* **Terragrunt.** Discovery walks `terragrunt.hcl` files, then reuses the existing Terraform pipeline (one `terragrunt render-json` call per stack, fall back to HCL parsing if the binary is absent). Lowest-effort multi-tool win; validates the \"multi-IaC\" claim that the name implies without inventing a new page template.\n\n**Different paradigms, different page templates:**\n\n* **Ansible.** Different shape from Terraform — no resource graph; instead: hosts / groups targeted, role + task summary, packages / services managed, secrets touched. The interesting question is **Ansible Vault**: surface the **key names** present in each vault file (via `ansible-vault view` against a decrypt key mounted as a CI secret) without ever logging or rendering values. Different page template entirely. Worth doing once one person actively asks for it.\n\n**Considered, not currently planned:**\n\n* **Pulumi.** Multi-language (TS / Python / Go / .NET); proper static analysis requires executing user code via `pulumi preview --json`. Large effort, security headaches, and a much narrower user base than the items above. Open to it if someone shows up with a concrete use case + a willingness to maintain the parser.\n* **Puppet / Chef / Salt.** Declining adoption (mostly legacy fleets). Low ROI vs. effort. Open to PRs but unlikely to be a maintainer priority.\n\n## Contributing\n\nIssues and PRs welcome. The codebase is intentionally small and well-tested\n(see the coverage badge above); pick a roadmap item or open an issue describing the\nshape of the change before sending a PR for anything non-trivial.\n\n### Looking for collaborators\n\nSeveral of the roadmap items above need code from people who actually use the relevant tool day-to-day. The maintainer's home turf is Terraform / OpenTofu — the closer an item gets to \"different paradigm\", the more it benefits from a contributor with a live environment to test against.\n\nConcrete examples where help is especially welcome:\n\n* **Terragrunt** ([#96](https://github.com/vakaobr/iac-cartographer/issues/96)) — the maintainer has no Terragrunt in their own infrastructure, so even a clean implementation can't be validated end-to-end without a contributor's live monorepo.\n* **Ansible** ([#97](https://github.com/vakaobr/iac-cartographer/issues/97)) — different page shape, Vault key surfacing has real safety constraints; someone running Ansible in production today would catch design holes the maintainer wouldn't.\n* **Pulumi** (deliberately not on the active roadmap) — open if you'd own the parser long-term.\n\nWhat \"collaboration\" means here:\n\n* Open a discussion on the issue before writing code — design feedback is cheaper than rewrites.\n* Send a small first PR (test fixture, doc clarification, scaffold) before the big one — it builds review trust.\n* If you're using the tool against your own infrastructure, \"I tried X on a real repo, it produced Y\" is genuinely valuable signal even without a code change.\n\nPinging [`good first issue`](https://github.com/vakaobr/iac-cartographer/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) is the cleanest way in — those entries are scoped to one self-contained change.\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvakaobr%2Fiac-cartographer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvakaobr%2Fiac-cartographer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvakaobr%2Fiac-cartographer/lists"}