AWS CloudFormation logo

Backstage AWS CloudFormation Plugin

Created by Purple Technology

AWS CloudFormation is an infrastructure as code service. You write templates that describe resources. CloudFormation provisions them as stacks and keeps track of their lifecycle. Teams use it to create, update, and tear down cloud resources in a repeatable way.

The AWS CloudFormation Backstage plugin brings those stacks into your software catalog. It reads Backstage entities from the Metadata section of your stacks. It can index a single stack or an entire region. It can work across multiple AWS accounts by using different profiles. While loading entities, it can resolve variables from your stacks such as region, account id, stack id, stack name, and outputs. That means your catalog items can include values that match what is actually deployed.

This is useful when you already manage services with CloudFormation and want the catalog to reflect reality. You can bootstrap a large catalog without writing many catalog files by hand. You can keep services in sync as stacks change over time. It also helps when you run in several accounts or regions and want one place where engineers can find what exists, who owns it, and how to reach it.

Be mindful when scanning very large regions. CloudFormation APIs have limits, so plan indexing accordingly.

Installation Instructions

These instructions apply to self-hosted Backstage only.

Install the package

Copy
# from your Backstage root
cd packages/backend
yarn add backstage-aws-cloudformation-plugin

Configure AWS credentials

Create or update your AWS profile file.

Copy
# ~/.aws/credentials
[myProfile]
aws_access_key_id=AKIAIOSFODNN7EXAMPLE
aws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

Add Backstage entities to CloudFormation metadata

Put Backstage entities under this path in your template. Metadata.Backstage.Entities

Copy
AWSTemplateFormatVersion: 2010-09-09
Resources:
  MyLambdaFunction:
    Type: 'AWS::Lambda::Function'
    Properties:
      FunctionName: my-lambda
Metadata:
  Backstage:
    Entities:
      - apiVersion: backstage.io/v1alpha1
        kind: Component
        metadata:
          name: petstore
          namespace: external-systems
          description: Petstore
        spec:
          type: service
          lifecycle: experimental
          owner: 'group:pet-managers'
          providesApis:
            - petstore
            - internal/streetlights
            - hello-world
      - apiVersion: backstage.io/v1alpha1
        kind: API
        metadata:
          name: petstore
          description: The Petstore API
        spec:
          type: openapi
          lifecycle: production
          owner: [email protected]
          definition:
            $text: 'https://petstore.swagger.io/v2/swagger.json'

Register the processors in the legacy backend

Add the processors to your catalog builder.

Copy
// packages/backend/src/plugins/catalog.ts
import { Router } from 'express';
import { CatalogBuilder } from '@backstage/plugin-catalog-backend';
import { PluginEnvironment } from '../types';

import {
  CloudFormationRegionProcessor,
  CloudFormationStackProcessor,
} from 'backstage-aws-cloudformation-plugin';

export default async function createPlugin(env: PluginEnvironment): Promise<Router> {
  const builder = await CatalogBuilder.create(env);

  builder.addProcessor(new CloudFormationStackProcessor(env.config));
  builder.addProcessor(new CloudFormationRegionProcessor(env.config));

  const {
    entitiesCatalog,
    locationsCatalog,
    locationService,
    processingEngine,
    locationAnalyzer,
  } = await builder.build();

  await processingEngine.start();

  return Router();
}

Register the processors in the new backend system

Create a small backend module that hooks into the catalog processing extension point. Then add it to your backend.

Copy
// packages/backend/src/modules/cloudformationCatalogModule.ts
import { coreServices, createBackendModule } from '@backstage/backend-plugin-api';
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-backend/alpha';
import {
  CloudFormationRegionProcessor,
  CloudFormationStackProcessor,
} from 'backstage-aws-cloudformation-plugin';

export const cloudformationCatalogModule = createBackendModule({
  pluginId: 'catalog',
  moduleId: 'cloudformation-processors',
  register(env) {
    env.registerInit({
      deps: {
        config: coreServices.rootConfig,
        processing: catalogProcessingExtensionPoint,
      },
      async init({ config, processing }) {
        processing.addProcessor(new CloudFormationStackProcessor(config));
        processing.addProcessor(new CloudFormationRegionProcessor(config));
      },
    });
  },
});

Add the module to your backend entry.

Copy
// packages/backend/src/index.ts
import { createBackend } from '@backstage/backend-defaults';
import { catalogPlugin } from '@backstage/plugin-catalog-backend';
import { cloudformationCatalogModule } from './modules/cloudformationCatalogModule';

const backend = createBackend();

// core catalog plugin
backend.add(catalogPlugin());

// custom processors for CloudFormation
backend.add(cloudformationCatalogModule());

backend.start();

Set environment for AWS SDK

Enable profile loading so the SDK can read your profile files.

Copy
// package.json at repo root
{
  "scripts": {
    "dev": "AWS_SDK_LOAD_CONFIG=true concurrently \"yarn start\" \"yarn start-backend\"",
    "start": "AWS_SDK_LOAD_CONFIG=true yarn workspace app start",
    "start-backend": "AWS_SDK_LOAD_CONFIG=true yarn workspace backend start"
  }
}

Optional default AWS profile

You can set a default profile in your Backstage config.

Copy
# app-config.yaml
integrations:
  aws:
    profile: myProfile

Add catalog locations

Add locations for stacks or for whole regions. Use an AWS profile prefix when you want a non default profile. Format is profileName@Target

Copy
# app-config.yaml
catalog:
  locations:
    # one stack through a named profile
    - type: aws:cloudformation:stack
      target: myProfile@arn:aws:cloudformation:ap-southeast-1:123456789000:stack/some-stack/123-345-12-1235-123123

    # one stack through the default profile
    - type: aws:cloudformation:stack
      target: arn:aws:cloudformation:eu-central-1:123456789000:stack/other-stack/532-123-59-593-19481

    # whole region through a named profile
    - type: aws:cloudformation:region
      target: myProfile@ap-southeast-1

    # whole region through the default profile
    - type: aws:cloudformation:region
      target: eu-central-1

Variables you can use in metadata

You can inject values inside your entity yaml in Metadata.Backstage.Entities. The plugin replaces these at read time.

Region example

Copy
metadata:
  description: Service in ${Region} region

AccountId example

Copy
metadata:
  title: AWS Account ID is ${AccountId}

StackId example

Copy
metadata:
  title: Stack ARN is ${StackId}

StackName example

Copy
metadata:
  title: Stack Name is ${StackName}

Outputs example

Copy
metadata:
  name: lambda-${Outputs.GetClientsLambdaName}

Escape a variable when you do not want replacement.

Copy
metadata:
  note: \${Outputs.SomeVariable}

Notes

The plugin can handle throttling. Be careful when you scan a whole region with many stacks.

Set up Backstage in minutes with Roadie