Hamburger Cross Icon
Backstage Cataloger - Lunar Cataloger

Backstage Cataloger

Cataloger Experimental Service Catalog

Sync components and domains from a Backstage software catalog into Lunar. Maps Backstage entities to Lunar catalog with owner, domain, and tags, using the Backstage REST API. Authenticates with a Bearer token or with AWS SigV4 for Backstage APIs behind AWS IAM auth.

Add backstage to your lunar-config.yml:
uses: github://earthly/lunar-lib/catalogers/backstage@v1.0.5

What This Integration Syncs

This integration includes 1 cataloger that sync data from your systems.

Cataloger cron

sync

Fetches entities from the Backstage catalog REST API and writes them to the Lunar catalog. Component entities populate .components with owner, domain, and tags; Domain entities populate .domains with description and owner. Components are keyed by a configurable annotation (defaults to github.com/project-slug) so they line up with components discovered by the github-org cataloger or by repo-based collectors.

Schedule: 0 2 * * *
service catalog backstage components domains ownership sync
Book a demo

How Catalogers Fit into Lunar

Lunar catalogers sync component metadata into your Lunar catalog from external systems or source code. They can run on a schedule or be triggered by code changes to keep your service registry up-to-date.

By automatically discovering components from GitHub organizations, service registries, or by detecting technology usage in source code, catalogers ensure your guardrails apply to all relevant services without manual configuration.

Learn How Lunar Works
1
Catalogers Sync Context This Integration
Sync component metadata from service catalogs, ownership systems, and external APIs
2
Guardrails Engine
Once cataloged, components are automatically analyzed by collectors and evaluated against your guardrails

Example Catalog Entry

This cataloger syncs component metadata into your Lunar catalog. Here's an example of a catalog entry it creates:

{ } catalog entry Catalog JSON
{
  "components": {
    "github.com/acme/payment-api": {
      "owner": "group:default/team-payments",
      "domain": "platform.payments",
      "tags": ["bs-payments", "bs-tier1", "bs-type-service", "bs-lifecycle-production"]
    },
    "github.com/acme/web-app": {
      "owner": "group:default/team-web",
      "domain": "platform.frontend",
      "tags": ["bs-frontend", "bs-type-website", "bs-lifecycle-production"]
    }
  },
  "domains": {
    "platform.payments": {
      "description": "Payment processing and billing",
      "owner": "group:default/platform-leads"
    },
    "platform.frontend": {
      "description": "Customer-facing web surfaces",
      "owner": "group:default/platform-leads"
    }
  }
}

Configuration

Configure this cataloger in your lunar-config.yml.

Inputs

Input Required Default Description
backstage_url Required Base URL of the Backstage instance (e.g. https://backstage.example.com). The cataloger appends `<api_path_prefix>/catalog/entities` to this URL (by default `/api/catalog/entities`). Required.
api_path_prefix Optional /api Path prefix prepended before `/catalog/entities` when calling the Backstage catalog API. Defaults to `/api`, matching the standard Backstage layout (`<backstage_url>/api/catalog/entities`). Set to an empty string (`""`) when the catalog API is mounted at the root — e.g. behind an API gateway that already strips the `/api` hop, where `<backstage_url>/catalog/entities` is the live endpoint and `/api/catalog/entities` returns 403/404. A leading slash is optional and any trailing slash is ignored (`api`, `/api`, `/api/` are equivalent).
entity_kinds Optional Component,Domain Comma-separated list of Backstage entity kinds to sync. Each kind is routed to the appropriate Catalog JSON path: - Component, API, Resource → `.components` - Domain, System → `.domains` Other kinds (User, Group, Location) are ignored.
namespace Optional default Backstage namespace to query. Use `*` to query all namespaces.
component_id_annotation Optional github.com/project-slug Annotation key on a Backstage Component whose value identifies the underlying repo. The cataloger reads this annotation and prepends `component_id_prefix` to build the Lunar component ID. Typical Backstage convention: `github.com/project-slug` (value `owner/repo`) or `backstage.io/source-location` (value `url:https://github.com/owner/repo`).
component_id_prefix Optional github.com/ String prepended to the value of `component_id_annotation` to form the Lunar component ID. For the default annotation `github.com/project-slug` the value is already `owner/repo`, so the prefix is `github.com/` to produce `github.com/owner/repo`.
tag_prefix Optional bs- Prefix added to Backstage `metadata.tags` when mapped to Lunar tags. Also applied to derived tags like `type-<spec.type>` and `lifecycle-<spec.lifecycle>`. Empty string disables the prefix.
include_derived_tags Optional true When `true`, emits derived tags from `spec.type` (e.g. `bs-type-service`) and `spec.lifecycle` (e.g. `bs-lifecycle-production`) in addition to `metadata.tags`.
owner_format Optional as-is How to write `spec.owner` from Backstage into the Lunar `owner` field. Backstage entity refs typically look like `group:default/team-payments` or `user:default/jane`. - `as-is` — pass the Backstage value through verbatim. Matches what the existing `policies/backstage/*` checks accept (`team-payments`, `group:infra`, `user:alice` are all valid). - `bare-name` — strip the `<kind>:<namespace>/` prefix and write only the trailing name (e.g. `team-payments`). Useful when downstream systems want plain names. Email resolution via Backstage User/Group entities is out of scope for v1.
default_owner Required Fallback owner applied (verbatim) to components and domains that have no `spec.owner` in Backstage. Format is whatever you want — entity ref, email, plain string — Lunar stores it as-is. Leave empty to skip entities without an owner.
domain_default_description Required Fallback description for domains that have no `metadata.description` set in Backstage.
filter Required Additional raw Backstage filter expression (passed through to the `?filter=` query parameter). Use to restrict the sync to a subset of entities (e.g. `metadata.annotations.team=platform`). Empty means no extra filter.
auth_mode Optional bearer How the cataloger authenticates to the Backstage API. - `bearer` — send `Authorization: Bearer <BACKSTAGE_TOKEN>`. Default; unchanged behavior. - `sigv4` — sign every request with AWS Signature V4, for a Backstage API fronted by AWS IAM auth (typically Amazon API Gateway). Credentials are resolved at runtime from the standard AWS credential chain — IRSA (EKS), ECS task role, EC2 instance profile, or `AWS_*` env — so they **auto-refresh and are never supplied by hand**. See the README "AWS SigV4 Authentication" section for the (one-time) IAM-role setup.
aws_region Required AWS region used to scope the SigV4 signature and to reach STS when assuming a web-identity (IRSA) role. Required when `auth_mode: sigv4`, unless `AWS_REGION` is already present in the cataloger pod's environment (IRSA and most AWS runtimes set it, but not always). If `auth_mode: sigv4` and neither this input nor the `AWS_REGION` env var is set, the cataloger fails fast at startup with a clear `aws_region required for sigv4` error rather than signing with an empty region (which would surface as an opaque downstream AWS error). Ignored for `auth_mode: bearer`.
aws_service Optional execute-api AWS service name for the SigV4 signature scope. For a Backstage API behind Amazon API Gateway this is `execute-api` (the default). Override only for other fronting (e.g. a Lambda function URL signs as `lambda`). Ignored for `auth_mode: bearer`.

Secrets

This cataloger requires the following secrets to be configured in Lunar:

Secret Description
BACKSTAGE_TOKEN Bearer token for the Backstage API. Used when `auth_mode: bearer` (the default). Required if the Backstage instance enforces bearer auth; many internal deployments do. Not used for `sigv4`.
AWS_ACCESS_KEY_ID Optional, `auth_mode: sigv4` only. A static-credentials escape hatch for runners with **no** attached IAM identity (no IRSA / ECS / EC2 role). Leave unset for role-based auth, which is the recommended path because it self-refreshes — static keys do **not** auto-rotate.
AWS_SECRET_ACCESS_KEY Optional, `auth_mode: sigv4` only. Paired with `AWS_ACCESS_KEY_ID` for the static-credentials escape hatch (see that secret's note).
AWS_SESSION_TOKEN Optional, `auth_mode: sigv4` only. Set alongside the static AWS keys when they are themselves temporary credentials. Because temporary keys expire, this path needs manual refresh — prefer role-based auth.

Documentation

View on GitHub

Backstage Cataloger

Syncs components and domains from a Backstage software catalog into Lunar.

Overview

This cataloger reads entities from a Backstage instance via its REST API (/api/catalog/entities) and writes them into Lunar. Component entities populate .components (with owner, domain, tags); Domain entities populate .domains (description, owner). Use this when you run a Backstage instance and want Lunar to inherit its ownership/domain/tag metadata. Pair with the separate backstage-catalog-info cataloger for per-repo catalog-info.yaml augmentation (component-cron, layerable). The per-repo backstage collector is a different shape entirely — it writes .catalog.native.backstage during local / CI Lunar runs.

Synced Data

This cataloger writes to the following Catalog JSON paths:

Path Type Description
.components[*].owner string spec.owner of the Backstage Component (or default_owner fallback)
.components[*].domain string spec.domain of the Backstage Component
.components[*].tags[] array metadata.tags plus derived type-* / lifecycle-* tags, all with tag_prefix
.domains[*].description string metadata.description of the Backstage Domain
.domains[*].owner string spec.owner of the Backstage Domain
Example Catalog JSON output
{
  "components": {
    "github.com/acme/payment-api": {
      "owner": "group:default/team-payments",
      "domain": "platform.payments",
      "tags": ["bs-payments", "bs-tier1", "bs-type-service", "bs-lifecycle-production"]
    },
    "github.com/acme/web-app": {
      "owner": "group:default/team-web",
      "domain": "platform.frontend",
      "tags": ["bs-frontend", "bs-type-website", "bs-lifecycle-production"]
    }
  },
  "domains": {
    "platform.payments": {
      "description": "Payment processing and billing",
      "owner": "group:default/platform-leads"
    },
    "platform.frontend": {
      "description": "Customer-facing web surfaces",
      "owner": "group:default/platform-leads"
    }
  }
}

Catalogers

This integration provides the following catalogers:

Cataloger Description
sync Fetches entities from the Backstage catalog API and writes Components, Domains (and optionally Systems, APIs, Resources) to the Lunar catalog

Hook Type

Hook Schedule Description
cron 0 2 * * * Runs daily at 02:00 UTC

Daily is the conservative default because a full /api/catalog/entities walk paginates through every entity in the Backstage instance — at thousands of components this is a non-trivial fetch against both the Backstage server and the Lunar Runner. Ownership, domain, and tag metadata also change on the order of hours-to-days, not minutes, so a nightly cycle covers the data velocity for almost every catalog. Smaller catalogs are free to tighten the cadence by overriding hook.schedule in their forked copy of lunar-cataloger.yml — promoting schedule to a with: input is a candidate v2 if anyone needs per-deployment tunability without a fork.

Installation

Add to your lunar-config.yml:

catalogers:
  - uses: github.com/earthly/lunar-lib/catalogers/backstage@v1.0.0
    with:
      backstage_url: "https://backstage.example.com"

Authenticated Backstage

Most internal Backstage deployments require a bearer token. Configure it as a Lunar secret:

lunar secret set BACKSTAGE_TOKEN <your-token>

The cataloger reads LUNAR_SECRET_BACKSTAGE_TOKEN automatically — no extra with: is needed.

AWS SigV4 Authentication (IAM-role-signed)

Some Backstage APIs sit behind AWS IAM authentication (commonly Amazon API Gateway) and reject Bearer tokens — every request must carry an AWS Signature V4. Set auth_mode: sigv4 to sign requests instead of sending a Bearer token:

catalogers:
  - uses: github.com/earthly/lunar-lib/catalogers/backstage@v1.1.0
    with:
      backstage_url: "https://backstage.example.com"
      auth_mode: "sigv4"
      aws_region: "us-east-1"
      aws_service: "execute-api"   # default; API Gateway. Override for other fronting.

No credentials are configured as Lunar secrets, and nothing needs manual rotation. In sigv4 mode the cataloger resolves AWS credentials at runtime from the standard AWS credential provider chain and re-resolves them on every run, so short-lived IAM-role credentials always sign with a fresh, valid signature. The chain is tried in this order:

  1. IRSA (EKS) — recommended. The cataloger pod runs under a service account annotated with an IAM role; EKS injects a web-identity token, which the cataloger exchanges for temporary credentials via STS. The projected token rotates automatically and each run re-exchanges it — zero human involvement.
  2. ECS task role — the container credentials endpoint (AWS_CONTAINER_CREDENTIALS_*).
  3. EC2 instance profile — IMDSv2 on the node.
  4. Static keys — only if the AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY (/ AWS_SESSION_TOKEN) secrets are set. This is an escape hatch for runners with no attached IAM identity; static keys do not self-refresh, so prefer one of the role-based sources above.

One-time setup: attach the role to the cataloger's service account

Catalogers execute in operator-spawned snippet pods, which run under their own service account (OPERATOR_POD_SERVICE_ACCOUNT, the Lunar chart's <release>-script-pod) — not the Lunar hub's service account. So annotate that service account with the role that is allowed to invoke your Backstage API:

# service account used by cataloger/collector/policy snippet pods
metadata:
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/lunar-backstage-sigv4

The role's trust policy must allow the snippet-pod service account to assume it, and its permissions must allow execute-api:Invoke (or the appropriate action) on your Backstage API. Annotating the hub service account instead is the most common setup mistake — the hub doesn't make the catalog request.

Alternative: a standalone aws-sigv4-proxy service (no plugin config)

If you'd rather keep signing out of the cataloger entirely, run AWS's aws-sigv4-proxy as its own Kubernetes Deployment + Service, leave auth_mode: bearer with no token, and point backstage_url at the proxy's in-cluster DNS name:

with:
  backstage_url: "http://sigv4-proxy.<namespace>.svc.cluster.local:8080"

The proxy signs every forwarded request with its own pod's IAM role (IRSA on the proxy Deployment's service account) via the same credential chain, so this self-refreshes too — it just moves signing out of the plugin and into a separate service you operate.

Not a same-pod sidecar. Catalogers run in operator-spawned snippet pods whose container list is fixed by the Lunar operator — one snippet container (which OPERATOR_SNIPPET_CONTAINER_SPEC_* replaces, it does not append) plus the built-in Lunar sidecar. There is no hook to inject an extra container, so aws-sigv4-proxy cannot ride inside the cataloger's pod; it has to be its own Deployment reached over the cluster network. (For purely local testing, you can instead run the proxy on your laptop and point backstage_url at http://host.docker.internal:8080.)

A static custom auth header cannot substitute for SigV4 — signatures are per-request and time-bound (they cover an X-Amz-Date within a ~15-minute window plus a payload hash), so there is nothing static to configure.

API Path Prefix

By default the cataloger calls <backstage_url>/api/catalog/entities, matching a standard Backstage deployment. Some setups put the catalog API behind a gateway mounted at the root, so the live endpoint is <backstage_url>/catalog/entities and the /api hop returns 403/404. Set api_path_prefix to an empty string to drop the prefix:

catalogers:
  - uses: github.com/earthly/lunar-lib/catalogers/backstage@v1.0.0
    with:
      backstage_url: "https://backstage.example.com"
      api_path_prefix: ""   # gateway is mounted at root — no /api hop

api_path_prefix defaults to /api, so existing configs are unaffected. Any other prefix works too (e.g. /backstage/api); the leading slash is optional and a trailing slash is ignored. The resolved endpoint is echoed on the first line of the cataloger's output, so lunar cataloger dev backstage --verbose shows exactly which URL it will call.

Layering with the GitHub Org Cataloger

For organisations that already run github-org to enumerate repos, run Backstage after it so its owner/domain/tag values override the GitHub defaults:

catalogers:
  - uses: github.com/earthly/lunar-lib/catalogers/github-org@v1.0.0
    with:
      org_name: "acme"

  - uses: github.com/earthly/lunar-lib/catalogers/backstage@v1.0.0
    with:
      backstage_url: "https://backstage.example.com"

Per Lunar's merge precedence, catalogers declared later override earlier ones.

Mapping Components to Repos

Backstage components are matched to Lunar components by reading an annotation on each Backstage Component entity. Defaults assume the standard github.com/project-slug annotation:

catalogers:
  - uses: github.com/earthly/lunar-lib/catalogers/backstage@v1.0.0
    with:
      backstage_url: "https://backstage.example.com"
      component_id_annotation: "github.com/project-slug"  # value: "acme/payment-api"
      component_id_prefix: "github.com/"                    # → "github.com/acme/payment-api"

For GitLab or other forges, point at the appropriate annotation:

with:
  component_id_annotation: "gitlab.com/project-slug"
  component_id_prefix: "gitlab.com/"

Restricting Synced Kinds

By default, Component and Domain entities are synced. Include other kinds explicitly:

with:
  entity_kinds: "Component,Domain,System,API"
Backstage kind Synced to
Component, API, Resource .components
Domain, System .domains
Other kinds (User, Group, Location, …) Ignored

Filtering Entities

Pass a raw Backstage filter expression through filter:

with:
  filter: "metadata.annotations.team=platform"

Owner Format

Backstage spec.owner is typically an entity reference like group:default/team-payments or user:default/jane, not an email. By default this cataloger passes the value through verbatim — matching what the existing policies/backstage/owner-set policy already accepts (team-payments, group:infra, user:alice are all valid).

If you'd rather store bare names, set owner_format: bare-name to strip the <kind>:<namespace>/ prefix. Resolving entity refs to emails by looking up the User/Group entity is intentionally out of scope for v1 — it adds API calls and only works when User/Group entities carry spec.profile.email.

default_owner is also written verbatim, so you can use whatever convention you prefer (entity ref, email, plain string).

Source System

This cataloger calls the Backstage Catalog REST API — specifically the /catalog/entities endpoint, reached at <backstage_url>/api/catalog/entities by default (see API Path Prefix to change the /api segment). It requires:

  1. Network reach from the Lunar Runner to the Backstage instance
  2. Authentication if the instance enforces it — either a bearer token (LUNAR_SECRET_BACKSTAGE_TOKEN, the default) or AWS SigV4 signing (auth_mode: sigv4; see AWS SigV4 Authentication)
  3. Read access to the kinds configured in entity_kinds

Pagination is handled automatically; the cataloger streams pages until all matching entities are fetched.

Open Source

This cataloger is open source and available on GitHub. Contribute improvements, report issues, or fork it for your own use.

View Repository

Ready to Automate Your Standards?

See how Lunar can turn your AGENTS.md, engineering wiki, compliance docs, or postmortem action items into automated guardrails with our 200+ built-in guardrails.

Works with any process
check AI agent rules & prompt files
check Post-mortem action items
check Security & compliance policies
check Testing & quality requirements
Automate Now
Paste your AGENTS.md or manual process doc and get guardrails in minutes
Book a Demo