yption.S3_MANAGED,
lifecycleRules: [{
expiration: cdk.Duration.days(90),
transitions: [{
storageClass: s3.StorageClass.INFREQUENT_ACCESS,
transitionAfter: cdk.Duration.days(30)
}]
}]
});
// 2. Create CloudWatch Log Group
const logGroup = new logs.LogGroup(this, 'BedrockInvocationLogsGroup', {
retention: logs.RetentionDays.TWO_WEEKS,
removalPolicy: cdk.RemovalPolicy.DESTROY
});
// 3. Create IAM Role for Bedrock to write logs
const loggingRole = new iam.Role(this, 'BedrockLoggingRole', {
assumedBy: new iam.ServicePrincipal('bedrock.amazonaws.com'),
inlinePolicies: {
BedrockLoggingPolicy: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: ['s3:PutObject', 's3:GetBucketLocation'],
resources: [`${logBucket.bucketArn}/*`]
}),
new iam.PolicyStatement({
actions: ['logs:CreateLogStream', 'logs:PutLogEvents'],
resources: [logGroup.logGroupArn]
})
]
})
}
});
// 4. Enable Invocation Logging Configuration
new bedrock.CfnInvocationLoggingConfiguration(this, 'BedrockLoggingConfig', {
loggingConfig: {
cloudWatchConfig: {
logGroupName: logGroup.logGroupName,
roleArn: loggingRole.roleArn
},
s3Config: {
bucketName: logBucket.bucketName,
roleArn: loggingRole.roleArn,
prefix: 'invocations/'
}
}
});
}
}
**Rationale:** The CDK construct ensures idempotent setup across regions. The S3 lifecycle policy moves data to Infrequent Access after 30 days, optimizing storage costs for historical analytics. The IAM role is scoped strictly to the required actions, adhering to least-privilege principles.
#### Phase 2: Create Application Inference Profiles
Application inference profiles are account-scoped copies of system models that carry metadata tags. These tags are the mechanism for attribution. When an application invokes Bedrock using a profile ARN, every log entry is stamped with the associated tags.
**Key Architecture Decision:** Use geographic prefixes to enforce data residency.
* `us.` prefix: Routes traffic across US regions. Suitable for general production workloads.
* `eu.` prefix: Routes traffic across EU regions. Mandatory for GDPR compliance.
* `ap.` prefix: Routes traffic across Asia-Pacific regions. Optimizes latency for APAC users.
**Creating a Profile via AWS SDK (TypeScript):**
```typescript
import { BedrockClient, CreateInferenceProfileCommand } from "@aws-sdk/client-bedrock";
const client = new BedrockClient({ region: 'eu-west-1' });
export async function createAppProfile(appName: string, team: string) {
// Source ARN uses 'eu.' prefix for EU data residency
const sourceModelArn = 'arn:aws:bedrock:eu-west-1::inference-profile/eu.anthropic.claude-haiku-4-5-20251001-v1:0';
const command = new CreateInferenceProfileCommand({
inferenceProfileName: `${appName}-haiku-profile`,
description: `Profile for ${appName} managed by ${team}`,
modelSource: {
copyFrom: sourceModelArn
},
tags: {
app: appName,
team: team,
environment: 'production',
costCenter: 'ai-platform'
}
});
const response = await client.send(command);
if (response.inferenceProfileArn) {
console.log(`Profile created: ${response.inferenceProfileArn}`);
return response.inferenceProfileArn;
}
throw new Error('Failed to create inference profile');
}
Migration Strategy: Existing applications require no code changes. The migration involves updating the configuration parameter that holds the modelId.
- Before:
modelId = 'us.anthropic.claude-haiku-4-5-20251001-v1:0'
- After:
modelId = 'arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/abc123...'
The SDK response shape remains identical. The profile ARN is transparent to the application logic but visible in logs and billing.
Phase 3: Analytics with Amazon Athena
S3 logs are structured as JSON. Amazon Athena allows SQL-based querying of these logs to derive token usage and estimated costs per tagged entity.
Athena Table Definition:
CREATE EXTERNAL TABLE IF NOT EXISTS bedrock_invocations (
invocation_id STRING,
timestamp STRING,
model_id STRING,
input_tokens BIGINT,
output_tokens BIGINT,
latency_ms BIGINT,
identity_arn STRING,
tags MAP<STRING, STRING>
)
PARTITIONED BY (dt STRING)
STORED AS JSONFILE
LOCATION 's3://your-bedrock-logs-bucket/invocations/'
TBLPROPERTIES ("projection.enabled"="true",
"projection.dt.type"="date",
"projection.dt.range"="2024/01/01,NOW",
"projection.dt.format"="yyyy/MM/dd",
"projection.dt.interval"="1",
"projection.dt.interval.unit"="DAYS");
Cost Attribution Query:
WITH daily_usage AS (
SELECT
tags['app'] AS application,
tags['team'] AS team,
DATE(timestamp) AS usage_date,
SUM(input_tokens) AS total_input_tokens,
SUM(output_tokens) AS total_output_tokens,
COUNT(*) AS request_count
FROM bedrock_invocations
WHERE dt >= '2024/10/01'
GROUP BY tags['app'], tags['team'], DATE(timestamp)
),
pricing AS (
SELECT 'claude-haiku' AS model_family, 0.0008 AS input_price_per_1k, 0.0040 AS output_price_per_1k
UNION ALL
SELECT 'claude-sonnet', 0.0030, 0.0150
UNION ALL
SELECT 'claude-opus', 0.0150, 0.0750
)
SELECT
u.application,
u.team,
u.usage_date,
u.total_input_tokens,
u.total_output_tokens,
u.request_count,
ROUND((u.total_input_tokens / 1000.0 * p.input_price_per_1k) +
(u.total_output_tokens / 1000.0 * p.output_price_per_1k), 4) AS estimated_cost_usd
FROM daily_usage u
CROSS JOIN pricing p
WHERE u.application IS NOT NULL
ORDER BY estimated_cost_usd DESC;
Rationale: The query uses partition projection to avoid scanning the entire S3 bucket, reducing query latency and cost. The pricing CTE centralizes rate management, making it easy to update rates when AWS adjusts pricing. The tags['app'] extraction enables grouping by business unit.
Pitfall Guide
-
Logging Configuration Gap
- Explanation: Creating inference profiles without enabling invocation logging results in logs that lack the necessary metadata. Profiles alone do not generate logs; the account-level logging configuration must be active.
- Fix: Always verify logging status via
aws bedrock get-invocation-logging-configuration before creating profiles. Implement logging as a mandatory step in the account onboarding pipeline.
-
Geographic Prefix Mismatch
- Explanation: Using a
us. prefixed profile for workloads containing EU personal data violates GDPR. The prefix dictates the routing pool, and traffic may traverse regions outside the intended boundary.
- Fix: Audit all profile ARNs for prefix compliance. Enforce
eu. prefixes for EU workloads via SCPs or CI/CD validation checks. Document the mapping of data classification to region prefixes.
-
Cost Allocation Tag Activation Delay
- Explanation: AWS Cost Explorer requires cost allocation tags to be activated in the billing console. Activation can take up to 24 hours to propagate. Teams expecting immediate budget alerts may be disappointed.
- Fix: Activate tags
app and team in the Billing console well in advance. Use Athena for immediate cost visibility while waiting for Cost Explorer propagation. Set up CloudWatch alarms on log metrics as an interim alerting mechanism.
-
Hardcoded Profile ARNs
- Explanation: Embedding profile ARNs directly in application source code makes rotation and environment management difficult. If a profile is recreated or migrated, code changes are required.
- Fix: Store profile ARNs in AWS Systems Manager Parameter Store or AWS Secrets Manager. Applications should retrieve the ARN at startup or via configuration service. Use CDK outputs to populate parameters automatically.
-
IAM Caller vs. Profile Attribution Confusion
- Explanation: Logs contain both the IAM caller ARN and the profile tags. Queries that only filter by IAM caller may miss application-level attribution, especially when multiple apps share a service role.
- Fix: Design analytics queries to prioritize profile tags for application attribution. Use IAM caller ARN only for forensic analysis of direct developer invocations or to detect unauthorized usage bypassing profiles.
-
Athena Scan Costs on Unpartitioned Data
- Explanation: Querying S3 logs without partitioning can result in full table scans, leading to high Athena costs and slow performance, especially as log volume grows.
- Fix: Implement partition projection on the
dt (date) column as shown in the Athena table definition. This allows Athena to prune partitions efficiently. Consider archiving logs older than 90 days to a separate bucket with a different table definition.
-
Ignoring IAM-Level Attribution
- Explanation: Focusing solely on application profiles may miss spend from developers testing models directly from their local environments using personal credentials.
- Fix: Include queries that group by
identity_arn to identify individual developer spend. Use this data to enforce policies requiring developers to use specific test profiles or to identify training needs for cost optimization.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| EU Data Residency Required | Use eu. prefixed profiles | Ensures inference routing stays within EU regions, satisfying GDPR. | No direct cost change; avoids compliance fines. |
| Low-Latency APAC Users | Use ap. prefixed profiles | Routes traffic to APAC regions, reducing network latency. | Potential regional pricing variance; check local rates. |
| Real-Time Spend Monitoring | CloudWatch Logs + Metrics | Provides sub-minute visibility into token usage and errors. | CloudWatch Logs ingestion costs; negligible for most workloads. |
| Historical Cost Analysis | Athena + S3 Logs | Enables complex SQL queries across months of data. | Athena query costs based on data scanned; partitioning reduces cost. |
| Budget Alerting per Team | AWS Cost Explorer + Tags | Native integration with budgets and SNS alerts. | No additional cost; requires tag activation. |
| Developer Testing | Dedicated dev profiles | Isolates test spend from production; allows cheaper model selection. | Reduces production cost contamination; enables accurate ROI. |
Configuration Template
CDK Construct for Inference Profile Management:
import * as cdk from 'aws-cdk-lib';
import * as bedrock from 'aws-cdk-lib/aws-bedrock';
import * as ssm from 'aws-cdk-lib/aws-ssm';
import { Construct } from 'constructs';
interface InferenceProfileProps {
appName: string;
team: string;
sourceModelArn: string;
region: string;
}
export class InferenceProfileStack extends cdk.Stack {
public readonly profileArn: string;
constructor(scope: Construct, id: string, props: InferenceProfileProps) {
super(scope, id, props);
const profile = new bedrock.CfnInferenceProfile(this, 'AppProfile', {
inferenceProfileName: `${props.appName}-profile`,
description: `Profile for ${props.appName}`,
modelSource: {
copyFrom: props.sourceModelArn
},
tags: {
app: props.appName,
team: props.team,
managedBy: 'cdk'
}
});
// Store ARN in SSM for application consumption
new ssm.StringParameter(this, 'ProfileArnParam', {
parameterName: `/bedrock/profiles/${props.appName}/arn`,
stringValue: profile.attrInferenceProfileArn,
description: `ARN for ${props.appName} inference profile`
});
this.profileArn = profile.attrInferenceProfileArn;
}
}
Usage:
const haikuProfile = new InferenceProfileStack(app, 'HaikuProfile', {
appName: 'fraud-detection',
team: 'risk-engineering',
sourceModelArn: 'arn:aws:bedrock:eu-west-1::inference-profile/eu.anthropic.claude-haiku-4-5-20251001-v1:0',
region: 'eu-west-1'
});
Quick Start Guide
- Deploy Logging: Run the
BedrockLoggingStack CDK construct in your target account/region. Wait for the CREATE_COMPLETE status.
- Create Profile: Execute the
InferenceProfileStack with your application details. Note the output ARN or retrieve it from SSM Parameter Store.
- Update Config: In your application's configuration file or environment variables, replace the existing
modelId with the new profile ARN.
- Verify: Trigger a test invocation. Check CloudWatch Logs for the
/aws/bedrock/invocations group and confirm the log entry contains your app and team tags.
- Query: Run the Athena cost attribution query to see the test invocation reflected in the results within minutes.