import * as cdk from "aws-cdk-lib"; import * as s3 from "aws-cdk-lib/aws-s3"; import * as iam from "aws-cdk-lib/aws-iam"; import * as logs from "aws-cdk-lib/aws-logs"; import { Construct } from "constructs"; /** * Destinations + delivery role for Bedrock model invocation logging (audit * H-20). `seahaven-alex` is employee-facing and returns payments, invoices, * WO/PO, and HR/SA8000 data — model I/O needs an audit trail. * * CloudFormation has no resource type for the logging configuration itself * (account-level `PutModelInvocationLoggingConfiguration`), so — like the * Config recorder (INFRA-17) — the toggle is applied via CLI after deploy: * * aws bedrock put-model-invocation-logging-configuration --logging-config '{ * "cloudWatchConfig": { * "logGroupName": "", * "roleArn": "", * "largeDataDeliveryS3Config": {"bucketName": "", "keyPrefix": "large-payloads"} * }, * "s3Config": {"bucketName": "", "keyPrefix": "invocation-logs"}, * "textDataDeliveryEnabled": true, * "imageDataDeliveryEnabled": true, * "embeddingDataDeliveryEnabled": false * }' */ export class BedrockLogging extends Construct { public readonly bucket: s3.Bucket; public readonly logGroup: logs.LogGroup; public readonly deliveryRole: iam.Role; constructor(scope: Construct, id: string) { super(scope, id); const stack = cdk.Stack.of(this); this.bucket = new s3.Bucket(this, "InvocationLogsBucket", { bucketName: `seahaven-bedrock-invocation-logs-${stack.account}`, encryption: s3.BucketEncryption.S3_MANAGED, blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, enforceSSL: true, versioned: false, lifecycleRules: [ { id: "transition-and-expire", transitions: [ { storageClass: s3.StorageClass.GLACIER, transitionAfter: cdk.Duration.days(90), }, ], expiration: cdk.Duration.days(365), abortIncompleteMultipartUploadAfter: cdk.Duration.days(7), }, ], removalPolicy: cdk.RemovalPolicy.RETAIN, }); // Bedrock writes invocation logs to S3 directly via bucket policy — no role. this.bucket.addToResourcePolicy( new iam.PolicyStatement({ sid: "AmazonBedrockLogsWrite", effect: iam.Effect.ALLOW, principals: [new iam.ServicePrincipal("bedrock.amazonaws.com")], actions: ["s3:PutObject"], resources: [this.bucket.arnForObjects("*")], conditions: { StringEquals: { "aws:SourceAccount": stack.account }, ArnLike: { "aws:SourceArn": `arn:aws:bedrock:${stack.region}:${stack.account}:*`, }, }, }), ); this.logGroup = new logs.LogGroup(this, "InvocationLogGroup", { logGroupName: "/aws/bedrock/model-invocations", retention: logs.RetentionDays.THREE_MONTHS, removalPolicy: cdk.RemovalPolicy.RETAIN, }); // CloudWatch delivery requires a role Bedrock can assume, scoped to this // account/source and to the one log group. this.deliveryRole = new iam.Role(this, "DeliveryRole", { roleName: "seahaven-bedrock-invocation-logging", assumedBy: new iam.ServicePrincipal("bedrock.amazonaws.com", { conditions: { StringEquals: { "aws:SourceAccount": stack.account }, ArnLike: { "aws:SourceArn": `arn:aws:bedrock:${stack.region}:${stack.account}:*`, }, }, }), }); this.deliveryRole.addToPolicy( new iam.PolicyStatement({ actions: ["logs:CreateLogStream", "logs:PutLogEvents"], resources: [this.logGroup.logGroupArn, `${this.logGroup.logGroupArn}:log-stream:*`], }), ); new cdk.CfnOutput(this, "BedrockLogBucketName", { value: this.bucket.bucketName }); new cdk.CfnOutput(this, "BedrockLogGroupName", { value: this.logGroup.logGroupName }); new cdk.CfnOutput(this, "BedrockLoggingRoleArn", { value: this.deliveryRole.roleArn }); } }