Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions sfn-invoicing-bedrock-chargeback-cdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Multi-BU Invoice Reconciliation with AWS Step Functions, AWS Invoicing APIs, Amazon Bedrock, and Amazon DynamoDB

This pattern deploys an AWS Step Functions state machine that uses a Map state to iterate over AWS Invoice Units (business units), fetches invoice summaries for each unit via AWS Invoicing APIs, runs Amazon Bedrock analysis for chargeback allocation, and stores results in an Amazon DynamoDB ledger.

Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/sfn-invoicing-bedrock-chargeback-cdk

Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example.

## Requirements

* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources.
* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured
* [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [Node.js 20+](https://nodejs.org/en/download/) installed
* [AWS CDK v2](https://docs.aws.amazon.com/cdk/v2/guide/getting-started.html) installed and bootstrapped (`cdk bootstrap`)
* [Amazon Bedrock model access](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) enabled for Anthropic Claude Sonnet in your target region
* At least one [AWS Invoice Unit](https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/manage-invoice-units.html) configured in your account

## Architecture

```
┌────────────────────────────────────────────────────────────────────────────┐
│ AWS Step Functions │
│ │
│ ┌─────────────────┐ ┌──────────────────────────────────────────────┐ │
│ │ ListUnitsTask │───▶│ Map State (parallel per Invoice Unit) │ │
│ │ (AWS Lambda) │ │ │ │
│ │ Invoicing API: │ │ ┌─────────────────┐ ┌──────────────────┐ │ │
│ │ ListInvoiceUnits│ │ │ FetchInvoicesTask│─▶│ AnalysisTask │ │ │
│ └─────────────────┘ │ │ (AWS Lambda) │ │ (AWS Lambda) │ │ │
│ │ │ Invoicing API: │ │ Bedrock Converse │ │ │
│ │ │ ListInvoice │ │ + DynamoDB Write │ │ │
│ │ │ Summaries │ │ │ │ │
│ │ └─────────────────┘ └──────────────────┘ │ │
│ └──────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────┘
┌──────────────────┐
│ Amazon DynamoDB │
│ Chargeback Ledger │
└──────────────────┘
```

## How it works

1. **ListUnitsTask** — An AWS Lambda function calls the AWS Invoicing `ListInvoiceUnits` API with pagination to discover all business units in the account.

2. **Map State** — AWS Step Functions iterates over each Invoice Unit in parallel (max concurrency 5), processing them independently.

3. **FetchInvoicesTask** — For each unit, an AWS Lambda function calls `ListInvoiceSummaries` filtered by account, retrieving the last 30 days of invoice data.

4. **AnalysisTask** — An AWS Lambda function sends the invoice data to Amazon Bedrock (Claude Sonnet) for FinOps analysis: total spend calculation, month-over-month trend detection, chargeback allocation recommendations, and cost optimization opportunities. Results are written to Amazon DynamoDB.

5. **Amazon DynamoDB Ledger** — Stores chargeback allocations per business unit per period, creating an auditable financial record.

## Deployment Instructions

1. Clone the repository:
```bash
git clone https://github.com/aws-samples/serverless-patterns
cd serverless-patterns/sfn-invoicing-bedrock-chargeback-cdk/cdk
```

2. Install dependencies:
```bash
npm install
```

3. Deploy the stack:
```bash
cdk deploy
```

4. (Optional) Use a different model:
```bash
cdk deploy --context modelId=us.anthropic.claude-sonnet-4-20250514-v1:0
```

## Testing

1. Start a Step Functions execution:
```bash
aws stepfunctions start-execution \
--state-machine-arn <StateMachineArn from stack outputs> \
--input '{}'
```

2. Monitor the execution:
```bash
aws stepfunctions describe-execution \
--execution-arn <execution ARN from above>
```

3. Once complete (status: SUCCEEDED), check the Amazon DynamoDB chargeback ledger:
```bash
aws dynamodb scan \
--table-name <ChargebackTableName from stack outputs>
```

4. Expected result: One item per Invoice Unit with fields including `unitName`, `invoiceCount`, `analysis` (JSON with totalSpend, trend, chargebackAllocation, optimizations), and `analyzedAt` timestamp.

## Cleanup

```bash
cdk destroy
```

> **Warning:** This deletes the Amazon DynamoDB table and all chargeback history. The table uses `RemovalPolicy.DESTROY` for cleanup convenience. In production, use `RETAIN` or enable point-in-time recovery.

----
Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved.

SPDX-License-Identifier: MIT-0
6 changes: 6 additions & 0 deletions sfn-invoicing-bedrock-chargeback-cdk/cdk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules
build
cdk.out
cdk.context.json
*.js
*.d.ts
15 changes: 15 additions & 0 deletions sfn-invoicing-bedrock-chargeback-cdk/cdk/bin/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env node
// Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT-0

import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { SfnInvoicingBedrockChargebackStack } from '../lib/sfn-invoicing-bedrock-chargeback-stack';

const app = new cdk.App();
new SfnInvoicingBedrockChargebackStack(app, 'SfnInvoicingBedrockChargebackStack', {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION || 'us-east-1',
},
});
6 changes: 6 additions & 0 deletions sfn-invoicing-bedrock-chargeback-cdk/cdk/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"app": "npx ts-node --prefer-ts-exts bin/app.ts",
"context": {
"modelId": "us.anthropic.claude-sonnet-4-20250514-v1:0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT-0

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as sfn from 'aws-cdk-lib/aws-stepfunctions';
import * as tasks from 'aws-cdk-lib/aws-stepfunctions-tasks';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as path from 'path';

export class SfnInvoicingBedrockChargebackStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);

const modelId = this.node.tryGetContext('modelId') ||
'us.anthropic.claude-sonnet-4-20250514-v1:0';

// DynamoDB table for chargeback ledger
const chargebackTable = new dynamodb.Table(this, 'ChargebackLedger', {
partitionKey: { name: 'invoiceUnitId', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'period', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});

// Lambda: List Invoice Units
const listUnitsFn = new lambda.Function(this, 'ListInvoiceUnits', {
runtime: lambda.Runtime.PYTHON_3_12,
handler: 'list_units.handler',
code: lambda.Code.fromAsset(path.join(__dirname, '..', '..', 'src')),
timeout: cdk.Duration.seconds(30),
memorySize: 256,
});

listUnitsFn.addToRolePolicy(new iam.PolicyStatement({
actions: ['invoicing:ListInvoiceUnits'],
resources: ['*'], // Invoicing API does not support resource-level permissions
}));

// Lambda: Fetch Invoices per Unit
const fetchInvoicesFn = new lambda.Function(this, 'FetchInvoices', {
runtime: lambda.Runtime.PYTHON_3_12,
handler: 'fetch_invoices.handler',
code: lambda.Code.fromAsset(path.join(__dirname, '..', '..', 'src')),
timeout: cdk.Duration.seconds(60),
memorySize: 256,
});

fetchInvoicesFn.addToRolePolicy(new iam.PolicyStatement({
actions: ['invoicing:ListInvoiceSummaries', 'sts:GetCallerIdentity'],
resources: ['*'], // Invoicing API does not support resource-level permissions
}));

// Lambda: Bedrock Analysis + DynamoDB write
const analysisFn = new lambda.Function(this, 'BedrockAnalysis', {
runtime: lambda.Runtime.PYTHON_3_12,
handler: 'analysis.handler',
code: lambda.Code.fromAsset(path.join(__dirname, '..', '..', 'src')),
timeout: cdk.Duration.seconds(120),
memorySize: 512,
environment: {
MODEL_ID: modelId,
TABLE_NAME: chargebackTable.tableName,
},
});

analysisFn.addToRolePolicy(new iam.PolicyStatement({
actions: ['bedrock:InvokeModel'],
resources: [
`arn:aws:bedrock:${this.region}:${this.account}:inference-profile/${modelId}`,
'arn:aws:bedrock:*::foundation-model/*',
],
}));

chargebackTable.grantWriteData(analysisFn);

// Step Functions definition
const listUnitsTask = new tasks.LambdaInvoke(this, 'ListUnitsTask', {
lambdaFunction: listUnitsFn,
outputPath: '$.Payload',
});

const fetchInvoicesTask = new tasks.LambdaInvoke(this, 'FetchInvoicesTask', {
lambdaFunction: fetchInvoicesFn,
outputPath: '$.Payload',
});

const analysisTask = new tasks.LambdaInvoke(this, 'AnalysisTask', {
lambdaFunction: analysisFn,
outputPath: '$.Payload',
});

// Map state: iterate over each invoice unit in parallel
const mapState = new sfn.Map(this, 'ProcessEachUnit', {
itemsPath: '$.units',
maxConcurrency: 5,
});

mapState.itemProcessor(
fetchInvoicesTask.next(analysisTask)
);

const definition = listUnitsTask.next(mapState);

const stateMachine = new sfn.StateMachine(this, 'InvoiceReconciliation', {
definitionBody: sfn.DefinitionBody.fromChainable(definition),
timeout: cdk.Duration.minutes(15),
});

// Outputs
new cdk.CfnOutput(this, 'StateMachineArn', {
value: stateMachine.stateMachineArn,
});

new cdk.CfnOutput(this, 'ChargebackTableName', {
value: chargebackTable.tableName,
});
}
}
21 changes: 21 additions & 0 deletions sfn-invoicing-bedrock-chargeback-cdk/cdk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "sfn-invoicing-bedrock-chargeback-cdk",
"version": "1.0.0",
"bin": {
"app": "bin/app.ts"
},
"scripts": {
"build": "tsc",
"synth": "cdk synth"
},
"dependencies": {
"aws-cdk-lib": "^2.185.0",
"constructs": "^10.4.2",
"source-map-support": "^0.5.21"
},
"devDependencies": {
"@types/node": "^20.17.0",
"ts-node": "^10.9.2",
"typescript": "^5.4.5"
}
}
17 changes: 17 additions & 0 deletions sfn-invoicing-bedrock-chargeback-cdk/cdk/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["es2020"],
"declaration": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noImplicitThis": true,
"alwaysStrict": true,
"esModuleInterop": true,
"outDir": "./build",
"rootDir": "."
},
"exclude": ["node_modules", "build"]
}
69 changes: 69 additions & 0 deletions sfn-invoicing-bedrock-chargeback-cdk/example-pattern.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"title": "Multi-BU Invoice Reconciliation with AWS Step Functions, Amazon Bedrock, and Amazon DynamoDB",
"description": "Deploy an AWS Step Functions Map state that iterates Invoice Units, fetches per-BU invoices, runs Amazon Bedrock chargeback analysis, and writes to Amazon DynamoDB.",
"language": "Python",
"level": "400",
"framework": "AWS CDK",
"introBox": {
"headline": "How it works",
"text": [
"This pattern deploys an AWS Step Functions state machine with a Map state that processes each AWS Invoice Unit in parallel. For each business unit, it fetches invoice summaries via the AWS Invoicing APIs, sends the data to Amazon Bedrock for FinOps analysis (spend trends, chargeback allocation, optimization recommendations), and writes the results to an Amazon DynamoDB ledger.",
"The composition is non-reducible: AWS Step Functions orchestrates parallel processing across units, AWS Invoicing APIs provide the financial data, Amazon Bedrock generates intelligent chargeback allocations that no rule-based system can replicate, and Amazon DynamoDB provides the durable audit trail. Removing any component breaks the architecture."
]
},
"gitHub": {
"template": {
"repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/sfn-invoicing-bedrock-chargeback-cdk",
"templateURL": "serverless-patterns/sfn-invoicing-bedrock-chargeback-cdk",
"projectFolder": "sfn-invoicing-bedrock-chargeback-cdk",
"templateFile": "cdk/lib/sfn-invoicing-bedrock-chargeback-stack.ts"
}
},
"resources": {
"bullets": [
{
"text": "AWS Invoicing APIs",
"link": "https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/manage-invoice-units.html"
},
{
"text": "AWS Step Functions Map State",
"link": "https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-map-state.html"
},
{
"text": "Amazon Bedrock Converse API",
"link": "https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-call.html"
},
{
"text": "Amazon DynamoDB",
"link": "https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html"
}
]
},
"deploy": {
"text": [
"<code>cd sfn-invoicing-bedrock-chargeback-cdk/cdk</code>",
"<code>npm install</code>",
"<code>cdk deploy</code>"
]
},
"testing": {
"text": [
"Start a Step Functions execution: <code>aws stepfunctions start-execution --state-machine-arn &lt;StateMachineArn&gt; --input '{}'</code>",
"Monitor: <code>aws stepfunctions describe-execution --execution-arn &lt;executionArn&gt;</code>",
"Check results: <code>aws dynamodb scan --table-name &lt;ChargebackTableName&gt;</code>"
]
},
"cleanup": {
"text": [
"Delete the stack: <code>cdk destroy</code>.",
"Warning: This deletes the Amazon DynamoDB chargeback ledger and all history."
]
},
"authors": [
{
"name": "Nithin Chandran R",
"bio": "Technical Account Manager at AWS",
"linkedin": "nithin-chandran-r"
}
]
}
Loading