diff --git a/bedrock-iam-cost-allocation-cdk/.gitignore b/bedrock-iam-cost-allocation-cdk/.gitignore new file mode 100644 index 000000000..ffa11f083 --- /dev/null +++ b/bedrock-iam-cost-allocation-cdk/.gitignore @@ -0,0 +1,5 @@ +node_modules +build +cdk.out +*.js +*.d.ts diff --git a/bedrock-iam-cost-allocation-cdk/.npmignore b/bedrock-iam-cost-allocation-cdk/.npmignore new file mode 100644 index 000000000..783d56649 --- /dev/null +++ b/bedrock-iam-cost-allocation-cdk/.npmignore @@ -0,0 +1,2 @@ +build +cdk.out diff --git a/bedrock-iam-cost-allocation-cdk/README.md b/bedrock-iam-cost-allocation-cdk/README.md new file mode 100644 index 000000000..1512738b0 --- /dev/null +++ b/bedrock-iam-cost-allocation-cdk/README.md @@ -0,0 +1,81 @@ +# Amazon Bedrock IAM Cost Allocation with Lambda + +This pattern deploys two Lambda functions with separately tagged IAM roles that invoke Amazon Bedrock. Costs are automatically attributed to each IAM principal in AWS Cost and Usage Report (CUR 2.0) and Cost Explorer. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/bedrock-iam-cost-allocation-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. +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [Node and NPM](https://nodejs.org/en/download/) installed +* [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/cli.html) installed + +## How it works + +Amazon Bedrock now supports cost allocation by IAM principal (April 2026). This pattern demonstrates: + +- **Two Lambda functions** with separate IAM roles, each tagged with `team` and `cost-center` +- **Team A** (data-science, cost-center 10001) and **Team B** (engineering, cost-center 10002) +- Both invoke Bedrock — costs are automatically attributed to the calling IAM role in CUR 2.0 +- After activating cost allocation tags in AWS Billing, you can filter by team in Cost Explorer + +``` +Team A Lambda (role: team=data-science) → Bedrock → CUR 2.0: attributed to Team A role +Team B Lambda (role: team=engineering) → Bedrock → CUR 2.0: attributed to Team B role +``` + +## Deployment Instructions + +1. Clone the repository and navigate to the pattern directory: + ```bash + git clone https://github.com/aws-samples/serverless-patterns + cd serverless-patterns/bedrock-iam-cost-allocation-cdk + ``` + +2. Install dependencies: + ```bash + npm install + ``` + +3. Deploy the stack: + ```bash + cdk deploy + ``` + +## Testing + +Invoke each team's function and compare costs in CUR 2.0: + +```bash +# Team A invocation +aws lambda invoke --function-name \ + --payload '{"prompt":"Explain serverless cost optimization in 2 sentences"}' \ + --cli-binary-format raw-in-base64-out output.json && cat output.json | python3 -m json.tool + +# Team B invocation +aws lambda invoke --function-name \ + --payload '{"prompt":"What is Amazon Bedrock? Answer in 2 sentences"}' \ + --cli-binary-format raw-in-base64-out output.json && cat output.json | python3 -m json.tool +``` + +## Activating Cost Allocation Tags + +1. Open the [AWS Billing console](https://console.aws.amazon.com/billing/) +2. Navigate to **Cost allocation tags** +3. Find tags `team` and `cost-center` under the **IAM** category +4. Select and **Activate** them +5. Costs appear in Cost Explorer and CUR 2.0 within 24-48 hours + +## Cleanup + +```bash +cdk destroy +``` + +---- +Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/bedrock-iam-cost-allocation-cdk/bin/app.ts b/bedrock-iam-cost-allocation-cdk/bin/app.ts new file mode 100644 index 000000000..4846c4036 --- /dev/null +++ b/bedrock-iam-cost-allocation-cdk/bin/app.ts @@ -0,0 +1,6 @@ +#!/usr/bin/env node +import * as cdk from 'aws-cdk-lib'; +import { BedrockIamCostAllocationStack } from '../lib/bedrock-iam-cost-allocation-stack'; + +const app = new cdk.App(); +new BedrockIamCostAllocationStack(app, 'BedrockIamCostAllocationStack'); diff --git a/bedrock-iam-cost-allocation-cdk/cdk.context.json b/bedrock-iam-cost-allocation-cdk/cdk.context.json new file mode 100644 index 000000000..0add183f3 --- /dev/null +++ b/bedrock-iam-cost-allocation-cdk/cdk.context.json @@ -0,0 +1,9 @@ +{ + "acknowledged-issue-numbers": [ + 33623, + 34635, + 33623, + 34635, + 32775 + ] +} diff --git a/bedrock-iam-cost-allocation-cdk/cdk.json b/bedrock-iam-cost-allocation-cdk/cdk.json new file mode 100644 index 000000000..27fe6d2ec --- /dev/null +++ b/bedrock-iam-cost-allocation-cdk/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "npx ts-node bin/app.ts" +} diff --git a/bedrock-iam-cost-allocation-cdk/example-pattern.json b/bedrock-iam-cost-allocation-cdk/example-pattern.json new file mode 100644 index 000000000..ffe031e42 --- /dev/null +++ b/bedrock-iam-cost-allocation-cdk/example-pattern.json @@ -0,0 +1,38 @@ +{ + "title": "Amazon Bedrock IAM Cost Allocation with Lambda", + "description": "Deploy tagged IAM roles for per-team Bedrock cost attribution in AWS Cost Explorer and CUR 2.0", + "language": "TypeScript", + "level": "200", + "framework": "CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern creates two Lambda functions with separately tagged IAM roles that invoke Amazon Bedrock.", + "Each role is tagged with team and cost-center attributes. When the functions invoke Bedrock, costs are automatically attributed to the IAM principal in CUR 2.0.", + "After activating cost allocation tags in the AWS Billing console, you can filter and group Bedrock inference costs by team, project, or cost center in Cost Explorer." + ] + }, + "gitHub": { + "template": "https://github.com/aws-samples/serverless-patterns/tree/main/bedrock-iam-cost-allocation-cdk", + "templateURL": "serverless-patterns/bedrock-iam-cost-allocation-cdk" + }, + "resources": { + "bullets": [ + { "text": "Introducing granular cost attribution for Amazon Bedrock", "link": "https://aws.amazon.com/blogs/machine-learning/introducing-granular-cost-attribution-for-amazon-bedrock/" }, + { "text": "IAM principal cost allocation documentation", "link": "https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/iam-principal-cost-allocation.html" } + ] + }, + "deploy": { + "text": ["cdk deploy"] + }, + "cleanup": { + "text": ["cdk destroy"] + }, + "authors": [ + { + "name": "Nithin Chandran R", + "bio": "Technical Account Manager at AWS", + "linkedin": "nithin-chandran-r" + } + ] +} diff --git a/bedrock-iam-cost-allocation-cdk/lib/bedrock-iam-cost-allocation-stack.ts b/bedrock-iam-cost-allocation-cdk/lib/bedrock-iam-cost-allocation-stack.ts new file mode 100644 index 000000000..0cf3eecca --- /dev/null +++ b/bedrock-iam-cost-allocation-cdk/lib/bedrock-iam-cost-allocation-stack.ts @@ -0,0 +1,74 @@ +import * as cdk from 'aws-cdk-lib'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import { Construct } from 'constructs'; + +export class BedrockIamCostAllocationStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const modelId = new cdk.CfnParameter(this, 'BedrockModelId', { + type: 'String', + default: 'us.anthropic.claude-sonnet-4-20250514-v1:0', + description: 'Bedrock model ID (inference profile)', + }); + + // Team A role — tagged for cost allocation + const teamARole = new iam.Role(this, 'TeamABedrockRole', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')], + }); + cdk.Tags.of(teamARole).add('team', 'data-science'); + cdk.Tags.of(teamARole).add('cost-center', '10001'); + + teamARole.addToPolicy(new iam.PolicyStatement({ + actions: ['bedrock:InvokeModel'], + resources: ['*'], + })); + + // Team B role — tagged for cost allocation + const teamBRole = new iam.Role(this, 'TeamBBedrockRole', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')], + }); + cdk.Tags.of(teamBRole).add('team', 'engineering'); + cdk.Tags.of(teamBRole).add('cost-center', '10002'); + + teamBRole.addToPolicy(new iam.PolicyStatement({ + actions: ['bedrock:InvokeModel'], + resources: ['*'], + })); + + // Team A Lambda + const teamAFn = new lambda.Function(this, 'TeamAFunction', { + runtime: lambda.Runtime.NODEJS_22_X, + handler: 'index.handler', + code: lambda.Code.fromAsset('src'), + role: teamARole, + timeout: cdk.Duration.seconds(30), + environment: { + MODEL_ID: modelId.valueAsString, + TEAM_NAME: 'data-science', + }, + }); + + // Team B Lambda + const teamBFn = new lambda.Function(this, 'TeamBFunction', { + runtime: lambda.Runtime.NODEJS_22_X, + handler: 'index.handler', + code: lambda.Code.fromAsset('src'), + role: teamBRole, + timeout: cdk.Duration.seconds(30), + environment: { + MODEL_ID: modelId.valueAsString, + TEAM_NAME: 'engineering', + }, + }); + + new cdk.CfnOutput(this, 'TeamAFunctionName', { value: teamAFn.functionName }); + new cdk.CfnOutput(this, 'TeamBFunctionName', { value: teamBFn.functionName }); + new cdk.CfnOutput(this, 'CostAllocationNote', { + value: 'Activate cost allocation tags (team, cost-center) in AWS Billing console. Costs appear in CUR 2.0 within 24-48 hours.', + }); + } +} diff --git a/bedrock-iam-cost-allocation-cdk/package.json b/bedrock-iam-cost-allocation-cdk/package.json new file mode 100644 index 000000000..243f7e55d --- /dev/null +++ b/bedrock-iam-cost-allocation-cdk/package.json @@ -0,0 +1,21 @@ +{ + "name": "bedrock-iam-cost-allocation-cdk", + "version": "0.1.0", + "bin": { + "bedrock-iam-cost-allocation-cdk": "bin/app.js" + }, + "scripts": { + "build": "tsc", + "cdk": "cdk" + }, + "devDependencies": { + "@types/node": "22.7.9", + "aws-cdk": "2.1003.0", + "ts-node": "^10.9.2", + "typescript": "~5.6.3" + }, + "dependencies": { + "aws-cdk-lib": "2.189.1", + "constructs": "^10.0.0" + } +} diff --git a/bedrock-iam-cost-allocation-cdk/src/index.js b/bedrock-iam-cost-allocation-cdk/src/index.js new file mode 100644 index 000000000..cab95691c --- /dev/null +++ b/bedrock-iam-cost-allocation-cdk/src/index.js @@ -0,0 +1,28 @@ +const { BedrockRuntimeClient, InvokeModelCommand } = require('@aws-sdk/client-bedrock-runtime'); + +const client = new BedrockRuntimeClient(); + +exports.handler = async (event) => { + const prompt = event.prompt || 'What is Amazon Bedrock? Answer in 2 sentences.'; + const teamName = process.env.TEAM_NAME || 'unknown'; + + const response = await client.send(new InvokeModelCommand({ + modelId: process.env.MODEL_ID, + contentType: 'application/json', + accept: 'application/json', + body: JSON.stringify({ + anthropic_version: 'bedrock-2023-05-31', + max_tokens: 256, + messages: [{ role: 'user', content: prompt }], + }), + })); + + const body = JSON.parse(new TextDecoder().decode(response.body)); + return { + statusCode: 200, + team: teamName, + costAllocationTags: { team: teamName, 'cost-center': process.env.COST_CENTER || 'tagged-on-role' }, + response: body.content[0].text, + note: 'This invocation is attributed to the IAM role tagged with team and cost-center. Check CUR 2.0 for line_item_iam_principal.', + }; +}; diff --git a/bedrock-iam-cost-allocation-cdk/tsconfig.json b/bedrock-iam-cost-allocation-cdk/tsconfig.json new file mode 100644 index 000000000..7ddcfe705 --- /dev/null +++ b/bedrock-iam-cost-allocation-cdk/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["es2020"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "outDir": "./build", + "rootDir": "." + }, + "exclude": ["node_modules", "build"] +}