Skip to content

Modernize the connected-account stack: BCM Data Exports, de-nesting, native CUR, single connection role, and terminology rename - #110

Open
silvexis wants to merge 10 commits into
developfrom
feature/discovery-cur2-data-exports
Open

Modernize the connected-account stack: BCM Data Exports, de-nesting, native CUR, single connection role, and terminology rename#110
silvexis wants to merge 10 commits into
developfrom
feature/discovery-cur2-data-exports

Conversation

@silvexis

@silvexis silvexis commented Jun 15, 2026

Copy link
Copy Markdown
Member

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-provisioned JSON 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/GetExport and grants the role read access to every export's S3 destination bucket (type-agnostic; ingest stays COST_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 nested ConnectionResources stack. The notification Lambda no longer scrapes sibling stack outputs — it takes direct properties — and drops AWSCloudFormationReadOnlyAccess.

3. Native CUR (was a custom-resource Lambda)

Custom::CostAndUsageReport → native AWS::S3::Bucket + AWS::S3::BucketPolicy + AWS::CUR::ReportDefinition, gated to us-east-1 (the only region the CUR API/resource exists in) with DeletionPolicy: Retain. Billing connections needing a new CUR must launch in us-east-1; otherwise the role is still created and BillingCurStatus reports the skip.

4. Removed deprecated audit + cloudtrail-owner connection types

Templates, discovery detection, and cloudtrail:DescribeTrails removed. Their reactor payload keys remain and emit null/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 the connected_account.yaml launch filename are not renamed. terraform/ and policies/*.json are out of scope (follow-up).

6. Single fixed-name connection role

The resource-owner and master-payer roles collapse into one role cloudzero-connection (shared CommonAccessManagedPolicy + a billing-only inline CUR-bucket policy). The single ARN is reported into both the resource_owner and master_payer link slots. Fixed name ⇒ launch requires CAPABILITY_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 the DetectedLegacyConnectionStacks output. A Custom::CleanupGuide writes CLOUDZERO_CLEANUP_GUIDE.md to 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

  • Launch now requires CAPABILITY_NAMED_IAM.
  • Billing connections that need a new CUR must be launched in us-east-1.
  • "Migrating from an older CloudZero stack" section added to the README.

Verification

  • cfn-lint clean on all templates.
  • Discovery: 27 unit tests; Notification: 13 unit tests — including an explicit assert that the reactor payload key set is unchanged, plus legacy-detection and cleanup-guide tests.
  • Grep guard: no stray old terminology outside the fixed payload keys; no AWS-identifier renames.
  • aws cloudformation validate-template not run locally (no creds) — relying on cfn-lint + CI.

See docs/releases/1.0.102.md for the full release notes.

🤖 Generated with Claude Code

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>
@silvexis
silvexis requested a review from a team as a code owner June 15, 2026 19:06
@silvexis
silvexis requested a review from khill2018 June 15, 2026 19:06
Comment thread services/discovery/src/app.py Outdated
Comment thread services/discovery/tests/unit/test_app.py Outdated

@khill2018 khill2018 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

approved, but I agree with greptile about the test additions. Let's add those in (happy to do it myself) and then merge! Edit to add I see Erik is on a plane going to make those changes myself and have somebody additionally review the PR

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>
@Cloudzero Cloudzero deleted a comment from greptile-apps Bot Jun 16, 2026
@khill2018

Copy link
Copy Markdown
Contributor

@greptile

@greptile-apps

greptile-apps Bot commented Jun 16, 2026

Copy link
Copy Markdown

Greptile Summary

This PR modernizes the CloudZero connected-account onboarding stack with several significant changes while preserving the fixed reactor payload contract and existing data identifiers.

  • BCM Data Exports discovery: Discovery Lambda now calls ListExports/GetExport with per-page and per-export failure isolation; 6 nested stacks collapse to 1 (ConnectionResources), with Discovery and Notification inlined as native AWS::Lambda::Function resources in the parent template.
  • Native CUR: Custom::CostAndUsageReport replaced by AWS::S3::Bucket + AWS::S3::BucketPolicy + AWS::CUR::ReportDefinition (us-east-1 gated, DeletionPolicy: Retain); a single fixed-name cloudzero-connection role replaces the separate resource-owner and master-payer roles.
  • Migration tooling: detect_legacy_connection_stacks surfaces older CloudZero stacks; a new Custom::CleanupGuide writes CLOUDZERO_CLEANUP_GUIDE.md to the connected CUR bucket root.

Confidence Score: 5/5

The 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

Filename Overview
services/discovery/src/app.py Adds BCM Data Exports (CUR 2.0) discovery with per-export error isolation and per-page pagination failure resilience; legacy-stack detection added; pagination is handled for list_exports but not for describe_report_definitions
services/notification/src/app.py Notification handler simplified to receive all values as direct CFN properties (no sibling-stack scraping); fixed reactor payload contract preserved with deprecated cloudtrail/audit keys emitting null
services/connected_account.yaml De-nested from 6 stacks to 1; Discovery and Notification Lambdas inlined as native resources; ConnectionResources is the single remaining nested stack; parameter wiring is clean
services/account_type/account_resources.yaml Single fixed-name connection role with CommonAccessManagedPolicy; native CUR resources with DeletionPolicy:Retain; cleanup guide custom resource; IAM conditions and resource ARNs are correctly structured
services/discovery/src/cleanup_guide.py New cleanup guide Lambda: writes CLOUDZERO_CLEANUP_GUIDE.md to CUR bucket root; region-aware S3 client; best-effort (failures logged, never fail the stack)
services/discovery/tests/unit/test_app.py 27 unit tests covering BCM Data Exports pagination, per-export failure isolation, mid-pagination failure, legacy stack detection, and CUR format selection; previously flagged gaps now addressed
services/notification/tests/unit/test_app.py 13 unit tests including explicit guard on fixed reactor payload key set; billing/resource slot routing verified; deprovisioned message type tested

Reviews (10): Last reviewed commit: "Preserve collected export ARNs on mid-pa..." | Re-trigger Greptile

@khill2018
khill2018 requested a review from qiuz-cz June 16, 2026 14:43
Comment thread services/discovery/src/app.py
Comment thread services/discovery/src/app.py Outdated
Comment thread services/discovery/tests/unit/test_app.py
Comment thread docs/releases/1.0.101.md
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>
Comment thread services/discovery/src/app.py Fixed
Comment thread services/notification/src/app.py Fixed
- 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>
@silvexis

Copy link
Copy Markdown
Member Author

Thanks for the reviews. Addressed the feedback in 0f5f29c (on top of the stack-flattening work):

CodeQL — clear-text logging of sensitive data (py/clear-text-logging-sensitive-data)
The discovery and notification handlers no longer log the full event / payload / output — those carry ExternalId and account-identifying fields. They now log only non-sensitive summary fields (RequestType, message_type, response status, the boolean classification flags).

@qiuz-czexcept ClientError too narrow (app.py:122)
Broadened the BCM Data Exports list/get catches to (ClientError, BotoCoreError) so connectivity errors (e.g. EndpointConnectionError) degrade gracefully. On the import-time UnknownServiceError concern: requirements.txt pins boto3>=1.34.91, and bcm-data-exports has shipped in botocore since 1.34.0, so the client constructs fine on the python3.12 runtime.

@qiuz-cz — lazy %s logging (app.py:134) ✅ Switched the per-export GetExport warning to logger.warning('... %s', export_arn, ...).

@qiuz-cz — coverage gap for the if bucket: guard ✅ Added test_handler_skips_export_with_no_destination_bucket.

@qiuz-cz — hardcoded version/date in docs/releases/1.0.101.md — confirmed: release notes here are hand-written and committed (see 1.0.95.md, 1.0.100.md); the ${...} compare-URL is just the file's own convention, not semantic-release generation. Same pattern followed for the new 1.0.102.md.

Greptile P2s (per-export error isolation + pagination test) — these were resolved by @khill2018's 50c9b31 and are preserved through the refactor (the discovery handler was rewritten; the isolation + pagination behavior and tests carried over).

Also dropped the now-unused toolz dependency from both Lambda requirements.

Heads-up @khill2018: this PR now also contains the connected-account stack flattening (6 nested stacks → 1), native AWS::CUR::ReportDefinition, and removal of the deprecated audit / cloudtrail-owner account types — see docs/releases/1.0.102.md. Your 50c9b31 discovery change was preserved through the rewrite.

Comment thread services/notification/src/app.py Fixed
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>
Comment thread services/discovery/src/app.py Fixed
silvexis added 3 commits June 16, 2026 14:48
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>
@silvexis
silvexis requested review from khill2018 and qiuz-cz June 24, 2026 12:46
@silvexis

Copy link
Copy Markdown
Member Author

Pushed a sizable follow-on in c9deb84 — terminology rename + single connection role + migration tooling. Flagging for a fresh look @qiuz-cz @khill2018.

Terminology (internal only): master payer → billing, resource owner → resource, the CloudZero "account" concept → connection. The reactor payload keys are unchanged (master_payer/resource_owner/is_master_payer_account/…), as are AWS account identifiers (AccountId/ReactorAccountId/AWS::AccountId/MasterAccountId) and the existing-data identifiers (cz-cur-hourly-csv, cloudzero-cur-hourly-csv).

Single role: the two roles collapse into one fixed-name role cloudzero-connection. The single ARN is reported into both the resource_owner and master_payer link slots (and legacy) to keep the link contract; master_payer stays null for a resource-only connection. Fixed name ⇒ launch now requires CAPABILITY_NAMED_IAM (added to the Makefile deploy/update targets and documented in the README).

Migration / cleanup: discovery re-adopts an existing CUR (no duplicate), and now detects prior CloudZero stacks via cloudformation:DescribeStacks, surfaced in the DetectedLegacyConnectionStacks output. A Custom::CleanupGuide writes CLOUDZERO_CLEANUP_GUIDE.md to the root of the connected CUR bucket with the safe deletion steps (handler services/discovery/src/cleanup_guide.py, unit-tested).

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. aws cloudformation validate-template wasn't run here (no creds) — relying on cfn-lint + CI.

Note the README's "Migrating from an older CloudZero stack" section and the CAPABILITY_NAMED_IAM requirement are the two customer-facing behavior changes worth a close look.

Comment thread services/discovery/src/app.py Fixed
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>
@silvexis silvexis changed the title Discover BCM Data Exports CUR 2.0 buckets for IAM policy Modernize the connected-account stack: BCM Data Exports, de-nesting, native CUR, single connection role, and terminology rename Jun 24, 2026
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>
@jordan-piscitelli-cloudzero

Copy link
Copy Markdown

Why would discovery do the report selection? Why not send all discovered reports to reactor and let reactor select the one correct one?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants