Modernize the connected-account stack: BCM Data Exports, de-nesting, native CUR, single connection role, and terminology rename - #110
Conversation
Discovery now calls bcm-data-exports ListExports/GetExport to find the S3 destination buckets of CUR 2.0 exports and folds every export's bucket into MasterPayerBillingBucketArns. Bucket access is granted type-agnostically (only COST_AND_USAGE_REPORT is ingested), and the discovery Lambda role gains the two bcm-data-exports actions. Author: Erik Peterson <erik@cloudzero.com>
Address review feedback on BCM Data Exports discovery: - Wrap each get_export call in its own try/except so a transient failure on a single export no longer drops buckets already resolved from other exports; the failing export is logged and skipped. - Keep list_exports failure as graceful fallback to empty exports. - Add tests for the multi-page list_exports NextToken pagination path and for per-export get_export error isolation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Greptile SummaryThis PR modernizes the CloudZero connected-account onboarding stack with several significant changes while preserving the fixed reactor payload contract and existing data identifiers.
Confidence Score: 5/5The PR is safe to merge: the reactor payload contract is preserved, previously flagged issues (per-export isolation, mid-pagination failure resilience, pagination test coverage) are all addressed, and billing data is protected by DeletionPolicy:Retain throughout. The architectural changes are well-scoped and the test suite explicitly guards the fixed external contract key set. The only open item is the pre-existing absence of pagination in list_cur_report_definitions, which is extremely unlikely to matter in practice but is not closed by this PR. services/discovery/src/app.py — the list_cur_report_definitions function lacks pagination (pre-existing); all other changed files look correct. Important Files Changed
Reviews (10): Last reviewed commit: "Preserve collected export ARNs on mid-pa..." | Re-trigger Greptile |
Collapse the 6 nested stacks into one: connected_account.yaml inlines the Discovery and Notification Lambdas and provisions account resources through a single nested AccountResources stack (merged resource_owner + master_payer). Replace the Custom::CostAndUsageReport Lambda with native AWS::S3::Bucket, AWS::S3::BucketPolicy and AWS::CUR::ReportDefinition, gated on us-east-1 with DeletionPolicy: Retain. Remove the deprecated audit and cloudtrail-owner account types (templates, discovery detection, cloudtrail:DescribeTrails); the reactor payload contract is unchanged, emitting null/false for the deprecated fields. Rewrite the discovery and notification Lambdas into clear procedural flows; notification now reads direct resource properties instead of scraping stack outputs. Add a Stage parameter so prod/dev parents differ by one line. Author: Erik Peterson <erik@cloudzero.com>
- CodeQL (py/clear-text-logging-sensitive-data): stop logging the full event / payload / output in the discovery and notification handlers; they carry ExternalId and account-identifying fields. Log only non-sensitive summary fields. - Broaden the BCM Data Exports list/get catch to BotoCoreError so connectivity errors degrade gracefully (per review feedback). - Use lazy %s logging for the per-export GetExport warning. - Add tests for an export with no destination bucket (the if-bucket guard). - Drop now-unused toolz from discovery/notification requirements. Author: Erik Peterson <erik@cloudzero.com>
|
Thanks for the reviews. Addressed the feedback in CodeQL — clear-text logging of sensitive data ( @qiuz-cz — @qiuz-cz — lazy @qiuz-cz — coverage gap for the @qiuz-cz — hardcoded version/date in Greptile P2s (per-export error isolation + pagination test) — these were resolved by @khill2018's Also dropped the now-unused Heads-up @khill2018: this PR now also contains the connected-account stack flattening (6 nested stacks → 1), native |
The redacted logs still read tainted values: the notification payload dict carries ExternalId (so any field read is flagged) and the discovery log interpolated the CUR bucket name. Log a static message when posting to the reactor, and log only the (literal) billing report format when selecting a CUR. Author: Erik Peterson <erik@cloudzero.com>
CodeQL's clear-text-logging 'private data' heuristic flags the variable name billing_report_format (contains 'billing'), not its value (a literal like 'aws'). The log carried no real value beyond the format already present in the discovery output, so remove it rather than fight the scanner. Author: Erik Peterson <erik@cloudzero.com>
The resource-owner and master-payer roles carried 19 identical read-only statements. Extract them into one CommonAccessManagedPolicy (AWS::IAM::ManagedPolicy) attached to both roles; the master-payer role keeps only its CUR-bucket S3 statement inline. Effective permissions unchanged; single source of truth. Policy is unnamed so deployment still needs only CAPABILITY_IAM. Author: Erik Peterson <erik@cloudzero.com>
Adopt billing (was master payer), resource (was resource owner), and connection (the CloudZero 'account' concept) terminology across the CFN templates, Lambda internals, and docs. The reactor payload keys, AWS account identifiers, and existing-data identifiers (cz-cur-hourly-csv etc.) are unchanged. Collapse the resource-owner and master-payer roles into one fixed-name role 'cloudzero-connection' (path /cloudzero/), reported into both link slots to keep the reactor link contract. Fixed name ⇒ launch requires CAPABILITY_NAMED_IAM. Discovery now detects previously-deployed CloudZero stacks (cloudformation:DescribeStacks) and surfaces them via DetectedLegacyConnectionStacks; a Custom::CleanupGuide writes CLOUDZERO_CLEANUP_GUIDE.md to the connected CUR bucket root listing the old stack(s) and safe deletion steps. The new stack re-adopts an existing CUR, so migration is seamless. Present the reactor and connections accounts as co-equal trust principals. Author: Erik Peterson <erik@cloudzero.com>
|
Pushed a sizable follow-on in Terminology (internal only): master payer → billing, resource owner → resource, the CloudZero "account" concept → connection. The reactor payload keys are unchanged ( Single role: the two roles collapse into one fixed-name role Migration / cleanup: discovery re-adopts an existing CUR (no duplicate), and now detects prior CloudZero stacks via Also: reactor and connections accounts are now presented as co-equal trust principals. Verification: cfn-lint clean on all templates; discovery 27 tests, notification 13 tests (incl. an explicit assert that the reactor payload key set is unchanged); grep guard confirms no stray old terminology outside the fixed payload keys and no AWS-identifier renames. Note the README's "Migrating from an older CloudZero stack" section and the |
CodeQL's clear-text-logging 'private data' heuristic flags the IsBillingConnection token (contains 'billing') in the completion log. Log a static message instead. Author: Erik Peterson <erik@cloudzero.com>
Move the try/except inside the list_exports pagination loop so a failure on a later page keeps the export ARNs already collected from earlier pages instead of discarding them. Addresses Greptile's remaining confidence concern. Author: Erik Peterson <erik@cloudzero.com>
|
Why would discovery do the report selection? Why not send all discovered reports to reactor and let reactor select the one correct one? |
This PR started as "discover BCM Data Exports (CUR 2.0) buckets" and grew, by request, into a broader modernization of the customer-facing onboarding stack. All of it preserves the fixed reactor payload contract (the
account-link-provisionedJSON keys are unchanged) and the existing-data identifiers (cz-cur-hourly-csv,cloudzero-cur-hourly-csv), so it is seamless from CloudZero's side.What's in this PR
1. BCM Data Exports (CUR 2.0) discovery
Discovery now calls
bcm-data-exports:ListExports/GetExportand grants the role read access to every export's S3 destination bucket (type-agnostic; ingest staysCOST_AND_USAGE_REPORT-only). Per-export failures are isolated; pagination covered.2. De-nesting: 6 nested stacks → 1
The parent inlines the Discovery and Notification Lambdas (native
AWS::Lambda::Function+ role) and provisions through one nestedConnectionResourcesstack. The notification Lambda no longer scrapes sibling stack outputs — it takes direct properties — and dropsAWSCloudFormationReadOnlyAccess.3. Native CUR (was a custom-resource Lambda)
Custom::CostAndUsageReport→ nativeAWS::S3::Bucket+AWS::S3::BucketPolicy+AWS::CUR::ReportDefinition, gated tous-east-1(the only region the CUR API/resource exists in) withDeletionPolicy: Retain. Billing connections needing a new CUR must launch in us-east-1; otherwise the role is still created andBillingCurStatusreports the skip.4. Removed deprecated audit + cloudtrail-owner connection types
Templates, discovery detection, and
cloudtrail:DescribeTrailsremoved. Their reactor payload keys remain and emitnull/false(contract unchanged).5. Terminology rename (internal only)
master payer → billing, resource owner → resource, the CloudZero "account" concept → connection. Reactor payload keys, AWS account identifiers (
AccountId/ReactorAccountId/AWS::AccountId/MasterAccountId), and theconnected_account.yamllaunch filename are not renamed.terraform/andpolicies/*.jsonare out of scope (follow-up).6. Single fixed-name connection role
The resource-owner and master-payer roles collapse into one role
cloudzero-connection(sharedCommonAccessManagedPolicy+ a billing-only inline CUR-bucket policy). The single ARN is reported into both theresource_ownerandmaster_payerlink slots. Fixed name ⇒ launch requiresCAPABILITY_NAMED_IAM(Makefile + README updated).7. Migration + guided cleanup
Discovery re-adopts an account's existing CUR (no duplicate) and detects previously-deployed CloudZero stacks (
cloudformation:DescribeStacks), surfaced via theDetectedLegacyConnectionStacksoutput. ACustom::CleanupGuidewritesCLOUDZERO_CLEANUP_GUIDE.mdto the root of the connected CUR bucket with safe deletion steps. README documents the migration path.8. Co-equal CloudZero principals
The reactor and connections accounts are presented as equally-important trust principals.
Customer-facing behavior changes
CAPABILITY_NAMED_IAM.Verification
cfn-lintclean on all templates.aws cloudformation validate-templatenot run locally (no creds) — relying on cfn-lint + CI.See
docs/releases/1.0.102.mdfor the full release notes.🤖 Generated with Claude Code