Home› Blog› Week 10 - Centralized Logging Platform: One Query …
AWS Weekly Lab AWS Terraform

Centralized Logging Platform: One Query Across Every Account

Every account in an AWS organization hoards its own CloudWatch log groups — so "show me every error across the platform in the last hour" means logging into N accounts and running the same query N times, and log evidence dies with the account that produced it. This platform fixes both with CloudWatch's 2025-era native primitives: cross-account observability (OAM) for query-in-place, and organization-wide centralization rules for durable copies — no OpenSearch cluster, no Firehose pipeline, and the first centralized copy is free.

AWS Platform Engineering Lab · Week 10

Lab Overview

"Show me every ERROR across the platform in the last hour." With one account that is a query. With ten accounts it is ten logins, ten consoles and the same query typed ten times — and if one of those accounts is the one that got compromised, its logs are sitting inside the blast radius.

The old fix was expensive enough that most teams skipped it: subscription filters into Kinesis Firehose into an OpenSearch cluster, comfortably $1,000 a month at scale plus someone's half-time job. In 2025 AWS made cross-account querying native and the first centralized copy free. This lab builds the version that exists now — and runs into one feature that was not ready.

What it doesQueries every account's logs from one place, and keeps a copy of the evidence in an account the source cannot write to
Core servicesCloudWatch Logs, CloudWatch OAM, Logs centralization rules, Logs Insights, Lambda, EventBridge, SNS, AWS Organizations
Terraform resources17 across five modules, one HCP workspace, two providers (management account and an assumed role in the source account)
OutcomeOAM sharing, cross-account queries, the dashboard and the alarm all worked. The org-wide centralization rule did not — AWS refused to create it for over 24 hours. See Troubleshooting
Lab runtimeNot recorded
CostEstimated under $2/month while running. Destroyed: $0
StatusPublished 19 July 2026 — resources destroyed, one AWS-side item still open at publish time

Cloud Services and Concepts

Service or conceptIts job here
CloudWatch OAM (sink + link)Links accounts so the monitoring account can query logs and metrics in place. Nothing moves, nothing extra is billed, and it works across regions
Logs centralization rulesOrganizations-integrated rules that physically replicate matching log groups into one destination account
Logs InsightsThe query layer; now speaks OpenSearch PPL and SQL as well as its own syntax
Lambda + EventBridgeA generator emitting ~25 structured JSON lines every five minutes, so every layer has live data to prove itself
SNSCarries the alarm to a human
AWS OrganizationsTrusted access for CloudWatch — the prerequisite with no Terraform resource

Sharing and copying are different tools

OAM answers "query everything, now, for free" but the only copy of the data still lives in the account that produced it. A centralization rule answers "keep evidence that outlives the account" at the cost of storing it twice. Neither replaces the other, which is why this build runs both paths.

Log class is an irreversible choice

The Infrequent Access class halves ingestion cost — $0.25/GB against $0.50/GB — but permanently disables metric filters, subscription filters and Live Tail, and a log group's class can never be changed after creation. The destination groups here stay Standard, because the alarm needs a metric filter and first-copy ingestion is free anyway. IA would have bought nothing and cost the alerting design.

The pattern AWS is retiring

Subscription filters into Firehose into OpenSearch is the architecture AWS is itself sunsetting: the official "Centralized Logging with OpenSearch" solution retires in December 2026, with customers steered to the native capabilities. Worth re-checking a "best practice" diagram against the current platform before rebuilding it.

Architecture and Workflow

Centralized Logging Platform — share in place (OAM) + copy for keeps (centralization rule) Member account (source) ent-wkld-dev-sandbox · Dev OU EventBridge schedule rate(5 minutes) log_generator Lambda structured JSON · INFO / WARN / ERROR App log group /platform-lab/week10/log-generator 14-day retention OAM link (opt-in, per account) shares Logs + Metrics with the sink — no copy Management account monitoring + destination Centralized log group same name · streams tagged with source account + region · 30-day retention · Standard Metric filter → alarm → SNS ≥ 5 ERROR / 5 min → email to owner Cross-account dashboard central errors + source Lambda metrics OAM sink org-scoped policy · logs + metrics only feeds share in place query cross-account · $0 physical copy first copy free · new data only Centralization rule scope: OrganizationId LogGroupName LIKE '/platform-lab/%'
  1. Generate. An EventBridge rule in the source account invokes a 128 MB Lambda every five minutes, emitting around 25 structured JSON lines per run with weighted INFO/WARN/ERROR levels.
  2. Land. The function's logging_config routes its output into /platform-lab/week10/log-generator with 14-day retention, rather than the default /aws/lambda/* group, so one group carries the whole story.
  3. Share, and copy. The OAM link makes the source account's logs and metrics queryable from the monitoring account instantly and at no charge. In parallel, the org-wide centralization rule is meant to replicate those groups into the management account, tagging each stream with its source account and region.
  4. Watch the copy, not just store it. A metric filter counts ERROR events in the centralized stream; an alarm fires at five or more errors in five minutes and publishes to SNS. A cross-account dashboard puts that error count next to the source account's own Lambda metrics — the OAM half made visible.

Prerequisites

RequirementWhyHow to check
Organizations trusted access for CloudWatch Centralization rules will not be created without it, and the Terraform AWS provider has no resource for it — this is a one-time manual step before any apply aws organizations list-aws-service-access-for-organization shows observabilityadmin.amazonaws.com
The centralization service-linked role The console path creates it for you; the CLI path does not, and rule creation fails later without it aws iam create-service-linked-role --aws-service-name logs-centralization.observabilityadmin.amazonaws.com
Terraform ≥ 1.10, AWS provider ~> 6.39 The centralization-rule resource only exists from provider 6.21.0, so the 5.x pin earlier weeks carry will not work here terraform version
A role the management account can assume in the source account Hub and spoke resources live in one plan with no static credentials; the aliased provider assumes OrganizationAccountAccessRole The plan shows resources under both providers
Destination log groups on the Standard class The alarm depends on a metric filter, which Infrequent Access disables permanently Set at creation — it cannot be changed afterwards

Implementation

ModuleResourcesIts job
oam_hub2The sink in the monitoring account, plus an org-scoped sink policy
oam_spoke1The link, created under the source-account provider
log_generator7The Lambda, its role and policy, log group, and the EventBridge schedule
log_centralization1The org-wide rule — the one resource AWS refused to create
alerting6Metric filter, alarm, SNS topic and subscription, and the cross-account dashboard

One HCP workspace (week-10-dev, VCS-driven) applies all of it. The root declares two providers: the default one for the management account, and an aliased one that assumes a role in the source account. Hub and spoke therefore appear in a single plan, and no static credentials exist anywhere — HCP authenticates to AWS over OIDC.

HCP Terraform run history for the week-10-dev workspace
HCP Terraform run history for week-10-dev — VCS-driven plans from GitHub pushes, applies confirmed manually.
CloudWatch monitoring account view showing one linked source account sharing Logs and Metrics
The monitoring account after the apply: one linked source account, sharing Logs and Metrics through the OAM sink.

Validation and Evidence

TestExpectedActualStatus
Generator writes to the intended log group Structured JSON in /platform-lab/week10/log-generator Events landing every five minutes with the expected level mix Pass
Cross-account query from the monitoring account One Logs Insights query, scope "All accounts", returns results from the source account Query executed across two log groups and grouped errors by account Pass
Alarm fires on an error spike Five or more ERROR events in five minutes moves the alarm to ALARM and emails SNS Alarm entered ALARM after a synthetic metric injection; the email arrived with the state change and threshold Pass
Cross-account dashboard Centralized error count beside the source account's Lambda metrics Rendered, with the centralized error widget empty because the rule never created Partial
Org-wide centralization rule Matching log groups replicated into the management account AWS refused creation for over 24 hours with "Centralization is currently initializing"; no copy was ever made Blocked
Source account log group showing structured JSON log events at INFO, WARN and ERROR levels emitted by the generator Lambda
Structured JSON events landing in the source account's app log group — the raw material for everything downstream.
Log Analytics query in the monitoring account executing across 2 log groups with All accounts scope, returning error counts by service from the source account
Run from the monitoring account, scope "All accounts": the query executes across 2 log groups — the local one and the source account's, shared via OAM — and returns error counts per service across the org. 1,010 records scanned in 3.1 seconds.

Security and Cost

  • The sink policy is scoped twice. Only accounts inside the organization can link to it (aws:PrincipalOrgID), and only for logs and metrics (oam:ResourceTypes) — no outside account, no other telemetry types.
  • Evidence outlives the account. Centralized copies live where the source account has no write access. That is the whole security argument for physical copies over pure sharing, and it is the half of this build that did not land.
  • No static credentials. HCP authenticates over OIDC, the spoke is reached by role assumption, and the service-linked role bounds what the centralization service can do.
  • Retention as a control. 14 days at source, 30 days centralized. Logs are a liability as much as an asset; unbounded default retention is both a cost leak and an exposure surface.

Cost

Estimated from published rates in July 2026; the deployed footprint was small enough that no meaningful charge was measured.

ComponentCostNote
OAM sink and link$0Sharing logs and metrics cross-account is not billed
Centralization rule, first copy$0 ingestionAdditional copies, such as a backup region, would be $0.05/GB
Log storage, both copies~$0.03/GB-monthBounded by the retention settings above
Lambda, EventBridge, SNS~$0Around 8,640 invocations a month at 128 MB — well inside the free tier
TotalUnder $2/monthDestroyed: $0

For scale, the retired Firehose-to-OpenSearch pattern at 50 accounts and 100 GB a day runs $1,000–2,500 a month before anyone queries anything. The native equivalent is $0 first-copy ingestion, roughly $90 a month of storage at 30-day retention, and per-GB query charges only when a query runs.

Troubleshooting and Lessons

"Centralization is currently initializing. Please try again later."

This is the one that did not get fixed. After trusted access was enabled, CreateCentralizationRuleForOrganization returned a ValidationException and kept returning it, retried every few minutes for more than 24 hours, while read APIs on the same service answered normally. No documentation mentions an initialization window at all.

Root cause: never established. Everything checkable was ruled out — the service-linked role exists, trusted access reads On, a delegated administrator was registered, and CloudTrail confirms the calls arrive and are rejected. The console's own wizard accepts the entire configuration and then refuses the final create with the same message, and its "turn off trusted access" button greys out while initializing, so the setting cannot even be re-toggled. One suspect worth naming without claiming it: this organization contains eight closed accounts sitting in their 90-day post-closure SUSPENDED state, and an org-wide provisioning workflow that touches every member account may not enjoy meeting them.

Lesson, which stands whenever AWS finishes: never put an org-wide enablement with an unbounded activation time on the critical path of a build. The OAM half of this architecture was queryable in minutes; the half that depended on an organization-wide backend was not, and no amount of correct Terraform changed that.

CloudWatch centralization rule wizard review page rejecting creation with the error Centralization is currently initializing, please try again later
The console wizard's review page: configuration fully valid, creation refused — the org-wide backend was still initializing many hours after trusted access was enabled.

The OAM link raced a policy it never referenced

First apply: 15 of 17 resources created, then aws_oam_link failed with AccessDeniedException ... explicit deny in a resource-based policy. The policy was correct — the identical CreateLink call succeeded from the CLI minutes later.

Root cause: Terraform called CreateLink 14 seconds after creating the sink policy, before the resource policy had propagated. The dependency graph permitted it, because the link references the sink's ARN and nothing ordered it after the policy.

Fix: a module-level depends_on = [module.oam_hub] on the spoke. The general shape is worth keeping: when resource A must satisfy a resource-based policy created by resource B, referencing A's parent does not order it after B. Say the dependency out loud.

A new feature has four clocks

Docs, console, CLI and Terraform provider each described this feature slightly differently in the same week — including an undocumented initialization wait. Verify each layer live rather than assuming they agree.

Cleanup and Key Takeaways

The workspace is VCS-connected, so destroy through HCP: week-10-dev → Settings → Destruction and Deletion → Queue destroy plan. Afterwards confirm three things are actually gone: log groups under /platform-lab/week10 in both accounts, the OAM sink and link pair, and the centralization rule. Organizations trusted access is an org-level setting rather than a billed resource — leaving it on costs nothing and saves the initialization wait next time.

  • Sharing and copying are different tools; run both. OAM gives you the query, centralization gives you the evidence that survives the account that produced it.
  • The 2025 primitives deleted a whole architecture. Three billed hops became one native feature. Re-check inherited "best practice" diagrams against the current platform.
  • Log class is irreversible and invisible. Infrequent Access halves ingestion and permanently kills metric filters. Decide per log group, at creation, with the alerting design in front of you.
  • Terraform's graph only knows what you reference. When correctness depends on a policy existing, depends_on is the honest fix rather than a retry loop.
  • Publish the part that did not work. Four of five paths worked; one was refused by AWS for a day with no documented reason. A lab writeup that quietly omits that is a worse artifact than one that names it.

References

Comments

How was your experience?
Your feedback helps improve this site.
PoorExcellent