diff --git a/lambda-invoicing-bedrock-cdk/.gitignore b/lambda-invoicing-bedrock-cdk/.gitignore new file mode 100644 index 0000000000..281f59f142 --- /dev/null +++ b/lambda-invoicing-bedrock-cdk/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +build/ +cdk.out/ +cdk.context.json +*.js +*.d.ts diff --git a/lambda-invoicing-bedrock-cdk/README.md b/lambda-invoicing-bedrock-cdk/README.md new file mode 100644 index 0000000000..d5645777b7 --- /dev/null +++ b/lambda-invoicing-bedrock-cdk/README.md @@ -0,0 +1,101 @@ +# Automated AWS Invoice Retrieval and Analysis with Amazon Bedrock + +This pattern deploys an AWS Lambda function that automatically retrieves AWS invoices using the new programmatic Invoicing APIs, archives invoice PDFs to Amazon S3, and generates cost analysis summaries using Amazon Bedrock. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/lambda-invoicing-bedrock-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 +* [Node.js 20+](https://nodejs.org/en/download/) installed +* [AWS CDK v2](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) installed (`npm install -g aws-cdk`) +* [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed + +## Deployment Instructions + +1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: + ```bash + git clone https://github.com/aws-samples/serverless-patterns + ``` +2. Change directory to the pattern directory: + ```bash + cd serverless-patterns/lambda-invoicing-bedrock-cdk + ``` +3. Install dependencies: + ```bash + npm install + ``` +4. Bootstrap CDK (if not already done): + ```bash + cdk bootstrap + ``` +5. Deploy the stack: + ```bash + cdk deploy + ``` + +## How it works + +This pattern uses four AWS services in composition: + +1. **Amazon EventBridge** triggers the Lambda function on the 2nd of each month (after invoices become available). +2. **AWS Lambda** orchestrates the workflow: calls the AWS Invoicing API to list invoices and download PDFs. +3. **Amazon S3** stores the invoice PDFs with date-partitioned keys and lifecycle transitions to Infrequent Access after 90 days. +4. **Amazon Bedrock** (Claude Sonnet) analyzes the invoice data and generates an executive summary with spend breakdown, tax analysis, and cost optimization recommendations. + +The pattern leverages the new `ListInvoiceSummaries` and `GetInvoicePDF` APIs (launched June 2026) to programmatically access invoices that previously required manual console downloads. + +### Architecture + +``` +Amazon EventBridge (monthly) → AWS Lambda → Invoicing APIs → Amazon S3 (PDF archive) + → Amazon Bedrock (analysis) → Amazon S3 (summary) +``` + +## Testing + +Invoke the AWS Lambda function manually: + +```bash +aws lambda invoke \ + --function-name $(aws cloudformation describe-stacks \ + --stack-name LambdaInvoicingBedrockStack \ + --query 'Stacks[0].Outputs[?OutputKey==`FunctionName`].OutputValue' \ + --output text) \ + --payload '{}' \ + response.json && cat response.json +``` + +Expected output shows invoices retrieved, PDFs archived, and Bedrock analysis: + +```json +{ + "statusCode": 200, + "body": "{\"message\": \"Processed 1 invoice(s)\", \"period\": \"2026-05\", \"totalInvoices\": 1, \"analysisKey\": \"analysis/2026/05/summary.json\", \"analysis\": \"...\"}" +} +``` + +Check the Amazon S3 bucket for archived PDFs and analysis: + +```bash +aws s3 ls s3://$(aws cloudformation describe-stacks \ + --stack-name LambdaInvoicingBedrockStack \ + --query 'Stacks[0].Outputs[?OutputKey==`InvoiceBucketName`].OutputValue' \ + --output text) --recursive +``` + +## Cleanup + +> **Warning:** Running `cdk destroy` will delete the Amazon S3 bucket and all archived invoice PDFs. Download any needed invoices before destroying. + +```bash +cdk destroy +``` + +---- +Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/lambda-invoicing-bedrock-cdk/bin/app.ts b/lambda-invoicing-bedrock-cdk/bin/app.ts new file mode 100644 index 0000000000..53b6a1cfba --- /dev/null +++ b/lambda-invoicing-bedrock-cdk/bin/app.ts @@ -0,0 +1,12 @@ +#!/usr/bin/env node +import 'source-map-support/register'; +import * as cdk from 'aws-cdk-lib'; +import { LambdaInvoicingBedrockStack } from '../lib/lambda-invoicing-bedrock-stack'; + +const app = new cdk.App(); +new LambdaInvoicingBedrockStack(app, 'LambdaInvoicingBedrockStack', { + env: { + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION, + }, +}); diff --git a/lambda-invoicing-bedrock-cdk/cdk.json b/lambda-invoicing-bedrock-cdk/cdk.json new file mode 100644 index 0000000000..7af9e2af42 --- /dev/null +++ b/lambda-invoicing-bedrock-cdk/cdk.json @@ -0,0 +1,7 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/app.ts", + "watch": { + "include": ["**"], + "exclude": ["node_modules", "build", "cdk.out"] + } +} diff --git a/lambda-invoicing-bedrock-cdk/example-pattern.json b/lambda-invoicing-bedrock-cdk/example-pattern.json new file mode 100644 index 0000000000..566a826adb --- /dev/null +++ b/lambda-invoicing-bedrock-cdk/example-pattern.json @@ -0,0 +1,101 @@ +{ + "title": "Automated AWS Invoice Retrieval and Analysis with Amazon Bedrock", + "description": "Retrieve AWS invoices programmatically, archive PDFs to Amazon S3, and generate cost analysis summaries using Amazon Bedrock.", + "language": "Python", + "level": "200", + "framework": "AWS CDK", + "introBox": { + "headline": "How it works", + "text": [ + "An Amazon EventBridge scheduled rule triggers an AWS Lambda function on the 2nd of each month.", + "The AWS Lambda function calls the AWS Invoicing API to list invoice summaries and download invoice PDFs via presigned URLs.", + "Invoice PDFs are archived to Amazon S3 with date-partitioned prefixes and lifecycle transitions to Infrequent Access.", + "Amazon Bedrock analyzes the invoice data and generates an executive summary with cost optimization recommendations.", + "The analysis results are stored alongside the PDFs in Amazon S3 for historical tracking and audit compliance." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/lambda-invoicing-bedrock-cdk", + "templateURL": "serverless-patterns/lambda-invoicing-bedrock-cdk", + "projectFolder": "lambda-invoicing-bedrock-cdk", + "templateFile": "lib/lambda-invoicing-bedrock-stack.ts" + } + }, + "resources": { + "bullets": [ + { + "text": "Automate AWS Invoice Retrieval with New Programmatic APIs", + "link": "https://aws.amazon.com/blogs/aws-cloud-financial-management/automate-aws-invoice-retrieval-with-new-programmatic-apis/" + }, + { + "text": "AWS Invoicing API Reference", + "link": "https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_invoicing_ListInvoiceSummaries.html" + } + ] + }, + "deploy": { + "text": [ + "cdk bootstrap", + "npm install", + "cdk deploy" + ] + }, + "testing": { + "text": [ + "Invoke the AWS Lambda function manually to test invoice retrieval:", + "aws lambda invoke --function-name $(aws cloudformation describe-stacks --stack-name LambdaInvoicingBedrockStack --query 'Stacks[0].Outputs[?OutputKey==`FunctionName`].OutputValue' --output text) --payload '{}' response.json && cat response.json" + ] + }, + "cleanup": { + "text": [ + "Warning: This will delete the Amazon S3 bucket and all archived invoices.", + "cdk destroy" + ] + }, + "authors": [ + { + "name": "Nithin Chandran R", + "bio": "Technical Account Manager at AWS", + "linkedin": "nithin-chandran-r" + } + ], + "patternArch": { + "icon1": { + "x": 10, + "y": 50, + "service": "eventbridge", + "label": "Amazon EventBridge" + }, + "icon2": { + "x": 35, + "y": 50, + "service": "lambda", + "label": "AWS Lambda" + }, + "icon3": { + "x": 60, + "y": 30, + "service": "s3", + "label": "Amazon S3" + }, + "icon4": { + "x": 60, + "y": 70, + "service": "bedrock", + "label": "Amazon Bedrock" + }, + "line1": { + "from": "icon1", + "to": "icon2" + }, + "line2": { + "from": "icon2", + "to": "icon3" + }, + "line3": { + "from": "icon2", + "to": "icon4" + } + } +} diff --git a/lambda-invoicing-bedrock-cdk/lib/lambda-invoicing-bedrock-stack.ts b/lambda-invoicing-bedrock-cdk/lib/lambda-invoicing-bedrock-stack.ts new file mode 100644 index 0000000000..ea0a868ccc --- /dev/null +++ b/lambda-invoicing-bedrock-cdk/lib/lambda-invoicing-bedrock-stack.ts @@ -0,0 +1,82 @@ +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as s3 from 'aws-cdk-lib/aws-s3'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as events from 'aws-cdk-lib/aws-events'; +import * as targets from 'aws-cdk-lib/aws-events-targets'; +import * as path from 'path'; + +export class LambdaInvoicingBedrockStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // Amazon S3 bucket for invoice PDF archive + const invoiceBucket = new s3.Bucket(this, 'InvoiceBucket', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + autoDeleteObjects: true, + encryption: s3.BucketEncryption.S3_MANAGED, + lifecycleRules: [ + { + transitions: [ + { + storageClass: s3.StorageClass.INFREQUENT_ACCESS, + transitionAfter: cdk.Duration.days(90), + }, + ], + }, + ], + }); + + // AWS Lambda function for invoice retrieval and Amazon Bedrock analysis + const invoiceFn = new lambda.Function(this, 'InvoiceFunction', { + runtime: lambda.Runtime.PYTHON_3_12, + handler: 'index.handler', + code: lambda.Code.fromAsset(path.join(__dirname, '..', 'src')), + timeout: cdk.Duration.minutes(5), + memorySize: 512, + environment: { + BUCKET_NAME: invoiceBucket.bucketName, + MODEL_ID: 'us.anthropic.claude-sonnet-4-20250514-v1:0', + }, + }); + + // Grant Amazon S3 write access + invoiceBucket.grantWrite(invoiceFn); + + // Grant Invoicing API access + invoiceFn.addToRolePolicy(new iam.PolicyStatement({ + actions: [ + 'invoicing:ListInvoiceSummaries', + 'invoicing:GetInvoicePDF', + ], + resources: ['*'], + })); + + // Grant Bedrock model invocation + invoiceFn.addToRolePolicy(new iam.PolicyStatement({ + actions: ['bedrock:InvokeModel'], + resources: [ + `arn:aws:bedrock:*::foundation-model/anthropic.claude-sonnet-4-20250514-v1:0`, + `arn:aws:bedrock:${this.region}:${this.account}:inference-profile/us.anthropic.claude-sonnet-4-20250514-v1:0`, + ], + })); + + // Amazon EventBridge rule - runs on the 2nd of each month (invoices available after month-end) + const rule = new events.Rule(this, 'MonthlyInvoiceRule', { + schedule: events.Schedule.cron({ minute: '0', hour: '8', day: '2', month: '*' }), + }); + rule.addTarget(new targets.LambdaFunction(invoiceFn)); + + // Outputs + new cdk.CfnOutput(this, 'InvoiceBucketName', { + value: invoiceBucket.bucketName, + description: 'Amazon S3 bucket storing invoice PDFs and Amazon Bedrock analysis summaries', + }); + + new cdk.CfnOutput(this, 'FunctionName', { + value: invoiceFn.functionName, + description: 'AWS Lambda function name for manual invocation', + }); + } +} diff --git a/lambda-invoicing-bedrock-cdk/package.json b/lambda-invoicing-bedrock-cdk/package.json new file mode 100644 index 0000000000..3a091ae35a --- /dev/null +++ b/lambda-invoicing-bedrock-cdk/package.json @@ -0,0 +1,21 @@ +{ + "name": "lambda-invoicing-bedrock-cdk", + "version": "1.0.0", + "bin": { + "app": "bin/app.ts" + }, + "scripts": { + "build": "tsc", + "cdk": "cdk" + }, + "dependencies": { + "aws-cdk-lib": "^2.170.0", + "constructs": "^10.0.0", + "source-map-support": "^0.5.21" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "~5.6.0", + "aws-cdk": "^2.170.0" + } +} diff --git a/lambda-invoicing-bedrock-cdk/src/index.py b/lambda-invoicing-bedrock-cdk/src/index.py new file mode 100644 index 0000000000..abaea76aec --- /dev/null +++ b/lambda-invoicing-bedrock-cdk/src/index.py @@ -0,0 +1,149 @@ +""" +AWS Invoice Retrieval and Bedrock Analysis + +Retrieves invoice summaries and PDFs via AWS Invoicing APIs, +archives PDFs to S3, and generates cost analysis using Amazon Bedrock. +""" + +import json +import os +import urllib.request +from datetime import datetime, timedelta + +import boto3 + +BUCKET_NAME = os.environ['BUCKET_NAME'] +MODEL_ID = os.environ['MODEL_ID'] + +s3_client = boto3.client('s3') +invoicing_client = boto3.client('invoicing', region_name='us-east-1') +bedrock_client = boto3.client('bedrock-runtime') + + +def handler(event, context): + """Retrieve invoices, archive PDFs to Amazon S3, and analyze with Amazon Bedrock.""" + try: + # Determine billing period (previous month) + today = datetime.utcnow() + first_of_month = today.replace(day=1) + last_month_end = first_of_month - timedelta(days=1) + last_month_start = last_month_end.replace(day=1) + + start_date = last_month_start.strftime('%Y-%m-%dT00:00:00') + end_date = last_month_end.strftime('%Y-%m-%dT23:59:59') + + account_id = context.invoked_function_arn.split(':')[4] + + # Step 1: List invoice summaries + response = invoicing_client.list_invoice_summaries( + Selector={'ResourceType': 'ACCOUNT_ID', 'Value': account_id}, + Filter={'TimeInterval': {'StartDate': start_date, 'EndDate': end_date}} + ) + + invoices = response.get('InvoiceSummaries', []) + if not invoices: + return { + 'statusCode': 200, + 'body': json.dumps({'message': 'No invoices found for the billing period', 'period': f'{last_month_start.strftime("%Y-%m")}'}), + } + + results = [] + for invoice in invoices: + invoice_id = invoice['InvoiceId'] + total_amount = invoice.get('BaseCurrencyAmount', {}).get('TotalAmount', '0') + currency = invoice.get('BaseCurrencyAmount', {}).get('CurrencyCode', 'USD') + + # Step 2: Get invoice PDF + pdf_response = invoicing_client.get_invoice_pdf(InvoiceId=invoice_id) + document_url = pdf_response['InvoicePDF']['DocumentUrl'] + + # Download PDF via presigned URL + pdf_data = urllib.request.urlopen(document_url).read() + + # Step 3: Archive PDF to Amazon S3 + s3_key = f"invoices/{last_month_start.strftime('%Y/%m')}/{invoice_id}.pdf" + s3_client.put_object( + Bucket=BUCKET_NAME, + Key=s3_key, + Body=pdf_data, + ContentType='application/pdf', + ) + + results.append({ + 'invoiceId': invoice_id, + 'amount': total_amount, + 'currency': currency, + 's3Key': s3_key, + }) + + # Step 4: Analyze with Bedrock + summary_prompt = build_analysis_prompt(invoices, last_month_start.strftime('%Y-%m')) + analysis = invoke_bedrock(summary_prompt) + + # Store analysis in Amazon S3 + analysis_key = f"analysis/{last_month_start.strftime('%Y/%m')}/summary.json" + s3_client.put_object( + Bucket=BUCKET_NAME, + Key=analysis_key, + Body=json.dumps({'analysis': analysis, 'invoices': results}, indent=2), + ContentType='application/json', + ) + + return { + 'statusCode': 200, + 'body': json.dumps({ + 'message': f'Processed {len(invoices)} invoice(s)', + 'period': last_month_start.strftime('%Y-%m'), + 'totalInvoices': len(invoices), + 'analysisKey': analysis_key, + 'analysis': analysis, + }), + } + + except Exception as e: + print(f"Error: {str(e)}") + raise + + +def build_analysis_prompt(invoices, period): + """Build a prompt for Bedrock to analyze invoice data.""" + invoice_data = [] + for inv in invoices: + invoice_data.append({ + 'invoiceId': inv.get('InvoiceId'), + 'type': inv.get('InvoiceType'), + 'total': inv.get('BaseCurrencyAmount', {}).get('TotalAmount'), + 'currency': inv.get('BaseCurrencyAmount', {}).get('CurrencyCode'), + 'subtotal': inv.get('BaseCurrencyAmount', {}).get('AmountBreakdown', {}).get('SubTotalAmount'), + 'discounts': inv.get('BaseCurrencyAmount', {}).get('AmountBreakdown', {}).get('Discounts', {}).get('TotalAmount'), + 'taxes': inv.get('BaseCurrencyAmount', {}).get('AmountBreakdown', {}).get('Taxes', {}).get('TotalAmount'), + }) + + return f"""Analyze the following AWS invoice data for billing period {period}. +Provide: +1. A concise executive summary of total spend +2. Breakdown by invoice type (if multiple) +3. Tax and discount analysis +4. Month-over-month trend observation (note if this is the first month of data) +5. Actionable cost optimization recommendations based on the amounts + +Invoice data: +{json.dumps(invoice_data, indent=2)} + +Respond in structured JSON with keys: executiveSummary, breakdown, taxAnalysis, recommendations.""" + + +def invoke_bedrock(prompt): + """Invoke Amazon Bedrock for invoice analysis.""" + response = bedrock_client.invoke_model( + modelId=MODEL_ID, + contentType='application/json', + accept='application/json', + body=json.dumps({ + 'anthropic_version': 'bedrock-2023-05-31', + 'max_tokens': 1024, + 'messages': [{'role': 'user', 'content': prompt}], + }), + ) + result = json.loads(response['body'].read()) + return result['content'][0]['text'] diff --git a/lambda-invoicing-bedrock-cdk/tsconfig.json b/lambda-invoicing-bedrock-cdk/tsconfig.json new file mode 100644 index 0000000000..469b08b610 --- /dev/null +++ b/lambda-invoicing-bedrock-cdk/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["es2020"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "outDir": "./build", + "rootDir": ".", + "typeRoots": ["./node_modules/@types"] + }, + "exclude": ["node_modules", "build"] +}