Architecture Overview

The inventory Lambda runs on a 15-minute schedule inside the target AWS account. It reads from five AWS service APIs, writes seven JSON artifacts to an SSE-KMS encrypted S3 bucket, and promotes them to a latest/ prefix. The OpenICF connector in PingOne IDM reads those artifacts (plus live Bedrock Agent APIs) to surface AI governance data during reconciliation.

Bedrock inventory Lambda architecture diagram Shows the inventory Lambda reading from IAM, Bedrock, Lambda, CloudWatch Logs, and CloudTrail APIs, writing encrypted artifacts to S3, with the OpenICF connector consuming from both S3 and live Bedrock APIs. AWS ACCOUNT IAM Bedrock Agents Lambda CloudWatch Logs CloudTrail Roles, users, groups, policies Agents, aliases, action groups GetFunction (tool roles) Insights queries (activity) LookupEvents (activity) bedrock-core-tools -inventory Lambda Β· Python 3.11 Every 15 min (EventBridge) S3 Inventory Bucket SSE-KMS encrypted agent-bindings.json model-bindings.json principals.json agent-tool-creds.json models.json activity-logs.json manifest.json written last runs/<ts>/ β†’ latest/ PutObject CopyObject KMS CMK Encrypt /aws/lambda/… (90d ret.) Encrypt PINGONE IDM / OPENIDM HOST OpenICF Connector awsbedrock-connector (Java) Reconciliation β†’ IGA platform GetObject (cached) Live: ListAgents, GetAgent, ListAliases, GetGuardrail… LEGEND Lambda reads S3 write / read Connector live API KMS encryption

Shell Variables

All commands reference shell variables. Set these in your terminal before running anything. Later steps capture additional ARNs as resources are created.

bash β€” environment setup
export ACCOUNT_ID=<your-account-id>
export AWS_REGION=us-east-1

export INVENTORY_BUCKET=bedrock-core-inventory
export DEPLOY_BUCKET=bedrock-core-tools-inventory-deploy
export ROLE_NAME=bedrock-core-inventory-lambda-role
export FUNCTION_NAME=bedrock-core-tools-inventory
export LOG_GROUP=/aws/lambda/${FUNCTION_NAME}
export CMK_ALIAS=alias/bedrock-core-inventory
export EVENTBRIDGE_RULE=bedrock-core-inventory-schedule
Heredoc note
JSON policy blocks use unquoted heredocs (<< EOF, not << 'EOF') so shell variables expand. None of the policies use IAM policy variables of the form ${aws:...}. If you add such variables later, switch the specific block to a quoted heredoc.

Prerequisites

Before starting, ensure you have:

The four prerequisite steps (P1–P4) must all complete before Step 1.

P1 Create the KMS CMK

The CMK encrypts both inventory artifacts in S3 (SSE-KMS) and the Lambda's CloudWatch log group. A single key is used for both. Created with a minimal root-only policy; the Lambda execution role grant is added in Step 2e after the role exists.

bash β€” bootstrap key policy + create CMK
cat > /tmp/cmk-bootstrap-policy.json << EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "RootAccountFullControl",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::${ACCOUNT_ID}:root" },
      "Action": "kms:*",
      "Resource": "*"
    }
  ]
}
EOF

export CMK_ARN=$(aws kms create-key \
  --description "Encryption key for bedrock-core-tools-inventory artifacts and log group" \
  --key-usage ENCRYPT_DECRYPT \
  --key-spec SYMMETRIC_DEFAULT \
  --policy file:///tmp/cmk-bootstrap-policy.json \
  --region ${AWS_REGION} \
  --query KeyMetadata.Arn \
  --output text)

echo "CMK_ARN=${CMK_ARN}"

Create a friendly alias

bash
aws kms create-alias \
  --alias-name ${CMK_ALIAS} \
  --target-key-id ${CMK_ARN} \
  --region ${AWS_REGION}

P2 Create the Inventory Bucket

bash
aws s3api create-bucket \
  --bucket ${INVENTORY_BUCKET} \
  --region ${AWS_REGION} \
  --create-bucket-configuration LocationConstraint=${AWS_REGION}

aws s3api put-public-access-block \
  --bucket ${INVENTORY_BUCKET} \
  --public-access-block-configuration \
    BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
Bucket name conflict
S3 bucket names are globally unique. If ${INVENTORY_BUCKET} is taken, use an account-suffixed value:
export INVENTORY_BUCKET=bedrock-core-inventory-${ACCOUNT_ID}

P3 Apply the Inventory Bucket Policy

Enforces three invariants: all access must use TLS, all PutObject requests must use SSE-KMS, and SSE-KMS puts must specify the correct CMK ARN from P1.

bash β€” bucket policy
cat > /tmp/inventory-bucket-policy.json << EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyNonTLS",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::${INVENTORY_BUCKET}",
        "arn:aws:s3:::${INVENTORY_BUCKET}/*"
      ],
      "Condition": {
        "Bool": { "aws:SecureTransport": "false" }
      }
    },
    {
      "Sid": "DenyUnencryptedPuts",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::${INVENTORY_BUCKET}/*",
      "Condition": {
        "StringNotEquals": {
          "s3:x-amz-server-side-encryption": "aws:kms"
        }
      }
    },
    {
      "Sid": "DenyWrongKMSKey",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::${INVENTORY_BUCKET}/*",
      "Condition": {
        "StringNotEquals": {
          "s3:x-amz-server-side-encryption-aws-kms-key-id": "${CMK_ARN}"
        }
      }
    }
  ]
}
EOF

aws s3api put-bucket-policy \
  --bucket ${INVENTORY_BUCKET} \
  --policy file:///tmp/inventory-bucket-policy.json \
  --region ${AWS_REGION}

P4 Create the Lambda Log Group

Pre-created so retention and CMK encryption can be set explicitly. Without this, Lambda auto-creates the log group on first invocation with default settings (never expire, AWS-managed encryption).

bash
aws logs create-log-group \
  --log-group-name ${LOG_GROUP} \
  --region ${AWS_REGION}

aws logs put-retention-policy \
  --log-group-name ${LOG_GROUP} \
  --retention-in-days 90 \
  --region ${AWS_REGION}

aws logs associate-kms-key \
  --log-group-name ${LOG_GROUP} \
  --kms-key-id ${CMK_ARN} \
  --region ${AWS_REGION}

The CloudWatch Logs service principal grant on the CMK is added in Step 2e alongside the Lambda role grant.

Step 1 Create the Deployment Bucket

This bucket stages the Lambda ZIP during deployment. It is separate from the inventory output bucket.

bash
aws s3api create-bucket \
  --bucket ${DEPLOY_BUCKET} \
  --region ${AWS_REGION} \
  --create-bucket-configuration LocationConstraint=${AWS_REGION}

aws s3api put-public-access-block \
  --bucket ${DEPLOY_BUCKET} \
  --public-access-block-configuration \
    BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
Bucket name conflict
If ${DEPLOY_BUCKET} is taken, use: export DEPLOY_BUCKET=bedrock-core-tools-inventory-deploy-${ACCOUNT_ID}

Step 2 Create the IAM Execution Role

2a β€” Trust policy document

bash
cat > /tmp/lambda-trust-policy.json << EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF

2b β€” Create the role

bash
export ROLE_ARN=$(aws iam create-role \
  --role-name ${ROLE_NAME} \
  --assume-role-policy-document file:///tmp/lambda-trust-policy.json \
  --description "Execution role for bedrock-core-tools-inventory Lambda" \
  --query Role.Arn \
  --output text)

echo "ROLE_ARN=${ROLE_ARN}"

2c β€” Attach CloudWatch Logs managed policy

bash
aws iam attach-role-policy \
  --role-name ${ROLE_NAME} \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

2d β€” Custom inventory policy

The KmsArtifactEncryption statement grants the role permissions on the CMK. Other statements are unchanged.

bash β€” inventory IAM policy
cat > /tmp/inventory-policy.json << EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "IAMReadForBindings",
      "Effect": "Allow",
      "Action": [
        "iam:ListRoles",
        "iam:ListRolePolicies",
        "iam:GetRolePolicy",
        "iam:ListAttachedRolePolicies",
        "iam:ListUsers",
        "iam:ListUserPolicies",
        "iam:GetUserPolicy",
        "iam:ListAttachedUserPolicies",
        "iam:ListGroupsForUser",
        "iam:ListGroupPolicies",
        "iam:GetGroupPolicy",
        "iam:ListAttachedGroupPolicies",
        "iam:GetPolicy",
        "iam:GetPolicyVersion"
      ],
      "Resource": "*"
    },
    {
      "Sid": "BedrockRead",
      "Effect": "Allow",
      "Action": [
        "bedrock:ListFoundationModels",
        "bedrock:ListAgents",
        "bedrock:GetAgent",
        "bedrock:ListAgentActionGroups",
        "bedrock:GetAgentActionGroup"
      ],
      "Resource": "*"
    },
    {
      "Sid": "LambdaReadForToolCredentials",
      "Effect": "Allow",
      "Action": "lambda:GetFunction",
      "Resource": "*"
    },
    {
      "Sid": "CloudWatchLogsQuery",
      "Effect": "Allow",
      "Action": [
        "logs:StartQuery",
        "logs:GetQueryResults"
      ],
      "Resource": "*"
    },
    {
      "Sid": "CloudTrailLookup",
      "Effect": "Allow",
      "Action": "cloudtrail:LookupEvents",
      "Resource": "*"
    },
    {
      "Sid": "S3WriteInventory",
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:CopyObject"
      ],
      "Resource": "arn:aws:s3:::${INVENTORY_BUCKET}/*"
    },
    {
      "Sid": "KmsArtifactEncryption",
      "Effect": "Allow",
      "Action": [
        "kms:GenerateDataKey",
        "kms:Decrypt",
        "kms:DescribeKey"
      ],
      "Resource": "${CMK_ARN}"
    },
    {
      "Sid": "STSCallerIdentity",
      "Effect": "Allow",
      "Action": "sts:GetCallerIdentity",
      "Resource": "*"
    }
  ]
}
EOF

aws iam put-role-policy \
  --role-name ${ROLE_NAME} \
  --policy-name BedrockCoreInventoryPolicy \
  --policy-document file:///tmp/inventory-policy.json

2e β€” Finalize the KMS key policy

Now that the execution role exists, install the full policy granting the Lambda role SSE-KMS operations, the CloudWatch Logs service principal log-group encryption, and root account full control.

bash β€” full CMK key policy
cat > /tmp/cmk-full-policy.json << EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "RootAccountFullControl",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::${ACCOUNT_ID}:root" },
      "Action": "kms:*",
      "Resource": "*"
    },
    {
      "Sid": "LambdaExecutionRoleArtifactEncryption",
      "Effect": "Allow",
      "Principal": { "AWS": "${ROLE_ARN}" },
      "Action": [
        "kms:GenerateDataKey",
        "kms:Decrypt",
        "kms:DescribeKey"
      ],
      "Resource": "*"
    },
    {
      "Sid": "CloudWatchLogsLogGroupEncryption",
      "Effect": "Allow",
      "Principal": { "Service": "logs.${AWS_REGION}.amazonaws.com" },
      "Action": [
        "kms:Encrypt",
        "kms:Decrypt",
        "kms:ReEncrypt*",
        "kms:GenerateDataKey*",
        "kms:Describe*"
      ],
      "Resource": "*",
      "Condition": {
        "ArnEquals": {
          "kms:EncryptionContext:aws:logs:arn":
            "arn:aws:logs:${AWS_REGION}:${ACCOUNT_ID}:log-group:${LOG_GROUP}"
        }
      }
    }
  ]
}
EOF

aws kms put-key-policy \
  --key-id ${CMK_ARN} \
  --policy-name default \
  --policy file:///tmp/cmk-full-policy.json \
  --region ${AWS_REGION}
IAM Propagation
Wait ~10 seconds for IAM propagation before proceeding to Step 5.

Step 3 Package the Lambda

bash
# Extract the inventory package provided by your CSOM
tar xzf bedrock-core-tools-inventory.tar.gz
cd bedrock-core-tools-inventory

pip install -r requirements.txt -t package/
cp src/*.py package/

cd package
zip -r ../bedrock-core-tools-inventory.zip .
cd ..

Verify the ZIP contains the handler at the root (no subdirectory prefix):

bash β€” verify
unzip -l bedrock-core-tools-inventory.zip | grep handler.py
handler.py (at root, no path prefix)

Step 4 Upload ZIP to Deployment Bucket

bash
aws s3 cp bedrock-core-tools-inventory.zip \
  s3://${DEPLOY_BUCKET}/bedrock-core-tools-inventory.zip \
  --region ${AWS_REGION}

Step 5 Create the Lambda Function

The INVENTORY_KMS_KEY_ID env var is required. The Lambda fails at startup with RuntimeError if missing.

bash
export FUNCTION_ARN=$(aws lambda create-function \
  --function-name ${FUNCTION_NAME} \
  --runtime python3.11 \
  --handler handler.handler \
  --role ${ROLE_ARN} \
  --code S3Bucket=${DEPLOY_BUCKET},S3Key=bedrock-core-tools-inventory.zip \
  --timeout 900 \
  --memory-size 256 \
  --environment "Variables={REGION=${AWS_REGION},CORE_INVENTORY_BUCKET=${INVENTORY_BUCKET},OUTPUT_PREFIX=bedrock-core-inventory/,ACCOUNT_ID=${ACCOUNT_ID},INVENTORY_KMS_KEY_ID=${CMK_ARN}}" \
  --region ${AWS_REGION} \
  --query FunctionArn \
  --output text)

echo "FUNCTION_ARN=${FUNCTION_ARN}"

Optional: Supplemental tool log groups

If any tool Lambdas use a custom loggingConfig (log group other than /aws/lambda/{function-name}), add TOOL_LOG_GROUPS. Comma-separated log group names, merged with auto-discovered groups.

bash β€” optional
aws lambda update-function-configuration \
  --function-name ${FUNCTION_NAME} \
  --environment "Variables={REGION=${AWS_REGION},CORE_INVENTORY_BUCKET=${INVENTORY_BUCKET},OUTPUT_PREFIX=bedrock-core-inventory/,ACCOUNT_ID=${ACCOUNT_ID},INVENTORY_KMS_KEY_ID=${CMK_ARN},TOOL_LOG_GROUPS=/custom/log/group1,/custom/log/group2}" \
  --region ${AWS_REGION}

Step 6 Create the EventBridge Schedule

6a β€” Create the rule

bash
export RULE_ARN=$(aws events put-rule \
  --name ${EVENTBRIDGE_RULE} \
  --schedule-expression "rate(15 minutes)" \
  --state ENABLED \
  --description "Triggers bedrock-core-tools-inventory every 15 minutes" \
  --region ${AWS_REGION} \
  --query RuleArn \
  --output text)

echo "RULE_ARN=${RULE_ARN}"

6b β€” Add the Lambda as the target

bash
aws events put-targets \
  --rule ${EVENTBRIDGE_RULE} \
  --targets "Id=LambdaTarget,Arn=${FUNCTION_ARN}" \
  --region ${AWS_REGION}

6c β€” Grant EventBridge permission to invoke

bash
aws lambda add-permission \
  --function-name ${FUNCTION_NAME} \
  --statement-id EventBridgeInvoke \
  --action lambda:InvokeFunction \
  --principal events.amazonaws.com \
  --source-arn ${RULE_ARN} \
  --region ${AWS_REGION}

Step 7 Invoke Manually & Verify

7a β€” Trigger a test invocation

bash
aws lambda invoke \
  --function-name ${FUNCTION_NAME} \
  --payload '{}' \
  --region ${AWS_REGION} \
  /tmp/response.json && cat /tmp/response.json
{"statusCode": 200, "run_prefix": "bedrock-core-inventory/runs/20260516T...Z/"}
Status 500?
The Lambda ran but failed internally. Check CloudWatch Logs (7b) before proceeding. Common causes: INVENTORY_KMS_KEY_ID not set (RuntimeError), KMS AccessDenied (re-check Step 2e key policy), bucket policy denial (re-check P3 ARN matches ${CMK_ARN}).

7b β€” Check CloudWatch Logs

bash
aws logs tail ${LOG_GROUP} \
  --follow \
  --region ${AWS_REGION}

Look for uploaded agent-bindings.json and uploaded activity-logs.json. Final log line should read "statusCode": 200.

7c β€” Verify S3 artifacts

bash
aws s3 ls s3://${INVENTORY_BUCKET}/latest/ --region ${AWS_REGION}

Expected β€” seven files:

activity-logs.json agent-bindings.json agent-tool-credentials.json manifest.json model-bindings.json models.json principals.json

7d β€” Inspect agent-bindings.json

bash
aws s3 cp s3://${INVENTORY_BUCKET}/latest/agent-bindings.json - \
  --region ${AWS_REGION} | python3 -m json.tool
Confirm
accountId matches ${ACCOUNT_ID}, region matches ${AWS_REGION}, and bindings array is non-empty (assuming IAM roles with bedrock:InvokeAgent exist).

7e β€” Inspect agent-tool-credentials.json

bash
aws s3 cp s3://${INVENTORY_BUCKET}/latest/agent-tool-credentials.json - \
  --region ${AWS_REGION} | python3 -m json.tool

Confirm records with credentialType values: LAMBDA_EXECUTION_ROLE, CONFLUENCE_SECRET, S3_READ, or NONE.

7f β€” Inspect manifest.json

bash
aws s3 cp s3://${INVENTORY_BUCKET}/latest/manifest.json - \
  --region ${AWS_REGION} | python3 -m json.tool

Confirm agentBindingCount and agentToolCredentialCount are non-zero, generatedAt reflects current timestamp.

7g β€” Inspect activity-logs.json

bash
aws s3 cp s3://${INVENTORY_BUCKET}/latest/activity-logs.json - \
  --region ${AWS_REGION} | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'{len(d)} records'); print(set(r['event_type'] for r in d))"

Confirm non-zero record count and expected event types (tool_invocation, model_invocation, etc.).

7h β€” Verify SSE-KMS on a written artifact

bash
aws s3api head-object \
  --bucket ${INVENTORY_BUCKET} \
  --key latest/manifest.json \
  --region ${AWS_REGION} \
  --query '[ServerSideEncryption, SSEKMSKeyId]' \
  --output text
aws:kms arn:aws:kms:us-east-1:123456789012:key/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

Confirms encryption is enforced and the correct CMK is in use.

Updating the Lambda Code

After any change to src/:

bash β€” re-package, upload, deploy
# Re-package
rm -rf package bedrock-core-tools-inventory.zip
pip install -r requirements.txt -t package/
cp src/*.py package/
cd package && zip -r ../bedrock-core-tools-inventory.zip . && cd ..

# Upload
aws s3 cp bedrock-core-tools-inventory.zip \
  s3://${DEPLOY_BUCKET}/bedrock-core-tools-inventory.zip \
  --region ${AWS_REGION}

# Deploy
aws lambda update-function-code \
  --function-name ${FUNCTION_NAME} \
  --s3-bucket ${DEPLOY_BUCKET} \
  --s3-key bedrock-core-tools-inventory.zip \
  --region ${AWS_REGION}

Resource Summary

ResourceVariable / Name
Lambda function${FUNCTION_NAME} β€” bedrock-core-tools-inventory
Lambda execution role${ROLE_NAME} β€” bedrock-core-inventory-lambda-role
KMS CMK alias${CMK_ALIAS} β€” alias/bedrock-core-inventory
KMS CMK ARN${CMK_ARN} β€” captured in P1
Deployment bucket${DEPLOY_BUCKET} β€” bedrock-core-tools-inventory-deploy
Inventory bucket${INVENTORY_BUCKET} β€” bedrock-core-inventory
Lambda log group${LOG_GROUP} β€” /aws/lambda/bedrock-core-tools-inventory
Log retention90 days
EventBridge rule${EVENTBRIDGE_RULE} β€” bedrock-core-inventory-schedule
ScheduleEvery 15 minutes
Handlerhandler.handler
RuntimePython 3.11
Timeout900s
Memory256 MB